# Sub-Agents

PyClaw's sub-agent system lets the main agent delegate self-contained tasks to
specialized personas with isolated context windows, dedicated models, and tool
whitelists. Each persona is a single Markdown file with YAML frontmatter; no
plugin install, no class authoring, no rebuild.

> **Status**: Sub-agent infrastructure shipped 2026-05-18. PyClaw bundles
> **zero** persona files (license-clean MIT distribution); users author or
> import their own personas.

## TL;DR — Five-Minute Setup

1. Drop a manifest at one of five discovery directories:

   ```
   <workspace>/agents/<name>.md          ← project-local, highest priority
   <workspace>/.agents/agents/<name>.md  ← project (cross-ecosystem standard)
   <workspace>/.claude/agents/<name>.md  ← project (Claude Code compat)
   ~/.openclaw/agents/<name>.md          ← managed (PyClaw user-shared)
   ~/.agents/agents/<name>.md            ← personal cross-ecosystem fallback
   ```

2. Author a minimal manifest:

   ```markdown
   ---
   name: oracle
   description: Deep reasoning specialist for architecture review.
   ---
   You are the Oracle. State the question, list constraints, reason
   step-by-step, then return findings to the main agent.
   ```

3. Restart PyClaw. The main agent now has a `sub_agent` tool. Invoke from
   chat: `please consult the oracle about X`.

4. Inspect what's installed:

   ```
   /agents list
   /agents info oracle
   /agents reload   # re-scan after dropping a new .md
   ```

## Why Sub-Agents

A single 770-line agent loop accumulates context across every tool call,
every memory hit, and every back-and-forth with the user. By turn 20+ of a
complex task, prompt-cache pollution and context-budget pressure degrade
reasoning quality. Sub-agents solve this with a **fresh isolated context per
delegation**:

- **Architecture / debugging specialist** ("oracle") spawned for hard
  problems, returns findings to main agent
- **Codebase researcher** ("explore") spawned in parallel to grep / read /
  understand, returns concise file paths + line numbers
- **External docs researcher** ("librarian") spawned to fetch / search /
  digest external library docs

The convergent industry pattern (Claude Code `Task`, OpenCode `task` +
`agent`, Roo Code custom modes, Gemini CLI `invoke_agent`) ships exactly this
shape: a Markdown file with YAML frontmatter defines the persona.

## Manifest Schema

PyClaw's `AgentManifest` is a strict subset of the cross-agent SKILL.md /
agent-frontmatter standard. Required fields:

- `name: str` — lowercase letters/digits/hyphens, 1–64 chars, **must equal**
  the filename without `.md`. Example: `oracle.md` → `name: oracle`.
- `description: str` — 1–1024 chars. Used by the main agent to decide
  whether to invoke. Be specific: not "a code reviewer", but "Reviews PRs
  for Python type-safety, security, and SOP compliance; cites file:line".

Optional fields:

- `model: str | null` — provider/model id (e.g., `anthropic/claude-opus-4-7`).
  Overrides `subagent.model_routing` in `pyclaw.json`.
- `allowed_tools: list[str]` — whitelist of tool names; if unset, sub-agent
  inherits all parent-accessible tools (minus the always-blocked memory
  tools — see below).
- `max_turns: int` — sub-agent loop iteration cap. Default 30.
- `timeout_seconds: float` — wall-clock timeout (hard ceiling 1800s).
  Default 300.
- `tools: list[str]` — alias for `allowed_tools` (Anthropic-style frontmatter).

The `extra = "ignore"` Pydantic config means cross-ecosystem fields like
`mode`, `disable-model-invocation`, `paths`, `hooks` parse silently. The
subset that PyClaw actually honors is the list above; everything else is
documented but not enforced.

### Example: Read-Only Code Reviewer

```markdown
---
name: code-reviewer
description: Reviews PR diffs for Python type-safety, security issues, and SOP violations. Cites file:line for every concern. Returns findings as a structured list.
model: anthropic/claude-opus-4-7
allowed_tools: [read, grep, glob]
max_turns: 8
timeout_seconds: 180
---
You are a senior Python code reviewer with deep expertise in:
- Type safety (no `Any`, no `# type: ignore` without justification)
- Security (no hardcoded secrets, no SSRF, no SQL injection)
- SOP compliance (matches team conventions in CLAUDE.md)

