Ir para o conteúdo

36 · Vendo o que aconteceu

Idiomas

O seletor entende turnos escritos em inglês. O português é a sua próxima gramática e ainda não foi lançado; até lá, um turno escrito em português não é lido como português.

Quando uma execução faz algo que você não esperava, você quer vê-la passo a passo: o que o roteador leu, o que cada verificação decidiu e por quê, qual chamada rodou, o que foi desfeito. stalwart trace mostra uma execução como uma linha do tempo, a partir do armazenamento que o seu agente já mantém ou dos spans que um tracer gravou, e cada decisão nela nomeia a regra que a tomou.

Uma execução como linha do tempo

stalwart trace tem três subcomandos:

  • stalwart trace show: a linha do tempo no terminal, um bloco por etapa, cada verificação com a sua resposta e os seus motivos, cada pausa por uma pessoa, cada desfazer e a etapa que ele desfaz.
  • stalwart trace html ... -o run.html: a mesma linha do tempo como um único arquivo HTML autocontido, sem script e sem requisição a lugar nenhum, para um ticket ou uma auditoria.
  • stalwart trace diff a.jsonl b.jsonl: onde duas execuções da mesma entrada seguiram caminhos diferentes: a primeira etapa cujas decisões diferem, e o que só cada execução fez ali.

show e html leem um arquivo de spans ou, com --thread e --store, o armazenamento do seu agente.

O mesmo só a partir do armazenamento

Uma execução que o seu agente guardou em storage: pode ser lida de volta sem nada gravado antes. O pedido que falhou do E6 (capítulo 34), com o armazenamento da loja em disco:

desk = Agent.from_file("agent.yaml")          # storage: sqlite:///runs.db

reply = desk.chat("charge order 4411 40.00, then update the crm for order 4411, "
                  "then provision the account for order 4411", user="u-17")
while reply.waiting:
    reply = desk.confirm("u-17", "approve")   # each write approved as it comes
print(reply.text)
desk.close()
This run stopped: provision_account failed (RuntimeError). Undone, latest first: update_crm (by restore_crm), charge_card (by refund_card).

A conversa é desk/u-17: o nome do agente e o da pessoa. Lida de volta a partir do armazenamento:

$ stalwart trace show --thread desk/u-17 --store sqlite:///runs.db
Run: thread desk/u-17
step -1
  node      input
step 0
  node      run
step 1
  node      run
step 2
  node      run
  check     charge_card -> confirm
  interrupt charge_card [write] (confirm)
step 3
  node      run
  check     charge_card -> allow
step 4
  node      run
  call      charge_card
step 5
  node      run
step 6
  node      run
step 7
  node      run
  check     update_crm -> confirm
  interrupt update_crm [write] (confirm)
step 8
  node      run
  check     update_crm -> allow
step 9
  node      run
  call      update_crm
step 10
  node      run
step 11
  node      run
step 12
  node      run
  check     provision_account -> confirm
  interrupt provision_account [write] (confirm)
step 13
  node      run
  check     provision_account -> allow
step 14
  node      run
  call      provision_account
step 15
  node      run
  undo      restore_crm -> allow undoes step 3
step 16
  node      run
step 17
  node      run
  undo      refund_card -> allow undoes step 1
step 18
  node      run
step 19
  node      run
step 20
  node      run

Cada escrita foi verificada e perguntada (confirm, [write]), aprovada (allow) e executada; provision_account falhou na etapa 14, e os dois desfazeres vêm em seguida, cada um verificado, o mais recente primeiro: restore_crm desfaz a atualização do CRM (etapa 3 do histórico) e refund_card a cobrança (etapa 1 do histórico). Uma etapa sem nada embaixo é uma que só fez a execução avançar.

Spans para qualquer ferramenta de OpenTelemetry

Um tracer grava um span em torno de cada um destes, gerados a partir do tracer do pacote:

Span Aberto em torno de
stalwart.node cada etapa de uma execução
stalwart.plan o roteador lendo um turno
stalwart.tool_check a verificação de ferramentas decidindo uma chamada
stalwart.tool uma ferramenta rodando
stalwart.compensate um desfazer rodando
stalwart.rollback uma execução que falhou sendo desfeita
stalwart.rewind uma execução retrocedida
stalwart.interrupt uma execução pausando por uma pessoa
stalwart.resume uma execução retomando com a resposta da pessoa

O span de uma verificação de ferramenta carrega a sua decisão, como estes atributos e como um evento stalwart.decision:

