Checkpoint C0: grafo tipado y auditable
convertir el agente-bucle de Elcano en un StateGraph tipado, con ciclo controlado e inspeccionable, y justificar sobre una traza qué bug del bucle implícito el grafo hace visible o imposible. Importa porque ese grafo es la base que N1–N4 endurecen: sin él, no hay dónde colgar durabilidad, gate humano ni retries.
5.1El problema: la auditoría que tu equipo te va a pedir
Imagina la reunión que cierra N0. Elcano —tu agente de operaciones— duplicó un reembolso en producción. Tu jefe de equipo no quiere "ya lo subí de pasos". Quiere dos cosas: ver dónde decidió reembolsar, y una garantía de que ese fallo es visible la próxima vez, no invisible como ahora.
Con el agente-bucle no puedes dar ninguna de las dos. El estado vive en una variable local; la decisión y el efecto comparten línea; la parada es un contador. Con el grafo de L4, sí: abres la traza por stream, señalas el superstep donde el modelo pidió emitir_reembolso, y muestras que un reembolso doble aparece como dos supersteps de tools que cualquiera puede leer.
Este checkpoint produce ese artefacto. No aprendes API nueva: integras lo de L2, L3 y L4 en un entregable que resiste esa reunión. Es la tarea auténtica de N0 —la que demuestra que sabes modelar un agente como máquina de estados, no como un bucle con suerte—.
5.2Qué vas a poder hacer
Al terminar esta lección podrás:
- Entregar el agente-bucle de Elcano reescrito como
StateGraphtipado, con paridad de comportamiento. - Aplicar la rúbrica C0 a tu propio grafo, dimensión a dimensión, con honestidad.
- Justificar, sobre una traza concreta, qué bug del bucle implícito tu grafo hace visible o imposible.
- Diagnosticar los fallos típicos de un mal entregable: ciclo sin condición, estado sin reducers, traza ilegible.
Esta lección no introduce conceptos nuevos. Recoge los cuatro hilos de N0 y los anuda en C0.
5.3Recupera (integración del nivel)
Antes de montar el entregable, recupera las cuatro piezas que lo sostienen. Responde de memoria antes de mirar la pista; recuperar fija mejor que releer. Las preguntas mezclan los cuatro ejes a propósito: C0 los integra, no los trata por separado.
Pregunta 1 (estado, de L2). Elcano tiene plan: list[str] y pasos_hechos. Uno debe sobrescribirse al replanificar y el otro debe acumular. ¿Cuál es cuál, y qué anotación lleva el que acumula?
Pista
plan → overwrite (sin reducer): replanificar reemplaza el plan vigente. pasos_hechos → acumula: Annotated[list, operator.add]. La regla: overwrite para el valor actual, reducer para la historia que crece (§2.4).
Pregunta 2 (ciclo, de L3). ¿Qué hace que el ciclo agent → tools → agent termine, y por qué eso es mejor que for _ in range(MAX_PASOS)?
Pista
La arista condicional tools_condition (o tu hay_tools) devuelve END cuando el modelo deja de pedir tools. Es una condición de la lógica, legible en el grafo —no un fusible que confunde "terminó" con "se rindió"— (§3.8).
Pregunta 3 (prebuilt, de L4). ¿Por qué el nodo de tools debe llamarse "tools" cuando usas tools_condition?
Pista
Porque tools_condition enruta por defecto al nodo llamado "tools". Con otro nombre, la decisión apunta a un destino que no existe y las tools nunca se ejecutan —sin lanzar error— (§4.7).
Pregunta 4 (inspección, de L4). Quieres mostrar en la reunión qué hizo Elcano en cada paso. ¿Qué stream_mode usas para ver quién tocó el estado y cuál para ver cómo quedó?
Pista
"updates" → los cambios por nodo (quién tocó qué). "values" → el estado completo tras cada superstep (cómo queda). Con los dos auditas el run sin instrumentar la lógica (§4.5).
Si fallaste alguna, vuelve a la lección indicada antes de montar el entregable. Es común confundir overwrite con acumulación en el plan: la clave es valor-actual (overwrite) frente a registro-que-crece (reducer).
5.4El entregable: la tarea de C0
Esta es la tarea auténtica del checkpoint, tal como está fijada para el curso. Léela entera antes de empezar a codificar.
Partes de un agente "loop implícito" provisto —un ReAct a mano: while con if y tool-calls, el ops_agent.py de Elcano—. Tu trabajo:
- (a) Re-modélalo como
StateGraphcon estado tipado (TypedDict+ reduceradd_messages), nodos de responsabilidad única y ≥1 edge condicional (routing modelo↔tools contools_condition). - (b) Introduce un ciclo controlado —vuelve al nodo de modelo tras ejecutar tools— que termina por condición explícita (
END), no por un contador mágico. - (c) Hazlo inspeccionable: en cualquier superstep el estado es legible (
streamconupdates/values).
Entregable: el mismo comportamiento que el agente original, pero como grafo tipado, con el diagrama del grafo y una traza que muestra el estado en cada paso.
Sin checkpointer ni HITL todavía —eso es N1—. C0 demuestra estructura: estado y flujo explícitos, auditables.
Los seis pasos para entregarlo
No es nuevo; es el orden en que encadenas N0.
- Tipa el estado (L2):
EstadoOpsheredandoMessagesState, conplan(overwrite) ypasos_hechos(operator.add). Sin campos muertos. - Escribe el nodo de modelo (L3/L4): recibe el estado, invoca tu modelo con tools, devuelve
{"messages": [...]}. - Pon el nodo de tools (L4):
ToolNode(TOOLS), llamado"tools". - Cablea el grafo (L3/L4):
START → agent,add_conditional_edges("agent", tools_condition),add_edge("tools", "agent"). Compila. - Genera el diagrama del grafo y guárdalo con el entregable.
- Captura una traza con
stream("updates"y"values") sobre un caso real de Elcano, y señala en ella el paso de la acción consecuente.
Cómo probar la paridad de comportamiento
El grafo debe resolver los mismos casos que el bucle. Toma 3–4 peticiones que el ops_agent.py original resolvía —"reembolsa el último cargo", "baja mi plan", una consulta de solo-lectura— y comprueba que el grafo produce el mismo resultado. La paridad es la dim 4 de la rúbrica.
5.5Míralo: un entregable comentado (bueno en tres dims, flojo en una)
Vas a leer el esqueleto de un entregable de C0 sobre una variante de Elcano, distinta de la tuya. Léelo para calibrar el listón, no para copiarlo. Es sólido en tres dimensiones y flojo en una; te digo cuál y por qué, para que veas dónde está la frontera del aprobado.
1# C0 — Elcano como grafo tipado (variante "solo reembolsos")
2
3## 1. El estado (L2)
4class EstadoOps(MessagesState):
5 cuenta_id: str # overwrite
6 plan: list[str] # overwrite (replanificar reemplaza)
7 pasos_hechos: Annotated[list, operator.add] # acumula
8
9## 2. El grafo (L3/L4)
10StateGraph(EstadoOps)
11 nodos: "agent" (llamar_modelo), "tools" (ToolNode(TOOLS))
12 START -> agent
13 add_conditional_edges("agent", tools_condition) # ¿tools? -> tools ; ¿no? -> END
14 add_edge("tools", "agent") # ciclo
15
16## 3. Diagrama
17[START] -> (agent) --¿tools?--> (tools) --+
18 ^ |no |
19 | v |
20 | [END] |
21 +----------------------------+
22
23## 4. Traza (stream_mode="updates"), petición "reembolsa el último cargo"
24{'agent': {'messages': [AI: tool_call consultar_cargos(cuenta_id=...)]}}
25{'tools': {'messages': [Tool: [{cargo_id: c_88, importe: 19.0}, ...]]}}
26{'agent': {'messages': [AI: tool_call emitir_reembolso(c_88, 19.0)]}} # ← acción consecuente
27{'tools': {'messages': [Tool: {ok: true, reembolso_id: r_12}]}}
28{'agent': {'messages': [AI: "Hecho: reembolsé 19.0 € del cargo c_88."]}}
29
30## 5. Paridad
313 casos del ops_agent.py original; mismo resultado en los 3.
32
33## 6. Justificación
34El reembolso doble del bucle aparecería aquí como DOS supersteps {'tools': emitir_reembolso}.
35En el grafo es visible en la traza; en el bucle estaba dentro de un for sobre una var local.Dónde está flojo: la dimensión 5 (justificación). El entregable afirma que el reembolso doble sería visible, pero no lo demuestra sobre una traza concreta. Dice "aparecería como dos supersteps" en abstracto. Para un aprobado holgado, la justificación debe apoyarse en una traza real —forzar el caso del reembolso doble (p. ej. simulando el reintento de tool) y pegar los dos supersteps de tools que lo delatan—, no describirlo en condicional. La diferencia entre "se vería" y "aquí se ve" es la diferencia entre una corazonada y una prueba.
Las otras tres dimensiones evaluadas están sólidas. El estado tiene reducers bien elegidos y sin campos muertos (dim 1). El grafo tiene un ciclo condicional que termina por END (dim 2). Y la traza es legible superstep a superstep (dim 3). La paridad (dim 4) está cubierta con 3 casos.
Self-explanation —respóndela antes de seguir: ¿por qué la dim 5 (justificación) es la que separa "monté un grafo" de "entiendo qué me compra el grafo"?
Razónalo y comprueba
Porque montar el grafo demuestra que sabes la mecánica; justificar sobre una traza demuestra que entiendes para qué sirve. Cualquiera puede cablear ToolNode y tools_condition. Lo que prueba criterio es señalar, sobre una traza real, el fallo concreto que el bucle escondía y el grafo expone —el reembolso doble como dos supersteps de tools—. Esa es la dim que conecta N0 con el enduring understanding: el grafo no es el fin; es lo que hace el fallo auditable. Sin esa justificación, tienes la forma sin el porqué.
5.6Hazlo tú: el checkpoint C0
Esta práctica es el checkpoint C0. No es calentamiento: es la tarea que cierra N0 y produce la base que N1–N4 endurecen.
Enunciado
Sobre el ops_agent.py de Elcano (el agente-bucle de L1), entrega:
- (a) El grafo:
StateGraphcon estado tipado (add_messages+ reducers elegidos), nodos de responsabilidad única, ≥1 edge condicional (tools_condition). - (b) El ciclo controlado: vuelve al modelo tras tools, termina por
END(no por contador). - (c) Inspeccionable: una traza por
stream("updates"y/o"values") que muestra el estado en cada paso.
Entregable: el grafo con paridad de comportamiento, su diagrama, una traza que muestre el estado paso a paso, y una justificación —sobre la traza— de qué bug del bucle tu grafo hace visible o imposible.
Elaborative interrogation
Antes de autoevaluarte, responde por escrito. Escribir antes de leer fija el razonamiento. ¿Por qué la rúbrica exige el ✔ en las dimensiones 1, 2 y 3, y no en las cinco?
Razónalo y comprueba
Porque 1–3 son el mínimo irreductible de "agente como máquina de estados". Sin estado tipado con reducers correctos (1), sin grafo explícito con ciclo que termina por condición (2) y sin inspeccionabilidad (3), no tienes un grafo auditable: tienes el bucle de L1 con otra sintaxis. La paridad (4) y la justificación (5) elevan el entregable de "monté un grafo" a "entiendo qué me compra". El gate las trata como las dimensiones que confirman criterio, no como el suelo. El umbral mide que modelaste de verdad la máquina de estados, exigiendo las tres que la definen.
5.7El mastery gate: la rúbrica C0
La rúbrica es exactamente la de la arquitectura del curso. Aprobado = ✔ en las dimensiones 1, 2 y 3. Las dimensiones 4 y 5 elevan el entregable; trabájalas para un aprobado holgado. Evalúa tu grafo contra cada una con honestidad.
1. Estado tipado correcto — TypedDict/schema con reducers apropiados (add_messages para mensajes; overwrite vs. operator.add bien elegidos); sin campos muertos.
2. Grafo explícito — nodos de responsabilidad única; ≥1 edge condicional real; ciclo que termina por condición, no por límite arbitrario.
3. Inspeccionable — el estado es legible en cada superstep (stream); el flujo se entiende leyendo el grafo, no ejecutándolo.
4. Paridad de comportamiento — resuelve los mismos casos que el agente-bucle original.
5. Justificación — argumenta, sobre una traza concreta, qué bug del bucle implícito el grafo hace visible o imposible.
Feedback formativo por dimensión
Para cada dimensión, busca tu situación y aplica el feedback. Está escrito como te lo daría un mentor que revisa tu entregable: qué hiciste bien y por qué importa, qué falta y cuál es el siguiente paso.
Dimensión 1 — Estado tipado.
- Si tu estado hereda
MessagesStatey eliges overwrite/operator.addpor el significado de cada campo: tu estado modela bien qué es valor actual y qué es historia, y no arrastra campos muertos. Siguiente paso: revisa que ningún campo declarado quede sin escribir por ningún nodo —eso es un campo muerto—. - Si pusiste todos los campos sin reducer (o todos con
operator.add): tu estado pisa la historia o concatena valores que deberían sustituirse. ¿Qué campos son un registro que crece y cuáles un valor actual? Revisa N0·L2, "Normaliza el error".
Dimensión 2 — Grafo explícito.
- Si tu ciclo termina porque
tools_conditiondevuelveEND: tu parada es una condición legible, no un fusible —exactamente lo que separa el grafo del bucle—. Siguiente paso: confirma que cada nodo tiene responsabilidad única (el de modelo decide, el de tools ejecuta; no mezclados). - Si tu grafo termina por una arista fija o un contador: tu solución reproduce el problema de L1 con otra sintaxis. Una arista fija
agent → toolscicla sin salida; un contador no distingue terminar de rendirse. Reescribe la salida del modelo como condicional —revisa N0·L3, §3.7—.
Dimensión 3 — Inspeccionable.
- Si capturaste una traza con
streamlegible superstep a superstep: puedes auditar el run sin un debugger, que es lo que tu equipo pedirá. Siguiente paso: añade la traza en"values"además de"updates", para mostrar también cómo queda el estado completo. - Si entregaste solo la salida de
invoke: tienes el resultado final, no la traza. Nadie puede ver dónde decidió Elcano la acción consecuente. Cambiainvokeporstream(stream_mode="updates")y pega los supersteps —revisa N0·L4, §4.5—.
Dimensión 4 — Paridad de comportamiento.
- Si tu grafo resuelve los mismos 3–4 casos que el bucle original: demostraste que reescribiste el agente, no que escribiste otro. Siguiente paso: incluye un caso de solo-lectura (sin acción consecuente) para cubrir las dos ramas.
- Si solo probaste un caso, o ninguno: no puedes afirmar paridad. El siguiente paso es correr el bucle original y el grafo sobre los mismos casos y comparar las salidas; una divergencia es información, no un fracaso.
Dimensión 5 — Justificación.
- Si señalaste, sobre una traza real, el bug que el grafo hace visible o imposible: entiendes qué te compra el grafo, no solo cómo se monta. Siguiente paso: nombra explícitamente cuál es —visible (lo ves en la traza) o imposible (la estructura lo impide)—; el reembolso doble es visible, no imposible (eso lo logra N1).
- Si justificaste en abstracto ("se vería mejor"): describes una intuición, no una prueba. Fuerza el caso del reembolso doble y pega los dos supersteps de
toolsque lo delatan. La diferencia entre "se vería" y "aquí se ve" es la dim entera —revisa §5.5—.
Si no llegas al umbral (✔ en 1, 2 y 3), no es un fracaso: es información sobre qué pieza de N0 reforzar. Vuelve a la lección de esa dimensión, corrige y reintenta. El gate mide tu grafo, no tu aptitud.
5.8Conecta: cierras N0, abres N1
Vuelve a la reunión del §5.1, y al reembolso doble que abrió N0 en L1. Tenías un bucle opaco y un compañero proponiendo subir el contador. No podías mostrar dónde se decidió el reembolso.
Ahora la situación es otra. Abres tu grafo, corres stream, y señalas el superstep exacto donde el modelo pidió emitir_reembolso. Si el reembolso se duplica, aparece como dos supersteps de tools que cualquiera lee. El fallo dejó de ser una anécdota: es una traza auditable. Eso es lo que cambia entre el inicio de N0 y este punto.
Pero fíjate en lo que tu grafo todavía no resuelve. emitir_reembolso sigue ejecutándose sin que nadie lo apruebe. Si el proceso muere a mitad de tarea, el agente empieza de cero. El reembolso doble es visible, pero no imposible.
Ahí empieza N1. Tu grafo de C0 es el baseline que harás durable: sobrevivirá a una caída y reanudará desde el último checkpoint. Y le colgarás un gate de aprobación humana con interrupt() antes de tocar el dinero. La frontera entre decidir y ejecutar que separaste en N0 es justo donde irá esa compuerta. Y aprenderás por qué reanudar puede duplicar un efecto —y cómo la idempotencia lo impide—.
Este grafo es la base. Lo que viene es endurecerla, propiedad de producción a propiedad de producción.
5.9Reflexiona
Tómate dos minutos. La metacognición —pensar sobre cómo aprendes— tiene efecto sostenido; no te la saltes. Esta reflexión cierra el nivel entero.
- ¿Qué aprendiste en N0? Resume en una frase qué distingue un agente como máquina de estados de un bucle con suerte.
- ¿Qué sigue sin estar claro? ¿Te costó elegir reducers? ¿El ciclo condicional? ¿La traza con
stream? Nómbralo: ese es tu punto de repaso antes de N1. - ¿Qué harías distinto? Si mañana heredaras un agente-bucle en producción que no es Elcano, ¿por cuál de los seis pasos de §5.4 empezarías, y por qué?
Referencia rápida
El entregable de C0 — los seis pasos (en orden):
- Estado (L2) —
EstadoOpsconadd_messages+ reducers elegidos, sin campos muertos. - Nodo de modelo (L3/L4) — invoca el modelo con tools, devuelve
{"messages": [...]}. - Nodo de tools (L4) —
ToolNode(TOOLS), llamado"tools". - Cableado (L3/L4) —
START → agent,add_conditional_edges("agent", tools_condition),add_edge("tools", "agent"),compile(). - Diagrama del grafo.
- Traza por
stream("updates"/"values") que muestra el estado en cada paso.
Cuatro fallos típicos: estado sin reducers (pisa la historia) · ciclo sin condición (arista fija o contador) · nodo de tools mal nombrado · traza solo de invoke (sin pasos).
Rúbrica C0 (aprobado = 1 + 2 + 3 ✔):
- Estado tipado correcto · 2. Grafo explícito (ciclo por condición) · 3. Inspeccionable (
stream) · 4. Paridad de comportamiento · 5. Justificación sobre traza.
Lo que viene en N1: durabilidad (checkpointer, reanudar tras una caída, durability="async" explícito) + gate humano (interrupt()) + idempotencia. Este grafo es el baseline a endurecer.