Streaming
Show a viewer tokens as the model produces them and still get one durable result - the stream replays from token 0, the memoized value is the full text.
A step.ai.generate call can stream the model's tokens as
they arrive: each delta is appended to the run's durable
timeline as an ai_chunk frame. The step's durable result is
still the complete text, memoized on replay, so streaming changes what a viewer sees while the step
runs, never what the step produces.
Turning on streaming
Pass stream: true to generate:
const result = await ctx.step.ai.generate("summarize-thread", {
model: "claude-opus-4-8",
prompt: `Summarize this support thread:\n\n${thread}`,
stream: true,
});
// result.text is the complete summary - identical to a non-streaming call.res, err := duraton.Generate(c, "summarize-thread", duraton.GenerateOptions{
Model: "claude-opus-4-8",
Prompt: "Summarize this support thread:\n\n" + thread,
Stream: true,
})
// res.Text is the complete summary - identical to a non-streaming call.The return value is unchanged: result.text is the full response and the usage counts are the same.
stream: true only adds the live delta feed alongside the durable result.
Live deltas need a live channel back to Duraton, which the connect runner
transport provides. Over an HTTP serve runner - which has no open back-channel mid-invoke - the call
transparently falls back to a plain generate: the result is identical, but only the final text is
recorded, not the intermediate tokens.
On the timeline
Streamed deltas ride the same per-run timeline as status transitions and logs, as an ai_chunk frame
kind:
kind | Fields beyond seq / ts / runId |
|---|---|
ai_chunk | step, attempt, index, delta, ttftMs? |
Tail them with runs.watch and reconstruct the text by concatenating deltas in index order:
import { createClient } from "@duraton/sdk/client";
const duraton = createClient({ url: process.env.DURATON_URL! });
let text = "";
for await (const frame of duraton.runs.watch(runId)) {
if (frame.kind === "ai_chunk" && frame.step === "summarize-thread") {
text += frame.delta;
if (frame.ttftMs !== undefined) console.log("time to first token:", frame.ttftMs, "ms");
}
}ttftMs (time to first token) rides only the first delta of a stream, so you can surface latency the
moment generation begins. index is a per-stream counter; frames are appended in order and each carries
the run's monotonic seq, so the lossless reconnect
rules apply unchanged - resume past the last seq you saw and you never miss or double-count a delta.
Resumability
Because every delta is a durable row, the stream is replayable, not ephemeral:
- A viewer that opens the run after generation started replays every delta from token 0, rebuilds the full text, then tails the rest live.
- A refresh mid-stream loses nothing:
runs.watchreplays the history, so the text reconstructs exactly. - Deltas are keyed by
(step, attempt). If a crash re-runs the step on a later attempt, its stream carries a newattempt, so a resumed stream never mixes with the abandoned one - render only the latest attempt's deltas.
On replay, the generate step is memoized from its recorded result and returns the complete text without calling the model again - so a replay does not re-stream, and there is no re-spend on a recorded call.
In React
The @duraton/react kit exposes a useStream hook that does the reconstruction for
you. It rides the same durable timeline, so it is replay-safe and reconnecting by construction:
import { useStream } from "@duraton/react";
function SummaryStream({ runId }: { runId: string }) {
const { text, ttftMs, tokenCount, streaming } = useStream(runId, "summarize-thread");
return (
<div>
<p>{text}{streaming && <span className="caret" />}</p>
<small>
{tokenCount} tokens{ttftMs !== undefined && ` · ttft ${ttftMs}ms`}
</small>
</div>
);
}useStream(runId, step) returns the reconstructed text, the ttftMs once the first delta lands, a live
tokenCount, and a streaming flag that stays true until the step reaches a terminal status. It picks
the latest attempt automatically, so a crash-retried stream renders cleanly. The hook must be used under a
DuratonProvider holding a client.
In the console
A streaming generate step gets a Stream tab in the run's step detail. It shows the text building live with a caret, the running token count, and the time to first token; after the step finishes it keeps the reconstructed text (no caret) - the same replay-from-token-0 view the API exposes.
See streaming running end to end in the examples.
AI agents
Build AI agents that survive a crash - step.ai turns model calls, tool calls, and agent loops into durable steps, plus the classic agent patterns.
Approvals
Stop your agent before a risky action and wait for a person - the run parks holding no worker, then resumes from exactly that checkpoint.