Skip to content

The Task class: per-turn agent core

源码版本v4.0.10

Responsibilities

The Task class is Cline's "per-turn agent" state machine. One Task instance corresponds to one task: it is born the moment the user hits enter and dies the moment the task is cancelled or finishes normally. It is not a long-running service — it is a throwaway object created by the Controller to run a single turn and then discarded. This "one instance per turn" model keeps concurrency straightforward: the same Task never handles two LLM requests at the same time, because only it is moving.

What it does falls into three phases. First, it assembles the current user input, message history, system prompt, MCP tool list, rule files, and so on into a complete API request. Then it drives the LLM streaming response, parsing the text on the fly with parseAssistantMessageV2 into text / tool_use / reasoning blocks. Finally, it hands those blocks to presentAssistantMessage, which presents them to the UI one by one and executes any tools they contain. Tool results flow back as the user content for the next round, triggering a recursive call, until the model stops emitting tool_use or the user interrupts.

You can think of it as a three-layer nested loop. The outermost layer, initiateTaskLoop, is a safety net that re-asks the model if it returns without calling any tools. The middle layer, recursivelyMakeClineRequests, runs one full API call plus tool execution per recursion. The innermost layer, presentAssistantMessage, incrementally advances blocks as the stream arrives. All UI interactions — asking questions, requesting user confirmation, reporting progress — go through the two outlets ask and say, which push messages into messageStateHandler and post them to the webview.

Design motivation

  • One instance per task: By isolating state in each Task instance, cancellation, rollback, and checkpoints all use the instance as their boundary. abortTask only needs to set the taskState.abort flag and every recursive path reads it and exits on its own (abortTask:1801).
  • Recursion instead of iteration: After each LLM response, the tool results are naturally the user content for the next round, so recursivelyMakeClineRequests calls itself again at the end (recurse:3830). This way the "single-round API call + tool execution" code is written only once; call-stack depth naturally reflects turn count, and the stack stays readable when something goes wrong.
  • Streaming parse and presentation split: The LLM response is parsed as it streams; parseAssistantMessageV2 re-parses the entire assistant text each time a new chunk arrives (parseAssistantMessageV2 call:3509), and presentAssistantMessage advances by block boundaries, so tools do not have to wait for the full response to start running.
  • One lock against state races: All Task state mutations go through withStateLock and acquire the same Mutex (withStateLock:205), preventing streaming callbacks, tool execution, and UI interactions from corrupting state through three-way concurrency.
  • YOLO mode and the mistake cap: When the consecutive-mistake count hits the cap, the task terminates immediately (mistake limit:2826), so the model cannot burn tokens in an infinite loop.

Key files

  • Task class definition:188export class Task, declaring all core fields: taskId, taskState, api, controller, messageStateHandler, etc.
  • constructor:310 — receives TaskParams, initializes clineIgnore, toolExecutor, streamHandler, presentationScheduler, and other dependencies.
  • startTask:1253 — entry point; prepares the initial user content, runs the TaskStart hook, then calls initiateTaskLoop.
  • initiateTaskLoop:1717 — the outer while loop; uses the noToolsUsed prompt to re-ask when the model only returns text and calls no tools.
  • recursivelyMakeClineRequests:2790 — the middle-layer recursive driver; handles the mistake-limit check, checkpoint initialization, and calls attemptApiRequest to pull the stream.
  • attemptApiRequest:2175 — waits for MCP connections, reads rule files, assembles the system prompt, and finally yield*s the LLM stream.
  • presentAssistantMessage:2630 — the innermost block advancer; dispatches by type: text strips thinking tags and goes through say, tool_use goes to toolExecutor.executeTool.
  • ask:789 — the outlet that asks the user for an answer; manages partial message updates and webview response callbacks.
  • say:969 — the one-way outlet that reports progress; partial mode is used to stream incremental updates to the same message.
  • abortTask:1801 — phased cancellation: first decides whether to run the TaskCancel hook, then sets the abort flag, then cancels hooks / background commands, and finally runs the hook.
  • ToolExecutor.executeTool:212 — Task delegates tool execution out; it never handles any concrete tool itself.

