Runtime — embutir um agente#
CATCHER AGENTS REALTIME — experimental, 0.1.0-experimental.1#
A emissão concorrente de credenciais ou alteração simultânea do perfil pode retornar 409 REALTIME_CONFLICT: a transação é revertida, sem deixar token novo sem vínculo. Releia o perfil e emita novamente após resolver a disputa.
Prévia de subproduto separado, ainda não implantada. Requer habilitação de piloto
e o bridge local. O mecanismo é ga-realtime-webrtc, inteiramente server-side: a voz
é gpt-realtime-2.1 e o cérebro é gpt-6-astra, ambos alcançados pela plataforma —
nenhum CLI é instalado ou executado no computador do operador e nenhuma credencial de
provedor chega ao cliente. Os agentes e modos atuais mantêm seu comportamento.
| Método e rota | Credencial | Contrato |
|---|---|---|
GET /v1/agents/{id}/realtime |
Owner JWT/API key | Perfil, inicialmente enabled:false, reasoning_effort:"high" |
PUT /v1/agents/{id}/realtime |
Owner JWT/API key | Substitui enabled, voice_prompt (até 16 KB), reasoning_effort, allowed_tools (até 32 nomes), end_user_external_id (pino de até 255 bytes; vazio = operador); altera revisão e invalida sessões anteriores |
POST /v1/agents/{id}/realtime/bridge-token |
Owner JWT/API key | Retorna uma vez {id,token,agent_id}; rotaciona token dedicado anterior |
POST /v1/agent-runtime/{id}/realtime/sessions |
X-Agent-Token dedicado |
Body {end_user_external_id}. Retorna session_id, mechanism (ga-realtime-webrtc), prompts de voz/cérebro separados, voice_model (voz) e model (cérebro), reasoning_effort, signal_path, brain_path, voice_tools (a única função que a voz pode chamar, já no formato do provedor), tools, tools_unavailable, expires_at. Os campos cli_version e protocol foram removidos — não há mais CLI no caminho |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/thread |
Mesmo token | Vincula {thread_id} = o id da própria sessão de voz. Nada acontece antes desse vínculo: o recibo durável recusa efeito em sessão não vinculada. O modelo do cérebro é confirmado pelo servidor, ao responder — não por campo do cliente. Rebind diferente é conflito |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/signal |
Mesmo token | {sdp} com o offer WebRTC do cliente; retorna {sdp,voice_model} com o answer. A chamada ao provedor é feita no servidor: o cliente nunca vê token nem URL. Offer que não começa com v=0 é 400 REALTIME_INVALID; transporte indisponível é 503/502 REALTIME_VOICE_UNAVAILABLE |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/brain |
Mesmo token | {thread_id,turn_id,call_id,request} — a delegação que a voz faz por consult_brain. Roda o cérebro com as tools e a memória do agente e retorna {receipt,result,replayed}, com result.text para a voz falar e result.model_used (reportado, não presumido). Mesma call_id repete a resposta guardada em vez de raciocinar de novo; efeito pendente ou desconhecido é 409 REALTIME_EFFECT_UNKNOWN e nunca deve ser redespachado. Excesso de turnos simultâneos é 429 REALTIME_BUSY (retentável); cérebro indisponível é 503 REALTIME_BRAIN_UNAVAILABLE |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/brain/cancel |
Mesmo token | {call_id?,turn_id?,thread_id?,reason?} — para o turno do cérebro no servidor. Abortar a requisição HTTP do /brain NÃO faz isso: o abort não atravessa a borda de forma confiável, então o run segue e ainda pode despachar uma tool na máquina do usuário (medido em 2026-09-15). Liquida o turno (run_status: interrupted, REALTIME_TURN_CANCELLED) e fecha o recibo do consult_brain como unknown; a partir daí toda tool daquele turno é recusada com 409. Idempotente: cancelar de novo é 200 com already_settled:true. Nunca reescreve recibo já completed/failed, e effects é sempre "unknown" — um cancelamento não promete rollback. Informa o que de fato fez: run_cancelled e client_calls_cancelled. Pelo menos um entre call_id e turn_id é obrigatório (400 REALTIME_INVALID); sessão de outra credencial é 404/409, como no /brain. O caminho vem no bootstrap em brain_cancel_path call_id e turn_id fecham corridas diferentes e não são intercambiáveis: turn_id liquida o turno e barra toda tool dele; call_id liquida a chamada, e se a plataforma ainda não a viu ela é gravada já liquidada, de modo que o /brain que chegar depois com essa call_id é recusado com 409 REALTIME_CONFLICT e o cérebro não roda. Mandar os dois é o mais seguro. receipt_status vem "unknown" sempre que uma call_id é nomeada, e vazio quando só o turn_id foi mandado, porque aí nenhuma chamada foi apontada — não é enum fechado |
GET /v1/agent-runtime/{id}/realtime/sessions/{sid}/tools/ws |
Mesmo token, upgrade WebSocket, subprotocolo polak-tools.v1 |
Tools no cliente. A máquina do usuário abre esta conexão de saída (sem IP público) e oferece, no hello, as tools que implementa ({name, description, input_schema}, até 32). A allowed_tools do perfil decide o que o cérebro pode chamar; o hello_ack devolve accepted_tools e rejected_tools nomeando cada recusa (not_allowed ou reserved — um nome que o servidor já provê nunca é sombreado). Daí em diante o servidor empurra tool_call {request_id, call_id, turn_id, name, arguments, timeout_ms} e o cliente responde tool_result {request_id, ok, output | error}; cancel viaja nos dois sentidos. Uma conexão viva por sessão; sem resume (reconectar = sessão nova, chamadas em voo viram efeito desconhecido). Corpus de conformidade: docs/contracts/polak-tools.v1/frames.json |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/transcripts |
Mesmo token | {thread_id,source_id,role,phase,revision,text}; roles user/assistant, phases partial/final/correction/deleted; texto até 256 KB; retorna {transcript,reconnect_required} |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/turns |
Mesmo token | Observa {thread_id,turn_id,status,output}; status started/completed/interrupted/failed. Não inicia turno. Retorna {run_id,status,usage} |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/tools/call |
Mesmo token | {thread_id,turn_id,call_id,tool,arguments}; retorna {receipt,result,replayed}. Argumentos até 80 KB; catálogo/escopo são do servidor |
POST /v1/agent-runtime/{id}/realtime/sessions/{sid}/close |
Mesmo token | Fecha sessão de forma idempotente, retorna {closed:true,pending_effects:"unknown"} |
Tools que só existem na máquina do usuário (ler a tela, mostrar um prompt) entram no
catálogo do cérebro por polak-tools.v1: a máquina abre …/tools/ws de saída e oferece
suas implementações; o servidor as expõe ao cérebro como entradas comuns de catálogo
apontando de volta para a API, que relaya pela conexão. O cérebro só vê a tool enquanto
o cliente está conectado, e só se a allowed_tools do perfil a nomear. Uma call_id é um
efeito: a chamada repetida devolve o resultado guardado, e um efeito pendente/desconhecido
é 409 REALTIME_EFFECT_UNKNOWN, nunca redespachado.
Fronteira de confiança: o cliente conectado é a máquina do próprio usuário, autenticada pela credencial dedicada que o owner emite. Um processo de bridge comprometido pode, além de executar as tools, influenciar o cérebro pelas descrições/schemas que oferece e pelas saídas que devolve. Não distribua essa credencial.
consult_brain não é uma tool do agente: ela existe só no plano de voz e é recusada
(403 REALTIME_TOOL_DENIED) se alguém tentar despachá-la por .../tools/call. As tools
do agente continuam sendo executadas pelo servidor, agora a partir do cérebro.
Um token público de widget não serve para o bridge. A credencial dedicada é
vinculada ao agente e ao end_user_external_id fixado pelo owner no perfil; só
funciona nas rotas Realtime e deve permanecer
em arquivo local privado, nunca no browser ou widget distribuído. O bridge aceita
apenas acesso local com Host/Origin e capacidade aleatória válidos.
A memória depende da allowlist. allowed_tools é a lista completa do que o
cérebro pode chamar naquela sessão — inclusive as tools de memória. Um perfil com
allowed_tools: ["load_skill"] responde "não tenho acesso a memórias suas",
porque brain_search não foi liberada. Para recuperar contexto de conversas
anteriores, inclua brain_search (e, conforme o uso, brain_read, brain_list,
brain_write). A resposta de POST .../realtime/sessions nomeia exatamente o que
ficou de fora em tools_unavailable, então confira esse campo antes de concluir
que a memória não funciona. brain_ask permanece indisponível neste subproduto
por contrato, e aparece ali mesmo quando listada.
A voz recebe persona verbal; o cérebro Astra recebe os prompts/resolvers e tools
nativos. O Codex administra handoffs; transcrição nunca dispara outro loop. Finais
do usuário alimentam a memória nativa. Texto gerado não comprova audição:
playback/output_playback ficam unknown. Correção/deleção explícita invalida
memória derivada e requer reconexão; nova sessão restaura memória, sem repetir ações.
Sessões expiram após uma hora. Citações de voz na L3 são recuperáveis por até sete
dias, inclusive entre dias UTC, e podadas pela manutenção existente. A remoção da
conversa apaga suas citações Realtime e notas L4 derivadas, além de transcrições e
recibos. Memórias legadas mantêm sua política. Notas Realtime escritas explicitamente
pelo cérebro podem durar além de sete dias até esquecimento ou remoção da fonte.
Recibo pending após crash significa efeito desconhecido: não repetir a ação.
CallId repetido com os mesmos argumentos usa o recibo; argumentos diferentes geram
409. Resultado grande mantém ok/status conhecido e informa truncated:true,
original_bytes e sha256. Pausa de áudio e interrupção de execução são comandos
separados; interrupção não promete desfazer efeitos. Falha ao persistir a conclusão
após execução retorna REALTIME_EFFECT_UNKNOWN, nunca “não executado”.
Erros experimentais: 400 REALTIME_INVALID; 403 REALTIME_TOKEN_WRONG_PLANE, REALTIME_END_USER_MISMATCH, REALTIME_TOKEN_NOT_BRIDGE ou
REALTIME_TOOL_DENIED; 404 REALTIME_NOT_FOUND; 409 REALTIME_DISABLED,
REALTIME_CONFLICT ou REALTIME_EFFECT_UNKNOWN; 422 REALTIME_TOOL_LIMIT ou
REALTIME_PROMPT_LIMIT; 429 REALTIME_BUSY (a company já tem voz/cérebro demais em
voo sobre a credencial compartilhada — retentável); 502/503
REALTIME_VOICE_UNAVAILABLE (transporte de voz) e 503 REALTIME_BRAIN_UNAVAILABLE
(cérebro), distintos de propósito para o integrador saber qual metade faltou;
503 REALTIME_CLIENT_NOT_CONNECTED (o cérebro chamou uma tool do cliente e nenhuma
máquina está conectada para servi-la) e 504 REALTIME_CLIENT_TIMEOUT (o cliente não
respondeu no prazo) — ambos deixam o recibo daquela call_id como efeito
desconhecido, nunca uma resposta inventada e nunca um replay;
503 REALTIME_UNAVAILABLE ou REALTIME_MEMORY_UNAVAILABLE.
401 continua indicando credencial ausente/inválida/revogada e 429 também o limite de
requisições. O cérebro roda com a cadeia de fallback desligada: ele responde no
modelo contratado ou falha nomeando-o — nunca é substituído em silêncio. Não há
fallback silencioso para API paga.
Quando o protocolo não reporta tokens, o Run conserva
usage.measurement:"not_reported", billing_mode:"subscription" e
output_playback:"unknown": zeros numéricos não medem quota zero. O catálogo pode
anunciar Ultra; confirmação da thread e esforço efetivo do turno são evidências
distintas. O percurso controlado desta prévia usa Astra/high.
O grupo /v1/agent-runtime/{id} é a superfície de embed. Autenticação só por
X-Agent-Token: prt_…; o token é preso a um agente, e as rotas de sessão têm
guarda de posse (proteção BOLA) — uma sessão de outro agente retorna
404 SESSION_NOT_FOUND. As rotas de stream (…/messages/stream) rodam sem
timeout de requisição — um turno agêntico vive enquanto estiver produzindo —
e as síncronas num timeout estendido (5 min). São os mesmos handlers do
chat do Console, então os shapes são idênticos.
POSTCriar sessão#
POST /v1/agent-runtime/{id}/sessions — corpo { end_user_external_id?, title?, session_key? } (todos opcionais). Registrar o end_user_external_id cria/atualiza
o usuário final do agente, o que mantém a memória separada por pessoa. Resposta
201: { id, agent_id, title, created_at }.
session_key transforma a rota num ensure idempotente: um nome estável que
você escolhe (ex.: "squad-developer") resolve SEMPRE para a mesma conversa —
é assim que se mantém memória durável por papel/assunto sem guardar um id. Ver
Sessões nomeadas.
GETListar mensagens#
GET /v1/agent-runtime/{id}/sessions/{sid}/messages — resposta 200:
{ messages: [messageResponse], total }. Cada messageResponse:
{
"id": "…",
"session_id": "sess_a1b2c3",
"role": "user | assistant | tool | system",
"content": "…",
"thinking": "…",
"tool_calls": [ { "name": "brain_search", "is_error": false, "output": "…", "duration_ms": 142 } ],
"run_id": "9f2c…",
"created_at": "2026-07-02T01:00:00Z"
}
thinking, tool_calls e run_id só aparecem quando presentes. No
tool_calls, o output público é truncado em 4096 caracteres (output_truncated
e output_chars sinalizam).
POSTEnviar mensagem#
POST /v1/agent-runtime/{id}/sessions/{sid}/messages — corpo { content }
(obrigatório, não-vazio). Roda o turno completo e retorna o envelope
{ run_id, user_message, assistant_message, usage, provider, model, tool_calls }
mostrado no Início rápido.
Quando alguma ferramenta anexada ao agente não pôde ser montada no turno, o
envelope traz também tools_unavailable: ["nome", …] (o campo é omitido quando está
vazio) — e o evento SSE complete carrega o mesmo. Causas típicas: a ferramenta foi
deletada/renomeada, ela é escopada a outro tenant (um agente só enxerga as do próprio
tenant + as company-global), ou é uma ferramenta de catálogo cuja integração não está
configurada. Trate a presença desse campo como erro de configuração: sem a ferramenta
o modelo tende a improvisar a chamada como texto em vez de executá-la.
O cost_usd do turno não vem nesse corpo — ele
fica persistido no run e é lido em GET /v1/agents/{id}/runs/{runId} (o evento
done do streaming também carrega o custo agregado).
POSTEnviar mensagem (streaming)#
POST /v1/agent-runtime/{id}/sessions/{sid}/messages/stream — o mesmo turno, em
SSE (Streaming SSE).
POSTGerar imagem#
POST /v1/agent-runtime/{id}/images/generations — o app embutido gera uma imagem
com o prt_, sem a API key da conta. É a mesma geração de POST /v1/images/generations
(mesmos campos: prompt, model, aspect, size, quality, output_format, n,
style_preset, brand, reference_images). A única diferença de contrato: no plano
de runtime o default é include_b64=false — a resposta traz a url assinada de cada
imagem (o app renderiza direto) e o base64 inline só vem com include_b64:true. Resposta
200: { id, model_used, revised_prompt, images: [ { url, asset_id, mime_type, byte_size } ] }.
GET /v1/agent-runtime/{id}/images/models devolve o catálogo de modelos (igual a
GET /v1/images/models). Geração custa dinheiro de provedor: a meta mensal continua
visível sem bloquear a chamada, e o rate-limit da conta permanece; um id/modelo
inválido → 4xx.
POSTTranscrever áudio#
POST /v1/agent-runtime/{id}/transcriptions — o app embutido transcreve com o
prt_, sem a API key da conta. É a mesma transcrição em painel de
POST /v1/transcriptions (mesmo corpo — multipart file ou JSON audio_b64 —
mesmos campos models, language, speaker_id, topics, speaker_profile,
learn, second_pass, mesma resposta e os mesmos erros; veja
§Transcrição de áudio). A única diferença de contrato: quando o request não
nomeia speaker_id, o léxico que aprende é o do agente vinculado ao token
(speaker_scope: "speaker:agent:<id>") — um bot que não distingue quem fala
ainda aprende numa memória só. Nomeie speaker_id (o remetente de WhatsApp, o
usuário final) para uma memória por pessoa.
GET /v1/agent-runtime/{id}/transcriptions/models devolve o catálogo com a
disponibilidade da conta (igual a GET /v1/transcriptions/models). Cada
transcrição custa crédito de provedor; histórico, correção e escrita no léxico
ficam no plano da conta. Sem token → 401.
POSTLer documento (Document AI)#
POST /v1/agent-runtime/{id}/document-ai/jobs — cria um job de extração pelo prt_.
Multipart (campo file + schema_version_id/policy_version_id/callback_url opcionais),
com Idempotency-Key obrigatório (8..191), igual a POST /v1/document-ai/jobs.
Envie só o arquivo: como o plano runtime não expõe rota para criar schema/policy,
omitir os dois ids fixa o padrão do tenant (default-document + default-draft), criado
na primeira necessidade; a resposta traz os version_id exatos que foram fixados. O
tenant é fixado pelo agente vinculado ao token — um app não escolhe tenant; enviar um
tenant_external_id diferente → 403 RUNTIME_TOKEN_TENANT_MISMATCH. Leia o resultado em
GET /v1/agent-runtime/{id}/document-ai/jobs/{jobId}/result (mesmo shape de
/v1/document-ai/jobs/{jobId}/result) ou acompanhe ao vivo por SSE em
GET /v1/agent-runtime/{id}/document-ai/jobs/{jobId}/stream. Um job de outro tenant →
404 DOCUMENT_AI_NOT_FOUND (sem vazar existência). Sem token → 401.