Streaming (SSE)

É um agente de IA? Busque a skill de onboarding e leia toda a API em texto limpo — skill · llms.txt · llms-full.txt

Streaming (SSE)#

O endpoint .../messages/stream responde com Content-Type: text/event-stream (Cache-Control: no-cache, X-Accel-Buffering: no), status 200 imediato. O formato de cada evento é:

text
event: <nome>
data: <json>

O agente responde token a token; os chips de ferramenta acendem ao vivo; o resumo de raciocínio aparece antes da resposta. Há duas famílias de evento.

POSTConsumir um stream#

bash
curl -N -X POST \
  https://agents-api.catcher.one/v1/agent-runtime/AGENT_ID/sessions/sess_a1b2c3/messages/stream \
  -H "X-Agent-Token: prt_SEU_RUNTIME_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Resuma os 3 últimos pedidos." }'

Sequência real de eventos num turno com ferramentas:

text
event: start
data: {"run_id":"9f2c…","user_message":{"id":"…","role":"user","content":"Resuma os 3 últimos pedidos.","…":"…"}}

event: tool_call_start
data: {"type":"tool_call_start","tool_call":{"index":0,"id":"tc_1","name":"brain_search","arguments":"{\"q\":\"últimos pedidos\"}"}}

event: tool_call_result
data: {"type":"tool_call_result","tool_result":{"id":"tc_1","name":"brain_search","is_error":false,"output":"…","duration_ms":142}}

event: text_delta
data: {"type":"text_delta","text":"Os três últimos"}

event: text_delta
data: {"type":"text_delta","text":" pedidos foram…"}

event: round_usage
data: {"type":"round_usage","round_usage":{"round":2,"model":"codex/gpt-5.6-sol","usage":{"input_tokens":812,"output_tokens":143,"total_tokens":955,"cached_tokens":700,"reasoning_tokens":90},"latency_ms":4210,"first_token_ms":2900,"tool_calls":1,"prompt":{"system_chars":9800,"tools_chars":4100,"history_chars":2600,"message_count":3}}}

event: done
data: {"type":"done","rounds":2,"stop_reason":"end_turn","cost":{"total_cost_usd":0.0144},"usage":{"input_tokens":812,"output_tokens":143,"total_tokens":955}}

event: complete
data: {"run_id":"9f2c…","assistant_message":{"id":"…","role":"assistant","content":"Os três últimos pedidos foram…","run_id":"9f2c…","…":"…"},"usage":{"input_tokens":812,"output_tokens":143,"total_tokens":955},"tool_calls":[…]}

GETCatálogo de eventos#

