Blue Banana

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\n

Ordering

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 25s

Every 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.