@truefoundry/trueforge-sdk. New here? Install and stream your first turn in the SDK Quickstart first. For the mental model (Agent → Session → Turn → Event → Delta), see Concepts; for every event field, see the turn events reference at the bottom of this page.
Prerequisite: a named agent in the registry, or an inline agent spec you pass when creating the session. The examples below run the
web-research-brief agent from the Quickstart; build it there first, or swap in your own agent name / inline spec.client from Install and connect.
Install and connect
baseUrl at your running TrueForge server; http://localhost:8790 is the Quickstart default. When OIDC login is enabled, pass token (an ID token from your IdP; see Get a token and connect). When login is off, omit it.
Quick Start
The Concepts page explains the model with a conceptualsupport-bot; here is a real agent you can run. In the Quickstart you built web-research-brief, an agent with Exa web search and the web-artifacts-builder skill. Here it is from the SDK: open a session on that agent, stream one research turn, and print the brief as it arrives. It fans out to subagents, so you’ll see their threads in the same stream. It has no approval gate, so it runs end to end without pausing; for the approval and question pauses, see Handle pauses.
turn.done. This version writes each delta straight to stdout. The next section keeps an id-keyed event index, the pattern the pause recipes build on. The rest of the page is a reference for each piece, plus the pauses (approvals, questions, MCP auth) a real agent runs into.
Sessions
A session is the conversation context for one issue; it persists across turns. Persistsession.id to resume later.
Open a session
List sessions
Newest-first; filter withagentId. The returned Page is an async iterable and auto-paginates.
Turns
A turn is one request/response cycle: send input, the agent runs until it finishes or pauses, then the stream closes. Turns chain automatically (previousTurnId defaults to "auto"), so you never resend history.
Creating a new turn in a session automatically cancels any turn still running in that session.
Create and stream a turn
createTurnStream returns a stream of turn events. stream.withMetadata() yields { data, id }, where data is the parsed event and id is the per-stream sequence number used to resume after a disconnect. The stream opens with turn.created and closes with turn.done; read the terminal result from turn.done.state.
Keep an id-keyed event index. Model output streams as an empty model.message base followed by model.message.delta fragments that share the base’s id; store each non-delta event under its id, and merge each delta into the base with isEventDelta / mergeEventDelta. (Deltas appear only while streaming; listTurnEvents returns them already merged.) The pause handlers below reuse this index to look up the model.message that emitted a tool call.
input accepts three item types (a single turn can’t mix a user.message with approval or response items):
Non-streaming turn
If you don’t need live events,sessions.createTurn (stream: false) returns immediately with state.status: "running" and runs in the background. Poll getTurn until the status is terminal (done, cancelled, error).
List turns
Oldest-first; each turn exposes itsinput and state. A done turn’s state.requiredActions lists any pauses it surfaced.
Attach images or files
Auser.message’s content can be an array of parts: text plus one or more files as data URIs (data:<mime>;base64,<payload>).
Whether a file is understood depends on the agent’s model, so send images only to a vision-capable model. Non-image files (e.g. PDFs) need the sandbox enabled on the agent; the harness uses it to process the document.
Cancel a turn
sessions.cancel(sessionId) stops the running turn: it aborts the in-flight model request, waits for running MCP tool calls to finish, and force-stops any sandbox the turn provisioned. It’s idempotent, and the backend closes the SSE stream gracefully (a terminal turn.done, then it ends, so don’t break). Continue by creating a new turn, which chains on the cancelled turn’s history.
Handle pauses
A turn ends paused when it needs something from you. Each pause populatesturn.done.state.requiredActions; you resume by creating a new turn with the matching response item(s). A single turn can surface more than one pending item (e.g. parallel threads each hit a gate), so collect them all.
For approvals and questions, each pending ref carries the sourceEventId of the model.message that made the call. Look it up in the event index to read the tool’s name and arguments.
web-research-brief won’t pause on its own; it has no gated tool or clarifying-question step. The snippets below are illustrative: point them at an agent that has a require_approval_for_tools tool, uses ask_user_question, or connects to an MCP server needing OAuth.Tool approvals
A gated tool (human approval) emitstool.approval_required. Collect the pending events while streaming, then resume with one user.tool_approval per pending call, either allowing it or denying it with a reason.
Agent questions
When the agent uses the built-inask_user_question tool, it emits tool.response_required; the pending call’s arguments carry the question and options. Resume with one user.tool_response per pending call.
MCP outbound auth
When a tool needs a server’s OAuth, the turn ends withmcp.auth_required, listing each server and its authUrl. Send the user there, then resume. See In-chat authentication.
After
mcp.auth_required, the resuming turn must not include a user.message; resume with empty input (or omit it).Resilience and threads
Resume a stream
If you lose thecreateTurnStream connection (e.g. a process restart), persist session.id, turnId, and lastSequenceNumber. To resume: call getTurn → if still running, reconnect with subscribeToTurn and afterSequenceNumber to skip events you already saw; if finished, rebuild from listTurnEvents.
Subagent threads
A turn stream interleaves the root agent and any parallel subagents, exactly what the Quick Start above does. Every event carries athreadId:
"main": the root agent.- A unique id: a subagent thread.
thread.createdandthread.donebracket its lifecycle. null: a turn-level event, such asturn.created,turn.done,sandbox.created, andmcp.auth_required.
Map<threadId, Map<id, event>>) and merge deltas within the matching thread’s bucket.
Replay a finished turn
sessions.listTurnEvents() yields a finished turn’s events in order (auto-paginates; order: "asc" default, or "desc"). Events are already merged, so you can use each one directly.
Only available for completed turns; a running turn has no stored log yet, so stream it with
createTurnStream or subscribeToTurn instead.Turn events reference
Field-level schemas for every event streamed while a turn runs. The recipes above link here for the exact shape of each event they handle.Field names in this reference use the HTTP/JSON wire format (snake_case, e.g.
turn_id, source_event_id). The TypeScript SDK returns the same fields camelCased (turnId, sourceEventId) — so a field you see here maps directly to its camelCase form in SDK code.
A stream always opens with
turn.created and closes with turn.done. When you list persisted session events afterwards, you get the same events with deltas pre-merged into their model.message.
Lifecycle events
turn.created
First event on every stream.
turn.done
Last event on every stream. state.status is one of:
done— carriesoutput(the finalmodel.message, ornullwhen the turn ended paused),required_actions(pendingtool.approval_required/tool.response_required/mcp.auth_requiredevents, empty when none),completed_at, and optionalmetrics.cancelled— carries areason:client-cancelled,server-execution-timeout,cancelled-for-next-turn, orabandoned.error— carries amessage.
metrics, when present, aggregates the whole turn: total_input_tokens, total_output_tokens, total_tokens, total_cache_read_tokens, total_cache_write_tokens, total_reasoning_tokens, and total_cost_in_usd.
turn.done — paused for approval
Model output
model.message
An assistant message — text content and/or tool calls. On a live stream the base event arrives first and fills in via deltas; the merged event carries:
model.message.delta
An incremental fragment of a model.message — text and/or tool-call chunks. All deltas share the base event’s id; append content chunks and merge tool-call fragments by index. The most frequent event on a live stream. Not present when listing persisted events.
Tool activity
tool.response
The result of a tool the harness executed, linked to its call by tool_call_id:
tool.approval_required
A tool call needs human approval before it can run. The turn ends after this event; resume with a user.tool_approval input.
tool.response_required
A client-side tool (e.g. ask_user_question) needs a result from your app. Same shape as tool.approval_required; resume with a user.tool_response input.
Subagent thread events
thread.created
A subagent started. Subsequent events with this thread_id belong to it.
thread.done
The subagent finished. state is { status: "done", output: <model.message> } or { status: "error", error, output? }. Does not close the turn stream — the root agent continues.
Environment events
mcp.initialize
MCP server connections were initialized for a thread. mcp_servers lists each server: { id, name, session_id?, transport_type? } (streamable-http or sse).
mcp.auth_required
One or more MCP servers need OAuth authorization before the agent can proceed. mcp_servers lists { id, name, auth_url } — send the user to auth_url, then start the next turn. The turn ends after this event.
sandbox.created
A sandbox was provisioned, carrying its sandbox_id. Emitted once per session — subsequent turns reuse the same sandbox and emit no new event.
User input events
These are not streamed — they are the input items you send when creating a turn, listed here for completeness:
A turn’s
input cannot mix user.message items with approval or tool-response items.