recursivelyMakeClineRequests: the recursive driver
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 directlyreturn trues to end the task, otherwiseask("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 coversstreamHandler.reset()andpresentationScheduler.reset(). - StreamChunkCoordinator dispatch: Rather than
for awaiting the stream directly, it wraps it in aStreamChunkCoordinator(stream coordinator:3368), which splits the stream intoreasoning/text/usagechunks dispatched to separate callbacks. reasoning goes to the reasoning handler, text goes toparseAssistantMessageV2for 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 forpresentAssistantMessageto finish processing all blocks. This guarantees all tool results have been accumulated intouserMessageContentbefore the next round. - noToolsUsed bumps the mistake counter: If the assistant turn contains no tool_use block at all, the
formatResponse.noToolsUsedtext is pushed into userMessageContent andconsecutiveMistakeCount++(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_messagetelemetry event is recorded, an error is said, andask("api_req_failed")lets the user decide whether to retry (empty response:3834), so the task does not silently continue.
Key files
recursivelyMakeClineRequests:2790— function entry; signature is(userContent, includeFileDetails?) => Promise<boolean>, returningdidEndLoop.abort check:2795— immediately checkstaskState.aborton entry; throwsTask instance abortedwhen cancelled.apiRequestCount++:2804— increments the request counter, used for focus chain list management.mistake limit check:2826— enters the mistake-handling branch whenconsecutiveMistakeCount >= maxConsecutiveMistakes.yolo fail:2841— in YOLO mode, says error and returns true to end.ask mistake_limit_reached:2860— in non-YOLO mode, asks the user; the user can give a new prompt to continue.reset streaming state:3302— full reset of streaming state; about a dozen fields zeroed plus handler/scheduler reset.attemptApiRequest call:3319— grabs the stream; a failure on the first chunk is converted by attemptApiRequest's internal try/catch into anapi_req_failedask.StreamChunkCoordinator:3368— the wrapper that splits the stream into reasoning/text/usage chunks for dispatch.while true chunk loop:3387— the main consumer loop; pulls the next chunk from the coordinator and switches on it.accumulate assistantMessage:3503— text chunks accumulate into assistantMessage + assistantTextOnly, then re-parse.force partial false:3792— after the stream ends, residual partial tool blocks are forced topartial = falseso presentAssistantMessage can finalize them.pWaitFor ready:3808— waits for all blocks to be processed; userMessageContentReady becomes true.noToolsUsed bump:3818— when no tools are called, pushes the noToolsUsed prompt and mistake++.recurse:3830— calls itself again with the accumulated userMessageContent; returns didEndLoop.empty response:3834— empty-response path; says error and asks api_req_failed.outer catch:3935— fallback catch; in theory attemptApiRequest already catches, this is double insurance.initiateTaskLoop:1717— outer while; handles the noToolsUsed prompt and didEndLoop exit.
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:
// 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 throwsTask instance abortedand 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_messagetelemetry event is recorded (empty telemetry:3840); the error text says the request ID, and thenask("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 theoryattemptApiRequestalready 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), sopresentAssistantMessagecan advance and finally setuserMessageContentReady = true; otherwisepWaitForwould wait forever. - Remote workspace detection unfinished:
await this.remoteWorkspaceDetectionPromiseis 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) andapiRequestsSinceLastTodoUpdate++, used for focus chain list management and todo update cadence. - Checkpoint before recursion: After all tools have finished and
userMessageContentReadyis set,checkpointManager.saveCheckpointruns (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