← Volver al blog

Del stack trace al pull request: el ciclo de corrección con un agente de IA

Cómo expongo los errores de producción en JSON para un agente de IA (y dejo que un humano apruebe el PR).

djangoobservabilityai-agentsproductivitysoftware-engineeringautomation

Cada vez que aparecía un error de producción en la pantalla de soporte del sistema, el proceso era el mismo: abría la página, copiaba el mensaje de error, lo pegaba en la conversación con la IA y pedía un diagnóstico.

Funcionaba. Pero era lento, frágil y dependía de mí en cada eslabón.

Hasta que un FileNotFoundError en una pantalla de manuales operacionales me hizo ver el problema real: no tenía forma de darle al agente de IA la capacidad de descubrir los errores por sí solo. Solo veía lo que yo pegaba.

La pregunta que me guió fue simple:

¿Cómo exponer los errores de producción en JSON, de forma segura, para que un agente de IA cierre el ciclo de diagnóstico y corrección — con un humano revisando y aprobando el pull request antes del deploy?

El resultado es una API pequeña, con decisiones grandes. Voy a contar qué construí, por qué lo construí así y qué aprendí en el camino.


El problema: errores atrapados en una pantalla HTML

El sistema es una aplicación Django. Los errores de producción eran capturados por un middleware y guardados en una base de datos, pero la única forma de verlos era una pantalla HTML protegida por login de staff.

Antes de seguir, un contexto que explica el tamaño de la solución. La aplicación no corre en una nube grande. Por ahorro, está alojada on-premise, para uso interno, pero expuesta públicamente en internet para algunos usuarios que trabajan de forma remota. Eso significa dos cosas al mismo tiempo: los errores de producción llegan a usuarios reales del lado de afuera — y no hay presupuesto ni equipo para una plataforma de observabilidad. No tenemos APM, tracing distribuido ni dashboard de métricas. Lo que tenemos es la propia aplicación, la base de datos que ya usa y esta API de errores que vive dentro de ella. La limitación no es falta de tiempo: es el presupuesto.

Eso creaba tres límites:

  • Ningún agente puede descubrir errores solo. Sin acceso programático, depende de que alguien pegue el texto.
  • No se puede filtrar por deploy. Sin saber de qué publicación vino el error, era imposible responder “¿esto se rompió después de la última publicación?”.
  • No hay seguimiento de lo que se corrigió. Nada marcaba un error como resuelto.

Lo que yo quería era lo contrario: el agente descubre, diagnostica y propone; el humano revisa, aprueba y publica.


La solución en una frase

Un middleware captura todo error HTTP 500+ y guarda un registro estructurado. Una API JSON, autenticada por token de entorno, expone esos registros al agente — con stack trace sanitizado, identificación del deploy y la posibilidad de marcar el error como corregido.

 navegador ──▶ Django
                  ├─ RequestErrorMiddleware  → graba RequestError
                  ├─ /support/errors         → humano (HTML)
                  └─ /api/request_errors     → agente de IA (JSON)
graph LR
    Nav[Browser] ---|Request| Django[Django]
    
    subgraph Django App
        Django --> Middleware[RequestErrorMiddleware]
        Django --> HTML_Route["/support/errors"]
        Django --> JSON_Route["/api/request_errors"]
    end

    Middleware --> DB[(writes RequestError)]
    HTML_Route -->|HTML| Human[Human]
    JSON_Route -->|JSON| AIAgent[AI Agent]

Lo que el agente puede hacer

GET    /api/request_errors/?status=AB&limit=20   # errores abiertos
GET    /api/request_errors/?boot_id=<deploy>     # lo que se rompió después del deploy X
GET    /api/request_errors/{id}                  # detalle
PATCH  /api/request_errors/{id}  {"status":"CO"} # marcar como corregido

Cuatro operaciones. Pocas a propósito: la acción destructiva de limpiar el historial no existe en la API — solo en la pantalla humana.


Las decisiones que importan (y por qué importan)

1. Token de entorno, deshabilitado por defecto

La API global usa cookie de sesión. Un agente HTTP no tiene sesión. Entonces el token va en un header (X-API-Token) y se compara en tiempo constante (secrets.compare_digest).

El detalle más importante: sin la variable de entorno configurada, el endpoint responde 401. El estado por defecto es “no expone nada”. Seguridad por omisión, no por configuración.

