Skip to content

Commit c4e31b5

Browse files
docs(subagent): document loader invariants for file-based agents (#2231)
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 70b8a36 commit c4e31b5

File tree

1 file changed

+166
-0
lines changed
  • openhands-sdk/openhands/sdk/subagent

1 file changed

+166
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Subagent loader (file-based agents): design + invariants
2+
3+
This package (`openhands.sdk.subagent`) centralizes **subagent discovery** and **registration**.
4+
It exists so that contributors (human or agentic) can answer:
5+
6+
- “Where did this agent come from?”
7+
- “Why did this definition win over the other one?”
8+
9+
without reverse-engineering `LocalConversation` and the loader.
10+
11+
## Scope
12+
13+
- **File-based agents**: Markdown files (`*.md`) with YAML frontmatter.
14+
- **Plugin agents**: `Plugin.agents` (already parsed by the plugin loader; registered here).
15+
- **Programmatic agents**: `register_agent(...)` (highest precedence, never overwritten).
16+
- **Built-in agents**: `subagent/builtins/*.md` (lowest precedence; used only as a fallback).
17+
18+
Relevant implementation files:
19+
20+
- `load.py`: filesystem discovery + parse-error handling.
21+
- `schema.py`: Markdown/YAML schema and parsing rules.
22+
- `registry.py`: registry API + “first registration wins” semantics.
23+
- `conversation/impl/local_conversation.py`: the **call order** that establishes precedence.
24+
25+
## Invariant 1: discovery locations & file rules
26+
27+
### Directories scanned
28+
29+
**Project-level (higher priority than user-level):**
30+
31+
1. `{project}/.agents/agents/*.md`
32+
2. `{project}/.openhands/agents/*.md`
33+
34+
**User-level:**
35+
36+
3. `~/.agents/agents/*.md`
37+
4. `~/.openhands/agents/*.md`
38+
39+
Notes:
40+
41+
- Only the **top-level** `*.md` files are scanned.
42+
- Subdirectories (e.g. `{project}/.agents/skills/…`) are ignored.
43+
- `README.md` / `readme.md` is always skipped.
44+
- Directory iteration is deterministic (`sorted(dir.iterdir())`).
45+
46+
### Parse failures must be non-fatal
47+
48+
If a single file fails to parse (invalid YAML frontmatter, malformed Markdown, etc.),
49+
loading must:
50+
51+
- log a warning (with stack trace), and
52+
- continue scanning other files.
53+
54+
(See `load_agents_from_dir` in `load.py`.)
55+
56+
## Invariant 2: resolution / precedence (“who wins”)
57+
58+
### Core rule: first registration wins
59+
60+
Once an agent name is registered in the global registry (`_agent_factories`), later
61+
sources must not overwrite it.
62+
63+
This is enforced by using:
64+
65+
- `register_agent(...)` (raises on duplicates; used for programmatic registration)
66+
- `register_agent_if_absent(...)` (skips duplicates; used for plugins, file agents, builtins)
67+
68+
### Effective precedence order
69+
70+
When a `LocalConversation` becomes ready, it establishes the following priority:
71+
72+
1. **Programmatic** `register_agent(...)` (pre-existing; must never be overwritten)
73+
2. **Plugin-provided** agents (`Plugin.agents``register_plugin_agents`)
74+
3. **Project** file-based agents
75+
- `{project}/.agents/agents/*.md` then `{project}/.openhands/agents/*.md`
76+
4. **User** file-based agents
77+
- `~/.agents/agents/*.md` then `~/.openhands/agents/*.md`
78+
5. **SDK built-ins** (`subagent/builtins/*.md`)
79+
80+
This is the order implemented by:
81+
82+
- `LocalConversation._ensure_plugins_loaded()` → registers plugin agents
83+
- `LocalConversation._register_file_based_agents()` → registers project/user file agents, then built-ins
84+
85+
### Deduplication rules inside file-based loading
86+
87+
File-based loading has *two* layers of “first wins” deduplication:
88+
89+
1. **Within a level** (`load_project_agents` / `load_user_agents`):
90+
- `.agents/agents` wins over `.openhands/agents` for the same agent name.
91+
2. **Across levels** (`register_file_agents`):
92+
- project wins over user for the same agent name.
93+
94+
If you change these rules, update the unit tests in `tests/sdk/subagent/`.
95+
96+
## Invariant 3: Markdown agent schema & semantics
97+
98+
### Frontmatter keys
99+
100+
Supported YAML frontmatter keys (see `AgentDefinition.load` in `schema.py`):
101+
102+
- `name` (default: filename stem)
103+
- `description`
104+
- `tools` (default: `[]`)
105+
- accepts either a string (`tools: ReadTool`) or a list
106+
- `model` (default: `inherit`)
107+
- `inherit` means “use the parent agent’s LLM instance”
108+
- any other string means “copy parent LLM and override the `model` field”
109+
- `color` (optional)
110+
111+
**Unknown keys are preserved** in `AgentDefinition.metadata`.
112+
113+
### Body → system prompt
114+
115+
The Markdown **body content** becomes the agent’s `system_prompt`.
116+
117+
Currently, when the agent is instantiated, this is applied as:
118+
119+
- `AgentContext(system_message_suffix=agent_def.system_prompt)`
120+
121+
meaning it is appended to the parent system message (not a complete replacement).
122+
123+
### Tools mapping
124+
125+
`tools` values are stored as tool names (`list[str]`) and mapped at instantiation time to:
126+
127+
- `Tool(name=tool_name)`
128+
129+
No validation is performed at load time beyond “stringification”.
130+
131+
### Trigger examples in description
132+
133+
The loader extracts `<example>…</example>` tags from `description` (case-insensitive)
134+
into `AgentDefinition.when_to_use_examples`.
135+
136+
These examples are used for triggering / routing logic elsewhere.
137+
138+
### Minimal example
139+
140+
```markdown
141+
---
142+
name: code-reviewer
143+
description: |
144+
Reviews code changes.
145+
146+
<example>please review this PR</example>
147+
<example>can you do a security review?</example>
148+
tools:
149+
- ReadTool
150+
- GrepTool
151+
model: inherit
152+
color: purple
153+
# Any extra keys are preserved in `metadata`:
154+
audience: maintainers
155+
---
156+
157+
You are a meticulous code reviewer.
158+
Focus on correctness, security, and clear reasoning.
159+
```
160+
161+
## User-facing documentation
162+
163+
User docs for Markdown agents live in the docs repo. If you change any of the
164+
invariants above, update both this file and the user docs.
165+
166+
- Docs PR tracking this feature: https://github.com/OpenHands/docs/pull/358

0 commit comments

Comments
 (0)