T19 — Sub-agent delegation via Task tool
Parent agent spawn child session qua Task tool — agent con làm search/explore/plan với clean context riêng, trả lại compact summary. Parent không bị ô nhiễm bởi tool output của sub-task.
Tổng quan Tool Design
Tại sao quan trọng. Agent chính (parent) thường có context đã đầy một phần — history conversation, tool outputs, code context. Nếu dùng parent để explore thêm (search, read nhiều file) → context overflow nhanh. Sub-agent là "thread ephemeral": bắt đầu với clean context, tập trung làm 1 task cụ thể (search code, plan steps, verify assumption), sau đó trả lại compact summary (100–500 tokens). Parent chỉ thêm summary vào context — không phải toàn bộ tool output của sub-agent.
Agent type specialization: Task tool nhận
subagent_type: "general-purpose", "explore", hoặc "plan". Mỗi type có system prompt tối ưu cho use case — explore agent giỏi search/read, plan agent giỏi break down task thành steps.
Phân tích code chi tiết Anatomy
Task tool definition
tool/task.ts — TaskTool definition
TS
{`
export const TaskTool = Tool.define(
"task",
Effect.gen(function* () {
const ops = yield* Session.Ops
return {
description: TASK_DESCRIPTION,
parameters: z.object({
description: z.string().describe("3-5 word task summary"),
prompt: z.string().describe("Self-contained task for sub-agent"),
subagent_type: z.enum(["general-purpose", "explore", "plan"])
.default("general-purpose"),
}),
async execute(args, ctx) {
// Child session thừa hưởng permission rules nhưng CÓ restrictions
const childSession = yield* ops.createChildSession(ctx.sessionId, {
agent: args.subagent_type,
inheritPermissions: true,
// Deny task + todowrite để tránh recursion và scope creep
deniedTools: ["task", "todowrite"],
})
// Recursive: gọi lại ops.prompt() trên child session
const result = yield* ops.prompt(childSession.id, {
message: args.prompt,
description: args.description,
})
// Chỉ trả về SUMMARY — không toàn bộ conversation của child
return {
summary: result.finalMessage.text,
toolCalls: result.toolCalls.length,
tokensUsed: result.usage.total,
}
},
}
})
)
`}
Parent Agent (context: 40k tokens)
│
│ model: "I need to search for all usages of X"
│
▼
Task tool call:
{ description: "Search X usages",
prompt: "Find all files that import X and list them",
subagent_type: "explore" }
│
▼
Child Session (clean context: 0 tokens)
├── read file A.ts
├── grep pattern B
├── read file C.ts
└── ... (20+ tool calls, 15k tokens used)
│
▼ returns compact summary:
"Found 8 files importing X: src/a.ts, src/b.ts, ..."
(500 tokens — not 15k)
│
▼
Parent context += 500 tokens (not 15k)
← context contamination avoided!
Generic sub-agent pattern
Generic spawn sub-agent
TS
{`
async function spawnSubAgent(opts: {
prompt: string
type: "explore" | "plan" | "general-purpose"
parent: Session
}) {
const child = await createSession({
agent: opts.type,
system: getAgentSystem(opts.type),
permissions: inheritFrom(opts.parent).deny(["task", "todowrite"]),
parent: opts.parent.id,
})
const result = await runLoop(child, opts.prompt)
// ONLY return summary to parent — not full conversation
return {
summary: result.finalText,
cost: result.cost,
}
}
`}Tương tác với kỹ thuật khác Interaction
- T7 (Overflow detection): Sub-agent là giải pháp chủ động tránh overflow: thay vì parent accumulate context từ explore tools, delegate cho child có clean context.
- T8 (Tail-turn compaction): Khi parent context vẫn đầy dù dùng sub-agent, T8 compact history. Sub-agent + compaction là 2 defense layer bổ sung nhau.
- T24–T25 (Permission model): Child session inherit permission rules từ parent nhưng deny thêm một số tool (task, todowrite). Đảm bảo child không tự spawn sub-sub-agent (recursion) hoặc claim task của parent.
- T14 (Effect lazy tool init): TaskTool được define qua Effect —
Session.Opsservice được inject tự động tại define-time.
Failure modes Failure
1. Prompt không self-contained
Sub-agent không có context của parent — nếu prompt dùng pronoun như "the file we just discussed" hoặc "that variable", sub-agent không hiểu. Prompt phải self-contained hoàn toàn.
Fix: Tool description dạy model cung cấp đủ context trong
prompt — bao gồm file path, variable name, project context cụ thể.
2. Cost nhân đôi
Parent token để frame task + child token để làm task = 2x cost tổng. Với task nhỏ (search 1 file), cost overhead có thể vượt benefit. Nên dùng Task tool cho task thật sự cần nhiều tool calls.
3. Debug khó
Khi sub-agent trả về summary sai, parent không thấy được reasoning và tool calls của child. Cần logging cho child session để investigate production issues.
So sánh với các harness khác Compare
| Harness | Sub-agent support | Context isolation | Notes |
|---|---|---|---|
| opencode | Task tool → child session, deny task/todowrite | ✅ clean context | Recursive calls via ops.prompt() |
| Claude Code | Task tool tương tự | ✅ clean context | Pattern giống nhất |
| OpenHarness | Subprocess + mailbox IPC | ✅ process-level isolation | Mạnh hơn nhưng phức tạp hơn |
| Aider | Không có sub-agent native | ❌ | Single-agent only |
| Cline | Không có sub-agent native | ❌ | Single-agent only |
Implementation recipe Recipe
TS
{`
// Minimal sub-agent implementation
interface SubAgentResult {
summary: string
toolCalls: number
tokens: number
}
async function runSubAgent(opts: {
prompt: string
agentType?: "explore" | "plan" | "general"
parentPerms: PermissionRules
}): Promise<SubAgentResult> {
const childPerms = opts.parentPerms.withDenied(["spawn_agent", "create_todo"])
const session = new Session({
systemPrompt: getSystemForType(opts.agentType ?? "general"),
permissions: childPerms,
// Không có memory của parent
})
const loop = new AgentLoop(session)
const result = await loop.run(opts.prompt)
// Parent nhận summary ngắn gọn, không phải full conversation
return {
summary: result.lastAssistantMessage,
toolCalls: result.totalToolCalls,
tokens: result.tokensUsed,
}
}
// Tool definition trong parent
const TaskTool = defineTool({
name: "task",
description: "Delegate a specific sub-task to an agent with a clean context.",
schema: z.object({
description: z.string().describe("3-5 word description of the task"),
prompt: z.string().describe("Self-contained prompt for the sub-agent"),
}),
execute: async (args) => runSubAgent({ prompt: args.prompt, parentPerms: currentSession.perms }),
})
`}