Output format:
- One finding per line: `<file>:<line> — <severity> — <description>`
- End with `OK to merge: yes/no`
```

## Inheritance Contracts

When the main agent calls `sub_agent(agent_name="oracle", task="...")`, the
executor builds a fresh `ToolContext` from the parent's, sharing seven fields
**by reference** with **zero override capability**:

| Field | Inherited | Why |
|---|---|---|
| `permission_tier` | YES (strict equality) | Sub-agents cannot escalate (read-only parent never spawns a yolo sub-agent) |
| `sandbox_policy` | YES (same instance) | srt / Seatbelt / bwrap+seccomp from Sprint 3 wraps every `bash` call |
| `workspace_path` | YES | Sub-agent reads/writes inside the same workspace boundary |
| `abort` (asyncio.Event) | YES (same instance) | Parent abort propagates to sub-agent loop within ~2s |
| `user_id`, `role`, `user_profile` | YES | Audit trail unbroken |
| `subagent_depth` | INCREMENTED | Used by depth-1 enforcement gate |
| `session_id` | REPLACED | `<parent>#sub:<uuid7>` for audit log correlation |

**Memory inheritance is read-only**: 4-layer memory search results inject
into the sub-agent's *system prompt* as `<memory>` XML, but the three memory
write tools (`memorize`, `forget`, `update_working_memory`) are blocked at
the executor layer regardless of `manifest.allowed_tools`. Even a manifest
declaring `allowed_tools: [memorize, ...]` cannot bypass this — sub-agents
return findings to the main agent, which decides what to memorize.

## Permission Tier — Strict, No Escalation

Per `tool-approval-tiers` capability spec, sub-agents inherit the parent's
effective tier exactly:

- `read-only` parent → sub-agent runs `read-only`. Write-class tool calls
  inside the sub-agent loop auto-deny per Sprint 1 rules.
- `approval` parent → sub-agent runs `approval`. Tool calls inside the
  sub-agent loop flow through the same `ToolApprovalHook` instance, scoped
  to the sub-agent's `session_id`.
- `yolo` parent → sub-agent runs `yolo`.

The `SubAgentTool` itself is `tool_class="write"` because the parent cannot
classify what the sub-agent will transitively call. This means:

- Under `read-only` tier, `sub_agent` calls **auto-deny** without prompting.
  Read-only sessions cannot fan out to potentially-expensive sub-agent
  reasoning. (Override: add `sub_agent` removed from
  `tools_requiring_approval` is a no-op; the read-only auto-deny is
  enforced by the runner regardless of channel config.)
- Under `approval` tier, `sub_agent` evaluates against the channel's
  `tools_requiring_approval` list. Operators may add `"sub_agent"` to that
  list to gate spawn behind user approval.

See [permissions guide](./permissions.md) for the full tier model and how
sub-agent calls slot into Sprint 3's per-call effective-tier resolution.

## Sandbox Sharing

Sprint 3's `SrtPolicy` (macOS Seatbelt / Linux bwrap+seccomp) wraps every
`bash` invocation inside a sub-agent loop with **zero new code** because
`sandbox_policy` is shared by reference. If the parent agent runs with
`sandbox.policy = "srt"`, every sub-agent's `bash` invocation goes through
the same `srt` wrapper. See [sandbox guide](./sandbox.md).

If `production_require_sandbox = true` in `pyclaw.json`, `NoSandboxPolicy`
refuses to load — meaning sub-agents inherit the same hard floor as the main
agent.

## Configuration

Top-level `subagent` block in `pyclaw.json` (all fields optional):

```json
{
  "subagent": {
    "enabled": true,
    "workspaceSubagentsDir": "agents",
    "projectSubagentsDir": ".agents/agents",
    "projectClaudeSubagentsDir": ".claude/agents",
    "personalSubagentsDir": "~/.agents/agents",
    "managedSubagentsDir": "~/.openclaw/agents",
    "modelRouting": {
      "oracle": "anthropic/claude-opus-4-7",
      "explore": "anthropic/claude-haiku-4-5"
    },
    "defaultMaxTurns": 30,
    "defaultTimeoutSeconds": 300,
    "maxDepth": 1
  }
}
```

The `model_routing` map lets you assign per-persona models. Resolution order:
`manifest.model > settings.modelRouting[name] > settings.agent.defaultModel`.
Use cheaper models (Haiku, Llama) for high-frequency simple personas and
reserve Opus for the oracle.

`maxDepth = 1` means sub-agents cannot themselves spawn sub-agents. V1 is
strict by default; raise it in a future change after observing real
recursive use cases.

## Operations: `/agents` Slash Command

```
/agents list           # per-source-label persona table with effective model
/agents info oracle    # full manifest dump + resolved model + paths
/agents reload         # rescan all 5 discovery directories (idle-guarded)
```

`/agents reload` rejects when a sub-agent invocation is in flight to avoid
in-flight registry mutation.

## License Note (Critical)