2. El traceback no se corta por el final

Esta fue la decisión más interesante — y la que más me enseñó.

La primera versión del traceback se truncaba a 40 líneas cortando por el final. El problema: el final del stack trace de Django es casi solo django/db/backends, que es donde Django re-lanza el error, no donde ocurrió.

Resultado: el agente recibía un montón de código de Django y no lograba hallar la causa. El fix pasó a mantener los frames del proyecto (marcados por /editora/ o /editor_site/), que quedan en el inicio del stack. Sin ningún frame del proyecto, ahí sí cae a las últimas 40 líneas — donde está el punto del raise.

Cómo cortas el stack trace es una decisión de producto. Entregar el stack equivocado es lo mismo que no entregar stack ninguno.

3. Lo que entra en el payload (y lo que sale)

El contexto de la solicitud se graba completo en la base de datos — la pantalla humana lo necesita. Pero en la entrega por la API, dos campos se eliminan:

  • session — la clave de sesión permite secuestrar la sesión de quien hizo la solicitud.
  • body — carga los datos de la solicitud, posiblemente sensibles.

Grabación completa, entrega sanitizada. Son niveles de exposición diferentes para públicos diferentes.

4. boot_id: el filtro que cierra el ciclo

Cada error carga el boot_id del proceso que lo generó. Eso le permite al agente responder a la pregunta más importante del ciclo de deploy:

“Después de que corregí y publiqué, ¿el error paró?”

Sin eso, la confirmación de la corrección depende de corazonadas.

5. Confiar en el secreto (de forma explícita)

El endpoint no verifica is_staff. Quien tenga el token ve los errores de todos los usuarios. Es el mismo nivel de exposición de la pantalla de soporte, pero vía secreto.

Es un compromiso consciente: el token es la barrera. Si se filtra, rotar la variable deshabilita el acceso inmediatamente.


El ciclo completo (lo que cambia en la práctica)

 1. Agente: GET /api/request_errors/?status=AB
 2. Agente: lee traceback + endpoint + boot_id + contexto
 3. Agente: diagnostica la causa y escribe la corrección
 4. Agente: abre un Pull Request
 5. Humano: revisa y aprueba
 6. Humano: hace el deploy
 7. Agente: GET /api/request_errors/?boot_id=<nuevo_deploy>  → confirma que paró
 8. Agente: PATCH /api/request_errors/{id}  {"status":"CO"}

El agente hace descubrimiento, diagnóstico y propuesta. El humano se queda con la decisión. Eso es lo que llamo automatización con supervisión: el trabajo repetitivo y de lectura sale de mis espaldas; la responsabilidad final sigue siendo mía.


En acción: una conversación con el agente

La teoría es linda. En la práctica, el ciclo empieza con una frase simple.

El prompt

verifique los errores en el ambiente de producción

Eso es todo. Ningún ID, ningún link, ningún copy-paste de stack trace.

Lo que el agente hace solo

El agente carga las variables de entorno (X_API_TOKEN, X_API_URL), arma las llamadas y consulta la API directamente:

# 1. Errores abiertos — lo que necesita atención ahora
curl -s -H "X-API-Token: $X_API_TOKEN" \
  "$X_API_URL/api/request_errors/?status=AB&limit=20"

# 2. Errores en análisis — ¿alguien ya miró?
curl -s -H "X-API-Token: $X_API_TOKEN" \
  "$X_API_URL/api/request_errors/?status=AN&limit=10"

# 3. Todos los errores — panorama completo
curl -s -H "X-API-Token: $X_API_TOKEN" \
  "$X_API_URL/api/request_errors/?limit=500"

La respuesta del agente

En segundos, devuelve un análisis estructurado:

Dashboard de Errores — Producción (Fly.io)

Métrica Valor
Total 3
🟢 Abiertos 0
🟡 En Análisis 1
🔵 Corregidos 2
ID Fecha Excepción Endpoint Estado Usuario Boot ID
3 15/10 11:50 FileNotFoundError manual_operacao AN guionardo 1792075704
2 08/10 13:23 OperationalError (database is locked) editora_pedidoitem_delete CO Mara 1790299626
1 09/09 10:39 FieldError (icontains en FK) editora_contato_changelist CO Akauana 1788617182

Diagnóstico por error

