SubagentRunner: isolated context for subtasks
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_TOOLSonly 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,
ATTEMPTis always added (force ATTEMPT:96); otherwise the subagent would have no way to finish. - Per-agent model override:
AgentConfigLoaderreads an individual agent'smodelId, andSubagentBuilder.applyModelOverrideswaps 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
compactConversationForContextWindowto 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
noToolsUsednudge and retries. After more thanMAX_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
SubagentRunner class:254— Holds agent, apiHandler, allowedTools, abort state.run method:330— Main loop; each round pulls the stream, parses tool_calls, executes, and feeds results back.while loop:485— Infinite loop; exits only on attempt_completion or abort.createMessageWithInitialChunkRetry:519— Pulls the stream and, if the first chunk reports "context window exceeded", compacts and retries.tool whitelist check:726— Returns toolError if a tool is not in allowedTools; does not execute.attempt_completion handling:702— On attempt, returns the result string to the main agent and exits.abort method:272— Calls api.abort and cancels any running command.compactConversationForContextWindow:895— Tries file read optimization first, then deletes a range viagetNextTruncationRange.shouldCompactBeforeNextRequest:977—useAutoCondense+ next-gen models use a 0.75 threshold; otherwise it is based onmaxAllowedSize.buildSystemPrompt:73— Assembles<generated> + <agent identity> + SUBAGENT_SYSTEM_SUFFIX.spawn runners:215— Where the main agent actually spawns SubagentRunner instances.
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:
// 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
missingToolParameterErroris 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 toMAX_INITIAL_STREAM_ATTEMPTStimes. - Canceling a running command on abort: abort does not just cancel the API stream; it also delegates to
cancelRunningCommandToolto 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
onProgressreports 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.