Eventos de stream do Agent

Blocos Agent podem emitir mais do que o texto da resposta enquanto executam: thinking exposto pelo provider (ou resumos de raciocínio) e um ciclo de vida de chamadas de ferramentas (nome + status). O Studio entrega isso como eventos tipados em um protocolo de stream opcional (opt-in).

O Studio não inventa thinking para providers que não o transmitem. O Bedrock Converse, muitos modelos compatíveis com a OpenAI e modelos de chat sem raciocínio permanecem apenas com texto (mais ferramentas, quando há um loop de ferramentas ao vivo conectado).

Dois interruptores independentes

Os eventos de Agent são governados por duas coisas que não dependem uma da outra.

A política decide quais frames existem. includeThinking liga os frames thinking, includeToolCalls liga os frames tool, e ambos vêm desligados por padrão.

SuperfícieOrigem da política
Chat em deploy (/api/chat/{identifier})Os toggles Thinking e Tool calls do deployment de chat
API de workflow (/api/workflows/{id}/execute)includeThinking / includeToolCalls por requisição, no corpo

O header declara a versão do protocolo. Enviá-lo diz que o cliente entende o framing v1:

X-Studio-Stream-Protocol: agent-events-v1

Ele faz duas coisas. Muda o texto da resposta para frames chunk ao vivo, token por token, que o chunk_reset pode retratar, e é obrigatório para qualquer frame thinking ou tool — um cliente que nunca declarou uma versão não tem contrato para o formato desses frames, então continua recebendo o stream apenas de texto que já entende.

Omitir o header é sempre válido e sempre seguro: você recebe o texto consolidado do turno final e nenhum frame de evento de Agent, que é exatamente o que toda integração já existente recebe. A resposta devolve o header quando o protocolo foi negociado.

O header por si só não expõe nada, então um chat com as duas políticas desligadas continua transmitindo token por token.

Na API de workflow, definir includeThinking ou includeToolCalls sem o header é rejeitado com 400. Sem isso, as flags seriam ignoradas em silêncio — exatamente o modo de falha que este protocolo existe para evitar. O chat em deploy degrada em vez de rejeitar, porque ali a política vem do deployment, e não da requisição.

API de workflow

curl -N https://agent-studio.seeyu.ai/api/workflows/{id}/execute \
  -H "X-API-Key: $STUDIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Studio-Stream-Protocol: agent-events-v1" \
  -d '{
    "stream": true,
    "selectedOutputs": ["agent_1.content"],
    "includeThinking": true,
    "includeToolCalls": true
  }'

Ambas as flags vêm como false por padrão, então uma integração existente recebe exatamente os frames que já recebe hoje. O header é obrigatório sempre que uma das flags for definida; se você omiti-lo, a requisição é rejeitada com 400 em vez de ser rebaixada em silêncio.

Vale conhecer duas diferenças em relação ao chat em deploy. Os frames tool carregam apenas nome e status nas duas superfícies, mas o envelope final terminal da API mantém os argumentos e resultados das ferramentas — o chat público os oculta. E o thinking é entregue somente como frames thinking; ele é removido de providerTiming.timeSegments no envelope em todas as superfícies, então habilitar a política é a única forma de recebê-lo.

Formatos dos frames SSE

O texto da resposta fica em chunk. Thinking e ferramentas nunca reutilizam chunk (assim, clientes antigos que concatenam todo chunk na resposta não conseguem vazar thinking).

FrameSignificado
{ "blockId", "chunk": "…" }Texto da resposta. Clientes legados recebem o texto consolidado do turno final de uma só vez; clientes que optaram pelo protocolo recebem ao vivo, conforme o modelo gera (veja abaixo)
{ "blockId", "event": "chunk_reset" }Apenas para clientes que optaram: descarte o texto de resposta transmitido do bloco — ele pertencia a um turno que terminou em chamadas de ferramentas
{ "blockId", "event": "thinking", "data": "…" }Delta de thinking / resumo de raciocínio
{ "blockId", "event": "tool", "phase": "start"|"end", "id", "name", "status?" }Ciclo de vida da ferramenta (sem argumentos / resultados)
{ "event": "final", "data": … }Envelope de resultado terminal para uma execução concluída. data.success pode ser false com data.error quando o próprio workflow falhou
{ "event": "error", "error": "…" }Falha terminal do stream (timeout, aborto do cliente, erro de processamento) — seguida por [DONE], nunca por final
{ "event": "stream_error", "blockId?", "error" }Problema de leitura não terminal no meio de um bloco; o stream continua
data: "[DONE]"Stream encerrado (sentinela codificada em JSON; sempre vem depois do frame terminal final ou error)

Texto de resposta ao vivo e turnos intermediários

Durante um loop de ferramentas ao vivo, o modelo não pode ser classificado no meio do turno: o texto que ele emite pode se revelar a resposta final ou um preâmbulo antes de uma chamada de ferramenta (o motivo de parada só chega ao fim do turno).

  • Clientes que enviam o header do protocolo (nenhuma política de eventos é necessária) recebem o texto da resposta como frames chunk ao vivo, token por token. Se o turno terminar em chamadas de ferramentas, um frame chunk_reset avisa o cliente para descartar o texto transmitido daquele bloco — o turno final é transmitido ao vivo novamente depois que as ferramentas se resolvem. Concatene chunk, respeite chunk_reset, e a resposta exibida sempre converge para o conteúdo final do bloco.
  • Clientes sem o header nunca veem texto provisório: apenas o texto consolidado do turno final é emitido como chunk, entregue de uma só vez quando o turno termina. Respeitar chunk_reset é o que garante a cadência ao vivo, então envie o header se você quiser isso.