#3 — FileNotFoundError

La vista manual_operacao intentó abrir ../../docs/MANUAL_OPERACIONAL.md desde /app/editora/views/. En el container de Fly.io, el path relativo no resuelve. El archivo probablemente no fue incluido en la imagen Docker. Sugerencia: usar path absoluto via settings o incluir el archivo en el build.

#2 — database is locked

Lock de escritura en SQLite durante eliminación de PedidoItem por el admin. El traceback se trunca en los frames del proyecto — en este caso, el relevante es django/db/backends/sqlite3/base.py:360, donde el lock explotó después de 30 segundos. Como fue un error aislado, indica transacción larga, no evento sistémico. Sugerencia: monitorear recurrencia; investigar transacciones en exclusoes.py.

#1 — FieldError

Búsqueda q=luana en el admin de Contato usó icontains en ForeignKey. Django rechaza lookup icontains directo en FK. Sugerencia: revisar search_fields del admin, usar cliente__nome__icontains.

El punto que importa

El usuario no necesitó abrir una pantalla, no copió nada, no tradujo boot_id à fecha de deploy. El agente hizo el puente entre la pregunta en lenguaje natural y los comandos estructurados de la API. El humano recibió contexto, hipótesis y acciones sugeridas — no un JSON crudo.


Lo que aprendí

  1. Exponer un error en JSON es exponer stack trace, nombre de archivo y contexto de usuario. No existe exposición cero — existe exposición controlada por secreto, con el mínimo en el payload.
  2. El detalle “aburrido” es el que rompe la automatización. El agente no falló por falta de inteligencia; falló porque el traceback entregado no tenía el frame del proyecto. La herramienta estaba mal, no el agente.
  3. Deshabilitar por defecto es una feature. Un endpoint que responde 401 sin configuración es un endpoint que nunca se filtra por olvido.
  4. La acción destructiva no pertenece a la API. Si el agente puede borrar historial, un error de diagnóstico se convierte en pérdida de datos.
  5. La confirmación objetiva vale oro. El boot_id transforma “creo que se resolvió” en “el error paró después de este deploy”.
  6. El presupuesto es un requisito de diseño. No comprar una plataforma de observabilidad no fue pereza: fue decisión. Eso empujó la solución hacia dentro de la aplicación, donde cuesta casi nada y resuelve el problema real.

Limitaciones honestas

Nada de esto es perfecto, y vale la pena decirlo — y, siendo justo, son limitaciones coherentes con el presupuesto y el tamaño del proyecto, no falta de tiempo:

  • Solo captura errores 500+. Los errores 4xx no entran (por decisión).
  • La deduplicación usa unique_together, así que un error recurrente no actualiza la fecha de la última ocurrencia — solo se guarda la primera.
  • No hay webhook: el agente necesita hacer polling.
  • El campo endpoint guarda el nombre de la ruta, no el path — detalle que ya me agarró (y que documenté).

Sanitizando stack traces: qué cortar, qué mantener

Un lector preguntó exactamente lo que importa: “En el momento que automatizás ese ciclo, los secrets empiezan a filtrarse si no tenés cuidado. ¿Cómo decidís qué truncar sin perder los frames que apuntan al bug?”

La respuesta corta: grabación completa, entrega mínima.

El problema: el traceback de Django es un documento de mala fe

Mirá un traceback típico de excepción en producción. Las primeras líneas son Django re-lanzando el error: asgiref, django/core/handlers/base.py, django/db/backends. Después viene tu código. Después Django relanza de nuevo. El error real está escondido en el medio de 80 líneas de framework.

La primera versión de la API cortaba las últimas 40 líneas. El agente recibía casi sólo código de Django. Veía execute_sql, thread_handler, sync.py — y no encontraba la línea de mi código. Entregar el stack equivocado es lo mismo que no entregar stack ninguno.

La estrategia: mantener los frames del proyecto (que quedan arriba)

La decisión no fue “cortar por el principio” o “cortar por el final”. Fue: identificar lo que es mío y preservarlo.

En Django, los frames del tu proyecto quedan en la parte de arriba de la pila — antes de que Django empiece a re-lanzar. Entonces la lógica pasó a ser:

  1. Buscar frames del proyecto — líneas con /editora/ o /editor_site/
  2. Mantenerlos arriba, con la línea de código que está justo debajo
  3. ¿Sin frames del proyecto? Caer a las últimas 40 líneas — donde está el raise
