Pradra StudioDocs

Streaming

A long answer that appears all at once after eight seconds feels broken. The same answer appearing word by word feels immediate, even though it finishes at exactly the same moment. Streaming delivers a generative answer as it is produced, and an agent run as it progresses.

for await (const ev of pradra.predictStream(capabilityId, { input: question })) {
  if (ev.type === "delta") process.stdout.write(ev.text);
  if (ev.type === "done") console.log("\ncost", ev.cost_usd);
}

Agent runs stream the same way with pradra.agents.stream(capabilityId, { goal }).

Streaming is a transport, not a second path

Every gate that applies to the ordinary call applies here, in the same order and from the same code: contract validation, the budget check before the first byte, the content filter, prediction history, metered spend, and the conversation turn. There is one copy of each rule rather than two kept in agreement — a gate cannot hold on one transport and lapse on the other.

An exhausted budget therefore arrives as an error event with code RATE_LIMITED and zero text.

The events

EventMeaning
statePhase changed: retrieving, calling_tool, generating.
deltaA chunk of answer text. Concatenating every delta is the answer.
tool_callThe agent is calling a tool (name only).
tool_resultThat call finished: name, ok/failed, duration.
approval_requiredThe run parked on a consequential call — with the exact arguments.
doneTerminal success: usage, cost, ids, content_filtered.
errorTerminal failure, carrying the same code the JSON endpoint would return.

New event types may be added; ignore ones you do not recognize rather than treating them as errors.

Why tool_result carries no payload

A tool result can be large, and it can hold data that must not reach the person watching the stream — the stream may end up rendered in your customer's browser. So the outcome is reported and the payload is not sent. tool_call carries only the tool name for the same reason.

approval_required is the deliberate exception: it does carry the proposed arguments, because an approver has to see exactly what will execute.

What you receive is what is stored

The content filter runs on the flow, not only on the finished text: a small tail is held back so a forbidden pattern split across two chunks is still caught before release. If it fires mid-answer, the already-sent text stands, the rest is withheld, and the fixed replacement is appended — and what the transcript stores is exactly the deltas you received. Bytes cannot be un-sent, so the guarantee that is actually worth making is that the audit matches what the user saw.

done.content_filtered: true marks it. Treat it as information, not as an error: it is the safety control working.

Agents stream progress, not thinking

An agent's model turns are structured actions, not prose. Streaming those tokens would leak the control-plane protocol to your end user and show them half-formed tool arguments as if they were an answer. So an agent streams state, tool_call and tool_result while it works, then its finalized answer as a single delta.

When the client hangs up

The two paths differ on purpose:

  • A generative stream stops. Nobody is reading, so nothing should keep being generated or billed.
  • An agent run does not. Its trace is run state, and a write that already fired cannot be un-fired by closing a socket. The run finishes and is recorded; what a disconnect costs is the answer, which you collect from the run afterwards.

A run parked for approval closes the stream cleanly rather than holding it open. A park can wait a day for a human, and a socket held that long is a leak, not a stream.

If you write your own client

  • Buffer to a blank line. Frames may be split anywhere across network chunks, including mid-character. Parsing each chunk as it arrives is the classic way to lose the last part of an answer.
  • Hang up by cancelling the reader, not by leaving the loop. A reader already waiting on the network stays parked until the server sends something — a stop built on the loop alone appears to do nothing on a stream that is thinking.
  • Failures before the stream opens are ordinary HTTP errors. A malformed request answers with a normal status code; only after the response has begun do failures arrive as an error event.
  • If you sit behind a proxy, disable response buffering for these routes. Otherwise every delta is held until the response ends — correct code, and the answer still appears all at once.

In the Studio

The Chat page streams both kinds of turn, shows the tool trail as it happens, and reports usage and cost when the turn completes.

Next