Sub-agents are background instances of the agent loop that handle independent tasks. The parent agent spawns them, continues its own work, and receives results when they complete.
New to sub-agents? Start with the practical guide for hands-on patterns and examples. This page covers the technical internals and the supervision model.
Why Sub-agents?¶
One context window isn't always enough. When the agent needs to: - Research something without losing its current context - Run a long task while continuing to respond to you - Parallelize independent work items
It spawns sub-agents. Each one gets its own context, its own tool set, and its own session.
Spawning¶
The agent uses the spawn tool:
spawn(
task="Search the web for Flask, FastAPI, and Django. For each, find year created, async support, ORM included. Write a comparison table to ~/workspace/frameworks.md.",
label="framework-research",
expected_duration=30,
timeout=120
)
Parameters:
task— The complete instructions for the sub-agent. This is all it receives — it cannot see the parent's conversation. Be specific: exact actions, file paths, output format, success criteria.label— Optional short name for display in status checks and notifications.expected_duration— Estimated seconds to complete. The system uses this to nudge the supervisor if the sub-agent is overdue. Not a hard limit.timeout— Hard maximum seconds. After this, the sub-agent is automatically cancelled.tools— Optional list of extra tools beyond the core set. Core tools (always available):read_file,write_file,edit_file,list_dir,exec. Extras you can request:web_search,web_fetch,kb,tool_search,browser,spawn. Omit for core-only (saves ~15k tokens in tool schema overhead).
What Sub-agents Get¶
Each sub-agent receives an isolated environment:
Always included (core tools):
- System prompt (identity, bootstrap files, efficiency guidance)
- Core tools: read_file, write_file, edit_file, list_dir, exec
Available on request (via tools param):
- web_search, web_fetch — web access
- kb — knowledge base access
- tool_search — dynamic tool discovery
- browser — Playwright automation (requires playwright)
- spawn — nest further sub-agents (only below max depth)
Never included:
- cron tool (scheduling is the parent's concern)
- Parent's conversation history or tool registry
Depth Tracking¶
Sub-agents can spawn their own sub-agents, up to a configurable maximum depth:
Parent (depth 0) → Sub-agent (depth 1) → Sub-agent (depth 2, max)
At max depth (default 2), the spawn tool is not registered. The sub-agent's system prompt tells it to complete work directly rather than delegating.
Configure the limit:
{
"agents": {
"defaults": {
"maxSubagentDepth": 2
}
}
}
Context Anchoring¶
When a sub-agent is spawned, a TODO.md entry is created:
- [ ] [subagent:abc123] asyncio-research — awaiting result (spawned 2026-03-03T10:15:30Z)
When the sub-agent completes or fails, the entry is updated:
- [x] [subagent:abc123] asyncio-research — completed: Found 5 key patterns, stored in domains/python-asyncio.md
This anchoring means the parent agent remembers what it delegated even after context compression. TODO.md is injected every turn, so spawned task references survive indefinitely.
Supervision Model¶
The supervision model determines how the parent monitors, waits for, and intervenes in sub-agent work. The design principles are:
- The supervisor stays informed, not blind. The parent can see what sub-agents are doing (tool calls, files written, iteration progress), not just whether they're done.
- Don't burn tokens waiting. Blocking waits cost zero tokens. Status checks should be deliberate and informed, not tight polling loops.
- Don't let the parent walk away from unfinished work. A stop hook prevents the ReAct loop from exiting while sub-agents are still running.
- The LLM decides what to do. The system provides information and nudges; the LLM decides whether to wait, check, intervene, or move on.
Result Delivery¶
Results reach the parent through two paths depending on timing:
Within a process_direct() call (in-loop delivery):
Sub-agent completions are pushed to an in-memory message queue on the SubagentManager. At the top of each ReAct loop iteration, the loop drains the queue and injects results as messages. The parent sees them naturally as part of its ongoing work, without calling any status tool.
Iteration 5: parent calls write_file(...)
← sub-agent "rust-research" completes, pushes to queue
Iteration 6: loop drains queue, injects result as message
→ LLM sees: [Sub-agent "rust-research" completed] ...
→ LLM integrates the result into its work
Between process_direct() calls (cross-session delivery):
Results are persisted to the sub-agent registry (~/.picobot/data/subagents/registry.json). On the next process_direct() call, pending results are prepended to the user message. The LLM sees them as natural conversation context.
In interactive mode, a console notification appears immediately so the user knows results have arrived.
Stop Hook: Preventing Premature Exit¶
The ReAct loop exits when the LLM responds without tool calls. Without a stop hook, the parent can spawn sub-agents and then just... respond, abandoning unfinished work.
The stop hook intercepts the loop exit point. Before breaking out of the loop, it checks for running sub-agents. If any exist, it injects a system message:
[System: You have 2 running sub-agents:
- rust-research (spawned 45s ago, expected ~60s, iteration 3/15)
- go-research (spawned 45s ago, expected ~60s, iteration 5/15)
Your task requires these results. Use wait_for_subagents to collect them,
or check_subagents to inspect progress before deciding.]
The loop continues instead of breaking. The LLM then decides:
- Wait: Call
wait_for_subagents(timeout=60)to block until results arrive. - Check: Call
check_subagentsto inspect progress and decide. - Move on: Respond without results (if the task doesn't actually need them).
The stop hook fires at most a few times — it's a nudge, not a loop. If the LLM responds without tool calls again after seeing the nudge, the loop exits. The LLM made a deliberate choice.
Waiting for Results¶
wait_for_subagents is the primary waiting mechanism. It blocks (via asyncio.wait) until sub-agents complete, costing zero tokens while waiting.
wait_for_subagents(ids=["abc123"], timeout=60)
The timeout parameter is critical — it prevents the supervisor from getting stuck if a sub-agent hangs. When the timeout fires, the parent gets back control with a note about which sub-agents are still running. It can then check their status and decide whether to wait longer, intervene, or proceed with partial results.
Without ids, it waits for all running sub-agents. With specific ids, it waits for just those.
Checking Status: Informed Supervision¶
check_subagents provides visibility into what sub-agents are actually doing, not just whether they're done.
List all:
check_subagents(action="list")
Returns all sub-agents with status, iteration progress, and timing.
Inspect one:
check_subagents(action="check", id="abc123")
Returns detailed live state:
ID: abc123
Label: rust-research
Status: running (iteration 7/15)
Spawned: 45s ago
Last activity: 3s ago
Expected duration: 60s (overdue by 0s)
Tool calls:
1. web_search("Rust memory safety") → 3 results
2. web_fetch("https://doc.rust-lang.org/...") → 4200 chars
3. web_search("Rust borrow checker") → 5 results
4. write_file("~/workspace/rust_notes.md") → ok
5. web_search("Rust concurrency") → 4 results
6. web_fetch("https://blog.rust-lang.org/...") → 3100 chars
7. write_file("~/workspace/rust_notes.md") → ok (appended)
Files written: ~/workspace/rust_notes.md
This visibility enables informed decisions:
- Making progress: "It's on iteration 7/15 and has written output. Let it finish."
- Stuck: "It called web_search with the same query 3 times. Something's wrong."
- Partial results available: "It's already written rust_notes.md. I can read that now even before it finishes."
- Going off the rails: "It's fetching URLs that have nothing to do with the task. I should intervene."
Cancel a stuck sub-agent:
check_subagents(action="cancel", id="abc123")
Cancels the asyncio task, marks the registry entry as failed, updates the TODO anchor. Use this when a status check reveals the sub-agent is stuck or off-track.
Status checks are deliberate, not polling. The supervisor checks status when it has reason to — after a wait timeout, after the stop hook nudge, or when it suspects something is wrong. Not in a tight loop.
Supervision Patterns¶
Pattern 1: Fire and wait (simple tasks)
spawn(task="...", expected_duration=30, timeout=120)
[do own work]
wait_for_subagents(timeout=60)
[integrate results]
Best for: predictable tasks where you trust the sub-agent to complete.
Pattern 2: Fire, work, check, wait (complex tasks)
spawn(task="...", expected_duration=60, timeout=180)
[do own work]
check_subagents(action="check", id="...") → inspect progress
wait_for_subagents(timeout=60)
[integrate results]
Best for: longer tasks where you want to verify progress before committing to a wait.
Pattern 3: Parallel with staggered collection
spawn(task="research A...", label="a", expected_duration=30)
spawn(task="research B...", label="b", expected_duration=45)
spawn(task="research C...", label="c", expected_duration=60)
[do own work]
wait_for_subagents(timeout=90) → blocks until all finish or timeout
[integrate all results]
Best for: parallel independent research where you need all results before proceeding.
Pattern 4: Supervised with intervention (unreliable tasks)
spawn(task="...", expected_duration=30, timeout=120)
[do own work]
→ stop hook nudge: "sub-agent overdue"
check_subagents(action="check", id="...")
→ see it's stuck (same tool called 3x)
→ read partial output file it already wrote
→ decide to use partial results and move on
Best for: tasks where the sub-agent might struggle and you need to be ready to step in.
Overdue Nudges¶
When expected_duration elapses and the sub-agent hasn't completed, a nudge is injected into the parent's message queue:
[System: Sub-agent "rust-research" (abc123) was expected to complete
in ~60s but is still running (now 75s, iteration 10/15).
Check its status or continue waiting.]
This is informational, not an error. The sub-agent keeps running. The parent decides whether to check, wait, or intervene. The nudge only fires once per sub-agent.
Token Efficiency¶
| Action | Token cost | When to use |
|---|---|---|
| In-loop message delivery | Zero (piggybacked on existing iterations) | Automatic — results injected when available |
wait_for_subagents |
Zero while waiting, one turn to process results | When parent is idle and needs results |
check_subagents (list) |
One LLM round-trip | Quick overview of all sub-agents |
check_subagents (check) |
One LLM round-trip | Detailed inspection before deciding to wait or intervene |
| Stop hook nudge | One additional LLM turn | Automatic — prevents premature exit |
| Overdue nudge | One additional message injection | Automatic — fires once at expected_duration |
Anti-pattern: tight polling. Calling check_subagents in a loop with sleep 5 between checks burns a full LLM round-trip per check (entire conversation context re-sent each time). For a 60-second wait with 5-second polls, that's 12 API calls with growing context. Use wait_for_subagents instead.
Future: Sub-supervisor Delegation¶
Backlog item. For complex multi-agent workflows, the parent could delegate supervision itself to a lightweight sub-supervisor with a minimal context window. The sub-supervisor would receive only: the task description, what to watch for, and the sub-agent IDs. It would monitor progress, handle stuck detection, and report back a summary. This avoids loading the parent's full conversation history for every status check. Not yet implemented — flagged for evaluation if token costs become a bottleneck in deep orchestration scenarios.
Internals¶
Registry¶
The sub-agent registry (~/.picobot/data/subagents/registry.json) persists state across process restarts:
- Task ID, label, task description
- Parent session key (for delivery routing)
- Status (running, completed, failed, timed_out)
- Depth (for display and recursion tracking)
- Spawned/completed timestamps
- Result or error message
- Delivery flag (has the parent seen this result?)
Message Queue¶
The in-memory message queue on SubagentManager handles intra-loop delivery:
- Sub-agent completion callback appends to the queue
- Overdue timer appends nudge messages to the queue
- ReAct loop drains the queue at the top of each iteration
- Queue is separate from the durable registry (registry handles cross-session; queue handles in-loop)
Tool Call Log (Live)¶
Each sub-agent's loop publishes tool call entries to a shared structure on the SubagentManager, keyed by task ID. Entries include tool name, arguments (truncated), result summary, and timestamp. This powers the enriched check_subagents output.
Token Tracking¶
Sub-agent token usage is accumulated in SubagentManager._subagent_tokens from each provider.chat() response. This total is added to the supervisor's AgentResult.total_tokens, giving callers (eval runner, CLI) a true picture of total cost including sub-agent work. The AgentResult.subagent_tokens field exposes the sub-agent portion separately.
Note: each ReAct iteration in the sub-agent resends the full message history (same quadratic cost pattern as the supervisor). Minimizing sub-agent iterations is the highest-leverage optimization. The efficiency preamble instructs sub-agents to combine operations into single shell commands.
Best Practices¶
- Write specific task descriptions. The sub-agent has no context from your conversation. Include exact file paths, expected formats, and success criteria. See the practical guide for examples.
- Set realistic
expected_duration. A web research task might take 30-60s. A file monitoring task with 3x 5-second checks takes at least 15-20s plus API latency. Over-optimistic estimates cause unnecessary nudges. - Use labels. A descriptive label makes status output readable and helps track what each sub-agent is doing.
- Keep tasks independent. Sub-agents can't communicate with each other — design tasks that don't need coordination.
- Use KB for large results. Have sub-agents store findings in the KB rather than returning long text. The parent can read the KB entry.
- Don't over-delegate. Sub-agents add latency (full agent loop per task). Use them for genuinely independent work, not simple tool calls.
- Watch depth. If you need more than 2 levels of sub-agents, the task decomposition probably needs rethinking.
- Check before you wait (when uncertain). If you're not sure the sub-agent is on track, call
check_subagentsto inspect its tool call log before committing to a blocking wait.