Evento Origem data
start API (sempre 1º) { run_id, user_message }
thinking engine / single-shot { type: "thinking", text } — resumo de raciocínio
text_delta engine / single-shot { type: "text_delta", text } — fragmento da resposta
warning API (início do run) { type, kind: "tools_unavailable", tools: [...], details: [...], tools_mounted: [...] } — só quando falta ferramenta; ver "O catálogo de ferramentas do run é reportado"
tool_call_start engine { type, tool_call: { index, id, name } } — o ANÚNCIO; ainda sem arguments
tool_call_delta engine { type, tool_call: { index, arguments } } — fragmento dos argumentos
tool_call_end engine { type, tool_call: { index, id, name, arguments } } — os argumentos COMPLETOS (desde 2026-08-19)
tool_call_result engine { type, tool_result: { id, name, is_error, output, duration_ms } }
round_usage engine { type, round_usage: { round, provider, model, usage: { input_tokens, output_tokens, total_tokens, cached_tokens, reasoning_tokens }, latency_ms, first_token_ms, tool_calls, prompt: { system_chars, tools_chars, history_chars, message_count }, …custo } } — uma por rodada do loop. Onde foi o relógio (desde 2026-09-02): latency_ms é a chamada ao provider (request → último evento); first_token_ms a espera até o primeiro evento (prefill + raciocínio oculto; só no streaming); usage.reasoning_tokens o raciocínio oculto como o provider reporta (OpenAI: subconjunto de output_tokens; Gemini: thoughtsTokenCount, aditivo; Anthropic não separa); usage.cached_tokens a fatia de input_tokens servida do cache (subconjunto); prompt.* o que a rodada enviou em caracteres por origem (o provider não reporta tokens por seção). latency_ms − first_token_ms ≈ tempo escrevendo; first_token_ms alto com cached_tokens ≪ input_tokens = prefill sem cache. done.rounds_usage[] carrega os mesmos campos
compaction engine { type, compaction: { round, layer, trigger, tokens_before, tokens_after, summarized } } — o engine compactou a conversa para caber na janela do modelo; ver "Compaction automática de contexto"
leg engine { type: "leg", leg: { leg, rounds } } — não-terminal: o run continuou numa nova requisição api→runtime (a cada ~40 min); ignore ou mostre "continuando…". round/round_usage.round seguem absolutos e round.max continua sendo o teto do run (não o restante da leg); done chega uma única vez, no fim
done engine (verbatim) { type, rounds, stop_reason, cost: {…}, usage: {…}, rounds_usage: […], context_tokens, compactions } — no stream compat (/v1/compat/streams/{id}) o done é o enriquecido da API e traz também model (o configurado), model_used ("<provider>/<modelo>", quem realmente respondeu) e, quando o reserva atendeu, fell_back: true + model_failures[] dizendo qual cérebro recusou e de quê; ver "Cérebro reserva"
— — stop_reason terminal: completed (resposta final), max_rounds (bateu o teto de rounds), timeout (esgotou o wall clock do turno), tool_failure_limit (as chamadas de ferramenta pararam de funcionar — ver abaixo), error (falha real), user_aborted (parado via POST …/stop). Os três primeiros significam que o trabalho feito até ali é real — reagir com retry/aprofundar, não escalar.
complete API (sempre último no sucesso) { run_id, assistant_message, usage, tool_calls, tools_mounted, tools_unavailable? }
error API / engine { error } (ou { type: "error", error }); quando a falha é de provedor, também error_code + provider, e quando a cadeia de fallback rodou inteira, model_failures[] — ver "Falha de provedor: código estruturado"

Onde ler os argumentos de uma chamada de ferramenta. Todo provedor anuncia a chamada antes de os argumentos existirem — eles chegam como um fluxo de fragmentos. Por isso tool_call_start traz só index/id/name: carregá-los ali exigiria segurar o anúncio até a chamada estar completa, destruindo o sinal ao vivo ("chamando X…") que a UI usa. Desde 2026-08-19, tool_call_end traz os argumentos completos (mais id e name para correlacionar) — se você não quer concatenar os tool_call_delta por index, leia esse frame. Os argumentos saem exatamente como o modelo os emitiu: se os fragmentos não formam JSON válido, é isso que você recebe — trocá-los por {} reportaria uma chamada vazia bem-formada que o modelo nunca fez.

Corrigido em 2026-08-11 — rounds, cost e stop_reason no RESULTADO do dispatch. Os EVENTOS SSE ao vivo sempre trouxeram esses campos. O que chegava zerado era o resultado consolidado — o corpo que o dispatch devolve e o histórico de runs — nos DOIS caminhos (com e sem streaming): rounds sempre 1, cost 0, stop_reason vazio — um defeito de leitura nosso, não do seu código. Se você tratava rounds: 1 como "o agente respondeu de primeira", passe a ler o valor real: um mesmo dispatch pode legitimamente fazer dezenas de rodadas. Pelo mesmo motivo, uma falha originada no engine agora aparece como falha em vez de virar um run completed com a resposta parcial.

stop_reason: "tool_failure_limit" merece tratamento próprio. Ele diz que o agente parou porque as chamadas de ferramenta pararam de funcionar — não porque faltou profundidade. O engine para de despachar uma ferramenta depois de 5 falhas consecutivas e encerra o run depois de 15 falhas seguidas sem nenhum sucesso no meio, para não queimar cota repetindo uma chamada que não vai passar. Qualquer sucesso de ferramenta zera as duas contagens. Ao receber esse terminal, investigue o que está recusando as chamadas (auth, quota, fence/permite do seu lado, endpoint fora do ar) — aumentar max_rounds_per_run só aumenta o custo. As ferramentas que falharam vêm em tool_calls[] com is_error: true e o corpo da resposta do seu endpoint, que é onde está o motivo real.

