Streaming
O app.run() devolve um RunHandle, não uma Promise. A regra: com await, você pede o resultado; sem await, você pede a execução.
const texto = await app.run({ prompt }); // o resultado
const exec = app.run({ prompt }); // a execuçãoEle é thenable, então as duas coisas convivem sem que o run precise virar dois métodos.
Por que um handle
Uma Promise não expressa três usos reais: cancelar, observar o que está acontecendo, e guardar a execução para reencontrá-la depois — o padrão de responder um runId num POST e acompanhar por SSE.
const exec = app.run({ prompt, observe: true });
exec.runId; // disponível de forma síncrona, antes do primeiro turno
exec.result; // uma Promise comum — compõe com Promise.all
exec.signal; // o seu, combinado com o abort() deste handle
exec.abort(reason); // cancelaDois streams
Token e passo são canais separados, de propósito: token não é um passo da execução — não tem início, fim nem status.
// o texto, conforme o modelo escreve
for await (const token of exec.textStream) process.stdout.write(token);
// os passos, conforme acontecem
for await (const evento of exec.eventStream) console.log(evento.kind);Ou com callbacks, cada um devolvendo como cancelar a assinatura:
const off = exec.onToken((t) => process.stdout.write(t));
exec.onEvent((e) => metricas.registrar(e));Quem assina atrasado recebe o que já passou
Quem assina depois do início recebe o que já aconteceu antes dos itens novos. Sem isso, um SSE que conecta três segundos após o POST veria a execução começando do meio — e um for await sobre o texto começaria no meio da frase.
O buffer de eventos tem teto de 500 eventos. Com report ligado cada evento carrega prompt e resposta, e o padrão POST+SSE mantém vários handles vivos ao mesmo tempo — o teto troca o começo de uma execução muito longa por um limite de memória previsível.
O streaming só acontece se o provider suportar
Um provider que ignore o onToken continua funcionando; o texto simplesmente chega inteiro no result.
A observação é opt-in
Esta é a parte que surpreende:
[thena] onEvent() will receive nothing: this run is not being observed.
Use `run({ observe: true })`, or turn on `report`, `log` or a plugin with `onEvent`.Uma execução sem observador não constrói a árvore de execução, não emite eventos e não pede streaming ao provider. É o caminho de custo zero e vale cerca de 2× em tempo de CPU por execução.
A observação liga sozinha quando há report, log, ou um plugin com onEvent. Quando o único consumidor é o handle, diga:
const exec = app.run({ prompt, observe: true });Você recebe o aviso uma vez, em vez de um for await que nunca rende.
O padrão POST + SSE
O motivo de o handle existir:
app.post("/runs", (req, res) => {
const exec = agente.run({
prompt: req.body.message,
observe: true,
signal: req.signal,
});
res.json({ runId: exec.runId }); // respondido antes do primeiro turno
execucoes.set(exec.runId, exec);
});
app.get("/runs/:id/stream", async (req, res) => {
const exec = execucoes.get(req.params.id);
res.setHeader("Content-Type", "text/event-stream");
for await (const evento of exec.eventStream) {
res.write(`data: ${JSON.stringify(evento)}\n\n`);
}
res.end();
});O runId é síncrono, então o POST responde na hora. O cliente conecta quando quiser e ainda assim vê a execução inteira, por causa do replay acima.
O eventStream como AsyncIterable dá backpressure num socket lento, o que a forma com callback não dá.
Erro não fica sem tratamento
Um handle cuja execução falha não derruba o processo com unhandledRejection, mesmo que ninguém tenha dado await ainda — que é exatamente a situação do POST+SSE. O erro continua disponível em .result; ele só deixa de ser "não tratado".
