4. Subagents
"Break big tasks down; each subtask gets a clean context"
New to this?
What is a subagent?
A child agent spawned by the parent with a fresh, empty messages array. It does its work, returns a short summary, and its entire conversation history is discarded. The parent stays clean.
Why not just keep everything in one conversation?
Context is finite and expensive. If the parent asks 'what testing framework does this project use?', the child might read 5 files to find the answer. The parent only needs the one-line answer, not the 5 file contents.
Can subagents spawn their own subagents?
In this design, no. The child gets all base tools except the 'task' tool, preventing recursive spawning. This keeps the architecture simple and avoids runaway agent chains.
What happens to the subagent's work?
The subagent's side effects (files written, commands run) persist on disk. Only the conversation history is discarded. The parent gets a text summary of what was done.
The Problem
As the agent works, its messages array grows. Every file read, every bash output stays in context permanently. βWhat testing framework does this project use?β might require reading 5 files, but the parent only needs the answer: βpytest.β
The Solution
Parent agent Subagent
+------------------+ +------------------+
| messages=[...] | | messages=[] | <-- fresh
| | dispatch | |
| tool: task | ----------> | while tool_use: |
| prompt="..." | | call tools |
| | summary | append results |
| result = "..." | <---------- | return last text |
+------------------+ +------------------+
Parent context stays clean. Subagent context is discarded.
How It Works
- The parent gets a
tasktool. The child gets all base tools excepttask(no recursive spawning).
PARENT_TOOLS = CHILD_TOOLS + [
{"name": "task",
"description": "Spawn a subagent with fresh context.",
"input_schema": {
"type": "object",
"properties": {"prompt": {"type": "string"}},
"required": ["prompt"],
}},
]
- The subagent starts with
messages=[]and runs its own loop. Only the final text returns to the parent.
def run_subagent(prompt: str) -> str:
sub_messages = [{"role": "user", "content": prompt}]
for _ in range(30): # safety limit
response = client.messages.create(
model=MODEL, system=SUBAGENT_SYSTEM,
messages=sub_messages,
tools=CHILD_TOOLS, max_tokens=8000,
)
sub_messages.append({"role": "assistant",
"content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": str(output)[:50000]})
sub_messages.append({"role": "user", "content": results})
return "".join(
b.text for b in response.content if hasattr(b, "text")
) or "(no summary)"
The childβs entire message history (possibly 30+ tool calls) is discarded. The parent receives a one-paragraph summary as a normal tool_result.
What Changed From TodoWrite
| Component | Before (TodoWrite) | After (Subagents) |
|---|---|---|
| Tools | 5 | 5 (base) + task (parent) |
| Context | Single shared | Parent + child isolation |
| Subagent | None | run_subagent() function |
| Return value | N/A | Summary text only |
Key Takeaway
Context isolation is the key insight. The subagent pattern lets the parent delegate messy, exploratory work without polluting its own context. The parent thinks in high-level goals; the child handles the details. Side effects persist on disk, but conversation noise is discarded.
Interactive Code Walkthrough
1def run_subagent(prompt: str) -> str:2 sub_messages = [{"role": "user", "content": prompt}]3 for _ in range(30): # safety limit4 response = client.messages.create(5 model=MODEL, system=SUBAGENT_SYSTEM,6 messages=sub_messages,7 tools=CHILD_TOOLS, max_tokens=8000,8 )9 sub_messages.append({"role": "assistant",10 "content": response.content})11 if response.stop_reason != "tool_use":12 break13 results = []14 for block in response.content:15 if block.type == "tool_use":16 handler = TOOL_HANDLERS.get(block.name)17 output = handler(**block.input)18 results.append({"type": "tool_result",19 "tool_use_id": block.id,20 "content": str(output)[:50000]})21 sub_messages.append({"role": "user", "content": results})22 return "".join(23 b.text for b in response.content if hasattr(b, "text")24 ) or "(no summary)"25 The subagent's sub_messages history is discarded after it returns. But its side effects (files written, commands run) persist. Why is this asymmetry important?
Hint
The parent cares about what was done (results on disk), not how it was done (the 30-turn conversation). Keeping the history would bloat the parent's context.
Modify spawn_subagent to pass a custom system prompt. Try making a 'code reviewer' subagent that reads files and returns a structured review.
Hint
Add a system parameter and pass it to client.messages.create
Add resource tracking to subagents: count turns, tokens used, and tools called. Return this metadata alongside the summary. Use it to detect runaway subagents that take too many turns.
Hint
Return a dict with 'summary', 'turns', 'tokens', 'tools_called' keys. Set an alert threshold.