Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/01_standalone_sdk/25_agent_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
get_logger,
)
from openhands.sdk.context import Skill
from openhands.sdk.subagent import register_agent
from openhands.sdk.tool import register_tool
from openhands.tools.delegate import (
DelegateTool,
DelegationVisualizer,
register_agent,
)
from openhands.tools.preset.default import get_default_tools

Expand Down
4 changes: 4 additions & 0 deletions openhands-sdk/openhands/sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
create_mcp_tools,
)
from openhands.sdk.plugin import Plugin
from openhands.sdk.subagent import (
register_agent,
)
from openhands.sdk.tool import (
Action,
Observation,
Expand Down Expand Up @@ -113,6 +116,7 @@
"Workspace",
"LocalWorkspace",
"RemoteWorkspace",
"register_agent",
"load_project_skills",
"load_skills_from_dir",
"load_user_skills",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
from openhands.sdk.security.confirmation_policy import (
ConfirmationPolicyBase,
)
from openhands.sdk.subagent import (
AgentDefinition,
register_file_agents,
register_plugin_agents,
)
from openhands.sdk.tool.schema import Action, Observation
from openhands.sdk.utils.cipher import Cipher
from openhands.sdk.workspace import LocalWorkspace
Expand Down Expand Up @@ -310,6 +315,7 @@ def _ensure_plugins_loaded(self) -> None:
return

all_plugin_hooks: list[HookConfig] = []
all_plugin_agents: list[AgentDefinition] = []

# Load plugins if specified
if self._plugin_specs:
Expand Down Expand Up @@ -347,6 +353,10 @@ def _ensure_plugins_loaded(self) -> None:
if plugin.hooks and not plugin.hooks.is_empty():
all_plugin_hooks.append(plugin.hooks)

# Collect agent definitions
if plugin.agents:
all_plugin_agents.extend(plugin.agents)

Comment thread
VascoSch92 marked this conversation as resolved.
# Update agent with merged content
self.agent = self.agent.model_copy(
update={
Expand All @@ -361,6 +371,13 @@ def _ensure_plugins_loaded(self) -> None:

logger.info(f"Loaded {len(self._plugin_specs)} plugin(s) via Conversation")

# Register plugin and file-based agent definitions into delegate registry
if all_plugin_agents:
register_plugin_agents(all_plugin_agents)

# Register file-based agents from .agents/ directories
register_file_agents(self.workspace.working_dir)

# Combine explicit hook_config with plugin hooks
# Explicit hooks run first (before plugin hooks)
final_hook_config = self._pending_hook_config
Expand Down
2 changes: 0 additions & 2 deletions openhands-sdk/openhands/sdk/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from openhands.sdk.plugin.loader import load_plugins
from openhands.sdk.plugin.plugin import Plugin
from openhands.sdk.plugin.types import (
AgentDefinition,
CommandDefinition,
Marketplace,
MarketplaceMetadata,
Expand All @@ -36,7 +35,6 @@
"PluginAuthor",
"PluginSource",
"ResolvedPluginSource",
"AgentDefinition",
"CommandDefinition",
# Plugin loading
"load_plugins",
Expand Down
2 changes: 1 addition & 1 deletion openhands-sdk/openhands/sdk/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
from openhands.sdk.logger import get_logger
from openhands.sdk.plugin.fetch import fetch_plugin
from openhands.sdk.plugin.types import (
AgentDefinition,
CommandDefinition,
PluginAuthor,
PluginManifest,
)
from openhands.sdk.subagent.schema import AgentDefinition


if TYPE_CHECKING:
Expand Down
100 changes: 0 additions & 100 deletions openhands-sdk/openhands/sdk/plugin/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -177,105 +176,6 @@ class PluginManifest(BaseModel):
model_config = {"extra": "allow"}


def _extract_examples(description: str) -> list[str]:
"""Extract <example> tags from description for agent triggering."""
pattern = r"<example>(.*?)</example>"
matches = re.findall(pattern, description, re.DOTALL | re.IGNORECASE)
return [m.strip() for m in matches if m.strip()]


class AgentDefinition(BaseModel):
"""Agent definition loaded from markdown file.

Agents are specialized configurations that can be triggered based on
user input patterns. They define custom system prompts and tool access.
"""

name: str = Field(description="Agent name (from frontmatter or filename)")
description: str = Field(default="", description="Agent description")
model: str = Field(
default="inherit", description="Model to use ('inherit' uses parent model)"
)
color: str | None = Field(default=None, description="Display color for the agent")
tools: list[str] = Field(
default_factory=list, description="List of allowed tools for this agent"
)
system_prompt: str = Field(default="", description="System prompt content")
source: str | None = Field(
default=None, description="Source file path for this agent"
)
# whenToUse examples extracted from description
when_to_use_examples: list[str] = Field(
default_factory=list,
description="Examples of when to use this agent (for triggering)",
)
# Raw frontmatter for any additional fields
metadata: dict[str, Any] = Field(
default_factory=dict, description="Additional metadata from frontmatter"
)

@classmethod
def load(cls, agent_path: Path) -> AgentDefinition:
"""Load an agent definition from a markdown file.

Agent markdown files have YAML frontmatter with:
- name: Agent name
- description: Description with optional <example> tags for triggering
- model: Model to use (default: 'inherit')
- color: Display color
- tools: List of allowed tools

The body of the markdown is the system prompt.

Args:
agent_path: Path to the agent markdown file.

Returns:
Loaded AgentDefinition instance.
"""
with open(agent_path) as f:
post = frontmatter.load(f)

fm = post.metadata
content = post.content.strip()

# Extract frontmatter fields with proper type handling
name = str(fm.get("name", agent_path.stem))
description = str(fm.get("description", ""))
model = str(fm.get("model", "inherit"))
color_raw = fm.get("color")
color: str | None = str(color_raw) if color_raw is not None else None
tools_raw = fm.get("tools", [])

# Ensure tools is a list of strings
tools: list[str]
if isinstance(tools_raw, str):
tools = [tools_raw]
elif isinstance(tools_raw, list):
tools = [str(t) for t in tools_raw]
else:
tools = []

# Extract whenToUse examples from description
when_to_use_examples = _extract_examples(description)

# Remove known fields from metadata to get extras
known_fields = {"name", "description", "model", "color", "tools"}
metadata = {k: v for k, v in fm.items() if k not in known_fields}

return cls(
name=name,
description=description,
model=model,
color=color,
tools=tools,
system_prompt=content,
source=str(agent_path),
when_to_use_examples=when_to_use_examples,
metadata=metadata,
)


class CommandDefinition(BaseModel):
"""Command definition loaded from markdown file.

Expand Down
32 changes: 32 additions & 0 deletions openhands-sdk/openhands/sdk/subagent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from openhands.sdk.subagent.builtins import get_default_agent
from openhands.sdk.subagent.load import (
load_project_agents,
load_user_agents,
)
from openhands.sdk.subagent.registry import (
get_agent_factory,
get_factory_info,
register_agent,
register_agent_if_absent,
register_file_agents,
register_plugin_agents,
)
from openhands.sdk.subagent.schema import AgentDefinition


__all__ = [
# loading
"load_user_agents",
"load_project_agents",
# agent registration
"register_agent",
"register_file_agents",
"register_plugin_agents",
"register_agent_if_absent",
"get_factory_info",
"get_agent_factory",
# Agent classes
"AgentDefinition",
# builtin agents
"get_default_agent",
]
4 changes: 4 additions & 0 deletions openhands-sdk/openhands/sdk/subagent/builtins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from openhands.sdk.subagent.builtins.default import get_default_agent


__all__ = ["get_default_agent"]
37 changes: 37 additions & 0 deletions openhands-sdk/openhands/sdk/subagent/builtins/default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from functools import cache

from openhands.sdk.subagent.registry import AgentFactory, _agent_definition_to_factory
from openhands.sdk.subagent.schema import AgentDefinition


@cache
def _build_default_agent_factory(
enable_browser: bool = True,
) -> AgentFactory:
"""Return an AgentFactory class describing for the default agent.

Args:
enable_browser: Whether to include browser tools.

Returns:
An AgentFactory class describing the default agent.
"""

tool_names = ["terminal", "file_editor", "task_tracker"]
if enable_browser:
tool_names.append("browser_tool_set")

agent_def = AgentDefinition(
name="default",
description="Default general-purpose agent",
model="inherit",
tools=tool_names,
)
return AgentFactory(
factory_func=_agent_definition_to_factory(agent_def),
description=agent_def.description or "Default general-purpose agent",
)


def get_default_agent(enable_browser: bool = False) -> AgentFactory:
Comment thread
VascoSch92 marked this conversation as resolved.
Outdated
return _build_default_agent_factory(enable_browser=enable_browser)
Loading
Loading