Skip to content

Commit c2034c3

Browse files
committed
feat(delegate): Built-in specialized agent types (Explore, Bash) (OpenHands#2201)
Cherry-pick from upstream c31cdee
1 parent 601cc3d commit c2034c3

File tree

15 files changed

+304
-76
lines changed

15 files changed

+304
-76
lines changed

examples/01_standalone_sdk/25_agent_delegation.py

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
import os
1111

12-
from pydantic import SecretStr
13-
1412
from openhands.sdk import (
1513
LLM,
1614
Agent,
@@ -26,30 +24,26 @@
2624
DelegateTool,
2725
DelegationVisualizer,
2826
)
29-
from openhands.tools.preset.default import get_default_tools
27+
from openhands.tools.preset.default import get_default_tools, register_builtins_agents
3028

3129

3230
ONLY_RUN_SIMPLE_DELEGATION = False
3331

3432
logger = get_logger(__name__)
3533

3634
# Configure LLM and agent
37-
# You can get an API key from https://app.all-hands.dev/settings/api-keys
38-
api_key = os.getenv("LLM_API_KEY")
39-
assert api_key is not None, "LLM_API_KEY environment variable is not set."
40-
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
4135
llm = LLM(
42-
model=model,
43-
api_key=SecretStr(api_key),
36+
model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"),
37+
api_key=os.getenv("LLM_API_KEY"),
4438
base_url=os.environ.get("LLM_BASE_URL", None),
4539
usage_id="agent",
4640
)
4741

4842
cwd = os.getcwd()
4943

50-
register_tool("DelegateTool", DelegateTool)
51-
tools = get_default_tools(enable_browser=False)
52-
tools.append(Tool(name="DelegateTool"))
44+
tools = get_default_tools(enable_browser=True)
45+
tools.append(Tool(name=DelegateTool.name))
46+
register_builtins_agents()
5347

5448
main_agent = Agent(
5549
llm=llm,
@@ -61,7 +55,7 @@
6155
visualizer=DelegationVisualizer(name="Delegator"),
6256
)
6357

64-
task_message = (
58+
conversation.send_message(
6559
"Forget about coding. Let's switch to travel planning. "
6660
"Let's plan a trip to London. I have two issues I need to solve: "
6761
"Lodging: what are the best areas to stay at while keeping budget in mind? "
@@ -72,7 +66,6 @@
7266
"They should keep it short. After getting the results, merge both analyses "
7367
"into a single consolidated report.\n\n"
7468
)
75-
conversation.send_message(task_message)
7669
conversation.run()
7770

7871
conversation.send_message(
@@ -81,18 +74,57 @@
8174
conversation.run()
8275

8376
# Report cost for simple delegation example
84-
cost_1 = conversation.conversation_stats.get_combined_metrics().accumulated_cost
85-
print(f"EXAMPLE_COST (simple delegation): {cost_1}")
77+
cost_simple = conversation.conversation_stats.get_combined_metrics().accumulated_cost
78+
print(f"EXAMPLE_COST (simple delegation): {cost_simple}")
8679

8780
print("Simple delegation example done!", "\n" * 20)
8881

89-
90-
# -------- Agent Delegation Second Part: User-Defined Agent Types --------
91-
9282
if ONLY_RUN_SIMPLE_DELEGATION:
83+
# For CI: always emit the EXAMPLE_COST marker before exiting.
84+
print(f"EXAMPLE_COST: {cost_simple}")
9385
exit(0)
9486

9587

88+
# -------- Agent Delegation Second Part: Built-in Agent Types (Explore + Bash) --------
89+
90+
main_agent = Agent(
91+
llm=llm,
92+
tools=[Tool(name=DelegateTool.name)],
93+
)
94+
conversation = Conversation(
95+
agent=main_agent,
96+
workspace=cwd,
97+
visualizer=DelegationVisualizer(name="Delegator (builtins)"),
98+
)
99+
100+
builtin_task_message = (
101+
"Demonstrate SDK built-in sub-agent types. "
102+
"1) Spawn an 'explore' sub-agent and ask it to list the markdown files in "
103+
"openhands-sdk/openhands/sdk/subagent/builtins/ and summarize what each "
104+
"built-in agent type is for (based on the file contents). "
105+
"2) Spawn a 'bash' sub-agent and ask it to run `python --version` in the "
106+
"terminal and return the exact output. "
107+
"3) Merge both results into a short report. "
108+
"Do not use internet access."
109+
)
110+
111+
print("=" * 100)
112+
print("Demonstrating built-in agent delegation (explore + bash)...")
113+
print("=" * 100)
114+
115+
conversation.send_message(builtin_task_message)
116+
conversation.run()
117+
118+
# Report cost for builtin agent types example
119+
cost_builtin = conversation.conversation_stats.get_combined_metrics().accumulated_cost
120+
print(f"EXAMPLE_COST (builtin agents): {cost_builtin}")
121+
122+
print("Built-in agent delegation example done!", "\n" * 20)
123+
124+
125+
# -------- Agent Delegation Third Part: User-Defined Agent Types --------
126+
127+
96128
def create_lodging_planner(llm: LLM) -> Agent:
97129
"""Create a lodging planner focused on London stays."""
98130
skills = [
@@ -190,10 +222,12 @@ def create_activities_planner(llm: LLM) -> Agent:
190222
conversation.run()
191223

192224
# Report cost for user-defined agent types example
193-
cost_2 = conversation.conversation_stats.get_combined_metrics().accumulated_cost
194-
print(f"EXAMPLE_COST (user-defined agents): {cost_2}")
225+
cost_user_defined = (
226+
conversation.conversation_stats.get_combined_metrics().accumulated_cost
227+
)
228+
print(f"EXAMPLE_COST (user-defined agents): {cost_user_defined}")
195229

196230
print("All done!")
197231

198232
# Full example cost report for CI workflow
199-
print(f"EXAMPLE_COST: {cost_1 + cost_2}")
233+
print(f"EXAMPLE_COST: {cost_simple + cost_builtin + cost_user_defined}")

openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
register_file_agents,
5252
register_plugin_agents,
5353
)
54-
from openhands.sdk.subagent.registry import register_builtins_agents
5554
from openhands.sdk.tool.schema import Action, Observation
5655
from openhands.sdk.utils.cipher import Cipher
5756
from openhands.sdk.workspace import LocalWorkspace
@@ -417,12 +416,9 @@ def _register_file_based_agents(self) -> None:
417416
then `{project}/.openhands/agents/*.md`)
418417
4. User-level file agents (`~/.agents/agents/*.md`,
419418
then `~/.openhands/agents/*.md`)
420-
5. SDK builtin agents (`subagent/builtins/*.md`)
421419
"""
422420
# register project-level and then user-level file-based agents
423421
register_file_agents(self.workspace.working_dir)
424-
# register builtins agents
425-
register_builtins_agents()
426422

427423
def _ensure_agent_ready(self) -> None:
428424
"""Ensure the agent is fully initialized with plugins and agents loaded.

openhands-sdk/openhands/sdk/subagent/registry.py

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def create_security_expert(llm):
3030

3131
from openhands.sdk.logger import get_logger
3232
from openhands.sdk.subagent.load import (
33-
load_agents_from_dir,
3433
load_project_agents,
3534
load_user_agents,
3635
)
@@ -43,8 +42,6 @@ def create_security_expert(llm):
4342

4443
logger = get_logger(__name__)
4544

46-
BUILTINS_DIR = Path(__file__).parent / "builtins"
47-
4845

4946
class AgentFactory(NamedTuple):
5047
"""Simple container for an agent factory function and its description."""
@@ -127,11 +124,15 @@ def agent_definition_to_factory(
127124
`AgentContext`.
128125
- `model: inherit` preserves the parent LLM; an explicit model name
129126
creates a copy via `model_copy(update=...)`.
127+
128+
Raises:
129+
ValueError: If a tool provided to the agent is not registered.
130130
"""
131131

132132
def _factory(llm: "LLM") -> "Agent":
133133
from openhands.sdk.agent.agent import Agent
134134
from openhands.sdk.context.agent_context import AgentContext
135+
from openhands.sdk.tool.registry import list_registered_tools
135136
from openhands.sdk.tool.spec import Tool
136137

137138
# Handle model override
@@ -147,7 +148,15 @@ def _factory(llm: "LLM") -> "Agent":
147148
)
148149

149150
# Resolve tools
150-
tools = [Tool(name=tool_name) for tool_name in agent_def.tools]
151+
tools: list[Tool] = []
152+
registered_tools: set[str] = set(list_registered_tools())
153+
for tool_name in agent_def.tools:
154+
if tool_name not in registered_tools:
155+
raise ValueError(
156+
f"Tool '{tool_name}' not registered"
157+
f"but was given to agent {agent_def.name}."
158+
)
159+
tools.append(Tool(name=tool_name))
151160

152161
return Agent(
153162
llm=llm,
@@ -235,35 +244,6 @@ def register_plugin_agents(agents: list[AgentDefinition]) -> list[str]:
235244
return registered
236245

237246

238-
def register_builtins_agents() -> list[str]:
239-
"""Load and register SDK builtin agents from ``subagent/builtins/*.md``.
240-
241-
They are registered via ``register_agent_if_absent`` and will not
242-
overwrite agents already registered by programmatic calls, plugins,
243-
or project/user-level file-based definitions.
244-
245-
Returns:
246-
List of agent names that were actually registered.
247-
"""
248-
builtins_agents_def = load_agents_from_dir(BUILTINS_DIR)
249-
250-
registered: list[str] = []
251-
for agent_def in builtins_agents_def:
252-
factory = agent_definition_to_factory(agent_def)
253-
was_registered = register_agent_if_absent(
254-
name=agent_def.name,
255-
factory_func=factory,
256-
description=agent_def.description or f"Agent: {agent_def.name}",
257-
)
258-
if was_registered:
259-
registered.append(agent_def.name)
260-
logger.info(
261-
f"Registered file-based agent '{agent_def.name}'"
262-
+ (f" from {agent_def.source}" if agent_def.source else "")
263-
)
264-
return registered
265-
266-
267247
def get_agent_factory(name: str | None) -> AgentFactory:
268248
"""
269249
Get a registered agent factory by name.

openhands-tools/openhands/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from openhands.tools.preset.default import (
2424
get_default_agent,
2525
get_default_tools,
26+
register_builtins_agents,
2627
register_default_tools,
2728
)
2829
from openhands.tools.task_tracker import TaskTrackerTool
@@ -44,4 +45,5 @@
4445
"get_default_agent",
4546
"get_default_tools",
4647
"register_default_tools",
48+
"register_builtins_agents",
4749
]

openhands-tools/openhands/tools/preset/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
setups.
1919
"""
2020

21-
from .default import get_default_agent
21+
from .default import get_default_agent, register_builtins_agents
2222
from .gemini import get_gemini_agent, get_gemini_tools
2323
from .gpt5 import get_gpt5_agent
2424
from .planning import get_planning_agent
@@ -30,4 +30,5 @@
3030
"get_gemini_tools",
3131
"get_gpt5_agent",
3232
"get_planning_agent",
33+
"register_builtins_agents",
3334
]

openhands-tools/openhands/tools/preset/default.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""Default preset configuration for OpenHands agents."""
22

3-
from openhands.sdk import Agent
3+
from pathlib import Path
4+
5+
from openhands.sdk import Agent, agent_definition_to_factory, load_agents_from_dir
46
from openhands.sdk.context.condenser import (
57
LLMSummarizingCondenser,
68
)
79
from openhands.sdk.context.condenser.base import CondenserBase
810
from openhands.sdk.llm.llm import LLM
911
from openhands.sdk.logger import get_logger
12+
from openhands.sdk.subagent import register_agent_if_absent
1013
from openhands.sdk.tool import Tool
1114

1215

@@ -83,3 +86,52 @@ def get_default_agent(
8386
),
8487
)
8588
return agent
89+
90+
91+
def register_builtins_agents(cli_mode: bool = False) -> list[str]:
92+
"""Load and register builtin agents from ``subagent/*.md``.
93+
94+
They are registered via `register_agent_if_absent` and will not
95+
overwrite agents already registered by programmatic calls, plugins,
96+
or project/user-level file-based definitions.
97+
98+
Args:
99+
cli_mode: Whether to load the default agent in cli mode or not.
100+
101+
Returns:
102+
List of agents which were actually registered.
103+
"""
104+
register_default_tools(
105+
# Disable browser tools in CLI mode
106+
enable_browser=not cli_mode,
107+
)
108+
109+
subagent_dir = Path(__file__).parent / "subagents"
110+
builtins_agents_def = load_agents_from_dir(subagent_dir)
111+
112+
# if we are in cli mode, we filter out the default agent (with browser tool)
113+
# otherwise, we filter out the default cli agent
114+
if cli_mode:
115+
builtins_agents_def = [
116+
agent for agent in builtins_agents_def if agent.name != "default"
117+
]
118+
else:
119+
builtins_agents_def = [
120+
agent for agent in builtins_agents_def if agent.name != "default cli mode"
121+
]
122+
123+
registered: list[str] = []
124+
for agent_def in builtins_agents_def:
125+
factory = agent_definition_to_factory(agent_def)
126+
was_registered = register_agent_if_absent(
127+
name=agent_def.name,
128+
factory_func=factory,
129+
description=agent_def.description or f"Agent: {agent_def.name}",
130+
)
131+
if was_registered:
132+
registered.append(agent_def.name)
133+
logger.info(
134+
f"Registered file-based agent '{agent_def.name}'"
135+
+ (f" from {agent_def.source}" if agent_def.source else "")
136+
)
137+
return registered
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
name: bash
3+
model: inherit
4+
description: >-
5+
Command execution specialist (terminal only).
6+
<example>Run a shell command</example>
7+
<example>Execute a build or test script</example>
8+
<example>Check system information or process status</example>
9+
tools:
10+
- terminal
11+
---
12+
13+
You are a command-line execution specialist. Your sole interface is the
14+
terminal — use it to run shell commands on behalf of the caller.
15+
16+
## Core capabilities
17+
18+
- Execute arbitrary shell commands (bash/sh).
19+
- Run builds, tests, linters, formatters, and other development tooling.
20+
- Inspect system state: processes, disk usage, environment variables, network.
21+
- Perform git operations (commit, push, rebase, etc.).
22+
23+
## Guidelines
24+
25+
1. **Be precise.** Run exactly what was requested. Do not add extra flags or
26+
steps unless they are necessary for correctness.
27+
2. **Check before destroying.** For destructive operations (`rm -rf`, `git
28+
reset --hard`, `DROP TABLE`, etc.), confirm the intent and scope before
29+
executing.
30+
3. **Report results clearly.** After running a command, summarize the outcome —
31+
exit code, key output lines, and any errors.
32+
4. **Chain when appropriate.** Use `&&` to chain dependent commands so later
33+
steps only run if earlier ones succeed.
34+
5. **Avoid interactive commands.** Do not run commands that require interactive
35+
input (e.g., `vim`, `less`, `git rebase -i`). Use non-interactive
36+
alternatives instead.

openhands-sdk/openhands/sdk/subagent/builtins/default.md renamed to openhands-tools/openhands/tools/preset/subagents/default.md

File renamed without changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
name: default cli mode
3+
description: Default general-purpose agent
4+
tools:
5+
- terminal
6+
- file_editor
7+
- task_tracker
8+
---

0 commit comments

Comments
 (0)