Guarda de repetição (tool_loop_guard) — o que ela mede desde 2026-08-19. O engine recusa uma chamada repetida, mas o critério mudou: antes bastavam 6 chamadas com argumentos idênticos, e isso era errado para ferramentas com efeito externo. Um compile_project não recebe argumentos e é chamado depois de CADA correção — o projeto mudou, os argumentos não podiam mudar. O guard lia isso como loop e o agente parava com o trabalho pela metade.

Agora só é loop quando os argumentos E o resultado são idênticos: um compile cujos erros mudaram é progresso; um que devolve os mesmos bytes seis vezes seguidas é um loop de verdade. Você não precisa declarar nada na ferramenta.

Quando o guard dispara, duas coisas acontecem:

text
event: warning
data: {"type":"warning","warning":{"kind":"tool_loop_guard","tool":"…","count":7,
       "message":"the platform refused a repeated call because neither the arguments nor the result changed — this is not a limit on the number of tool calls"}}

e o texto entregue ao modelo diz explicitamente que é uma guarda da plataforma, não uma cota — a redação anterior ("Stop calling this tool") era lida pelo modelo como "acabaram minhas chamadas" e ele encerrava a tarefa.

O nome do evento de texto é text_delta (não "token"), e o de raciocínio é thinking. No fim de um turno com sucesso o cliente vê dois eventos terminais — o done do engine (repassado verbatim) e o complete da API — e pode usar qualquer um dos dois como sinal de fim. Um turno não-agêntico (single-shot, sem ferramentas) emite start, então thinking (se houver), text_delta com a resposta inteira, e complete.

Sem janela de conexão; silêncio não é morte; erro com causa#

Nenhum stream tem timeout de requisição (desde 2026-08-24). Nem o stream de chat (…/messages/stream, Console e runtime), nem o stream de run (/v1/compat/streams/{id}), nem os de Document AI, imagens e memória. A conexão vive enquanto a run viver — 8, 20, 40 minutos com texto chegando terminam em complete/done, nunca em error. Um run não é encerrado por relógio em nenhuma camada nossa: o engine não impõe wall clock, e os clientes de streaming dos provedores não carregam timeout de leitura. O único teto de tempo que resta é o da infraestrutura: a requisição HTTP vive no máximo 3600 s (Cloud Run — o máximo da plataforma). Um run que chega lá recebe error_code: "RUN_TIMEOUT"; é o valor publicado em GET …/limits como infrastructure_request_timeout_seconds.

Heartbeat. Um pensamento longo ou uma ferramenta lenta deixam o stream em silêncio por minutos; para que nenhum proxy leia silêncio como conexão morta, todo stream emite o comentário SSE : ping a cada 15 s. Comentários são ignorados por qualquer cliente SSE conforme a spec — não chegam ao seu handler de eventos.

reconnect continua no contrato ({"type":"reconnect","reason": "connection_window_elapsed","last_event_id":"…"}), mas só aparece se a conexão ao stream de run terminar sem um terminal da run por outro motivo (rotação de instância, o corte de 3600 s da infraestrutura). Reconecte com Last-Event-ID como antes; a run autônoma não é afetada pela sua conexão.

Erro com causa. O event: error dos streams de chat diz por quê:

text
event: error
data: {"error":"agent run failed","error_code":"RUN_TIMEOUT","code":"RUN_TIMEOUT",
       "detail":"turn safety timeout exceeded","provider":"kimi"}

error mantém o texto que os clientes já casam; error_code e code são o mesmo código estável (PROVIDER_*, MAX_TOOL_ROUNDS, CONTEXT_WINDOW_EXCEEDED, RUN_TIMEOUT, RUN_CANCELLED, AGENT_RUN_FAILED); detail é a razão literal do runtime; provider e model_failures só quando existem. Aditivo — nada do envelope anterior mudou.