Streaming
Blue Banana exposes two long-lived Server-Sent Events endpoints. Each frame is a single line of data: <JSON>\n\n. Use curl --no-buffer -N or any standard EventSource client.
1 · POST /api/agent (per-invocation stream)
The body specifies the use case and the active tenant; the server streams the agent run in real time. The first frames are text-delta / tool-call / tool-result; the stream always ends with one of done or error.
Event grammar
data: {"type":"text-delta","delta":"…"}\n\n
data: {"type":"tool-call","id":"tc_01","name":"read_patient","args":{...}}\n\n
data: {"type":"tool-result","id":"tc_01","name":"read_patient","result":{...}}\n\n
data: {"type":"done","tool_call_count":3,"duration_ms":4123,"status":"ok"}\n\n
data: {"type":"error","message":"…"}\n\nOrdering
Tool calls and text deltas are interleaved. Each tool-call id is followed (eventually) by a matching tool-result with the same id — but other text deltas and tool calls may intervene. done or error always appears last.
curl
curl --no-buffer -N \
-X POST https://bluebananaehr.com/api/agent \
-H "Authorization: Bearer bb_…" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Summarize the chart for Maria Lopez.",
"useCase": "chart-summary"
}'Node consumer
const res = await fetch('https://bluebananaehr.com/api/agent', {
method: 'POST',
headers: {
'Authorization': 'Bearer bb_…',
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt, useCase: 'chart-summary' }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let i;
while ((i = buf.indexOf('\n\n')) !== -1) {
const raw = buf.slice(0, i);
buf = buf.slice(i + 2);
if (!raw.startsWith('data:')) continue;
const event = JSON.parse(raw.slice(5).trim());
handle(event);
}
}2 · GET /api/agent/runs/stream (tenant feed)
A long-lived feed of every webhook-fired agent run for the current tenant. Pair it with GET /api/agent/runs for the initial-load snapshot.
Event grammar
data: {"type":"open","tenant":"…"}\n\n
data: {"type":"run-started","deliveryId":"del_…","useCase":"…","subscriptionId":"sub_…","firedAt":"…","eventType":"…"}\n\n
data: {"type":"text-delta","deliveryId":"del_…","delta":"…"}\n\n
data: {"type":"tool-call","deliveryId":"del_…","id":"tc_01","name":"…","args":{...}}\n\n
data: {"type":"tool-result","deliveryId":"del_…","id":"tc_01","name":"…","result":{...}}\n\n
data: {"type":"run-done","deliveryId":"del_…","status":"ok","ms":4123}\n\n
data: {"type":"run-error","deliveryId":"del_…","message":"…"}\n\n
: keepalive\n\n <-- comment frame every 25sEvery event carries a deliveryId so you can correlate it with rows from /api/agent/runs.
Browser EventSource
const es = new EventSource('/api/agent/runs/stream');
es.onmessage = (ev) => {
const event = JSON.parse(ev.data);
switch (event.type) {
case 'run-started':
ui.beginRun(event.deliveryId, event.useCase);
break;
case 'text-delta':
ui.appendText(event.deliveryId, event.delta);
break;
case 'tool-call':
ui.recordToolCall(event.deliveryId, event);
break;
case 'tool-result':
ui.recordToolResult(event.deliveryId, event);
break;
case 'run-done':
case 'run-error':
ui.finishRun(event.deliveryId, event);
break;
}
};3 · Reconnection
The server emits a : keepalivecomment frame every 25s; intermediaries that drop idle connections (Cloudflare, Vercel's own proxy) will keep the stream alive.
If the connection drops, reconnect. There is no Last-Event-ID replay in v1 — the /api/agent/runs snapshot is the source of truth for any state you missed. EventSource reconnects automatically; in raw fetch consumers, wrap the read loop in an outer while (true) with backoff.
Note: long-running responses are capped at 300s. Reconnect before then if you need a longer feed.