wooden-umbrella-84098
03/30/2026, 1:47 PMcalm-artist-7206
03/30/2026, 1:52 PMmodern-fireman-99755
03/31/2026, 1:47 PMwooden-umbrella-84098
03/31/2026, 2:14 PMmodern-fireman-99755
03/31/2026, 3:53 PMwooden-umbrella-84098
03/31/2026, 6:01 PMwooden-umbrella-84098
03/31/2026, 6:21 PMThey can be generic. There is no requirement for a specific .md file — but there's a catch: a "default" agent factory must be registered for it to work.
Here's how the spawn flow works:
When you call spawn with agent_types, each type is looked up in the registry via get_agent_factory(name). If you omit agent_types (or leave entries blank), it falls back to "default".
get_agent_factory("default") looks for a factory registered with the name "default". If nothing named "default" is registered, it throws a ValueError.
Nobody auto-registers a "default" factory — not the CLI, not the agent-server. It only exists if:
You programmatically call register_agent("default", ...), or
You have an agent definition .md file with name: default
So in practice, you need at least one agent definition (.md file or programmatic registration) for each agent type you want to delegate to, including "default" if you want generic subagents. The DelegateTool won't work with zero registered agents — get_factory_info() would return "No user-registered agents yet" in the tool description, and any spawn call would fail with "Unknown agent 'default'".
That said, the .md definition for a generic default agent is trivial — it's basically just a name:
---
name: default
description: "General-purpose assistant"
tools:
- terminal
- file_editor
- glob
- grep
---
No system prompt needed — the body can be empty and the subagent inherits the parent's model.wooden-umbrella-84098
03/31/2026, 8:54 PMmodern-fireman-99755
04/01/2026, 7:40 AMregister_builtins_agents() before calling the agent-server. The method will expose the builtin agents for the agent-server.
The example is working for software-agent-sdk branch: main
Do you have a specific use case in mind for using subagents with the agent-server?
Asking because I can see a use case where you spin up the agent-server and then use another language to ping it (or ping it directly). We would likely need a way to tell the agent-server to register the built-in subagents when starting a new conversation, or?
"""
Delegation with Agent Server Example
Minimal example demonstrating DelegateTool used via RemoteConversation
(agent server). The main agent delegates two tasks to sub-agents that
run in parallel on the server side.
"""
import os
import subprocess
import sys
import tempfile
import threading
import time
from pydantic import SecretStr
from openhands.sdk import (
LLM,
Agent,
AgentContext,
Conversation,
RemoteConversation,
Tool,
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 import register_builtins_agents
from openhands.tools.delegate import DelegateTool
logger = get_logger(__name__)
def _stream_output(stream, prefix, target_stream):
try:
for line in iter(stream.readline, ""):
if line:
target_stream.write(f"[{prefix}] {line}")
target_stream.flush()
except Exception:
pass
finally:
stream.close()
class ManagedAPIServer:
def __init__(self, port: int = 8000, host: str = "127.0.0.1"):
self.port = port
self.host = host
self.process = None
self.base_url = f"http://{host}:{port}"
def __enter__(self):
print(f"Starting agent server on {self.base_url}...")
self.process = subprocess.Popen(
[
"python", "-m", "openhands.agent_server",
"--port", str(self.port),
"--host", self.host,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env={"LOG_JSON": "true", **os.environ},
)
threading.Thread(
target=_stream_output,
args=(self.process.stdout, "SERVER", sys.stdout),
daemon=True,
).start()
threading.Thread(
target=_stream_output,
args=(self.process.stderr, "SERVER", sys.stderr),
daemon=True,
).start()
import httpx
for _ in range(30):
try:
if httpx.get(f"{self.base_url}/health", timeout=1.0).status_code == 200:
print(f"Agent server ready at {self.base_url}")
return self
except Exception:
pass
if self.process.poll() is not None:
raise RuntimeError("Server process terminated unexpectedly.")
time.sleep(1)
raise RuntimeError("Server failed to start after 30 seconds")
def __exit__(self, *args):
if self.process:
self.process.terminate()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
time.sleep(0.5)
print("Agent server stopped.")
api_key = os.getenv("LLM_API_KEY")
assert api_key, "LLM_API_KEY environment variable is not set."
llm = LLM(
model=os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929"),
api_key=SecretStr(api_key),
base_url=os.getenv("LLM_BASE_URL"),
usage_id="agent",
)
def create_lodging_planner(llm: LLM) -> Agent:
return Agent(
llm=llm,
tools=[],
agent_context=AgentContext(
skills=[
Skill(
name="lodging_planning",
content=(
"You specialize in finding great places to stay in London. "
"Provide 3 hotel recommendations with neighborhoods and "
"quick pros/cons."
),
trigger=None,
)
],
system_message_suffix="Focus only on London lodging recommendations.",
),
)
def create_activities_planner(llm: LLM) -> Agent:
return Agent(
llm=llm,
tools=[],
agent_context=AgentContext(
skills=[
Skill(
name="activities_planning",
content=(
"You design concise London itineraries. Suggest 2-3 daily "
"highlights grouped by proximity."
),
trigger=None,
)
],
system_message_suffix="Plan practical days in London.",
),
)
# Register sub-agents and tool (client-side; forwarded to server automatically)
register_agent(
name="lodging_planner",
factory_func=create_lodging_planner,
description="Finds London lodging options.",
)
register_agent(
name="activities_planner",
factory_func=create_activities_planner,
description="Creates London activity itineraries.",
)
register_builtins_agents()
# NOTE: DelegateTool auto-registers as "delegate" (its canonical name) when
# its module is imported. We must use that same name in Tool(name=...).
# Registering under a different name (e.g. "DelegateTool") would work locally
# but fail on the agent server, because the server re-imports the module which
# only registers the canonical name.
register_tool(DelegateTool.name, DelegateTool)
with ManagedAPIServer(port=8002) as server:
from openhands.sdk import Workspace
temp_dir = tempfile.mkdtemp(prefix="delegation_demo_")
workspace = Workspace(host=server.base_url, working_dir=temp_dir)
main_agent = Agent(
llm=llm,
tools=[Tool(name=DelegateTool.name)],
)
conversation = Conversation(
agent=main_agent,
workspace=workspace,
)
assert isinstance(conversation, RemoteConversation), (
"Expected RemoteConversation when workspace has a host"
)
print("=" * 70)
print("Sending delegation request to agent server...")
print("=" * 70)
conversation.send_message(
"Plan a short trip to London. Use the delegation tool to handle "
"lodging and activities in parallel via the two registered sub-agents. "
"Keep responses concise (a few bullet points each). "
"Once you have results from both, give me a short combined summary."
)
conversation.run()
cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost
print(f"\nEXAMPLE_COST: {cost}")
print("Done!")
conversation.close()wooden-umbrella-84098
04/02/2026, 3:01 PMwooden-umbrella-84098
04/02/2026, 3:01 PMwooden-umbrella-84098
04/02/2026, 3:01 PMmodern-fireman-99755
04/02/2026, 3:02 PM