PyClaw is MIT-licensed. PyClaw bundles **zero persona files** in any
distribution. Where do personas come from?

1. **Author your own** — most common, fastest path.
2. **Import from MIT-licensed sources** — `obra/superpowers` is a curated
   collection of MIT-licensed agent skills/personas; drop the relevant
   `.md` files into `~/.openclaw/agents/`.
3. **Hand-write following individual non-commercial allowances** — some
   source-available agent collections (e.g., OhMyOpenAgent under Sustainable
   Use License v1.0) permit individual non-commercial use; you may
   hand-write personas inspired by their structure but cannot redistribute
   their files.

The driving constraint: OhMyOpenAgent's Sustainable Use License v1.0
prohibits derivative-to-compete distribution. PyClaw's "infrastructure
only, zero bundled personas" stance keeps PyClaw cleanly MIT-shippable.

## Forward-Compat Path (A → B)

This release ships option A' (file-based discovery + `register(source=...)`
forward-compat tax). A future change `add-plugin-protocol` (option B)
extends to a unified plugin system by:

1. Adding a sixth discovery source `("plugin:<name>", iter)`.
2. Plugin loaders calling `registry.register(name, manifest, source=f"plugin:{name}")`.

The 5-LOC tax in this change (public `register()` API + `source` parameter
+ `subagent_depth` field) buys zero-rework option B when the plugin trigger
fires (≥3 third-party plugin requests OR PyClaw user count ≥ 10k). See
[ROADMAP](../../DailyWork/planning/ROADMAP.md) (private).

## Troubleshooting

**Manifest doesn't show up in `/agents list`**:

- Check filename matches frontmatter `name` (case-sensitive). `oracle.md`
  must have `name: oracle`, not `Oracle` or `oracles`.
- Check the file ends with `.md`.
- Check it's at the directory's top level, not nested in a subdirectory.
- Run `/agents reload` (or restart PyClaw).
- Inspect logs for `subagent.discovery.parse_error` warnings.

**Sub-agent timeout truncates output**: 

The result is prefixed with `[Sub-agent timed out at <duration>s with <turns>
turns]\n` so the main agent sees the truncation. Audit log marks
`outcome: "timeout"`. Either raise `manifest.timeout_seconds` or simplify
the task.

**Sub-agent calls `memorize` and gets rejected**:

This is by design — see "Memory inheritance is read-only" above. Have the
sub-agent return findings; the main agent should call `memorize`.

**Sub-agent under `read-only` parent can't `bash`**:

This is by design. Read-only tier auto-denies write-class tool calls
regardless of which agent invokes them. To allow writes, the parent must
run under `approval` or `yolo`.

**Cross-ecosystem field silently ignored** (`disable-model-invocation`,
`mode`, `paths`, `hooks`):

PyClaw's `extra = "ignore"` Pydantic config drops unknown fields. The
honor list is documented above. If you need PyClaw to honor a new field,
file an issue — implementation is usually a one-line addition to
`AgentManifest`.

## Audit Log Schema

Every sub-agent spawn emits a JSON line on the `pyclaw.audit.subagent`
logger:

```json
{
  "event": "subagent_invocation",
  "event_type": "subagent_spawn",
  "ts": "2026-05-18T19:30:45Z",
  "subagent_name": "oracle",
  "subagent_source": "managed",
  "parent_session_id": "ses_abc",
  "subagent_depth": 1,
  "outcome": "success",
  "turns": 12,
  "tokens_input": 5432,
  "tokens_output": 1234,
  "duration_seconds": 23.4,
  "model_used": "anthropic/claude-opus-4-7",
  "tier_inherited": "approval"
}
```

Tool calls inside the sub-agent loop carry the four enrichment fields
(`subagent_name`, `subagent_source`, `parent_session_id`, `subagent_depth`)
on the existing `pyclaw.audit.tool_approval` channel, alongside Sprint 1/3
fields (`tier`, `decision`, `tier_source`, `forced_server`, `user_id`,
`role`, `sandbox_backend`).

`outcome` enumerates: `success`, `timeout`, `error`, `depth_exceeded`,
`tool_whitelist_violation`, `aborted`, `max_turns_exceeded`.

## See Also

- [permissions guide](./permissions.md) — how tier inheritance interacts with
  sub-agent invocation
- [sandbox guide](./sandbox.md) — how `srt` floor applies to sub-agent
  `bash` calls
- [skill-hub compatibility](./skill-hub-compatibility.md) — relationship
  between sub-agent personas and skills (different concerns: skills bundle
  knowledge + tool requirements; sub-agents bundle persona + isolation)
- [中文版](../zh/subagents.md)
