Skip to main content
createTurn is an async generator. Each value it yields is an envelope, not a bare event:
@truefoundry/trueforge-ui types TurnStreamingEvent openly as { type: string; [key: string]: unknown }, so TypeScript will not check the shapes on this page for you — emit an event whose fields do not match and it compiles cleanly, then fails to render. Treat these shapes as required regardless.The named event interfaces below are declared in @truefoundry/trueforge-assistant-ui-runtime. Import them from there if you want compile-time checking when building a server.
sequenceNumber must increase monotonically within a turn. It is what subscribeToTurn uses to resume — a reconnecting client passes the last number it saw as afterSequenceNumber, and you replay only what follows.

Turn lifecycle

Every turn is bracketed by two lifecycle events.
Emit turn.created first, then content events, then exactly one turn.done. Because turn.done’s state excludes running, it must carry a terminal state — done, cancelled, or error — each with a completedAt. A stream that ends without turn.done leaves the UI believing the turn is still running.

Content events

model.message — a complete assistant message

content is either a plain string or an array of parts when the message mixes text with images or a refusal. reasoningContent populates the reasoning card. toolCalls carries fully-formed calls — see the tool calls section below.
Deltas accumulate against the id of the message they belong to, so reuse one id across every delta for a given message. content on a delta is the increment, not the running total.contentBlocks carries block-level deltas, used for streaming images alongside text, keyed by index. Both the camelCase and snake_case spellings are accepted; emit contentBlocks in new code.
toolCallId must match the id of the ToolCall this responds to. content is a string — serialize structured results yourself.

Tool calls

function.arguments is a JSON string, not an object. toolInfo drives presentation: the mcp variant is what lets the UI label a call with the server it came from, and the open third variant lets you introduce your own tool categories. Streaming a call in pieces uses a delta shape keyed by position:
Every field except index is optional, because a call is assembled across several deltas — typically id and function.name first, then function.arguments in fragments. Accumulate by index within the message.

Pausing for the user

Three events tell the UI a turn cannot proceed unattended:
tool.response_required has the identical shape and asks the user to supply a result rather than approve one. A ToolCallRef needs both the tool call’s id and sourceEventId — the id of the event that emitted the call — so the UI can attach the prompt to the right card.
authUrl is the URL the popup opens. See MCP OAuth.
These events also appear in requiredActions on a done turn state. That is how a turn that finished while waiting on the user communicates what it needs — a done turn carrying requiredActions is paused, not complete.
The user’s reply comes back through createTurn as a user.tool_approval or user.tool_response input item, carrying the same toolCallId.

Sub-agents

Sub-agents are modelled as separate threads. Nearly every event carries a threadId, and the UI groups events by it.
parent is what nests a sub-agent under the tool call that spawned it: the parent thread’s threadId plus the toolCallId. agentInfo populates the sub-agent card — name, the input it was given, and optionally the model it runs.

Sandboxes and MCP setup

sandboxId is what you pass to downloadSandboxFile to retrieve artifacts the agent produced. mcp.initialize ({ type, id, createdAt, threadId }, plus any additional fields you attach) reports MCP server startup.

The unions

The distinction is that TurnEvent holds the settled record of a turn, while TurnStreamingEvent adds the transient deltas and lifecycle markers that only matter live. This is why replaying a turn from history yields no deltas — the assembled model.message is stored instead. listEvents returns a session-level wrapper that identifies the turn rather than a position:
Note the asymmetry between the two envelopes: createTurn yields { sequenceNumber, event }, while listEvents returns { turnId, event }. Streaming is ordered within one turn; history spans many, so it labels each event with its turn instead.