Logs, memória e a saída content do bloco sempre contêm apenas o texto do turno final — preâmbulos intermediários nunca são persistidos.

Abortar

A desconexão do cliente ou o Stop aborta o stream do provider. Ferramentas em andamento são finalizadas como cancelled. O cancelamento é diferente do timeout de execução.

Reconexão

Os eventos de execução do workflow builder stream:chunk, stream:chunk_reset, stream:thinking e stream:tool são apenas ao vivo (não ficam em buffer para replay na reconexão), assim como os chunks de resposta. Replay garantido por seq está fora de escopo.

Workflow builder (Run em rascunho)

Quando você clica em Run no builder, o caminho SSE de eventos de execução encaminha o mesmo sink. O workflow builder está sempre incluído — ele não envia (nem precisa) o header X-Studio-Stream-Protocol, e os interruptores de política não se aplicam a ele:

  • stream:thinking — { blockId, text }
  • stream:tool — { blockId, phase, id, name, status? }
  • stream:chunk — texto da resposta, ao vivo em execuções com eventos de Agent
  • stream:chunk_reset — { blockId }; descarte o texto transmitido do bloco (turno intermediário)

O painel de saída do terminal mostra o chrome de Thinking / Tools acima da saída do bloco quando esses eventos chegam. A ocultação de PII na saída do bloco continua desabilitando o encaminhamento ao vivo (regra do executor).

Toggles do deployment de chat

Em Deploy → Chat, habilite Include thinking para o thinking exposto pelo provider e Include tool calls para os nomes das ferramentas e o status do ciclo de vida. Os interruptores são independentes entre si, e ambos ainda exigem que o cliente envie o header do protocolo. Nenhum deles afeta a cadência do texto de resposta.

Argumentos e resultados de ferramentas nunca são expostos a um chat público — nem nos frames de ciclo de vida, nem pelo envelope final terminal, onde as chamadas de ferramenta do próprio bloco são reduzidas ao mesmo formato de nome e ciclo de vida. A API de workflow autenticada continua retornando os resultados completos das ferramentas. Faça o deploy novamente ou atualize o chat depois de alterar os modelos ou as ferramentas do Agent.

Honestidade sobre capacidades (visão geral)

O suporte por modelo é gerado a partir do registro de modelos na página do bloco Agent; a tabela abaixo resume por família de provider.

FamíliaThinkingFerramentas ao vivo
Anthropic / Azure AnthropicSim (incluindo blocos ocultados nos traces). As gerações mais recentes do Claude omitem o thinking completo; o Studio solicita thinking resumido para elas em execuções com streamingSim
Gemini / VertexSim, quando um nível de thinking está definido (resumos de raciocínio são solicitados em execuções com eventos de Agent)Sim
OpenAI ResponsesResumos de raciocínio quando transmitidos (exige verificação da organização na OpenAI; organizações não verificadas ficam sem resumos)Sim
Compatíveis com OpenAI (Groq, DeepSeek, …)Apenas se o fornecedor transmitir reasoning / reasoning_contentLoop ao vivo onde está conectado (ex.: Groq, DeepSeek)
BedrockNão é inventadoSim, quando o loop de ferramentas com streaming é usado

Consumindo o stream

Um cliente em conformidade deve quatro coisas ao stream:

  1. Discrimine antes de concatenar. Só é texto de resposta o frame que não tem o campo event. Verificar event === undefined em vez de “tem um campo chunk” é o que evita que tipos de frame futuros vazem para a resposta.
  2. Acumule por blockId. Um workflow pode transmitir mais de um bloco; os frames se intercalam.
  3. Respeite chunk_reset se você enviou o header do protocolo. Limpe o texto acumulado daquele bloco — ele pertencia a um turno que terminou em chamadas de ferramentas, e o turno final é transmitido novamente.
  4. Pare no frame terminal. Chega exatamente um final ou um error, seguido pela sentinela literal "[DONE]". stream_error não é terminal.

Cliente de referência

type Frame = Record<string, unknown>

async function consume(response: Response) {
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  const answers = new Map<string, string>()
  const thinking = new Map<string, string>()
  let buffer = ''

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })

    // SSE frames are newline-delimited; keep the trailing partial line.
    const lines = buffer.split('\n')
    buffer = lines.pop() ?? ''

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue
      const payload = line.slice(6)

      const frame = JSON.parse(payload) as Frame | string
      if (frame === '[DONE]') return { answers, thinking }

      const { blockId, event } = frame as { blockId?: string; event?: string }

      if (event === undefined && typeof frame.chunk === 'string') {
        answers.set(blockId!, (answers.get(blockId!) ?? '') + frame.chunk)
      } else if (event === 'chunk_reset') {
        answers.set(blockId!, '')
      } else if (event === 'thinking') {
        thinking.set(blockId!, (thinking.get(blockId!) ?? '') + String(frame.data))
      } else if (event === 'tool') {
        // frame.phase is 'start' | 'end'; frame.status is set on 'end'.
        renderToolChip(frame)
      } else if (event === 'final') {
        // Terminal. frame.data.success may be false with frame.data.error.
      } else if (event === 'error') {
        throw new Error(String(frame.error))
      } else if (event === 'stream_error') {
        // Non-terminal: log and keep reading.
      }
    }
  }
}

Valores de event desconhecidos devem ser ignorados em vez de tratados como erros — é isso que permite lançar novos tipos de frame sem quebrar clientes existentes.

Exemplo (chat público)

curl -N -X POST 'http://localhost:3000/api/chat/your-slug' \
  -H 'Content-Type: application/json' \
  -H 'X-Studio-Stream-Protocol: agent-events-v1' \
  -d '{"input":"Think briefly, then say hi"}'

Veja também Deployment de chat para controle de acesso e as configurações de eventos.