Skip to content

recursivelyMakeClineRequests: the recursive driver

源码版本v4.0.10

Responsibilities

recursivelyMakeClineRequests is the "one recursion = one full LLM call plus tool execution" core driver inside the Task class, defined in apps/vscode/src/core/task/index.ts on the Task class. It receives the previous round's accumulated userContent (tool results, user feedback, the noToolsUsed prompt, etc.), calls attemptApiRequest to grab the stream, hands the stream to StreamChunkCoordinator for dispatch, waits for all blocks to be processed by presentAssistantMessage once the stream ends, and then calls itself again with this round's accumulated taskState.userMessageContent. The agent loop's notion of a "turn" lives inside this recursion.

It sits in the middle of the three-layer nested loop. The outermost is initiateTaskLoop, which uses a while (!abort) to cover the case where the model only returns text and calls no tools, pushing a noToolsUsed prompt and re-entering the recursion (initiateTaskLoop:1717). The middle layer is recursivelyMakeClineRequests itself — each call represents one LLM API request. The innermost layer is presentAssistantMessage, woken repeatedly by stream callbacks or the scheduler to advance blocks. At the end, recursivelyMakeClineRequests calls itself again via await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) (recurse:3830); tool results are naturally the user content for the next round, with no extra orchestration.

Design motivation

  • Recursion instead of a while loop: After each LLM response, tool results are the user content for the next round, so the function naturally ends with await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) (recurse:3830). This writes "single-round API call + tool execution + state reset" only once; call-stack depth naturally reflects turn count, and the stack stays readable when debugging.
  • Mistake cap checked first: The function opens by checking consecutiveMistakeCount >= maxConsecutiveMistakes (mistake limit check:2826); YOLO mode directly return trues to end the task, otherwise ask("mistake_limit_reached") lets the user decide. This stops the model from burning tokens in an infinite loop.
  • Full reset of streaming state: Each round zeroes out currentStreamingContentIndex, assistantMessageContent, userMessageContent, didRejectTool, presentAssistantMessageLocked, and a dozen other fields (reset streaming state:3302), so the current round is unaffected by leftovers from the previous one. The reset also covers streamHandler.reset() and presentationScheduler.reset().
  • StreamChunkCoordinator dispatch: Rather than for awaiting the stream directly, it wraps it in a StreamChunkCoordinator (stream coordinator:3368), which splits the stream into reasoning / text / usage chunks dispatched to separate callbacks. reasoning goes to the reasoning handler, text goes to parseAssistantMessageV2 for re-parsing, and usage accumulates token counts.
  • pWaitFor userMessageContentReady: After the stream ends, it does not recurse immediately; instead it await pWaitFor(() => this.taskState.userMessageContentReady) (pWaitFor ready:3808) waits for presentAssistantMessage to finish processing all blocks. This guarantees all tool results have been accumulated into userMessageContent before the next round.
  • noToolsUsed bumps the mistake counter: If the assistant turn contains no tool_use block at all, the formatResponse.noToolsUsed text is pushed into userMessageContent and consecutiveMistakeCount++ (noToolsUsed:3818). The next round tells the model to "either call a tool or attempt_completion"; consecutive no-tool rounds are terminated by the mistake cap.
  • Empty responses take the error path: If the assistant turn has neither text nor tool_use, an empty_assistant_message telemetry event is recorded, an error is said, and ask("api_req_failed") lets the user decide whether to retry (empty response:3834), so the task does not silently continue.

Key files

Data flow

Every time recursivelyMakeClineRequests is entered, it runs the mistake-cap check and remote workspace detection, then resets streaming state and starts the stream. The snippet below is the core of the streaming-state reset plus stream start:

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;
this.taskState.presentAssistantMessageHasPendingUpdates = false;
this.taskState.didAutomaticallyRetryFailedApiRequest = false;
await this.diffViewProvider.reset();
this.streamHandler.reset();
this.presentationScheduler.reset();
this.taskState.toolUseIdMap.clear();

const { toolUseHandler, reasonsHandler } =
    this.streamHandler.getHandlers();
const stream = this.attemptApiRequest(previousApiReqIndex);

This sits near reset streaming state:3302. After the reset, StreamChunkCoordinator takes over the stream (stream coordinator:3368), and a while (true) loop pulls chunks from the coordinator. text chunks go into assistantMessage += chunk.text; assistantTextOnly += chunk.text; and then immediately parseAssistantMessageV2(assistantMessage) re-parses the whole text (parseAssistantMessageV2 call:3509); if the length grew, scheduleAssistantPresentation lets presentAssistantMessage advance to the new block. reasoning chunks go to the reasoning handler for incremental updates to the thinking message. usage chunks accumulate token counts and cost. After the stream ends, processNativeToolCalls handles native tool calls, flushAssistantPresentationOrThrow forces any residual partial blocks to finalize, and then await pWaitFor(() => userMessageContentReady) waits for all blocks to be processed by presentAssistantMessage (pWaitFor ready:3808). Then it checks whether there was any tool_use: if yes, it saves a checkpoint and recurses with userMessageContent; if no, it pushes the noToolsUsed prompt and recurses; a fully empty response takes the error path. The didEndLoop returned by the recursion propagates back up to initiateTaskLoop — true exits the while, false lets the outer layer push a noToolsUsed prompt and go another round.

Boundaries and failure modes

  • abort takes precedence over everything: The first line of the function checks taskState.abort (abort check:2795); when cancelled, it throws Task instance aborted and skips the streaming reset and API call. This ensures that even if a recursion is still pending, cancellation exits immediately.
  • Mistake cap: YOLO mode return trues to end the task (yolo return:2847); non-YOLO mode asks the user, and if the user provides a new prompt, it is used as the next round's userContent to recurse (ask mistake_limit_reached:2860).
  • Empty response: If the assistant turn has no text and no tool_use, an empty_assistant_message telemetry event is recorded (empty telemetry:3840); the error text says the request ID, and then ask("api_req_failed") lets the user decide whether to retry.
  • Mid-stream failure: Failures after the first chunk are caught by recursivelyMakeClineRequests's outer try/catch (outer catch:3935); in theory attemptApiRequest already catches first-chunk errors internally, and this catch is double insurance against unhandled rejections.
  • Residual partial blocks: When the stream ends with partial blocks still present (no closing tag seen), they are forced to partial = false (force partial false:3792), so presentAssistantMessage can advance and finally set userMessageContentReady = true; otherwise pWaitFor would wait forever.
  • Remote workspace detection unfinished: await this.remoteWorkspaceDetectionPromise is awaited at the start of the recursion (remote workspace wait:2801), so the presentation scheduler uses the correct cadence from the very first flush.
  • apiRequestCount increment: Each round does apiRequestCount++ (apiRequestCount++:2804) and apiRequestsSinceLastTodoUpdate++, used for focus chain list management and todo update cadence.
  • Checkpoint before recursion: After all tools have finished and userMessageContentReady is set, checkpointManager.saveCheckpoint runs (saveCheckpoint:3811), so the checkpoint reflects all file changes from this round. Only then does it enter the next round of recursion.

Summary

recursivelyMakeClineRequests is Cline's "per-turn driver" for the agent loop. It strings together "reset state → pull stream → parse → present/execute → wait for completion → recurse with tool results", and centralizes the mistake cap, empty responses, and abort boundaries here. To see its streaming and retry logic, go to /agent-loop/attempt-api-request; to see the innermost layer that pushes blocks to the UI and tools, go to /agent-loop/present-assistant-message; to see the boundary of the overall state machine, go to /agent-loop/task-class.

See official docs: Cline docs · README