TRACEBACK_LINES = 40
MARCADORES_APP = ('/editora/', '/editor_site/')

def truncar_traceback(traceback: str, max_linhas: int = TRACEBACK_LINES) -> str:
    if not traceback:
        return ''
    linhas = traceback.splitlines()
    if len(linhas) <= max_linhas:
        return traceback

    # Encontrar todos los frames que son de nuestro proyecto
    frames = [
        i
        for i, linha in enumerate(linhas)
        if linha.lstrip().startswith('File ')
        and any(m in linha for m in MARCADORES_APP)
    ]

    # ¿Sin frames del proyecto? Tomar el final — donde está el raise
    if not frames:
        return '\n'.join(linhas[-max_linhas:])

    # Coleccionar frame + la línea de código justo debajo
    escolhidas = {0}  # línea 0 = "Traceback (most recent call last):"
    for i in frames:
        escolhidas.add(i)
        if i + 1 < len(linhas):
            escolhidas.add(i + 1)

    # Si pasó del límite, cortar por el principio (frames del proyecto)
    if len(escolhidas) > max_linhas:
        return '\n'.join(linhas[:max_linhas])

    return '\n'.join(linhas[i] for i in sorted(escolhidas))

Cómo funciona en la práctica

Error #3 — FileNotFoundError: el traceback crudo tiene ~25 líneas. Las primeras 15 son de Django. La línea 16 es:

  File "/app/editora/views/manual_operacao.py", line 44, in manual_operacao_view
    with open(MANUAL_FILE, encoding='utf-8') as f:

La lógica mantiene sólo esas 2 líneas + el encabezado. El agente ve inmediatamente: “Error en la vista manual_operacao, línea 44, intentando abrir un archivo.”

Error #2 — database is locked: acá no hay frames del proyecto en el traceback. La lógica cae al fallback de últimas 40 líneas. El agente recibe django/db/backends/sqlite3/base.py:360 in execute — correcto, porque el bug no está en nuestro código, está en la interacción con SQLite.

Dónde realmente viven los secrets

El lector acertó: el peligro no está en el stack trace, está en el request context. El middleware graba el contexto completo — incluyendo session (permite secuestro de sesión) y body (puede tener contraseñas). La API elimina:

CAMPOS_SENSIVEIS = ('session', 'body')

def limpar_contexto(contexto: dict | None) -> dict:
    if not contexto:
        return {}
    return {k: v for k, v in contexto.items() if k not in CAMPOS_SENSIVEIS}

Lo que queda: method, params, user_agent, referer, headers — lo suficiente para que el agente entienda “fue un POST del admin, CSRF token, usuario Mara” sin exponer datos que permitan secuestro de sesión.

El principio general

Capa Qué guarda Quién accede
Base de datos Traceback completo + contexto con session/body Pantalla humana de soporte
API Traceback truncado (frames del proyecto) + contexto sin session/body Agente de IA

Grabación completa, entrega mínima. La base de datos almacena todo porque un humano en soporte puede necesitarlo. La API entrega sólo lo que un agente necesita para diagnosticar, y nada más.

Decidir cómo cortar el stack trace es una decisión de producto, no de ingeniería. Estás eligiendo: quién va a leer, qué es ruido, y qué es señal. El agente no falla por falta de inteligencia; falla porque la herramienta entregó el mapa equivocado.


Conclusión

Construir una API de errores para agentes de IA parecía un problema de “cómo expongo esto en JSON”. En la práctica, era un problema de cuánto exponer, para quién y con qué confirmación.

La feature es pequeña. Las decisiones, no. Y la ganancia real no fue “el agente descubrió un error” — fue dejar de ser el intermediario entre el error y la corrección.

Si trabajas con agentes de IA en producción, mi sugerencia es: empieza por la observabilidad. Un agente sin acceso estructurado a los errores es un agente adivinando. Y si el presupuesto es corto, recuerda: no hace falta una plataforma de observabilidad para darle al agente lo que necesita — a veces, una API de 100 líneas dentro de la propia aplicación resuelve.


¿Y tú? ¿Cómo descubren errores en producción tus agentes de IA? ¿Pegan stack traces a mano o ya tienen una API para eso? Cuéntame en los comentarios.