Skip to content

Observabilidade

O controle de fluxo de um agente é decisão de um modelo, então "o que aconteceu" não é dedutível do seu código. Tudo abaixo existe para torná-lo dedutível a partir de dados.

Quatro destinos, um stream

DestinoQuandoPersisteUse para
logao vivo, terminalnãodesenvolvimento, e uma requisição em produção
Flowao vivo, grafo no navegadornãoacompanhar uma execução lenta ou travada
reportdepoissimdepurar, medir, relatar bug
um pluginao vivo, onde você quiserseuprodução

Os quatro recebem o mesmo stream de ExecutionEvent.

Meça estrutura, não prosa

O ponto dos dados estruturados nos nós é que responder "com que frequência as tools falham" deveria ser uma contagem, não um regex:

PerguntaSinal
os laços estão convergindo?exhausted nos nós loop
com que frequência as tools falham?nós tool com status: "error"
o provider está instável?attempts presente nos nós chat
o modelo está no limite?toolCallSource: "rescued"
o histórico está crescendo?promptTokens por turno
quanto custa uma execução?costUsd no nó workflow

O toolCallSource merece atenção: "rescued" significa que o modelo escreveu a tool call como texto e o framework recuperou. Funciona, mas uma taxa crescente é aviso antecipado de que a tarefa passou do que o modelo dá conta.

Produção: um plugin, não o report

report: true grava uma pasta por execução — certo em desenvolvimento, errado num serviço movimentado. Encaminhe os eventos:

O statsd abaixo é o seu cliente de métricas — o trabalho do plugin é só entregar o evento a ele.

ts
export const metricas: ThenaPlugin = {
  name: "metricas",
  onEvent(evento) {
    if (evento.phase !== "end") return;
    statsd.timing(`agent.${evento.kind}`, evento.durationMs!);
  },
};
ts
await app.use(metricas);

É essa a interface inteira. Todo campo de que você precisa está no evento: kind, name, durationMs, status, runId.

Para tracing distribuído, o id e o parentId são o que permitem aninhar spans — abra um no phase: "start", feche no "end". Guarde os spans abertos por event.id, e remova cada um ao fechar; um processo de vida longa que nunca limpa isso vaza memória.

Use onEvent em vez de middleware tool/chat para observação: uma exceção no onEvent é engolida e não consegue quebrar uma execução. Essa garantia vale muito em produção.

Mantenha o report como opt-in por requisição

ts
await app.run({
  prompt,
  log: req.header("x-debug") ? "verbose" : false,
  report: req.header("x-debug") ? { dir: `report/${req.id}` } : false,
});

Depurar uma requisição de produção sem redeploy, sem flag global, e sem barulho do resto.

Sua própria telemetria

O ctx.meta() numa tool, o inv.meta() num middleware, grava no nó daquele passo:

ts
async execute(@input() args, @context() ctx: Context) {
  const linhas = await db.query(args.sql);
  ctx.meta({ rowCount: linhas.length, cached: false });
  return formatar(linhas);
}

Sem isso, um cache que acerta em 4ms só pode ser deduzido pela duração. É no-op quando nada está observando, então chame à vontade.

Correlacione com o seu sistema

O runId está disponível de forma síncrona, antes do primeiro turno:

ts
const exec = app.run({ prompt, data: { requestId: req.id } });
req.log.info({ runId: exec.runId }, "execução do agente iniciada");

Carregar o id da sua requisição no data e o runId nos seus logs é o que permite ir de uma reclamação de usuário até a árvore de execução exata.

A observação é opt-in

Nada é emitido a menos que alguém esteja olhando — report, log, um plugin com onEvent, ou observe: true. Senão a execução pula a árvore, os eventos e o pedido de streaming.

É uma economia deliberada de ~2× em CPU, e o motivo de o onEvent avisar uma vez em vez de silenciosamente não render nada.

Alertas que valem a pena

AlertaPor quê
costUsd por execução, p95primeiro sinal de um laço que parou de convergir
taxa de exhausted: truea condição de parada está degradando
taxa de toolCallSource: "rescued"o modelo está escorregando nesta tarefa
attempts presenteinstabilidade do provider, antes de virar falha
execuções encerradas por orçamentolimites de plano, ou uma regressão real

O último precisa do onExceeded — um orçamento no modo "stop" resolve normalmente, então uma execução que bateu no teto é, de outra forma, indistinguível de uma que terminou.

ts
budget: {
  maxCostUsd: 0.5,
  onExceeded: (info) => metricas.incrementar(`budget.${info.reason}`),
}

Privacidade

Tudo que é capturado passa antes pelo mascaramento, que vem ligado. É rede de proteção para formatos conhecidos, não garantia — não existe regex para o nome de um cliente.

Para execuções sensíveis, report: { content: false } mantém a árvore, as durações e a telemetria, e não grava nenhum texto. Você mantém todas as métricas desta página e perde só a capacidade de ler a conversa.

Relacionado