Skip to content

SubagentRunner: isolated context for subtasks

源码版本v4.0.10

Responsibilities

SubagentRunner is the executor Cline uses to encapsulate "running a subtask inside an LLM's own context window". When the main agent encounters a subagent tool call (new SubagentRunner:215), it spawns a runner per prompt. The runner runs a mini agent loop internally — pulling a stream, parsing tool_calls, executing, and pushing results back into the conversation — but uses a fully independent conversation array and its own ContextManager, so its message history never pollutes the main Task's.

It sits below the main Task and above the concrete tools. The main Task passes a prompt and a TaskConfig in; the runner uses SubagentBuilder to compute the subset of tools this subagent can use, its system prompt, and its API handler, and then loops in its own run. When the subagent finishes, it calls attempt_completion; the result string is returned verbatim to the main agent, which sees it as an ordinary tool result. This design lets the main Task confine expensive operations like "explore code, read several files, synthesize an answer" to an isolated window, so the intermediate tokens do not drag down the main context.

Design motivation

  • Independent context window: the subagent uses its own ClineStorageMessage[] array; the main Task never sees the subagent's intermediate tool calls (conversation init:463).
  • Tool allow-list: the subagent is read-only by default; SUBAGENT_DEFAULT_ALLOWED_TOOLS only includes file_read / list_files / search / list_code_def / bash / use_skill / attempt_completion (default allowed tools:14).
  • Force attempt_completion: no matter what the user configures, ATTEMPT is always added (force ATTEMPT:96); otherwise the subagent would have no way to finish.
  • Per-agent model override: AgentConfigLoader reads an individual agent's modelId, and SubagentBuilder.applyModelOverride swaps the apiHandler (applyModelOverride:57), so different subagents can use different models.
  • Auto context compaction: before each round, the runner checks the previous request's token count; if it exceeds the threshold, it calls compactConversationForContextWindow to do file read optimization first and then truncation (shouldCompactBeforeNextRequest:488).
  • Empty-response retry: if the model emits no tool_use in a round, it is treated as an empty response; the runner feeds a noToolsUsed nudge and retries. After more than MAX_EMPTY_ASSISTANT_RETRIES (3) attempts it fails outright (empty response retry:662).
  • Parallel runners, synchronized cancel: a single subagent tool call can carry multiple prompts; runners execute in parallel and the abort poll runs every 100ms (abort poll:216), interrupting all runners together.

Key files

Data flow

At startup the runner prepares the system prompt and the initial user content. The initial conversation holds only two entries: the user's prompt and a workspace metadata block; the latter is there for the server-side task loop validation:

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;
    // ...
  }
  // ...
}

This sits at conversation + while:463. Inside the loop, each round's createMessageWithInitialChunkRetry pulls the stream, and stream chunks are categorized into usage / text / tool_calls / reasoning. When the stream ends, finalized tool calls are executed one by one; a call that hits attempt_completion returns the result directly to the main agent (return on attempt:723). Other tools execute through coordinator.getHandler(toolName).execute, and the result is fed back as user content to keep the loop going.

Boundaries and failure modes

  • Tools outside the allow-list return toolError directly: if the subagent tries to call something like write_to_file, it is intercepted and the error string is pushed back as user content (whitelist check:726); the loop continues and does not abort the whole task.
  • attempt_completion without a result: when result is empty, a missingToolParameterError is pushed back instead of finishing (missing result:705).
  • First-chunk context window exceeded retry: if the first chunk fails with a context-window-related error, the conversation is compacted immediately and retried (context window retry:1036), up to MAX_INITIAL_STREAM_ATTEMPTS times.
  • Canceling a running command on abort: abort does not just cancel the API stream; it also delegates to cancelRunningCommandTool to stop any in-flight bash (cancel running command:281).
  • Native vs non-native tool call fallback: in non-native mode, when structured tool_calls chunks are received, they are still executed but the result is serialized as plain text and pushed back, avoiding tool_result pairing misalignment (non-native fallback:631).
  • Stats accumulation: every chunk updates inputTokens / outputTokens / cacheWrite / cacheRead / totalCost, and onProgress reports to the front-end in real time (stats accumulation:534).
  • Empty assistant still counts as a round: when assistant content is entirely empty, a "Failure: I did not provide a response." line is pushed before feeding the noToolsUsed nudge (empty assistant:671).

Summary

SubagentRunner lets the main agent outsource "large-scope exploration" to an isolated mini-agent. To see the context compaction strategy it depends on, read context-manager; to see how the main Task feeds tool results back and recurses, read agent-loop/task-class; to see how MCP tools are invoked by the main agent, read mcp-hub.

See official docs: Cline 文档 · README