diff --git a/openhands-sdk/openhands/sdk/subagent/AGENTS.md b/openhands-sdk/openhands/sdk/subagent/AGENTS.md
new file mode 100644
index 0000000000..165ef91c7b
--- /dev/null
+++ b/openhands-sdk/openhands/sdk/subagent/AGENTS.md
@@ -0,0 +1,166 @@
+# Subagent loader (file-based agents): design + invariants
+
+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.
+
+## Scope
+
+- **File-based agents**: Markdown files (`*.md`) with YAML frontmatter.
+- **Plugin agents**: `Plugin.agents` (already parsed by the plugin loader; registered here).
+- **Programmatic agents**: `register_agent(...)` (highest precedence, never overwritten).
+- **Built-in agents**: `subagent/builtins/*.md` (lowest precedence; used only as a fallback).
+
+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`: the **call order** that establishes precedence.
+
+## Invariant 1: discovery locations & file rules
+
+### Directories scanned
+
+**Project-level (higher priority than user-level):**
+
+1. `{project}/.agents/agents/*.md`
+2. `{project}/.openhands/agents/*.md`
+
+**User-level:**
+
+3. `~/.agents/agents/*.md`
+4. `~/.openhands/agents/*.md`
+
+Notes:
+
+- Only the **top-level** `*.md` files are scanned.
+ - Subdirectories (e.g. `{project}/.agents/skills/…`) are ignored.
+- `README.md` / `readme.md` is always skipped.
+- Directory iteration is deterministic (`sorted(dir.iterdir())`).
+
+### Parse failures must be non-fatal
+
+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`.)
+
+## Invariant 2: resolution / precedence (“who wins”)
+
+### Core rule: first registration wins
+
+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)
+
+### Effective precedence order
+
+When a `LocalConversation` becomes ready, it establishes the following priority:
+
+1. **Programmatic** `register_agent(...)` (pre-existing; must never be overwritten)
+2. **Plugin-provided** agents (`Plugin.agents` → `register_plugin_agents`)
+3. **Project** file-based agents
+ - `{project}/.agents/agents/*.md` then `{project}/.openhands/agents/*.md`
+4. **User** file-based agents
+ - `~/.agents/agents/*.md` then `~/.openhands/agents/*.md`
+5. **SDK built-ins** (`subagent/builtins/*.md`)
+
+This is the order implemented by:
+
+- `LocalConversation._ensure_plugins_loaded()` → registers plugin agents
+- `LocalConversation._register_file_based_agents()` → registers project/user file agents, then built-ins
+
+### Deduplication rules inside file-based loading
+
+File-based loading has *two* layers of “first wins” deduplication:
+
+1. **Within a level** (`load_project_agents` / `load_user_agents`):
+ - `.agents/agents` wins over `.openhands/agents` for the same agent name.
+2. **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/`.
+
+## Invariant 3: Markdown agent schema & semantics
+
+### Frontmatter keys
+
+Supported YAML frontmatter keys (see `AgentDefinition.load` in `schema.py`):
+
+- `name` (default: filename stem)
+- `description`
+- `tools` (default: `[]`)
+ - accepts either a string (`tools: ReadTool`) or a list
+- `model` (default: `inherit`)
+ - `inherit` means “use the parent agent’s LLM instance”
+ - any other string means “copy parent LLM and override the `model` field”
+- `color` (optional)
+
+**Unknown keys are preserved** in `AgentDefinition.metadata`.
+
+### Body → system prompt
+
+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 mapping
+
+`tools` values are stored as tool names (`list[str]`) and mapped at instantiation time to:
+
+- `Tool(name=tool_name)`
+
+No validation is performed at load time beyond “stringification”.
+
+### Trigger examples in description
+
+The loader extracts `…` tags from `description` (case-insensitive)
+into `AgentDefinition.when_to_use_examples`.
+
+These examples are used for triggering / routing logic elsewhere.
+
+### Minimal example
+
+```markdown
+---
+name: code-reviewer
+description: |
+ Reviews code changes.
+
+ please review this PR
+ can you do a security review?
+tools:
+ - ReadTool
+ - GrepTool
+model: inherit
+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-facing documentation
+
+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.
+
+- Docs PR tracking this feature: https://github.com/OpenHands/docs/pull/358