Data flow

The core path of one LLM request is "recurse → pull stream → parse → present → feed back → recurse again". When recursivelyMakeClineRequests enters, it first resets the streaming state for this round, then starts attemptApiRequest to grab the stream:

typescript
// apps/vscode/src/core/task/index.ts
// reset streaming state
this.taskState.currentStreamingContentIndex = 0;
this.taskState.assistantMessageContent = [];
this.taskState.didCompleteReadingStream = false;
this.taskState.userMessageContent = [];
this.taskState.userMessageContentReady = false;
this.taskState.didRejectTool = false;
this.taskState.didAlreadyUseTool = false;
this.taskState.presentAssistantMessageLocked = false;
// ...
const stream = this.attemptApiRequest(previousApiReqIndex); // yields only if the first chunk is successful

This sits near reset streaming state:3302. After the reset, StreamChunkCoordinator splits the stream into text / usage / reasoning chunks and dispatches them to separate callbacks. Each time a text chunk arrives, parseAssistantMessageV2 re-runs over the entire assistant text to split it into a block array (parseAssistantMessageV2 call:3509):

typescript
assistantMessage += chunk.text;
assistantTextOnly += chunk.text; // Accumulate text separately
// parse raw assistant message into content blocks
const prevLength = this.taskState.assistantMessageContent.length;

this.taskState.assistantMessageContent =
    parseAssistantMessageV2(assistantMessage);

When the block array changes, scheduleAssistantPresentation fires presentAssistantMessage. The latter branches on block.type: text goes through say("text", ...), tool_use goes through toolExecutor.executeTool(block) (executeTool call:2743). Tool results are pushed into taskState.userMessageContent; once the whole stream finishes and userMessageContentReady is set, recursivelyMakeClineRequests calls itself again with that user content (recurse:3830), and so on until the model emits no more tool_use. The outer initiateTaskLoop then uses the noToolsUsed prompt to re-ask, or the user ends the task.

Boundaries and failure modes

  • Mistake cap hit: When the consecutive-mistake count reaches maxConsecutiveMistakes, YOLO mode directly return trues to end the task; otherwise ask("mistake_limit_reached") lets the user decide (mistake limit:2826).
  • Empty response: The whole assistant turn contains no text or tool_use block; an empty_assistant_message telemetry event is recorded and the user is prompted to retry (empty response:3834).
  • User cancels mid-flight: abortTask first captures whether to run the TaskCancel hook, then sets the abort flag, so the hook is not misjudged after the flag is set (abortTask:1801).
  • Tool rejected: Once didRejectTool is set, subsequent text blocks are skipped outright and the stream is truncated by [Response interrupted by user feedback] (didRejectTool:3536).
  • MCP not connected: attemptApiRequest uses pWaitFor to wait up to 10 seconds; on timeout it only logs and does not block, and the system prompt is still generated (mcp wait:2177).
  • Initial checkpoint not finished: While the first checkpoint commit is running, non-read-only tools are blocked by await this.initialCheckpointCommitPromise; read-only tools can run concurrently (initialCheckpoint gate:2737).
  • Partial block finalization: Partial blocks left over when the stream ends are forced to partial = false so that presentAssistantMessage can advance normally and finally set userMessageContentReady (finalize partial blocks:3783).

Summary

The Task class is Cline's entire state machine along the "per-turn" axis. It wraps a task's lifecycle into a clear chain — "construct → start → recursively pull the stream → execute tools → wrap up" — and funnels all UI interactions into the ask / say outlets and all state mutations into a single Mutex. This "one instance per turn" tradeoff is what makes cancellation, checkpoints, and the mistake cap easy to implement with the instance as the unit.

To go deeper:

  • The recursion itself: /agent-loop/recursion
  • LLM calls and system-prompt assembly: /agent-loop/attempt-api-request
  • How assistant text is sliced into blocks: /agent-loop/parse-assistant-message

See official docs: Cline docs · README