Atributo Contém
gen_ai.operation.name execute_tool, como as convenções GenAI do OpenTelemetry nomeiam uma chamada de ferramenta
gen_ai.tool.name a ferramenta
gen_ai.tool.call.id o id da chamada
stalwart.outcome a resposta da verificação: allow, confirm ou refuse
stalwart.decided_by qual etapa da verificação decidiu
stalwart.rule a regra, o fluxo ou o motivo por trás da resposta
stalwart.reasons todos os motivos que a verificação deu
stalwart.labels as marcas que a chamada carregava
stalwart.args_digest os 16 primeiros caracteres hexadecimais do digest dos argumentos, nunca os valores
stalwart.history_token um digest curto do token do histórico da sessão, nunca o token

gen_ai.* são as convenções GenAI do OpenTelemetry, então qualquer ferramenta que as leia mostra as chamadas; stalwart.* são nossos. Com pip install "stalwart-agents[otel]", os spans vão para o tracer provider do OpenTelemetry, e dele para o exportador que você configurou. Sem nenhum exportador, um tracer JSON-lines os escreve num arquivo, que stalwart trace show e diff leem.

O conteúdo fica desligado por padrão

Nenhum valor de argumento, mensagem, resultado ou turno é colocado num span a menos que o tracer seja criado com capture_content=True. Com o conteúdo desligado, um digest de 16 caracteres fica no lugar de cada um, de modo que dois spans podem ser comparados sem que nenhum guarde o valor. Ligá-lo acrescenta exatamente estes atributos:

Atributo Conteúdo desligado (o padrão) Conteúdo ligado (capture_content=True)
stalwart.arguments stalwart.arguments_digest os argumentos da chamada, como valores
stalwart.result stalwart.result_digest o resultado da ferramenta
stalwart.message stalwart.message_digest uma mensagem da conversa
stalwart.turn stalwart.turn_digest o turno da pessoa, como digitado

Eles vão para onde vai todo outro atributo: para os exportadores do tracer provider que você lhe deu, ou para o arquivo JSON-lines que você nomeou. Um tracer não envia nada para nós, em nenhum dos casos: um trace é seu.

O validador de estado

O estado que uma execução mantém -- as suas mensagens, o seu histórico, o que espera por uma pessoa, o que desfazer -- precisa continuar consistente, e uma etapa que a sua equipe escreve pode quebrá-lo. O validador nomeia cada quebra:

Violação Significa
call_without_result uma chamada que o modelo pediu não tem resultado, e a conversa seguiu adiante
result_without_call um resultado não responde a nenhuma chamada da conversa
results_out_of_order os resultados de uma etapa não a seguem na ordem das suas chamadas
duplicate_message_id duas mensagens compartilham um id
message_after_interrupt uma mensagem foi acrescentada enquanto uma pausa por uma pessoa ainda estava aberta
stack_step_not_in_history a lista do que desfazer nomeia uma etapa que o histórico não tem
history_broken as etapas do histórico não são 0, 1, 2, ..., ou ele tem entradas e nenhum token
history_token_invalid o token do histórico não confere: uma entrada foi editada, removida ou reordenada fora da execução
label_on_removed_message uma marca nomeia uma mensagem que a conversa não guarda mais

stalwart trace show e html marcam a primeira quebra numa execução lida do armazenamento com <<< FIRST VIOLATION, na etapa que a causou, para que a linha do tempo mostre onde o estado deu errado, e não onde ele falhou mais tarde.

Seguindo uma decisão até a sua regra

Cada verificação numa linha do tempo nomeia a sua resposta e quem a decidiu, e o seu span nomeia a regra:

  • stalwart.decided_by é a etapa da verificação de ferramentas que decidiu (capítulo 32);
  • stalwart.rule é a regra, o fluxo ou o motivo: a recusa de um rulebook nomeia o seu fluxo (capítulo 45), uma regra de ordem nomeia o vínculo que quebrou;
  • stalwart.reasons lista cada motivo que uma confirmação deu.

Se você já usa LangGraph

Os spans são abertos pela execução que recebe um tracer: desk.graph(tracer=JsonLinesTracer("run.jsonl")) compila o grafo do agente com stalwart_agents.langchain.core.tracing.JsonLinesTracer, e stalwart_agents.langchain.otel.OtelTracer abre os mesmos spans no OpenTelemetry, com NodeSpans como o callback que abre um por nó. validate_state(state) e o invólucro de depuração validating(graph), que para na primeira violação com o nó e a etapa, estão em stalwart_agents.langchain.graph. O capítulo 27 os mostra nos termos do próprio LangGraph.

Os limites

  • Uma linha do tempo lida do armazenamento mostra o que o armazenamento guardou: as verificações, as chamadas, as pausas e os desfazeres, não os tempos dos spans, e não o que aconteceu dentro das suas ferramentas.
  • O conteúdo capturado vai para onde o seu exportador o envia. Ligue-o só onde esse destino pode guardar as palavras dos seus usuários e os resultados das suas ferramentas.