Criando debug skills que inspecionam sistemas em produção
O agente com quem trabalho no dia a dia persiste o estado dele no Postgres. Quando algo parece errado — uma resposta que não bate com a entrada, um pedaço de contexto faltando, um step que não executou — o movimento natural de debugging é ler o estado persistido e ver o que o agente realmente achou que tava fazendo.
A primeira vez que precisei fazer isso, levou vinte minutos entre lembrar o schema, achar a tabela certa, decodificar o blob serializado e imprimir as partes relevantes. Na segunda vez, eu escrevi uma debug skill. Depois disso, virou: verificar o último checkpoint dessa thread. O agente lê a skill, roda o script, parseia a saída, me diz o que tá errado. Tempo total, menos de um minuto.
Esse post é um walkthrough de uma debug skill dessas, de ponta a ponta. O exemplo é genérico, mas a estrutura é a que eu realmente uso nas oito debug skills que escrevi pra esse projeto. Se você quiser escrever uma dessas, é assim que as minhas são.
O que estamos construindo
Uma debug skill que recebe um identificador de thread e imprime o último estado persistido daquela thread, num formato que um humano (e o agente de IA lendo a skill) consegue interpretar rápido.
Especificamente:
- Recebe um argumento obrigatório (o thread ID).
- Lê de uma tabela Postgres que guarda o estado persistido.
- Decodifica o blob binário que o framework escreve naquela tabela.
- Imprime um resumo legível: quais keys estão setadas, em qual step o pipeline chegou, se os campos esperados estão presentes.
- Opcionalmente faz dump do JSON decodificado completo pra inspeção mais profunda.
- Importa o decoder real usado pelo sistema em execução, em vez de reimplementar um. Essa última escolha importa mais do que parece.
Layout dos arquivos
A skill vive no próprio diretório:
.cursor/skills/agent-debug-checkpoint/
├── SKILL.md
└── scripts/
└── debug_checkpoint.py
SKILL.md é o que o agente de IA lê. scripts/debug_checkpoint.py é o que roda. Dois arquivos. Só isso.
O SKILL.md
Aqui vai o arquivo de documentação da skill, sem nomes específicos do projeto:
---
name: agent-debug-checkpoint
description: Read and decode the latest persisted state for a given thread.
Shows which keys are set, current pipeline step, and presence of expected
fields. Use when debugging persisted graph state vs the input that produced it.
---
# Debug agent checkpoint
Queries the persisted-state table for a `thread_id` and decodes the
state blob into a human-readable summary. Best-effort msgpack/JSON
decode; pass `--raw-json` to dump the full structure if needed.
## Quick start
```bash
working_directory: ai-automation-backend
poetry run python ../ai-automation-dev-agents/.cursor/skills/agent-debug-checkpoint/scripts/debug_checkpoint.py \
--thread-id 1711900000.000100
```
## Options
| Flag | Purpose |
|----------------|---------------------------------------------|
| `--thread-id` | Thread identifier (required) |
| `--limit` | How many checkpoint rows, newest first (3) |
| `--raw-json` | Dump full decoded JSON instead of summary |
## Backend reference
- `apps/.../persistence/checkpoint.py` — checkpoint table writer/reader
- `apps/.../state/serde.py` — serializer used by the runtime
Esse é o corpo inteiro da skill. O agente lê isso, sabe o comando, sabe as flags, e sabe onde vive o código-fonte do sistema se algo não estiver claro. Cinco coisas pra notar:
- A
descriptioné sobre quando usar a skill. Não “isso lê estado de checkpoint” — isso é só o nome com outras palavras. “Use when debugging persisted graph state vs the input that produced it” diz ao agente quando pegar essa skill em vez de outra. - O Quick start é copy-paste ready. Incluindo o working directory. O agente não precisa adivinhar.
- Sem prosa sobre por que isso importa. O agente não precisa. A skill é um procedimento, não uma redação.
O script
O script é um arquivo Python enxuto. Stdlib + uns poucos imports do projeto. A estrutura que funcionou pra mim:
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from _shared.backend_imports import setup_backend_path
setup_backend_path()
from apps.persistence.checkpoint import open_checkpoint_reader # noqa: E402
from apps.state.serde import decode_checkpoint # noqa: E402
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--thread-id", required=True)
parser.add_argument("--limit", type=int, default=3)
parser.add_argument("--raw-json", action="store_true")
args = parser.parse_args()
with open_checkpoint_reader() as reader:
rows = reader.fetch_recent(thread_id=args.thread_id, limit=args.limit)
if not rows:
print(f"No checkpoint rows for thread_id={args.thread_id!r}")
return 0
for row in rows:
decoded = decode_checkpoint(row.blob)
if args.raw_json:
print(json.dumps(decoded, indent=2, default=str))
continue
print(f"--- checkpoint @ {row.created_at.isoformat()} ---")
print(f"keys present: {sorted(decoded.keys())}")
step = decoded.get("pipeline_step")
print(f"current step: {step!r}")
for required in ("user_input", "parsed_request", "supervisor_route"):
present = required in decoded
marker = "✓" if present else "✗"
print(f" {marker} {required}")
return 0
if __name__ == "__main__":
sys.exit(main())
Algumas decisões de design nesse script de trinta linhas estão fazendo mais trabalho do que parecem.
Ele importa o decoder real. decode_checkpoint é a mesma função que o runtime usa. Se a serialização do runtime muda, o decoder muda, e o script de debug continua funcionando sem eu ter que lembrar de atualizar. Reimplementar o decoder no script — o que era tentador na época — teria produzido uma ferramenta que apodrecia silenciosamente conforme o codebase evoluía.
É stdlib-only acima do import do projeto. Sem novas dependências. O script usa argparse, json, sys, pathlib. As únicas coisas específicas do projeto são os imports que o runtime já tem instalados. Isso significa que nunca preciso gerenciar um requirements.txt separado pra scripts de skills.
O modo resumo vem primeiro; o modo raw-json é opt-in. A saída padrão é legível pra humanos. O agente lendo a saída consegue resumir de volta pra mim sem parsear JSON. A flag --raw-json existe pros casos em que o resumo não basta, mas a maioria das invocações não precisa disso.
A saída é grep-friendly. keys present:, current step:, os check/cross marks — um humano consegue escanear isso em dois segundos, e o agente consegue extrair os fatos booleanos diretamente. Isso importa quando a saída da skill se torna a entrada pro próximo passo de raciocínio do agente.
Iterando no formato de saída
As primeiras versões imprimiam tudo ou não imprimiam nada útil. O que funcionou, depois de algumas iterações, foi um formato de resumo com três propriedades:
- Sempre mostra a mesma forma. O agente sabe o que esperar: keys presentes, step atual, presença-ou-ausência de uma lista conhecida de campos obrigatórios. A saída é previsível.
- Inclui as coisas que eu sempre quero saber. Qual step o pipeline alcançou. Se os campos esperados estão populados. O timestamp do checkpoint.
- Mostra o que está faltando. Um
✗ao lado de um campo ausente é mais informativo que um resumo limpo que omite o campo inteiramente. Ausência é dado.
Chegar nesse formato levou umas cinco iterações em sessões reais de debugging. Em cada sessão, quando o resumo não tinha a resposta que eu precisava, eu adicionava o campo que eu tinha procurado e não encontrado. Depois de mais ou menos uma semana o formato estabilizou.
Esse loop de iteração é normal. Não tente projetar a saída de uma debug skill no abstrato. Projete a V1 pra ser minimamente funcional. Itere a partir de bugs reais.
Outras debug skills que seguem o mesmo padrão
A de checkpoint é uma de oito debug skills que escrevi pro mesmo projeto. As outras seguem a mesma estrutura — um SKILL.md e um script que importa a coisa real — mas inspecionam superfícies diferentes.
- Uma debug skill de cache lê valores pré-computados e mostra a freshness em relação ao timestamp da fonte de verdade. “Esse score cacheado tá stale?”
- Uma debug skill de state explica como um novo input inicial faz merge com o último estado persistido. Útil pra entender por que uma key de um turn anterior sobreviveu inesperadamente.
- Uma debug skill de trace interpreta spans de tracing distribuído, mapeando nomes de span de volta pros nodes do pipeline. “Qual step realmente executou nessa run?”
- Uma debug skill de thread-context busca uma thread de chat e imprime o mesmo contexto resumido que o runtime veria. “O que o agente realmente tem acesso aqui?”
- Uma debug skill de pipeline roda um subset isolado do pipeline (só entity resolution, só routing, a chain completa) num input de amostra, opcionalmente com chamadas externas mockadas. “O que esse node único produz nesse input?”
Cada uma dessas começou como uma investigação manual que demorava demais. Cada uma agora leva segundos quando o agente a invoca. Cada uma segue as mesmas regras estruturais: importa o código em produção, formato de saída previsível, modo raw opt-in, escopo estreito.
O que faz uma debug skill não valer a pena escrever
Debug skills são baratas, mas não são de graça. Alguns padrões que aprendi a recusar.
Skills que encapsulam algo já trivialmente consultável. Se a resposta é SELECT * FROM users WHERE id = ?, você não precisa de uma skill. Você precisa de uma nota de uma linha no readme de debugging do time.
Skills que dependem de infraestrutura transitória. Se o único lugar pra ler esse estado é de um serviço que tá prestes a ser deprecado, a skill vai apodrecer antes de se pagar.
Skills sem um “quando” claro. Se você não consegue escrever a description como “use when …” — se o propósito da skill é só “debugging geral” — não é uma skill ainda. É uma pasta de scripts.
Skills que tentam ser espertas. Uma debug skill que tenta interpretar o estado e te dizer o que tá errado é uma fera diferente de uma que mostra o estado e te deixa decidir. As minhas mostram. Interpretação é trabalho do agente, não da skill.
O efeito composto
Depois de oito debug skills, algo mudou silenciosamente na forma como eu trabalho nesse projeto. Quando algo tá errado, eu não penso mais em como investigar. Eu penso em o que tá errado. A investigação foi pré-cacheada, na forma de corpos de skills que o agente pega automaticamente.
Esse é o resultado real. Não “eu tenho uma pasta de scripts.” Uma pasta de scripts é um diretório de ferramentas, e diretórios de ferramentas existiam antes de agentes de IA. A mudança é que a camada de invocação agora é inteligente. O agente lê a skill, decide qual se aplica, roda, interpreta a saída, e alimenta o resultado no próximo passo de raciocínio. A skill é só a ponte. A inteligência está nos dois lados.
É por isso que escrever elas parece tempo bem gasto. Cada uma move um pouco mais da investigação para fora da minha cabeça e para dentro do sistema. Quando existem skills suficientes, o sistema em si se torna inspecionável de um jeito que não era antes.