El agente llevaba seis minutos corriendo. El spinner seguía girando. Cuando lo maté, /info cifró la factura en 4,18 $, y /report contó ochenta y nueve llamadas a herramientas en una tarea que yo había estimado en tres llamadas y unos pocos céntimos. Había leído el mismo archivo once veces. Había buscado un símbolo con grep, obtenido un resultado vacío, reformulado el grep, obtenido otro resultado vacío, y siguió haciéndolo — con educación, con confianza, caro — porque nada en el bucle le decía que parara.
Lo peor no fueron los cuatro dólares. Fue que no tenía ni idea de por qué hasta que me puse a investigar. El agente no lanza un stack trace cuando se tuerce. No se cae. Simplemente hace lo incorrecto en silencio, informa de éxito y te pasa la factura. Desde fuera, una ejecución brillante y una catastrófica son idénticas: texto desfilando, herramientas disparándose, una respuesta final.
Este artículo es la instrumentación que ojalá hubiera tenido conectada antes de esa ejecución en vez de después. Todo viene en Octomind, nuestro runtime de agentes en Rust de código abierto, y la última sección añade OctoHub delante para capturar lo único que el propio agente no puede mostrarte: los bytes en bruto que de verdad llegaron al modelo.
Por Qué los Agentes Fallan en Silencio
El software tradicional falla a gritos. Un null deref, un 500, una aserción fallida — el fallo tiene una forma, una ubicación, un número de línea. Los fallos de los agentes no tienen nada de eso, porque desde la perspectiva del runtime no falló nada. Cada llamada a la API devolvió 200. Cada herramienta salió con 0. El modelo produjo texto gramatical y plausible en cada paso. El comportamiento agregado fue erróneo, pero ninguna operación individual lo fue.
En la práctica los fallos se agrupan en cuatro formas, y cada una es invisible justo en el momento en que querrías atraparla:
- Herramienta equivocada, confianza correcta. El modelo recurre a
grepcuando debería haber leído el archivo, o pega a un shell genérico cuando hay una herramienta precisa ahí mismo. No duda — una selección de herramienta errónea se ve exactamente igual que una correcta desde fuera. (Escribimos un artículo entero sobre estrechar la superficie de herramientas para combatir esto: los MCP personalizados pertenecen a tu repo.) - Truncamiento de contexto. La conversación creció más allá de la ventana, la compresión se activó y un dato que el agente necesitaba quedó resumido y perdido. Ahora razona desde una memoria con pérdidas y no ves la costura.
- Bucles desbocados. Resultado vacío → reformular → resultado vacío → reformular. Cada turno es individualmente razonable. El patrón es el bug, y solo lo notas por el conteo de llamadas a herramientas.
- Explosiones de tokens. Una herramienta volcó un archivo de 200 KB en el contexto, o un único argumento de llamada se hinchó, y ahora cada turno posterior reenvía todo eso. El coste se vuelve superlineal y el único síntoma es la factura.
No puedes depurar lo que no puedes ver. Así que el primer trabajo es hacer la ejecución observable — después del hecho y durante.
Nivel 0: /info — ¿A Dónde Se Fue el Dinero?
La pregunta más rápida de responder es "qué costó esta sesión en realidad, y con qué forma". Dentro de cualquier sesión interactiva, /info:
╭ /info
│ session my-feature-x
│ model openrouter:anthropic/claude-sonnet-5
│ tokens 214,883 total
│ breakdown 18,402 in · 9,114 out · 184,201 cache rd · 2,890 cache wr · 276 reasoning
│ cost $4.18661
│ throughput 41.3 tok/s
╰ /info my-feature-x
Cada campo aquí es un diagnóstico. El que destapó mi caso de bucle desbocado fue la fila breakdown. 184,201 cache rd frente a 18,402 in significa que el mismo contexto se releía en casi cada turno — la firma de un bucle que sigue anexando sin resolver nunca. El conteo de llamadas a herramientas que lo confirmó vive un nivel más abajo, en /report.
/info también desglosa el gasto más allá del bucle principal — los tokens y el coste propios del modelo de compresión, la colocación de marcadores de caché con sus totales de lectura/escritura, cualquier subagente y el supervisor tienen cada uno su propia sección — así que cuando la factura es alta puedes ver si fue la conversación principal o un proceso en segundo plano lo que se la comió.
Si la sesión se ha comprimido en algún momento, /info muestra un bloque de compression: cuántas compresiones se dispararon, mensajes eliminados, tokens ahorrados, ratio medio. Esa es tu alerta temprana de truncamiento. Si ves tres compresiones a nivel de proyecto en una sesión corta, el agente ha estado olvidando cosas, y eso conviene saberlo antes de fiarte de sus conclusiones.
Nivel 1: /report — Coste Por Petición, No Por Sesión
/info es el total de la sesión. /report es el recibo desglosado — una fila por petición de usuario, reconstruida a partir del registro de sesión:
╭ /report
│ # request cost tools task ai proc
│ ── ─────────────────────────────── ──────── ───── ─────── ─────── ───────
│ 1 add a health-check endpoint $0.04120 3 18s 12s 1s
│ 2 wire it into the router $0.02980 2 11s 9s 1s
│ 3 why is the test flaky $3.98120 84 5m 31s 3m 40s 20s
│ ── ─────────────────────────────── ──────── ───── ─────── ─────── ───────
│ Σ 3 request(s) $4.05220 89 6m 00s 4m 01s 22s
╰ /report 3 request(s) · $4.05220
Ahí está. La petición 3 — "why is the test flaky" — es toda la factura. Ochenta y cuatro llamadas a herramientas para una pregunta. Dos peticiones que se portaron bien rodean un desastre, y sin el desglose por petición todas se difuminan en el mismo total de sesión. /report es cómo encuentras qué prompt mandó al agente fuera de la vía, que es la pregunta que de verdad necesitas responder antes de poder arreglar nada.
Las columnas separan el tiempo task (toda la petición, de principio a fin) del tiempo ai (solo las llamadas al modelo). Cuando task supera enormemente a ai, tus herramientas son lentas. Cuando van a la par, el modelo está haciendo muchas idas y vueltas — otra vez la firma del bucle.
Nivel 2: /context — Lee en Qué Está Pensando Realmente el Agente
El coste y los conteos te dicen que algo salió mal. Para ver qué, lees los mensajes. /context vuelca la conversación en vivo como JSON estructurado, con filtros:
/context # todo
/context large # solo mensajes de más de 1000 chars — encuentra el devorador de contexto
/context tool # solo resultados de herramientas — ve qué recibió de verdad el agente
/context assistant # solo los turnos del propio modelo
/context large es el que uso ante explosiones de tokens. Saca a la luz exactamente los mensajes que inflan la ventana — normalmente un resultado de herramienta que devolvió un archivo gigante o un enorme blob de JSON — y ahora sabes a qué herramienta hay que enseñarle a paginar o truncar. En mi desastre del test flaky, /context tool mostró ochenta y pico resultados de grep todos vacíos. El modelo nunca ajustó la estrategia porque nada le decía que la estrategia no funcionaba. Leer los resultados de herramientas hizo el bucle obvio en unos quince segundos.
Nivel 3: El Registro de Sesión — El Historial Completo y Reproducible
Todo lo anterior lee de un solo artefacto: el archivo de sesión. Octomind escribe cada sesión en
~/.local/share/octomind/sessions/<name>.jsonl.zst
Es JSONL comprimido con zstd — un objeto JSON por línea. La mayoría de las líneas son los mensajes en bruto de la conversación (role: user|assistant|tool|system). Intercalados van marcadores tipados que el runtime necesita para reconstruir el estado al reanudar:
| Marcador | Qué registra |
|---|---|
STATS |
Totales acumulados — coste, tiempo de API, tiempo de herramientas — en ese punto |
COMPRESSION_POINT |
Se disparó una compresión: tipo, mensajes eliminados, tokens ahorrados |
RESTORATION_POINT |
Un checkpoint de /done — los mensajes previos se colapsan al recargar |
KNOWLEDGE_ENTRY |
Un dato extraído durante la compresión, reinyectado al reanudar |
COMMAND |
Un comando de runtime (/model, /role, /effort…) reproducido al reanudar |
PLAN_SNAPSHOT / SCHEDULE_SNAPSHOT |
El plan / la agenda activos, para que sobrevivan a un reinicio |
Este archivo es la verdad de base. /report se genera literalmente descomprimiéndolo y recorriendo las entradas STATS y USER/COMMAND para repartir el coste entre peticiones. Puedes hacer lo mismo tú:
zstd -dc ~/.local/share/octomind/sessions/my-feature-x.jsonl.zst \
| jq -r 'select(.role == "tool") | "\(.content | length)\t\(.name)"' \
| sort -n | tail
Ese one-liner ordena los resultados de herramientas por tamaño directamente desde el registro — tus devoradores de tokens, sin siquiera abrir una sesión. Como el registro es JSONL append-only, también es un artefacto de replay perfecto: reanuda la sesión exacta con octomind run --resume my-feature-x, o toma la más reciente del directorio actual con --resume-recent, y el runtime reconstruye el estado a partir de estas mismas líneas.
Nivel 4: RUST_LOG — Cuando Necesitas Ver Dentro del Runtime
El registro de sesión muestra cuál fue la conversación. Cuando necesitas ver qué hizo el runtime — por qué no cargó un servidor MCP, por qué se saltó una herramienta, qué devolvió de verdad el proveedor — enciende el trazado. Octomind está construido sobre el crate tracing, tras la variable de entorno estándar RUST_LOG. En modo CLI, deliberadamente no hay subscriber de trazado a menos que lo pidas — los usuarios obtienen salida limpia y coloreada, no una manguera. Pon RUST_LOG y la manguera se abre:
# Todo a debug
RUST_LOG=debug octomind run
# Acótalo a un módulo — mucho más útil en la práctica
RUST_LOG=octomind::mcp=debug octomind run
# Varios ámbitos, niveles mezclados
RUST_LOG=octomind::session=debug,octomind::mcp=trace octomind run
El alcance importa. RUST_LOG=debug en una sesión ocupada es ilegible; RUST_LOG=octomind::mcp=debug cuando una herramienta no aparece te dice exactamente qué candidato fue aceptado o rechazado y por qué. Este es el nivel en el que "el agente no ve mi herramienta" deja de ser un misterio.
En los modos de salida estructurada — ACP y WebSocket — stdout y stderr están reservados para el protocolo, así que el trazado va a archivos:
~/.local/share/octomind/logs/acp-debug.log ← trazado ACP
~/.local/share/octomind/logs/acp-errors.jsonl ← errores ACP, estructurados
~/.local/share/octomind/logs/websocket-debug.log ← trazado WebSocket
Si ejecutas Octomind detrás de un editor por ACP y algo va mal, esos archivos son donde vive la evidencia.
Nivel 5: --format jsonl — Canaliza la Ejecución a Tu Propio Tooling
Todo lo anterior es para un humano leyendo una terminal. En cuanto pones un agente en CI o en un pipeline, necesitas la ejecución como un flujo de eventos estructurados sobre los que puedas hacer aserciones. octomind run --format jsonl emite un objeto JSON por línea — el mismo flujo de eventos interno que usa el servidor WebSocket — etiquetado por type:
echo "audit the auth module" | octomind run developer:general --format jsonl
Cada línea es un evento discreto. Las variantes que te importarán:
type |
Lleva |
|---|---|
assistant |
Un fragmento del texto de respuesta del modelo |
thinking |
Contenido de razonamiento, separado de la respuesta |
tool_use |
tool, tool_id, server, params — el agente va a actuar |
tool_result |
tool, content, success — lo que volvió |
cost |
session_tokens, session_cost, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens |
error |
Un mensaje de fallo |
injected |
Un turno no-usuario — temporizador programado, agente en segundo plano, skill — con su source_kind |
skill |
Una skill activada, usada u olvidada |
Ahora los modos de fallo se vuelven aserciones. Cuenta eventos tool_use y rompe el build si una sola petición supera un umbral — ese es tu cable trampa para bucles desbocados. Vigila el session_cost del evento cost y alerta al pasar un presupuesto. Filtra tool_result por success: false. El agente que antes fallaba en silencio ahora falla de un modo que jq puede atrapar:
echo "run the migration check" \
| octomind run --format jsonl \
| jq -c 'select(.type == "tool_use") | .tool' \
| sort | uniq -c | sort -rn
Eso imprime un histograma de llamadas a herramientas de toda la ejecución. Si shell está arriba con un conteo de 60, has encontrado tu bucle antes de que encuentre tu cartera — y es un one-liner en un paso de CI, no una persona mirando un spinner.
Nivel 6: OctoHub — Captura Cada Petición Upstream
Hay una cosa que nada de lo anterior puede mostrarte, porque ocurre por debajo del agente: los bytes exactos que Octomind envió al proveedor y los bytes exactos que volvieron. La vista del agente son sus propios mensajes. No puede mostrarte la petición a nivel de cable — el modelo resuelto, el payload serializado completo, la respuesta en bruto del proveedor, la latencia real. Cuando sospechas que el problema está en la capa de traducción (un prompt de sistema que no es el que crees, un esquema de herramienta que el proveedor estropeó, un modelo que no es el que configuraste), necesitas ver el cable.
OctoHub es nuestro proxy de LLM, y el registro completo de petición/respuesta es su razón de existir. Apunta Octomind a él en vez de al proveedor, y cada completion aterriza en una base de datos con la entrada, la salida y las métricas adjuntas. Arráncalo:
./octohub # escucha en 127.0.0.1:8080 por defecto
Habla tanto POST /v1/completions (su forma nativa) como POST /v1/chat/completions (OpenAI clásico, drop-in para cualquier cliente compatible con OpenAI), y ambos pegan al mismo motor y escriben en la misma tabla completions con el mismo id de registro. Tras una ejecución, recupera los registros en bruto con la API de admin:
curl "http://127.0.0.1:8080/v1/admin/completions?limit=50" \
-H "Authorization: Bearer <master-key>"
Cada registro lleva la imagen completa que el agente no podía:
{
"id": "cmpl_<uuid>",
"session_id": "<uuid>",
"input_model": "my-model",
"resolved_model": "gpt-5.5",
"provider": "openai",
"usage": {
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cost": 0.0001,
"request_time_ms": 320
},
"input": [...],
"output": [...],
"created_at": 1700000000
}
input_model frente a resolved_model por sí solo atrapa toda una clase de bugs de "por qué se comporta distinto" — pediste un modelo, un alias resolvió a otro. Los arrays input y output son los payloads literales, así que un esquema de herramienta con el que el proveedor se atragantó está ahí para leerlo. request_time_ms es la latencia real del proveedor, separada de cualquier cosa que Octomind añadiera. Y GET /v1/admin/usage lo agrega todo por clave de API y bucket de tiempo, que es como pasas de "el agente es caro" a "esta clave, este modelo, esta hora" sin adivinar. (Si ejecutas agentes contra varios proveedores a la vez, el proxy es también donde eso se vuelve sensato — ve ejecutar un agente en muchos modelos.)
El proxy convierte el límite del modelo de un borde opaco en una superficie registrada y consultable. El almacenamiento por defecto es SQLite — db_url = "sqlite://octohub.db", o pon OCTOHUB_DB_URL — así que no hay infraestructura que levantar antes de empezar a leer peticiones.
Cuando el Agente Hace X, Mira Y
Todo el sentido es convertir "el agente hizo algo desconcertante" en una búsqueda conocida. Esta es la tabla pegada encima de mi escritorio:
| Síntoma | Mira primero | Qué buscas |
|---|---|---|
| Coste de sesión de la nada | /info |
cache rd ≫ in en la fila breakdown |
| Un prompt costó todo | /report |
La única fila que carga la factura |
| Ventana de contexto llena / el modelo "olvidó" | bloque compression de /info, luego /context large |
El conteo de compresión y el mensaje sobredimensionado |
| Bucle / llamadas a herramientas repetidas | columna tools de /report, o jsonl + uniq -c |
Misma herramienta, mismos args, sin progreso |
| Herramienta no disponible para el agente | RUST_LOG=octomind::mcp=debug |
Por qué se saltó el candidato |
| El resultado de herramienta se ve mal | /context tool |
Los bytes reales que recibió el modelo |
| Se comporta como otro modelo | OctoHub GET /v1/admin/completions |
input_model frente a resolved_model |
| Error de proveedor o latencia rara | input/output del registro de OctoHub, request_time_ms |
El payload en bruto y la temporización real |
| Hace falta un cable trampa en CI | --format jsonl |
Contar tool_use, vigilar cost, atrapar error |
| Reproducir una ejecución exacta | octomind run --resume <name> |
Replay desde <name>.jsonl.zst |
El Diagnóstico de Dos Minutos
Así habría ido el desastre del test flaky con esto conectado desde el principio, en vez del misterio de seis minutos que fue en realidad.
El spinner corre mucho. /report — la petición 3 es 3,98 $ y 84 llamadas a herramientas; las otras dos están bien. Así que es ese prompt. /context tool — ochenta resultados vacíos de grep, el modelo nunca cambiando de estrategia. Ahí está el bucle, y ahí está el porqué. Tiempo total hasta la causa raíz: unos noventa segundos, sin cuatro dólares de por medio, porque en un pipeline real el histograma de llamadas a herramientas de jsonl habría disparado un umbral y matado la ejecución en la llamada número veinte.
Nada de esto es exótico. Es el mismo instinto que el logging, las métricas y el trazado en cualquier sistema que pondrías en producción — aplicado a un sistema cuyos fallos son silenciosos por naturaleza. El agente no te dirá que está perdido. Pero la ejecución es totalmente observable si se la interrogas bien: /info para la forma, /report para el culpable, /context para el razonamiento, el registro de sesión para el historial, RUST_LOG para el runtime, --format jsonl para la máquina y OctoHub para el cable.
Conéctalo antes de la ejecución cara, no después.
— Don
Octomind y OctoHub son de código abierto bajo Apache-2.0. Si falta una superficie de depuración que necesitas, abre un issue — la observabilidad de este artículo existe en gran parte porque nuestros propios agentes no paraban de sorprendernos.



