Agent Mentor Learn
Gestión de estado y persistencia: que las tareas largas sobrevivan a una interrupción · Lección 3 de 6

Lección 3: Reanudar desde un punto de control: reiniciar el bucle

Objetivos de aprendizaje:

  • Decir por qué una «llamada colgante» aparece sí o sí en un escenario de recuperación tras una caída, y en qué se diferencia de una falla común de ejecución de herramienta
  • Escribir la ruta de reanudación completa desde loadCheckpoint() de vuelta al bucle, verificación de versión y reconstrucción del estado incluidas
  • Conciliar una llamada colgante según la naturaleza de la herramienta —las herramientas de solo lectura se vuelven a ejecutar directamente, las de alto impacto pasan primero al plan de repliegue— en vez de volver a ejecutar a ciegas o borrar a ciegas

Requisitos: Terminaste la Lección 2 y entiendes los campos de checkpoint.json y los dos puntos de guardado | Anterior: Lección 2 << | Siguiente: Lección 4 >>

Reanudar, no reiniciar

Un agente se cae a mitad de camino, y el primer impulso suele ser volver a ejecutarlo. Pero para una tarea larga que ya lleva una docena de turnos de profundidad y llamó herramientas varias veces, reiniciar es un mal canje: "restarts are expensive and frustrating for users"1 (los reinicios son costosos y frustrantes para los usuarios). La Lección 2 escribió la escena de ejecución en checkpoint.jsonversion, task, turns, tokensUsed, messages, pendingToolUse—, guardando una vez después de que el modelo responde (punto de guardado A) y otra después de que el resultado de la herramienta queda registrado (punto de guardado B). Lo que hace esta lección es convertir esa escena guardada de vuelta en un bucle que pueda avanzar: construir un sistema que pueda "resume from where the agent was when the errors occurred"1 (reanudar desde donde estaba el agente cuando ocurrieron los errores) en vez de arrancar de nuevo desde arriba cada vez.

La columna vertebral de la reanudación: una mitad es fácil

Empecemos por la mitad fácil. La columna vertebral de la reanudación son cuatro pasos: leer el archivo, hacerle JSON.parse, verificar version y desparramar los campos de vuelta en el estado en tiempo de ejecución. Una vez hechos esos cuatro, runAgent no necesita reconstruir un arreglo messages inicial: el punto de control ya sostiene uno completo, así que saltea la inicialización y cae directo en el bucle.

Con esas dos funciones en su lugar, el arranque de runAgent se vuelve una bifurcación simple:

Después de reanudar, lo primero que hace el bucle es exactamente lo que hace siempre: tomar state.messages y disparar el siguiente client.messages.create(). Los messages que ve el modelo son idénticos a los que veía antes de la caída: no tiene idea de que hubo un reinicio de proceso en el medio. Por eso la Lección 2 insistió en que messages entrara al punto de control sin tocar: mientras ese arreglo se restaure fielmente, la reanudación es invisible para el modelo.

La mitad difícil: conciliar una llamada colgante

El problema de verdad es el punto de control donde state.pendingToolUse no es null. Recuerda dónde van los dos puntos de guardado: el punto A viene después de la respuesta del modelo, y en ese momento pendingToolUse sostiene el {id, name, input} de esta respuesta; el punto B viene después de que el resultado de la herramienta queda registrado, y pendingToolUse vuelve a null. Si el proceso muere justo entre A y B —la herramienta todavía no se ejecutó, o terminó pero el resultado nunca llegó a messages—, lo que conserva el punto de control es un pendingToolUse que no es null.

Ahora la cola de messages es un mensaje assistant que carga un bloque tool_use, sin ningún tool_result que le corresponda. Este no es un estado en el que puedas seguir renqueando: el protocolo exige "return one tool_result for each tool_use block, all together in the next user message"2 (devolver un tool_result por cada bloque tool_use, todos juntos en el siguiente mensaje del usuario). Sin ese resultado, la reanudación no puede hacer la llamada siguiente en absoluto: lo que ve el modelo es un intercambio a medio terminar en el que arrancó una llamada a herramienta y nunca va a recibir respuesta. Hay que ocuparse de esta llamada colgante antes de volver a entrar al bucle.

Tres maneras de manejarlo, solo una se sostiene

Frente a este mensaje assistant colgante hay tres jugadas obvias, pero solo una se sostiene de verdad.

Jugada uno: borrar el mensaje assistant de messages y hacer de cuenta que nunca pasó. Parece lo más limpio: la conversación reanudada ya no tiene ningún hueco. Pero el costo viene en dos capas. Primero, el modelo olvida una decisión que ya tomó, así que puede recorrer la misma exploración otra vez y quemar un turno extra para nada. Segundo, y más peligroso: si esa llamada a herramienta de hecho ya se había ejecutado, y el proceso simplemente murió antes de registrar el resultado, borrar el mensaje no deshace el efecto secundario que ya ocurrió; solo hace que el modelo, y cada entrada de registro posterior, dejen de saber que ocurrió. Borrar esconde el hecho, no el riesgo.

Jugada dos: simplemente volver a ejecutar la herramienta y rellenar el resultado en un tool_result. Para una herramienta de solo lectura (read_file, grep y similares) esto es exactamente lo correcto: leer dos veces no se diferencia en nada de leer una vez, el efecto secundario es cero. Para una herramienta de alto impacto (enviar correo, escribir en una base de datos) es peligroso: es muy probable que la herramienta ya se haya ejecutado una vez, y volver a ejecutarla sin condiciones significa ejecutarla una segunda vez. Este es precisamente el problema de idempotencia que la Lección 4 toma en detalle; por ahora esta lección fija una regla accionable: las herramientas de solo lectura se vuelven a ejecutar directamente; las de alto impacto primero tienen que confirmar si ya se ejecutaron antes de decidir si volver a ejecutarlas.

