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.

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:

kindFields beyond seq / ts / runId
ai_chunkstep, 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.watch replays 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 new attempt, 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.

On this page