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.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
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.model.message.delta — incremental text
model.message.delta — incremental text
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.tool.response — the result of a tool call
tool.response — the result of a tool call
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:
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.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 athreadId, 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: