Skip to content

SubagentRunner:子任务独立上下文

源码版本v4.0.10

职责

SubagentRunner 是 Cline 把「让 LLM 在自己的上下文窗口里跑一个子任务」这件事封装出来的执行器。当主 agent 遇到 subagent 这种工具调用 (new SubagentRunner:215),会为每条 prompt 起一个 runner,runner 内部跑一个迷你的 agent loop——拉流、解析 tool_calls、执行、把结果塞回对话——但用的是完全独立的 conversation 数组和独立的 ContextManager,跟主 Task 的消息历史互不污染。

它的位置在主 Task 之下、具体工具之上。主 Task 把 prompt 和 TaskConfig 传进来,runner 借助 SubagentBuilder 算出该子 agent 能用的工具子集、系统提示、API handler,然后在自己的 run 里循环。子 agent 跑完调 attempt_completion,结果字符串原样回给主 agent,主 agent 看到的就是一条普通工具结果。这种设计让主 Task 可以把「去探索代码、读若干文件、综合答案」这种大开销动作关进一个隔离的窗口里,不让它的中间 token 拖累主上下文。

设计动机

  • 独立上下文窗口:子 agent 用自己的 ClineStorageMessage[] 数组,主 Task 看不到子 agent 的中间 tool 调用 (conversation init:463)。
  • 工具白名单:子 agent 默认只读不写,SUBAGENT_DEFAULT_ALLOWED_TOOLS 只放 file_read / list_files / search / list_code_def / bash / use_skill / attempt_completion (default allowed tools:14)。
  • 强制 attempt_completion:无论用户配置什么工具集,ATTEMPT 永远被加进去 (force ATTEMPT:96),否则子 agent 无法收尾。
  • 按 agent 配置覆盖模型:AgentConfigLoader 读单个 agent 的 modelIdSubagentBuilder.applyModelOverride 替换 apiHandler (applyModelOverride:57),让不同子 agent 用不同模型。
  • 自动压缩上下文:runner 在每轮请求前检查上一次请求的 token 数,超阈值就调 compactConversationForContextWindow 先做 file read 优化、再走 truncation (shouldCompactBeforeNextRequest:488)。
  • 空响应重试:模型这一轮没出 tool_use 就当空响应,喂 noToolsUsed 提示再问,超过 MAX_EMPTY_ASSISTANT_RETRIES (3) 次直接失败 (empty response retry:662)。
  • 并行多 runner 同步取消:一条 subagent 工具调用可以带多条 prompt,多个 runner 并行跑,abort 轮询 100ms 一次 (abort poll:216),把所有 runner 一起打断。

关键文件

数据流

runner 启动时先准备系统提示和初始 user content。初始对话只放两条:用户的 prompt + workspace 元数据块,后者是给 server-side task loop 校验用的:

typescript
// apps/vscode/src/core/task/tools/subagent/SubagentRunner.ts
const conversation: ClineStorageMessage[] = [
  {
    role: "user",
    content: [
      { type: "text", text: prompt } as ClineTextContentBlock,
      // Server-side task loop checks require workspace metadata to be present in the
      // initial user message of subagent runs.
      ...(workspaceMetadataEnvironmentBlock
        ? [{ type: "text", text: workspaceMetadataEnvironmentBlock } as ClineTextContentBlock]
        : []),
    ],
  },
];

while (true) {
  if (
    usageState.lastRequest &&
    this.shouldCompactBeforeNextRequest(usageState.lastRequest.totalTokens, api, providerInfo.model.id)
  ) {
    const compactResult = this.compactConversationForContextWindow(
      contextManager,
      conversation,
      contextState.conversationHistoryDeletedRange,
    );
    contextState.conversationHistoryDeletedRange = compactResult.conversationHistoryDeletedRange;
    // ...
  }
  // ...
}

这段在 conversation + while:463。进入循环后,每轮 createMessageWithInitialChunkRetry 拉流,stream chunk 分类为 usage/text/tool_calls/reasoning。流结束把 finalized tool calls 一个个跑,命中 attempt_completion 的 call 直接把 result 回传给主 agent (return on attempt:723);其他工具通过 coordinator.getHandler(toolName).execute 执行,结果作为 user content 回灌继续循环。

边界与失败

  • 工具不在白名单直接 toolError:子 agent 想调 write_to_file 这种被拦下,错误字符串塞回 user content (whitelist check:726),循环继续,不打断整条任务。
  • attempt_completion 缺 result:result 为空时塞 missingToolParameterError 而非收尾 (missing result:705)。
  • 首 chunk context window exceeded 重试:首 chunk 失败且错误是 context window 相关,立即压缩 conversation 再重试 (context window retry:1036),最多 MAX_INITIAL_STREAM_ATTEMPTS 次。
  • abort 时正在跑的命令:abort 不只是 cancel API 流,还委托 cancelRunningCommandTool 把当前 bash 也停掉 (cancel running command:281)。
  • native vs non-native tool calls fallback:non-native 模式收到结构化 tool_calls chunk 时,仍执行但把结果序列化成纯文本塞回,避免 tool_result pairing 错位 (non-native fallback:631)。
  • stats 累计:每次 chunk 都更新 inputTokens/outputTokens/cacheWrite/cacheRead/totalCost,并通过 onProgress 实时给前端 (stats accumulation:534)。
  • empty assistant 也算一轮:assistant content 完全空时主动塞一句「Failure: I did not provide a response.」再喂 noToolsUsed (empty assistant:671)。

小结

SubagentRunner 让主 agent 能把「大范围探索」外包给一个隔离的 mini-agent。想看它依赖的上下文压缩策略可以读 context-manager;想看主 Task 如何把工具结果回流并递归可以读 agent-loop/task-class;想看 MCP 工具如何被主 agent 调用可以读 mcp-hub

对照官方资料:Cline 文档 · README