attemptApiRequest : appel LLM en streaming et retries
Responsabilités
attemptApiRequest est la méthode générateur (generator) de Task responsable de « communiquer avec le provider LLM ». C'est une fonction async * qui yield les chunks du flux un à un au recursivelyMakeClineRequests appelant. Avant de yield, elle doit accomplir toute une série de préparatifs : attendre que les serveurs MCP soient connectés, lire les règles, assembler le system prompt, procéder à la troncature du contexte (context truncation) ; et en cas d'erreur sur le premier chunk, décider s'il faut retry automatiquement, demander à l'utilisateur, ou abandonner.
Elle se situe au niveau intermédiaire de l'agent loop. recursivelyMakeClineRequests appelle attemptApiRequest à chaque récursion, récupère le flux, puis c'est la boucle while externe qui consomme les chunks, les parse et appelle presentAssistantMessage. La méthode elle-même ne consomme pas le flux ; elle ne fait que « préparer le flux et le remettre de façon sûre ».
Son type de sortie est ApiStream, essentiellement un itérateur asynchrone. La couche appelante utilise d'abord iterator.next() pour tâter le premier chunk (first chunk probe:2389). Si le premier chunk réussit, on yield* iterator pour relayer tout le reste ; en cas d'échec, on passe à la classification d'erreur et à la logique de retry. Cette approche « tâter le premier chunk » sépare « échec avant le flux » et « échec pendant le flux », car le premier a un état propre et peut être retenté sans effet de bord, tandis que le second peut déjà avoir exécuté des outils partiels et ne peut pas être retenté simplement.
Motivation de conception
- Attente MCP bornée : on utilise
pWaitForpour attendre quemcpHub.isConnectingpasse à faux, avec un timeout de 10 secondes au-delà duquel on logge une error et on continue (mcp wait:2177). MCP ne doit pas bloquer la requête entière. - SystemPromptContext tout-en-un : on met cwd, infos IDE, provider, règles, clineignore, skills, onglets de l'éditeur, parallel tool calling, etc. dans un seul objet context (
promptContext:2300).getSystemPromptle consomme en une fois, pour éviter que la logique d'assemblage du prompt soit dispersée. - Troncature du contexte avant l'appel :
contextManager.getNewContextMessagesAndMetadataest chargé de ramener l'historique de conversation dans la fenêtre de contexte du modèle (context truncate:2354). Si la troncature modifie la plage supprimée, on réécrit immédiatement clineMessages pour persister. - Stratégie de sondage du premier chunk : un flag
isWaitingForFirstChunk+ un try/catch dédié autour deiterator.next(); en cas d'échec du premier chunk, l'état est encore propre et on peut retry sans risque (first chunk try:2388). - Classification des erreurs pour décider du retry : auth, spend limit, quota, entitlement, limite ClinePass, insufficient credits — toutes ces erreurs où « retry ne sert à rien » court-circuitent le retry automatique (
shouldRetry gate:2495) ; les autres erreurs sont retry au plus 3 fois avec un backoff exponentiel 2s/4s/8s (backoff:2509).
Fichiers clés
attemptApiRequest:2175— corps de la méthode, de l'attente MCP auyield*.pWaitFor mcpHub:2177— attend que les serveurs MCP soient connectés, timeout 10 s.SystemPromptContext:2300— collecte toutes les entrées du prompt dans un objet, prêt pourgetSystemPrompt.getSystemPrompt:2350— génère le systemPrompt final et le tableau de tools.getNewContextMessagesAndMetadata:2354— le gestionnaire de contexte tronque l'historique, renvoie les messages tronqués et les métadonnées.deleted range update:2366— quand la troncature met à jour la plage supprimée, on persiste immédiatement dans clineMessages.createMessage:2378— crée le flux via l'api handler, en passant systemPrompt, historique tronqué, tools.first chunk probe:2388— try/catch dédié pour prendre le premier chunk ; en cas d'échec, classification.context window check:2393—checkContextWindowExceededErrordétermine si on a dépassé la fenêtre de contexte.handleContextWindowExceededError:2409— en cas de dépassement, tronque automatiquement et retry une fois.shouldRetry gate:2495— exclut les types d'erreurs où retry est inutile.exponential backoff:2509— 3 retry automatiques 2s/4s/8s, délai croissant.yield* recurse:2620— après un retry réussi, rappelle soi-même pour relayer les chunks du nouveau flux à la couche appelante.
Flux de données
Le chemin principal de la requête, de la préparation au yield, est « attendre MCP → assembler le prompt → tronquer l'historique → créer le flux → sonder → relayer ». L'étape de troncature de l'historique est clé, c'est elle qui détermine ce que le modèle voit réellement :
// apps/vscode/src/core/task/index.ts
const contextManagementMetadata =
await this.contextManager.getNewContextMessagesAndMetadata(
this.messageStateHandler.getApiConversationHistory(),
this.messageStateHandler.getClineMessages(),
this.api,
this.taskState.conversationHistoryDeletedRange,
previousApiReqIndex,
await ensureTaskDirectoryExists(this.taskId),
this.stateManager.getGlobalSettingsKey("useAutoCondense") &&
isNextGenModelFamily(this.api.getModel().id),
);
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
this.taskState.conversationHistoryDeletedRange =
contextManagementMetadata.conversationHistoryDeletedRange;
await this.messageStateHandler.saveClineMessagesAndUpdateHistory();
// saves task history item which we use to keep track of conversation history deleted range
}Le ContextManager prend l'historique complet de conversation + la plage supprimée précédemment enregistrée + la consommation en tokens de la requête précédente, et calcule la nouvelle plage de troncature. Si la plage a changé, on écrit immédiatement clineMessages sur disque — parce que la prochaine requête dépendra de cette nouvelle plage. useAutoCondense est le commutateur de condensation automatique réservé aux nouveaux modèles (next-gen) ; quand il est activé, le modèle génère lui-même un résumé qui remplace les premiers messages (autocondense flag:2362).
Une fois la troncature faite, on crée le flux et on sonde :
// apps/vscode/src/core/task/index.ts
const stream = this.api.createMessage(
systemPrompt,
truncatedConversationHistory,
tools,
);
const iterator = stream[Symbol.asyncIterator]();
try {
// awaiting first chunk to see if it will throw an error
this.taskState.isWaitingForFirstChunk = true;
const firstChunk = await iterator.next();
yield firstChunk.value;
this.taskState.isWaitingForFirstChunk = false;
} catch (error) {
// ... classification d'erreur, retry automatique, ou ask à l'utilisateur
yield* this.attemptApiRequest(previousApiReqIndex);
return;
}Le flag isWaitingForFirstChunk signale à la couche externe « j'attends le premier chunk, ne me traite pas comme étant en milieu de flux ». En cas d'échec du premier chunk, le catch route selon le type d'erreur : si la fenêtre de contexte est dépassée et qu'on n'a pas déjà retenté automatiquement, on appelle handleContextWindowExceededError pour tronquer et retry ; pour les autres erreurs, shouldRetry décide entre retry automatique avec backoff ou ask("api_req_failed") qui laisse la décision à l'utilisateur.
Limites et échecs
- Timeout MCP non fatal : si MCP ne se connecte pas dans les 10 secondes, on se contente de logger une error et on continue (
mcp timeout catch:2179). Les outils MCP peuvent manquer dans le system prompt, mais la requête n'échoue pas pour autant. - Dépassement de contexte retenté une seule fois : le flag
didAutomaticallyRetryFailedApiRequestgarantit quehandleContextWindowExceededErrorn'est appelé qu'une fois (auto retry flag:2407). Au deuxième dépassement, on bascule surask("api_req_failed")et on laisse l'utilisateur décider. - Conversation déjà courte mais toujours trop longue : si après troncature le nombre de messages est ≤ 3, on dit à l'utilisateur « context window exceeded, retry to truncate » mais on ne retente pas la troncature automatique (
conversation bricked:2423). La conversation est essentiellement fichue dans ce cas, l'utilisateur doit intervenir. - Mise à jour de l'état api_req_started : à chaque retry, on retrouve le dernier message
api_req_startedet on met à jourstreamingFailedMessageetretryStatus(update api_req_started:2433). L'UI s'appuie sur ce champ pour afficher « retry en cours » / « retry épuisé ». - Dédoublonnage de l'affichage error_retry : pendant un retry automatique, on fait d'abord
say("error_retry", ...)avec l'info complète, puis on retirestreamingFailedMessagedeapi_req_startedpour éviter que la même erreur ne s'affiche à la fois dans ErrorRow et dans error_retry (dedupe error display:2548). - Remise à zéro manuelle du compteur de retry : quand l'utilisateur clique yes pour réessayer, on remet
autoRetryAttemptsà zéro (reset counter:2586), pour offrir 3 nouveaux retry automatiques en cas d'échec ultérieur. - Échec après le premier chunk non géré ici : un échec en milieu de flux après le premier chunk est capté par le try/catch externe de
recursivelyMakeClineRequests(stream-mid failure:3935). Ici on ne gère que les échecs « avant le début du flux ».
Résumé
attemptApiRequest réunit trois choses : préparation, sondage, retry. La phase de préparation enchaîne règles, skills, MCP et troncature du contexte ; le sondage utilise le premier chunk pour décider s'il faut retry ; le retry s'appuie sur trois niveaux — classification d'erreurs, backoff exponentiel, décision utilisateur. Une fois le flux lancé, il est yield* à l'extérieur et n'est plus de son ressort. Pour voir comment le flux est consommé et découpé en blocs, aller à /agent-loop/present-assistant-message ; pour voir le pilote récursif au-dessus, aller à /agent-loop/task-class.
Voir la documentation officielle : documentation Cline · README.