See the project root AGENTS.md for repository-wide policies and workflows.
This package (openhands.sdk.subagent) centralizes subagent discovery and registration.
It exists so that contributors (human or agentic) can answer:
- “Where did this agent come from?”
- “Why did this definition win over the other one?”
without reverse-engineering LocalConversation and the loader.
- File-based agents: Markdown files (
*.md) with YAML frontmatter. - Plugin agents:
Plugin.agents(already parsed by the plugin loader; registered here). - Programmatic agents: explicit
register_agent(...)calls. - Built-in agents: supplied by
openhands-tools, outside this SDK package.
Relevant implementation files:
load.py: filesystem discovery + parse-error handling.schema.py: Markdown/YAML schema and parsing rules.registry.py: registry API + “first registration wins” semantics.conversation/impl/local_conversation.py: lazy plugin and file-agent registration.openhands-tools/openhands/tools/preset/default.py: built-in agent discovery and registration.
Project-level (higher priority than user-level):
{project}/.agents/agents/*.md{project}/.openhands/agents/*.md
User-level:
~/.agents/agents/*.md~/.openhands/agents/*.md
Notes:
- Only the top-level
*.mdfiles are scanned.- Subdirectories (e.g.
{project}/.agents/skills/…) are ignored.
- Subdirectories (e.g.
README.md/readme.mdis always skipped.- Directory iteration is deterministic (
sorted(dir.iterdir())).
If a single file fails to parse (invalid YAML frontmatter, malformed Markdown, etc.), loading must:
- log a warning (with stack trace), and
- continue scanning other files.
(See load_agents_from_dir in load.py.)
Once an agent name is registered in the global registry (_agent_factories), later
sources must not overwrite it.
This is enforced by using:
register_agent(...)(raises on duplicates; used for programmatic registration)register_agent_if_absent(...)(skips duplicates; used for plugins, file agents, builtins)
LocalConversation._ensure_agent_ready() establishes this order for agents loaded
as part of conversation initialization:
- Existing registry entries, including explicit
register_agent(...)calls - Plugin-provided agents (
Plugin.agents→register_plugin_agents) - Project file-based agents
{project}/.agents/agents/*.mdthen{project}/.openhands/agents/*.md
- User file-based agents
~/.agents/agents/*.mdthen~/.openhands/agents/*.md
Built-ins are discovered and registered separately by openhands-tools through
register_builtins_agents(). Because all non-programmatic sources use
register_agent_if_absent(...), whichever source registers a name first keeps it.
Call built-in registration after higher-priority sources if built-ins should act as
fallbacks. The agent-server registers built-ins during tool-router import, before
per-conversation file discovery.
File-based loading has two layers of “first wins” deduplication:
- Within a level (
load_project_agents/load_user_agents):.agents/agentswins over.openhands/agentsfor the same agent name.
- Across levels (
register_file_agents):- project wins over user for the same agent name.
If you change these rules, update the unit tests in tests/sdk/subagent/.
Supported YAML frontmatter keys (see AgentDefinition.load in schema.py):
name(default: filename stem)descriptiontools(default:[]): one tool name or a list of namesskills(default:[]): a comma-separated string or a list of skill namesmodel(default:inherit):inheritreuses the parent LLM; another value is loaded as an LLM profile name fromprofile_store_diror the default profile storecolor(optional)max_iteration_per_run(optional, positive integer)max_budget_per_run(optional, positive number in USD)hooks(optional hook configuration)profile_store_dir(optional custom LLM profile directory)mcp_config(optional MCP server map);mcp_serversis a deprecated aliaspermission_mode(optional):always_confirm,never_confirm, orconfirm_risky; omission inherits the parent confirmation policycondenser(optional): omission uses the default summarizing condenser;noneorfalsedisables condensation; a mapping configures a condenser
Unknown keys are preserved in AgentDefinition.metadata.
The Markdown body content becomes the agent’s system_prompt.
Currently, when the agent is instantiated, this is applied as:
AgentContext(system_message_suffix=agent_def.system_prompt)
meaning it is appended to the parent system message (not a complete replacement).
tools values remain names until factory instantiation. Each name must already be
registered; unknown tools raise ValueError. Valid names become Tool(name=...).
skills resolve when the factory is created. Project skills take priority over user
skills, public skills are excluded, and an unknown skill raises ValueError.
The loader extracts <example>…</example> tags from description (case-insensitive)
into AgentDefinition.when_to_use_examples.
These examples are used for triggering / routing logic elsewhere.
---
name: code-reviewer
description: |
Reviews code changes.
<example>please review this PR</example>
<example>can you do a security review?</example>
tools:
- terminal
model: inherit
permission_mode: confirm_risky
color: purple
# Any extra keys are preserved in `metadata`:
audience: maintainers
---
You are a meticulous code reviewer.
Focus on correctness, security, and clear reasoning.User docs for Markdown agents live in the docs repo. If you change any of the invariants above, update both this file and the user docs.
- Published guide: https://docs.openhands.dev/sdk/guides/agent-file-based