Lección 6: Práctica: cablear una capa de observabilidad sobre el arnés
Objetivos de aprendizaje:
- Cablear una capa de observabilidad funcional en tu propio arnés: logs estructurados JSON Lines, árbol de trazas reconstruido desde los logs, resumen de métricas en una línea
- Recorrer una tarea con error real desde síntoma → filtrado → primer punto de divergencia → arreglo → comparación de la nueva ejecución (lado del modelo fijado con stubs para permitir reejecuciones completas; con APIs reales se vuelve a recuperar-desde-el-error), y explicar qué absurdos son causas frente a cuáles son contagio
- Trazar los límites de esta capa de observabilidad: cubre un proceso, una ejecución; el contenido viene desactivado por defecto; los umbrales no se inventan
Requisitos: Completar las lecciones 1–5, tener a mano y funcionando el bucle del arnés del curso 7 (Fundamentos del arnés de agentes: bucles y control) | Anterior: Lección 5 <<
El síntoma: una región de más en summary.md que no existe
Empecemos con un escenario concreto, de los que se huelen desde el escritorio.
Escribiste un agente pequeño para procesar informes semanales: el directorio data/ contiene tres CSV de ventas trimestrales, el agente los lee, agrega por región y escribe summary.md. Tres herramientas: list_files, read_file, write_file. Funcionó bien durante semanas.
El lunes por la mañana, un colega pregunta en el chat: «¿De dónde salió esta región Central China? Nosotros no tenemos una región Central China».
Abres summary.md y, en efecto:
Abres data/ y adentro hay tres archivos: 2026-q1-east.csv, 2026-q1-south.csv, 2026-q1-north.csv, y sus contenidos tienen los nombres de región East China, South China, North China. Buscas «Central China» en todo el directorio: cero coincidencias. South China se esfumó por completo, Central China apareció de la nada y el número 208000 salió de quién sabe dónde.
La pregunta ahora es: ¿en qué paso se torció esto?
Sin una capa de observabilidad tienes dos cosas: un summary.md mal escrito y la frase «el modelo se lo inventó». Esa frase no resuelve nada: no sabes si de entrada no logró leer el archivo, o si lo leyó pero calculó mal, o si leyó los tres pero mezcló filas al escribir. Y no puedes «reproducirlo con un breakpoint» para forzar la salida del bug: los agentes son no deterministas entre ejecuciones, y los mismos prompts con las mismas herramientas podrían tomar un camino completamente distinto pero igual de válido1. Lo ejecutas tres veces y las tres podrían salir bien, o la cuarta podría fallar de una manera nueva.
Peor todavía, los errores se componen. Que falle un paso puede desviar al agente hacia una trayectoria completamente distinta, y el resultado final parece no tener relación con el pequeño fallo original1. Así que no puedes quedarte mirando el punto final: el absurdo del punto final suele ser apenas contagio (la lección 1 lo llamó desvío de trayectoria, es lo mismo), y la lesión real está río arriba, en algún paso.
El trabajo de esta lección es convertir «no se puede decir» en «se puede verificar»: soldar una capa de observabilidad al arnés y después recorrer este bug real una vez para localizarlo. Todo lo de las cinco lecciones anteriores aterriza en un único archivo ejecutable.
El kit de observabilidad de tres piezas: qué registrar
Cuando ejecutas agentes en producción necesitas visibilidad sobre cuatro cosas: qué herramientas llamaron, cuánto tardó cada solicitud al modelo, cuántos tokens se gastaron, dónde ocurrieron los fallos2. El enfoque oficial es exportar esto como trazas, métricas y eventos de log de OpenTelemetry; esta lección no incorpora ninguna librería de OTel, armamos a mano una versión mínima de tres piezas:
- Logs estructurados: una entrada JSON Lines por solicitud al modelo, una por llamada a herramienta, escritas en
run.log.jsonl.
- Árbol de trazas: terminada la ejecución, reconstruir las relaciones padre-hijo desde ese JSONL e imprimirlo indentado.
- Resumen de métricas: una línea que saca rondas totales, cantidad de llamadas a herramientas, tokens, cantidad de errores y duración total.
Modelo de spans: quién es padre de quién
Con la telemetría mejorada activada, oficialmente cada paso del bucle del agente se vuelve un span inspeccionable: una interaction es el span raíz, y las solicitudes al modelo y las ejecuciones de herramientas son sus spans hijos2. Fíjate que en el árbol oficial las solicitudes al modelo y las llamadas a herramientas son hermanas al mismo nivel bajo la raíz: el árbol que reconstruiste en la lección 4 tiene esa forma. Nuestra versión mínima usa deliberadamente un enganche distinto: colgamos las llamadas a herramientas de la solicitud al modelo que las disparó, de modo que la forma del árbol muestra directamente «qué quiso hacer el modelo en esta ronda», con las herramientas paralelas bajo el mismo padre visibles de un vistazo. El mecanismo padre-hijo es exactamente el mismo, solo elegimos otro padre para las herramientas; ambos enganches son válidos, y cuál elijas depende de qué pregunta quieras que el árbol responda primero. Nuestras tres capas se ven así:
Las relaciones padre-hijo no dependen de una pila de llamadas en memoria, dependen de dos campos en los logs: cada registro lleva un span_id, más un parent_id que apunta a su padre. El árbol se reconstruye desde el JSONL en disco después de que termina la ejecución, no se imprime sobre la marcha. Este punto importa: cualquier cosa visible en el árbol tuvo que registrarse primero en los logs. Si encuentras que algo falta en el árbol, no es un problema del código que imprime, es un problema del código que registra.
Tabla de campos
El diseño de campos de abajo es el enfoque de ingeniería de esta lección, no una especificación oficial; oficialmente serían nombres de atributos de span de OTel, y cuando escribas tu propio arnés los nombres de campo los decides tú. Cuatro nombres difieren de las lecciones 3 y 4, así que mapeémoslos primero para que no los tomes por erratas: el type de la lección 3 se llama kind acá (en aquel momento había solo dos clases de registro, ahora tenemos agent_run, y un término semánticamente más amplio encaja mejor); input_tokens/output_tokens se pliegan en un objeto tokens:{input,output} (las cosas propias de la llamada al modelo empaquetadas juntas); tool_response se llama tool_result acá (el payload del hook lo llama response, pero acá el valor de retorno viene directo de la implementación de la herramienta, siguiendo la nomenclatura del bloque de contenido de la API); el parent_span_id de la lección 4 se acorta a parent_id. El costo también se dice: el stats.mjs de la lección 3 necesita dos cambios de nombre de campo para leer este log; esta es una demostración en vivo de que «alinear vocabulario importa más que nombrar bonito». El vocabulario de campos sí vale la pena alinearlo con los materiales oficiales, para que cuando termines conectando un backend no tengas que cambiar de conceptos, solo de ortografía:
El truco del trace_id se aprendió de lo oficial: un prompt de usuario dispara varias llamadas a la API y varias herramientas, y lo oficial usa un atributo prompt.id para atarlas todas de vuelta al prompt disparador; el enfoque oficial de trazado también es directo: para trazar toda la actividad disparada por un solo prompt, filtra los eventos por un valor concreto de prompt.id3. Acá usamos trace_id: una ejecución es una tarea, así que usamos trace_id, que hace exactamente lo mismo. Por cierto, acá no hay session_id: una ejecución de este script es una sesión, mantener ese campo no tendría sentido; los escenarios multi-ronda y multi-sesión lo vuelven a agregar, con la referencia de vocabulario de la lección 3.
La línea de métricas tampoco se eligió al azar. Además de la exactitud de alto nivel, lo oficial recomienda recolectar: el tiempo total de ejecución de llamadas a herramientas individuales y de tareas, la cantidad total de llamadas a herramientas, el consumo total de tokens y los errores de herramientas4; estas cuatro cosas tienen su lugar de aterrizaje en la línea de resumen y en el duration_ms de cada registro. rounds es el quinto número que agregué, cómodo para ver de un vistazo cuántas iteraciones de bucle hubo. Ya usaste el conjunto oficial en el curso 10 (Verificación y aseguramiento de calidad: que no se cuele lo que «se ve bien»): allá para calificar, acá para diagnosticar con la misma regla.
Cuánto contenido registrar: la única línea que esta lección te pide trazar a ti
tool_input y tool_result podrían ser un CSV entero, la entrada completa del usuario, un documento entero escrito. Registrarlo todo es técnicamente una línea de código, pero por defecto no habría que hacerlo.
La postura por defecto de la telemetría oficial es clara: lo estructural siempre se registra, el contenido nunca se registra; cada span tiene duración, nombre de modelo, nombre de herramienta y conteo de tokens registrado cuando la API devuelve datos de uso, mientras que el contenido que el agente lee y escribe por defecto no se recolecta2. Con los prompts de usuario pasa igual: por defecto solo se registra la longitud, y registrar el contenido exige una variable de entorno aparte3. Y lo oficial acompaña este tipo de interruptor con una afirmación dura: a menos que tu pipeline de observabilidad esté aprobado para almacenar los datos que maneja tu agente, deja estos sin definir2.
Nuestra capa deja un compromiso: por defecto registra shape (si es string u objeto, qué tan largo, qué claves), chars (cantidad de caracteres), más un fragmento de los primeros 60 caracteres como resumen de cabecera. El fragmento está para que puedas reconocer de un vistazo «qué archivo leyó esta vez» durante tu propia depuración, sin reejecutar una y otra vez. El HEAD_CHARS = 60 del código es donde se sitúa esa línea; ponlo en 0 y ni una palabra de contenido toca el disco. Dónde traces esa línea en tu propio proyecto depende de dónde aterrizan los logs, quién puede verlos y si se pasó la aprobación de datos: esta es una pregunta de cumplimiento normativo, no técnica.
El montaje de verificación: dónde quedan fijadas las diferencias de las tres versiones
Esta lección se ejecuta tres veces: una normal, una con bug, una arreglada. Las tres salidas tienen que ser comparables línea por línea, así que las respuestas del modelo no pueden ser reales: las respuestas reales difieren cada vez y no puedes usarlas para enseñar a localizar. Siguiendo el viejo enfoque de los cursos 8 a 10 de esta serie: cliente stub con cola de respuestas fija. client.messages.create() no manda ninguna solicitud de red, devuelve en orden objetos de respuesta preescritos de un arreglo, cada uno con su stop_reason, content y usage completos. El bucle del arnés no cambia ni una palabra: lo que recibe tiene la misma forma que lo que devuelve un cliente real.
Todas las diferencias de las tres versiones quedan fijadas en la tabla VERSIONS del código, y cada versión tiene dos cosas:
Fuera de esta tabla, cada otra línea de código es compartida por las tres versiones. Las herramientas leen y escriben disco de verdad: list_files hace un readdirSync real, read_file lee archivos de verdad y lanza de verdad porque el archivo no existe, write_file escribe summary.md de verdad en disco. Así que ese error de v-bug no es un objeto de error falsificado, es el sistema de archivos genuinamente no encontrando ese archivo.
Para que quede claro: los stubs resuelven «el lado del modelo es reproducible», no «el agente es determinista». Al ejecutar de verdad, el mismo prompt dos veces podría elegir herramientas distintas y tomar caminos distintos1. El valor de esta capa de observabilidad está justo acá: los caminos difieren cada vez, pero cada vez hay un registro para revisar.
Lo que hay que decir con claridad: los stubs fijan el lado del modelo para que las reejecuciones completas funcionen; con APIs reales se vuelve a recuperar-desde-el-error. Esta lección se anima a hacer reejecuciones completas justamente porque el lado del modelo está fijado con stubs: las reejecuciones no introducen variables nuevas y la comparación línea por línea se sostiene. Cuando conectes APIs reales, los stubs desaparecen y vuelves al enfoque de la lección 5: recuperar desde el error.
Código completo: observed-agent.mjs
Un archivo entero, cero dependencias, se ejecuta con node pelado. Guárdalo como observed-agent.mjs y después node observed-agent.mjs --version v-bug lo ejecuta.
Unos cuantos puntos que vale la pena señalar por separado:
- El bucle en sí no cambió. Ese
while (response.stop_reason === "tool_use") del curso 7 (Fundamentos del arnés de agentes: bucles y control) no movió ni una palabra, la observabilidad se envuelve por fuera: callModel() registra una marca de tiempo antes y después de la solicitud, y runToolUses() envolvió cada bloque de herramienta con un try/catch más un cronómetro. Quítale esos dos envoltorios y lo que queda es el bucle original.
MAX_ROUNDS es una compuerta dura. Los agentes necesitan condiciones de parada, como una cantidad máxima de iteraciones; esto es parte del control5. Superarla lanza un error, registra un harness_error y sale con código 2.
- Reparto de tareas de los códigos de salida. Este script solo se ocupa de ejecutar y registrar: si la ejecución termina, es 0; si los parámetros están mal o hay descontrol, es distinto de cero. «¿Es correcta la salida?» es trabajo de la suite verificadora del curso 10 (Verificación y aseguramiento de calidad: que no se cuele lo que «se ve bien»); fíjate que
v-bug también sale con 0, el arnés cree que terminó sin problemas. La verificación te dice si se rompió, esta capa te dice por qué.
- Los errores de herramienta no rompen el bucle. Los errores se envuelven en un
tool_result con is_error: true y se devuelven al modelo, y el bucle continúa. Esto es correcto: los agentes necesitan obtener «ground truth» del entorno en cada paso para evaluar su progreso5, y los errores también son retroalimentación. Todo el bug de esta lección ocurre en la segunda mitad de esa frase: hubo retroalimentación, pero fue pésima.
Primera ejecución, viento en popa: v-good
Veamos primero cómo se ve lo normal. La salida de terminal de abajo y todas las salidas de terminal siguientes están genuinamente ejecutadas, no son ejemplos escritos a mano.
Tu trace_id, tu span_id, tu ts y los milisegundos van a diferir de los míos: los ids se generan al azar en cada ejecución y los milisegundos son duración genuina. Fuera de eso, cada línea debería coincidir palabra por palabra.
Leer este árbol hacia abajo es una sola frase completa: primero listar el directorio (turn-1), después leer tres archivos en paralelo en una sola ronda (turn-2 con tres nodos hermanos debajo), después escribir el archivo (turn-3) y finalmente cerrar (turn-4, stop=end_turn). Esas tres líneas paralelas son tres bloques tool_use de la misma respuesta del modelo, así que su parent_id apunta al mismo model_call: la forma del árbol muestra directamente «qué quiso hacer el modelo en esta ronda».
No te tomes en serio esa columna de 0ms en model_call: el cliente stub no tiene ida y vuelta de red, así que la duración de las solicitudes al modelo es toda 0. Después de conectar APIs reales esta columna gana valor diagnóstico: rastrear las duraciones de las solicitudes a la API y los tiempos de ejecución de herramientas es exactamente para encontrar cuellos de botella de rendimiento3.
El archivo de log se ve así, un JSON completo por línea, se le puede hacer grep directamente:
La línea dos es ese list_files: su parent_id apunta al span_id de la línea uno (así que cuelga de turn-1), y adentro tool_input solo tiene forma, longitud y un fragmento chico, igual que tool_result: shape es string(52) y head tiene los tres nombres de archivo. Ni un byte de esta línea es «contenido de archivo», y sin embargo ya puedes responder «qué llamó este paso, qué forma de cosa recibió, si dio error».
Mira otra vez la línea del resumen de métricas: 4 rondas, 5 llamadas a herramientas, 0 errores, 6033 tokens, y al final de la línea también la duración total. Vale la pena echarle un vistazo a esta línea de números cada vez que termina una ejecución: la cantidad de llamadas a herramientas puede exponer rutinas fijas que el agente recorre repetidamente, y un montón de llamadas redundantes suele sugerir que habría que ajustar los parámetros de paginación o de límite de tokens; mientras que un montón de errores de parámetros inválidos podría decir que las descripciones de herramientas habría que escribirlas más claras y dar los ejemplos más completos4. Los tokens especialmente valen la pena mirarlos: al analizar el rendimiento de las evaluaciones, lo oficial encontró que el uso de tokens por sí solo explica el 80% de la varianza, y los otros dos factores explicativos son la cantidad de llamadas a herramientas y la elección del modelo1.
Reproducir el síntoma: v-bug
Ahora ejecutemos la versión con bug. La cola del stub tiene enterrada la divergencia real del inicio de la lección; no espíes primero, encuéntrala tú desde la salida.
El artefacto está efectivamente mal:
Fíjate primero en unas cuantas cosas invisibles desde afuera:
- La cantidad de rondas y la cantidad de llamadas a herramientas son idénticas a
v-good: 4 rondas, 5 llamadas. Mirando solo esos dos números, las dos ejecuciones se ven iguales.
- Los tokens suben apenas 67 (6100 frente a 6033). Si tu alerta es «los tokens superan un umbral», esta ni siquiera sonaría.
- Solo
errors=1, ese único número cambió. Por esto los errores de herramienta tienen que ser ciudadanos de primera clase en las métricas4: es la única señal a nivel de resumen de que esta ejecución se ve mal.
- Ese último
model_call es stop=end_turn: el agente cree que completó la tarea con éxito. No dio error, no pidió ayuda, no mencionó que le faltaba un pedazo de datos. Lo que omite en su retroalimentación suele ser más importante que lo que incluye4.
Localización en cinco pasos: del rastreo del síntoma al primer punto de divergencia
La localización en cinco pasos de la lección 5 es una orquestación general; los materiales de esta ronda son especiales —tres logs comparables línea por línea en la mano—, así que tres de los cinco pasos cambian de forma, escritos uno al lado del otro:
El paso cinco necesita explicación aparte. La lección 5 aboga por «después de arreglar, recuperar desde el error, no reejecutar desde cero», y la razón es que las reejecuciones completas reintroducen no determinismo y no puedes distinguir «lo arreglé bien» de «esta vez tuve suerte». Esta lección se anima a hacer reejecuciones completas justamente porque el lado del modelo está fijado con stubs: las reejecuciones no introducen variables nuevas y la comparación línea por línea se sostiene. Cuando conectes APIs reales, los stubs desaparecen y vuelves al enfoque de la lección 5: recuperar desde el error.
Aterrizado en los materiales de esta ronda, quedan los cinco pasos de abajo. Haz de cuenta que todavía no sabes la respuesta y recórrelos una vez.
Paso uno: fijar esta única ejecución
En un entorno de producción, los logs de todas las ejecuciones se mezclan en un solo flujo. Simulemos primero esa situación fusionando los logs de las tres ejecuciones:
De 32 líneas, solo 10 pertenecen a la ejecución con bug. Este paso usa el enfoque de trazado que da lo oficial: para trazar toda la actividad disparada por un prompt, filtra los eventos por ese id concreto3. Que se llame prompt.id o trace_id no importa; lo que importa es que ese id exista y que cada registro lo lleve.
Ya que estamos, podemos revisar cuántos errores hay en todo el flujo:
Dos: uno de v-bug, uno de v-fixed. v-good impecable.
Paso dos: identificar el primer punto de divergencia en el árbol
El árbol ya está impreso; recórrelo de arriba abajo y encuentra el primer registro que no coincide con lo esperado:
Bajo turn-2 hay tres lecturas paralelas y la del medio se rompió. La razón está escrita en tool_input: la ruta es data/2026-q1-sourth.csv, con south mal escrito como sourth. Esa línea de list_files en el árbol solo muestra ok string(52), y los nombres de archivo correctos hay que ir a excavarlos a los logs: saca ese registro (ya lo viste en el head -3 de arriba) y su tool_result.head dice 2026-q1-east.csv 2026-q1-north.csv 2026-q1-south.csv: el modelo sí recibió el nombre correcto. El árbol se ocupa de localizar, los logs se ocupan de los detalles, y las dos capas cooperan exactamente así.
Para ver ese registro completo, péscalo del flujo:
Este es el punto de divergencia. Fíjate en cómo se reconoció: no adivinando, sino con tres campos: trace_id acota el alcance a esta única ejecución, que error sea distinto de null lo separa de los otros diez registros, y tool_input.head te dice dónde se torcieron los parámetros. Tres campos, ninguno prescindible.
Paso tres: reconocer los absurdos de río abajo como contagio, no arreglarlos por separado
Después del punto de divergencia, en turn-3 el modelo escribe una agregación con una región Central China, y en turn-4 reporta «tarea completa». Ambos pasos parecen bastante absurdos, pero los dos son río abajo:
En los sistemas de agentes, que falle un paso alcanza para desviarlo hacia una trayectoria completamente distinta, con un resultado final impredecible1: este es el ejemplo más limpio. Si solo tuvieras el summary.md final, ¿adónde irías a arreglar? Probablemente a cambiar el prompt: «no inventes datos», «tienes que indicar las fuentes». Todos esos cambios le pegan al contagio, no le pegan a la lesión. La próxima vez cambia el método del error de tipeo y va a inventar igual.
Por cierto, por qué este síntoma creció hacia «Central China» en vez de hacia «falta South China»: el modelo sí recibió el nombre de archivo (2026-q1-south.csv está ahí mismo en el retorno de list_files), y las correspondencias east→East China y north→North China ya estaban en los contenidos de las dos lecturas exitosas iniciales; lo único que le faltaban eran los números concretos de esos tres meses. Pero ese ENOENT opaco no le dijo ni «reintenta con el nombre correcto» ni «detente y explica con claridad», así que eligió el camino más fácil: disfrazar el hueco de completo y rellenar tanto el nombre de la región como los números. La invención no ocurre porque no sepa nada, ocurre porque el error no le dio una salida mejor.
Paso cuatro: determinar la causa — la retroalimentación que recibió fue pésima
En este paso no te apures a culpar al modelo. Mira lo que ese error le dio realmente:
Esta línea tiene información suficiente para un ingeniero humano; para un agente que decide «qué hago después» está casi vacía. No puede leer ahí «qué archivos de este directorio se pueden leer», no puede leer «¿escribí mal o estos datos genuinamente no existen?», y menos todavía «ante esta situación debería detenerme y preguntar, no rellenar por mi cuenta». Los agentes necesitan apoyarse en la «ground truth» que el entorno da en cada paso para juzgar su progreso5, y este ENOENT es toda la retroalimentación que recibió.
La sugerencia oficial sobre ingeniería de herramientas apunta exactamente a este hueco: cuando las llamadas a herramientas lanzan errores, las respuestas de error deberían estar bien escritas, explicando con claridad mejoras específicas y accionables, en lugar de arrojar un código de error opaco o una traza de pila4. Así que lo que hay que cambiar esta vez no es el prompt, es el mensaje de error de read_file.
Paso cinco: comparación de la reejecución (los stubs fijaron el lado del modelo, acá se puede reejecutar completo)
El método de arreglo va en la sección siguiente; después de ejecutarlo volvemos a ver si los números cambiaron. La localización no termina en «sé cuál es la causa», termina en «después de arreglar, ese paso en la misma traza realmente es distinto».
Arreglar y reejecutar: v-fixed
Lo que cambió es la sección read_file del código, solo el mensaje de error:
Este mensaje mete tres cosas: el estado actual (qué hay realmente en el directorio), qué hacer después (reintentar con el nombre original) y cuándo detenerse (si los datos genuinamente no están, pregúntale a una persona, no estimes). Las dos primeras le dan al modelo un camino por donde andar; la tercera le bloquea el camino de la invención.
La cola de respuestas de v-fixed demuestra la reacción del modelo tras recibir este error: ya no inventa hacia abajo, sino que se da vuelta a buscar verificación en el entorno —relee una vez con el nombre de archivo original que aparece en el error— y al final, en la frase de cierre, además le pregunta de vuelta al usuario si hay datos de otras regiones fuera de data/, que le diga dónde está el archivo, que él no va a completar números por su cuenta.
La forma del árbol cambió: ese ERROR de turn-2 sigue en su lugar original, pero debajo le creció un turn-3 con una relectura usando el nombre de archivo correcto. El artefacto es correcto:
Los dos summary.md son idénticos byte a byte, y la región Central China desapareció.
Las tres ejecuciones lado a lado:
Hay dos cosas que decir con claridad, o este arreglo es fácil de malinterpretar:
Primero, errors no volvió a cero, ni debería. Esa lectura con el nombre mal escrito igual dio error; solo cambiamos el mensaje de error, dejando que el modelo saliera trepando del error. El arreglo que genuinamente devuelve errors a cero va en otra dirección: escribir descripciones de herramientas más explícitas, dar ejemplos, para que el modelo no escriba mal el nombre en primer lugar. La lectura diagnóstica oficial se corresponde exactamente así: grandes cantidades de errores por parámetros inválidos dicen que las descripciones de herramientas habría que hacerlas más claras y los ejemplos más completos4. La descripción de read_file en este script ya dice «La ruta debe usar el nombre de archivo original tal como lo devolvió list_files», y claramente todavía no alcanza: la próxima ronda habría que darle un ejemplo positivo.
Segundo, el arreglo no es gratis. Los tokens pasaron de 6033 a 8359, un aumento de 2326, un 38% más, y lo extra vino de esa ronda de ida y vuelta de la relectura. No es una explosión, pero tampoco es costo cero. El costo del arreglo hay que ponerlo sobre la mesa y calcularlo; no se puede mirar solo «el resultado es correcto» y darlo por terminado.
Dónde están los límites de esta capa de observabilidad
Esta cosa es chica y hay que enunciar sus límites con claridad, no sea que creas que cablearla significa que ya tienes observabilidad de producción.
Cubre un proceso, una ejecución. Los logs son un appendFileSync escribiendo directo a un archivo local, y aunque maten el proceso no se pierden: esto es deliberado. Conecta un backend real y ya no es el mismo trato: en el camino de OTLP, el fallo de exportación es silencioso por defecto, el endpoint inalcanzable o que rechaza, el agente se sigue ejecutando, la telemetría directamente descartada, sin que aparezca siquiera un error en tu aplicación; y la telemetría se agrupa en lotes antes de exportarse por intervalos, y si matan el proceso antes de exportar, lo que haya en el búfer del lote se perdió2. El «el pipeline de observabilidad te va a mentir en silencio» de la lección 4 habla de ese tramo. El archivo local esquiva ese pozo, y el costo es que solo está en la máquina local.
Conectar backends reales y la agregación multiproceso no están en esta lección. Para conectar esta capa a Honeycomb, Datadog, Grafana, Langfuse o un colector autoalojado hace falta el conjunto del protocolo OTLP2, los campos hay que remapearlos, y eso es otro tema. Cómo se agregan juntos los logs de varios procesos de agente, cómo distinguirlos por nombre de servicio, lo mismo.
Los umbrales de alerta: esta lección no da números. «A partir de qué tasa de error de herramientas habría que alertar», «cuántos tokens en una ejecución cuentan como anomalía»: la documentación oficial solo mencionó que las alertas debería hacerlas tu backend, sin dar ningún número3. Yo tampoco voy a inventar. Tus propios umbrales solo pueden crecer desde tu propia línea base: primero ejecuta un tiempo, mira cómo se ve la distribución de las ejecuciones normales, y después traza la línea.
El registro de contenido viene desactivado por defecto. Ese HEAD_CHARS = 60 de arriba dejó apenas un fragmento muy corto. Para activar de verdad el texto completo, el prerrequisito es que tu pipeline de observabilidad esté aprobado para almacenar los datos que maneja tu agente2: primero pasa la aprobación de datos, después cambia el código, no al revés.
La tasa de muestreo y la ventana de retención de logs tampoco se amplían. Una ejecución son decenas de líneas de JSONL, y unos cientos de ejecuciones locales no hay que gestionarlas; cuando necesites considerar esto, ya es un problema de backend.
Palabra final: el valor de esta capa de observabilidad no está en cuánto registró, está en que te deja hacer una pregunta específica. «¿Por qué inventó una región Central China?» es una pregunta sin respuesta; «en esta ejecución con trace_id=tr-ebad58f6, ¿cuál es el primer registro con error distinto de null, y cuáles son sus parámetros?» es una pregunta con respuesta. Después de cablear un trazado de producción completo, recién ahí puedes diagnosticar sistemáticamente por qué fallaron los agentes y arreglarlo sistemáticamente1.
💻 Ejercicios
Resumen
- Cada pieza del trío de observabilidad se ocupa de un tramo: los logs JSON Lines se ocupan de «dejarlo registrado», el árbol de trazas se ocupa de «ver claro el orden y la pertenencia», el resumen de métricas se ocupa de «ver de un vistazo si esta ejecución se ve normal»; el árbol y el resumen se reconstruyen ambos desde el JSONL en disco, y lo que no se registró en los logs jamás va a aparecer en el árbol
- Cada registro tiene que llevar
trace_id y parent_id: el primero encierra los registros dispersos de vuelta en la misma ejecución, el segundo les permite reconstruirse en árbol; esta es exactamente la misma técnica que usa lo oficial para atar con prompt.id todos los eventos disparados por un prompt, y filtrar por él para localizar3
- El contenido por defecto no se escribe completo: la postura por defecto de la telemetría oficial es que lo estructural se registra todo, el contenido leído y escrito por el agente no se recolecta, y de los prompts de usuario solo se registra la longitud; para activar el registro de contenido, el prerrequisito es que tu pipeline de observabilidad esté aprobado para almacenar este tipo de datos2
- Esos cinco números de métricas (duración, cantidad de llamadas, tokens, cantidad de errores, duración total) son el mismo conjunto que se usa para calificar en el curso 10 (Verificación y aseguramiento de calidad: que no se cuele lo que «se ve bien»)4, acá cambiado a uso diagnóstico; en la comparación de esta ronda, la cantidad de rondas, de llamadas y los tokens de
v-good y v-bug son casi idénticos, y lo único que cambió es la cantidad de errores
- La acción clave de la localización es identificar el primer punto de divergencia en el árbol, y después tratar uniformemente todos los absurdos de río abajo como contagio: que falle un paso alcanza para desviar al agente hacia una trayectoria completamente distinta1, e ir a arreglar esa capa del artefacto final equivale a arreglar una sombra
- Arreglar el mensaje de error de la herramienta es un arreglo que le pega a la lesión: las respuestas de error deberían explicar con claridad mejoras específicas y accionables, en lugar de arrojar un código de error opaco o una traza de pila4; después de este arreglo el modelo pasó de «inventar una región Central China» a «releer una vez con el nombre de archivo original, y darse vuelta a preguntarle al usuario si hay otros datos»
- Después de arreglar hay que reejecutar y comparar, y hay que reconocer la factura:
errors no volvió a cero (el error de tipeo sigue ahí), los tokens subieron un 38% (se agregó una ida y vuelta); «el resultado es correcto» no equivale a «el costo es cero»
La línea principal de las seis lecciones termina acá. La lección 1 dejó claro por qué no se puede decir: un agente toma caminos distintos en dos ejecuciones, y debajo de un síntoma se apretujan varias causas que desde afuera se ven idénticas. La lección 2 fijó la evidencia de primera mano en la transcripción cruda, no en su autoinforme. La lección 3 convirtió cada paso en datos con campos. La lección 4 hilvanó los datos dispersos en un árbol y, de paso, te dijo que ese pipeline mismo va a mentir en silencio. La lección 5 instaló sondas en las puertas del bucle y dio el método de caminar de la localización. Esta lección soldó las cinco lecciones anteriores en un archivo de unas 400 líneas y cero dependencias, y lo usó para rastrear genuinamente «de dónde salió la región Central China» hasta esa lectura con la ruta mal escrita de turn-2.
Este es también el curso 11 de esta serie. La próxima vez que tu agente no pueda decir dónde se torció, ya no vas a tener en la mano solo la frase «el modelo se lo inventó»: vas a tener un log al que hacerle grep, un árbol donde puedes señalar una línea concreta y hablar, una tabla de resumen con la que calcular el costo, y un conjunto de métodos de caminar que va del rastreo del síntoma al primer punto de divergencia. Lo que queda es mover enteros los tres tramos de observabilidad de observed-agent.mjs (logger, árbol de trazas, resumen de métricas) a tu propio arnés, envolver esas dos capas alrededor de tu bucle siguiendo el patrón de la sección 7 —los fixtures y los stubs son el andamiaje didáctico de esta lección, no te los lleves— y después ejecutar la primera tarea real, a ver qué hay en ese primer run.log.jsonl de lo que originalmente no tenías ni idea.