Jugada tres: agregar un tool_result con is_error: true que diga «estado de ejecución desconocido, por favor reevalúa», y devolverle la decisión al modelo. Este es el repliegue conservador para cuando no puedes saber si se ejecutó: el campo is_error está justamente para "Set to true if the tool execution resulted in an error"2 (ponerlo en true si la ejecución de la herramienta resultó en un error). Y resulta que "letting the agent know when a tool is failing and letting it adapt works surprisingly well"1 (avisarle al agente cuando una herramienta está fallando y dejarlo adaptarse funciona sorprendentemente bien): el modelo relee el contexto y decide si confirmar el resultado por otra vía, en lugar de quemarse con una acción silenciosa y posiblemente repetida.

Alinea las tres y la jugada uno queda descartada; las jugadas dos y tres cubren respectivamente los casos de «puedes saberlo» y «no puedes saberlo», y solo juntas forman la regla de conciliación completa.

reconcile(cp): convertir la conciliación en código

Convierte esa regla en una función: decidir a partir del nombre de la herramienta si es de solo lectura y, en ese caso, volver a ejecutarla; si no lo es, ir a consultar el «registro de efectos» para confirmar si esta llamada ya se ejecutó. Todavía no hay registro de efectos en esta lección, así que un comentario ocupa su lugar, y la Lección 4 da la implementación real. Cuando no puedes saberlo, cae al repliegue de is_error.

Una vez que reconcile() termina, la cola de cp.messages tiene rellenado el tool_result que corresponde y cp.pendingToolUse volvió a null. Este cp es ahora indistinguible de un punto de control que quedó registrado normalmente en el punto de guardado B, y puede ir directo al bucle while para seguir adelante.

Después de reanudar: contar turns y tokensUsed

Hay dos contadores que la ruta de reanudación desajusta con facilidad, y vale la pena detallarlos aparte.

turns no se reinicia al reanudar. Cuenta los turnos totales de la tarea desde el comienzo hasta ahora, no «cuántos turnos ejecutó esta instancia del proceso»: el turns del punto de control debería seguir incrementándose desde donde quedó, que es la única manera de que el tope MAX_TURNS fijado en la Lección 2 siga haciendo su trabajo. Pon turns en cero al reanudar y una tarea que se cae y se recupera una y otra vez puede esquivar el techo de turnos y ejecutarse para siempre.

tokensUsed funciona igual: se arrastra desde el punto de control, no se recalcula. Cuando «Ingeniería de contexto: gastar una atención finita donde más rinde» cubre la compactación de contexto, tokensUsed significa «el consumo de la ventana actual», y lo que guardó el punto de control es precisamente el consumo de esa ventana en el instante de la caída. Los dos cargan el mismo significado, así que al reanudar lo tomas y sigues, sin ninguna conversión extra.

Resumen

La columna vertebral de la reanudación no es difícil: leer el punto de control, verificar la versión, desparramar los campos de vuelta en el estado en tiempo de ejecución, saltear la inicialización y caer directo en el bucle; el modelo ni siquiera puede sentir que hubo una caída en el medio. Lo que sí hay que diseñar es la conciliación de la llamada colgante: borrar pierde una decisión y enmascara un efecto secundario que ya ocurrió; una herramienta de solo lectura se puede volver a ejecutar sin preocuparse; y para una herramienta de alto impacto cuyo estado de ejecución previa no puedes determinar, un repliegue a is_error es una elección más segura que volver a ejecutarla a ciegas. Pero esa regla todavía deja un problema sin resolver: ¿cómo verificas de verdad si una herramienta de alto impacto ya se ejecutó? Esta lección solo se replegó a «no se puede saber»; poder saberlo genuinamente requiere un registro de efectos, y eso es exactamente lo que resuelve la lección siguiente.

>> Lección 4: Efectos secundarios e idempotencia: qué herramientas es seguro volver a ejecutar al reanudar

Footnotes

  1. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2 3

  2. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

Ejercicios

01

Abajo hay tres puntos de control leídos al momento de reanudar (el contenido de messages está elidido para que se lea mejor). Para cada uno, escribe qué debería hacer runAgent al reanudar, y por qué.

Nivel 1: Tres puntos de control, tres acciones de reanudación
Criterios de finalización · marcado local
02

Un reporte de incidente de operaciones: «El proceso fue matado y reiniciado por el OOM killer después de la llamada a la herramienta send_email pero antes de que el resultado quedara registrado. Al reiniciar, el arnés hizo --resume automáticamente, y unos minutos después un usuario reportó haber recibido dos correos idénticos.»

Nivel 2: Encontrar la causa raíz del correo duplicado

El reconcile() que corría en producción en ese momento se veía así:

Determina la causa raíz y después reescribe este reconcile() en una versión que enrute según la naturaleza de la herramienta (pista: la regla que fijó esta lección es «solo lectura se vuelve a ejecutar directamente; una herramienta de alto impacto sin registro se repliega a is_error»). Una vez reescrito, ejecútalo con node y verifica que una herramienta de alto impacto como send_email ya no dispare executeTool().

Criterios de finalización · marcado local