attemptApiRequest: llamada streaming al LLM y reintentos
Responsabilidades
attemptApiRequest es el método generador (generator) dentro de Task encargado de «comunicarse con el provider del LLM». Es una función async * que va haciendo yield de cada chunk del stream para que lo consuma el recursivelyMakeClineRequests superior. Antes de hacer yield hace una cantidad considerable de preparación: esperar a que los servidores MCP conecten, leer los archivos de reglas, ensamblar el system prompt y hacer el truncado de contexto (context truncation); si el primer chunk falla, decide si reintentar automáticamente, si preguntar al usuario o si rendirse directamente.
Su posición es el nivel intermedio del agent loop. Cada recursión de recursivelyMakeClineRequests llama una vez a attemptApiRequest, y una vez obtenido el stream, es el while externo el que consume los chunks, los parsea y llama a presentAssistantMessage. Por eso este método no consume el stream: solo se encarga de «prepararlo y entregarlo de forma segura».
Su tipo de salida es ApiStream, en esencia un iterador asíncrono. El nivel superior usa primero iterator.next() para tantear el primer chunk (first chunk probe:2389). Si el primer chunk sale bien, hace yield* iterator para trasferir todo lo restante; si falla, entra en la lógica de clasificación de errores y de reintento. Este diseño de «tantear con un chunk» separa «fallo antes del stream» de «fallo en mitad del stream»: el primero deja el estado limpio y se puede reintentar sin efectos secundarios; el segundo puede haber ejecutado ya algunas herramientas y no se puede reintentar de forma simple.
Motivación de diseño
- La espera de MCP tiene límite: se usa
pWaitForpara esperar a quemcpHub.isConnectingpase a false; si pasa de 10 segundos, se registra un error y se sigue adelante (mcp wait:2177). Así MCP no bloquea toda la solicitud. - SystemPromptContext todo en un sitio: cwd, IDE, info del provider, archivos de reglas, clineignore, skills, pestañas del editor, parallel tool calling, etc. se meten en un único objeto de contexto (
promptContext:2300).getSystemPromptlo consume de una vez, evitando que la lógica de ensamblado del prompt quede esparcida. - Truncado de contexto antes de la llamada:
contextManager.getNewContextMessagesAndMetadatase encarga de recortar el historial de conversación a la ventana de contexto del modelo (context truncate:2354). Si durante el recorte se detecta que hay que actualizar el rango de borrado, se escribe enseguida a clineMessages para persistir. - Tanteo con el primer chunk: con el flag
isWaitingForFirstChunky un try/catch aparte paraiterator.next(), si el primer chunk falla el estado sigue limpio y se puede reintentar de forma segura (first chunk try:2388). - La clasificación de errores decide el reintento: auth, spend limit, quota, entitlement, límite de ClinePass, insufficient credits y otros errores en los que «reintentar no sirve de nada» se saltan el reintento automático (
shouldRetry gate:2495); el resto se reintenta como máximo 3 veces con backoff exponencial de 2s/4s/8s (backoff:2509).
Archivos clave
attemptApiRequest:2175— cuerpo del método; desde la espera de MCP hasta elyield*, todo está aquí.pWaitFor mcpHub:2177— espera a que los servidores MCP conecten; 10 segundos de timeout.SystemPromptContext:2300— recoge todas las entradas del prompt en un único objeto, listo paragetSystemPrompt.getSystemPrompt:2350— genera el systemPrompt final y el array de tools.getNewContextMessagesAndMetadata:2354— el context manager recorta el historial de conversación; devuelve los mensajes truncados y los metadatos.deleted range update:2366— si el recorte actualiza el rango de borrado, se persiste enseguida a clineMessages.createMessage:2378— crea el stream con el api handler; recibe systemPrompt, historial truncado y tools.first chunk probe:2388— try/catch aparte para sacar el primer chunk; si falla, va a la clasificación de errores.context window check:2393—checkContextWindowExceededErrorcomprueba si el contexto se pasó de largo.handleContextWindowExceededError:2409— si el contexto se excedió, trunca automáticamente y reintenta una vez.shouldRetry gate:2495— excluye los tipos de error en los que no tiene sentido reintentar.exponential backoff:2509— tres reintentos automáticos 2s/4s/8s con delay creciente.yield* recurse:2620— si el reintento sale bien, se llama a sí mismo recursivamente y transfiere los chunks del nuevo stream al nivel superior.
Flujo de datos
La ruta principal desde la preparación hasta el yield es «espera MCP → ensambla prompt → trunca historial → crea stream → tantea → transfiere». El paso de truncar el historial es clave: decide qué ve realmente el modelo:
// 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
}ContextManager toma el historial completo de la conversación + el último rango de borrado registrado + el uso de tokens de la última solicitud y calcula el nuevo rango de truncado. Si el rango cambia, escribe clineMessages a disco en el acto, porque la siguiente solicitud va a depender de ese nuevo rango. useAutoCondense es un switch de compresión automática exclusivo de los modelos nuevos (next-gen): si está activo, usa al propio modelo para generar un resumen que sustituye a los primeros mensajes (autocondense flag:2362).
Una vez truncado, se crea el stream y se hace el tanteo:
// 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) {
// ... clasificación de errores, reintento automático, o pregunta al usuario
yield* this.attemptApiRequest(previousApiReqIndex);
return;
}El flag isWaitingForFirstChunk le dice al nivel superior «estoy esperando el primer chunk, no me trates como en mitad del stream». El catch del primer chunk se ramifica según el tipo de error: si se excedió la ventana de contexto y no se ha reintentado todavía, se llama a handleContextWindowExceededError para truncar y reintentar; en el resto, shouldRetry decide entre reintento con backoff automático o ask("api_req_failed"), dejando la decisión al usuario.
Límites y fallos
- El timeout de MCP no es letal: si MCP no conecta en 10 segundos solo se registra un error y se sigue adelante (
mcp timeout catch:2179). Las herramientas MCP pueden faltar después en el system prompt, pero la solicitud no falla por eso. - El contexto excedido se reintenta solo una vez: el flag
didAutomaticallyRetryFailedApiRequestasegura quehandleContextWindowExceededErrorse llame una sola vez (auto retry flag:2407). Si la segunda vez sigue excedido, se va aask("api_req_failed")y la decisión queda en el usuario. - Conversación corta y aún excedida: si tras truncar el número de mensajes es ≤ 3, se le dice al usuario «context window exceeded, retry to truncate» pero ya no se trunca automáticamente (
conversation bricked:2423). La conversación está prácticamente muerta y hace falta intervención del usuario. - Actualización de estado api_req_started: cada reintento localiza el último mensaje
api_req_startedy actualiza sustreamingFailedMessageyretryStatus(update api_req_started:2433). La UI se apoya en ese campo para mostrar «reintentando» o «reintentos agotados». - Doble visualización de error_retry deduplicada: al reintentar automáticamente se hace primero
say("error_retry", ...)con la info completa y luego se borrastreamingFailedMessagedelapi_req_started, para que el mismo error no se muestre a la vez en ErrorRow y en error_retry (dedupe error display:2548). - Reset manual del contador: cuando el usuario dice sí a reintentar, se pone
autoRetryAttemptsa cero (reset counter:2586), dejando hueco para otros 3 reintentos automáticos en los fallos posteriores. - Fallos tras el primer chunk no se tratan aquí: los fallos del stream posteriores al primer chunk los asume el try/catch externo de
recursivelyMakeClineRequests(stream-mid failure:3935). Aquí solo se cubre el «fallo antes de que el stream empiece».
Resumen
attemptApiRequest reúne tres cosas: preparar, tantear y reintentar. La preparación pasa por reglas, skills, MCP y el truncado de contexto; el tanteo usa el primer chunk para decidir si reintenta; el reintento apoya en clasificación de errores + backoff exponencial + decisión del usuario, tres capas de respaldo. Una vez el stream arranca, se transfiere con yield* y ya no es cosa suya. Para ver cómo se consume el stream al salir y cómo se corta en bloques que avanzan, salta a /agent-loop/present-assistant-message; para ver el driver recursivo superior, salta a /agent-loop/task-class.
Véase la documentación oficial: Documentación de Cline · README