← opencode report

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.
Nhóm: C — Tool DesignFile: tool/task.ts · Lines 69–145ID: C.7 / T19Status: Stable

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.Ops service đượ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

HarnessSub-agent supportContext isolationNotes
opencodeTask tool → child session, deny task/todowrite✅ clean contextRecursive calls via ops.prompt()
Claude CodeTask tool tương tự✅ clean contextPattern giống nhất
OpenHarnessSubprocess + mailbox IPC✅ process-level isolationMạnh hơn nhưng phức tạp hơn
AiderKhông có sub-agent nativeSingle-agent only
ClineKhông có sub-agent nativeSingle-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 }),
})
`}

Tham khảo Refs