The data model
Rows, the chat snapshot, the run projection, and the views — every type the SDK publishes, with the semantics of each field.
Everything the SDK publishes, in three families: the chat projection (the transcript and its state), the run projection (which node is doing what), and the views derived from joining them. All camelCase — the wire and Rust stay snake, mapped at the boundary.
Rows — the ordered transcript
rows is one heterogeneous, ordered feed: a user turn, an assistant turn, an
output, a tool call, and its result are peers, which is what lets a UI
render them in the order they happened. Narrow on row.type before reaching
for kind-specific fields — the union makes a wrong reach a compile error.
Row<TName>
The union — UserRow | AssistantRow | OutputRow<TName> | ToolCallRow | ToolResultRow — discriminated on type. TName is the union of the
agent's declared output names (the generated client supplies it); it
defaults to string.
type | Variant | Is |
|---|---|---|
user | UserRow | Something the person said. |
assistant | AssistantRow | Something the agent said. |
output | OutputRow | A declared output, at the position it was produced. |
tool-call | ToolCallRow | A tool the agent invoked. |
tool-result | ToolResultRow | What that call returned. |
A tool call and its result are two rows, never one that mutates: the gap between them is unbounded (a durable client-tool pause spans hours and a page reload).
UserRow
{ type: "user"; content: string } + the spine. The user's
turn. Belongs to the conversation, not a node — nodeId/activation are
always absent.
AssistantRow
{ type: "assistant"; content: string; reasoning?: string; complete: boolean }
- the spine. One node's assistant turn:
contentaccumulates frommessage.delta,reasoningis the extended-thinking text (absent when empty),completeisfalsewhile streaming. One activation is often SEVERAL of these (a streamed artifact splits the turn; a server-side tool loop emits adjacent ones) —assistantsInActivationjoins them.
OutputRow<TName>
{ type: "output"; name: TName; value: unknown; status: BindingStatus; format?: ArtifactFormat; complete: boolean } + the spine. A declared output
at the position it happened: status is building while a streamed
artifact arrives, format is its render hint, complete is false while a
same-turn re-emission can still refine it. The generated <Base>OutputRow
union narrows value by name — that is the surface to render from.
ToolCallRow
{ type: "tool-call"; toolCallId: string; toolName: string; input: string; requiresClientResponse: boolean } + the spine. A tool the agent invoked;
input is the arguments as the model produced them, unparsed. Paired with
its result by toolCallId (the timeline index's byToolCallId joins them;
useToolCall subscribes to the pair).
requiresClientResponse is the client/server split: true for a client
tool (the run pauses until your app answers — the HITL surface), false
for a server-side tool (memory, web.search, an MCP connector's tool), which
never pauses and never enters pendingTools — the row pair is there for
observability. A server tool the tenant has marked private is emitted
redacted: real toolName, input === "{}", result content empty.
ToolResultRow
{ type: "tool-result"; toolCallId: string; output: string } + the spine.
What the call returned. A FAILED client tool arrives here too — the failure
is delivered AS the result and the wire can't tell them apart; a field with
no producer is worse than no field.
RowKind
The string union of the type values: "user" | "assistant" | "output" | "tool-call" | "tool-result". The kebab forms are the durable log's own
names — a live row and a replayed one discriminate identically.
Composite rows — the feed kinds
A generated client's snapshot.rows contains the durable kinds above and
the composite kinds its graph can produce. Codegen narrows that union per
agent: adding an accepted fan-out region, subgraph, or repeatable node adds a
case that an exhaustively checked renderer must handle at regeneration.
Without any of those shapes, only the five durable kinds are needed. Raw
useInlayChat without generated graph config stays flat.
Composites are derived, not durable. Their opaque key is a stable view
identity for rendering, not a durable row id or pagination cursor. They
deliberately have no id. Fan-out membership comes from the generated
REGIONS descriptors,
not an SDK graph prover. Unsupported static shapes have no descriptor;
missing runtime activation evidence also leaves affected rows flat rather
than dropping them or assigning invented structure. See the
renderer and key rules.
Identity/version guard: with a generated feed config, composition requires
the conversation's actual agentId to match the expected agent and its
agentVersion to match an explicit expected version greater than zero.
A version mismatch, an allowed foreign-agent reopen (even with the same version
number), draft version 0, or an unknown expected version disables all
composites. The full paced row array stays flat, including inner-subgraph rows;
the guard does not switch to the top-level-only snapshot or drop content.
To recover grouping for a historical conversation, use a regenerated module
pinned to its recorded agent and immutable version.
FanOutRow
{ type: "fan-out"; key; nodeId?; subgraphPath?; join; tracks: FeedRow[][]; branchStatus?; hasEarlier? }.
One fan-out region rendered as one row: nodeId/subgraphPath name the
SOURCE node (the codegen nodeNameOf join works on it), join is the
join node's absolute path, and tracks holds one feed-row list per branch —
the branch's own rows, interleave-free, including nested composites.
branchStatus ("running" | "done" | "failed" | "unknown" per branch) is live only for
the region's latest firing — the run projection tracks the latest visit.
A distinct firing produces a second FanOutRow only when the rows establish
that boundary. A silent source and silent join cannot establish a new firing;
the descriptor adds no runtime firing provenance. A region whose branches
produced nothing renders no card at all.
branchStatus currently reflects branch entry nodes, not the completion of
every downstream node in a track. See the progress guide
for this limitation and the separate taken/untaken edge flags.
SubagentRow
{ type: "subagent"; key; nodeId?; subgraphPath?; rows: FeedRow[]; running?; done?; failed?; status?; hasEarlier? }.
A subgraph node rendered as one row, with its recursively composited inner
feed in rows. Counts roll up descendants from the run projection;
status is the subgraph node's own status when known. A subgraph appears
when recorded content or run evidence establishes it ran. A started
black-boxed subgraph (hide_inner_events) can have empty rows; an
unreached subgraph has no feed card. Use node views to display an idle plan.
There is one SubagentRow per subgraph node, holding all its visits' inner
rows in order.
RepeatRow
{ type: "repeat"; key; nodeId?; subgraphPath?; mode: "parallel" | "sequential"; tracks: FeedRow[][]; done: number; total?: number; hasEarlier? }. One container per repeatable node path and activation:
ensemble samples use parallel mode; for_each_llm items use
sequential mode. Each track groups rows with the same one-based
iteration stamp, sorted by that stamp. Missing iterations do not produce
empty tracks, so a track's array index is not necessarily its item/sample
number. Use a constituent durable row's iteration for numbering and track
identity, as in the renderer example.
done counts iterations represented by served rows, not completed
executions. An ensemble's total, when available in the generated metadata,
is its static sample count (n). for_each.total stays absent even after
completion. Empty or abstaining
samples produce no track: an ensemble can finish with done: 3, total: 4.
Use node/run status to determine whether execution ended, not done === total.
Ensemble sample content arrives as complete rows, without per-sample token
streaming. Ordinary for_each content can stream, and one item's track can
contain several assistant turns plus tool-call/result rows. Both iteration
and activation stamps are required for repeat grouping; non-iterated rows
or rows without activation evidence remain flat. hasEarlier can mark a
card opened into by a windowed read.
Structured ensemble decisions appear as JSON text in assistant rows, not
individually typed output values, only when output_key names a declared
public field. Making that field public exposes individual decisions, not just
the final consensus. Private or undeclared destinations still compute
internally without decision-content rows; see field visibility.
A structured for_each has no per-item served rows and produces no repeat
tracks; read its declared public output/binding instead.
FeedRow<TName>
The general-purpose union Row<TName> | FanOutRow | SubagentRow | RepeatRow,
useful for reusable renderers and the recursively nested rows in composites.
The generated <Base>FeedRow narrows this to that graph's possible kinds.
Use row.type or the guards isFanOutRow, isSubagentRow, isRepeatRow,
and isDurableRow (the five durable kinds) before reading variant fields.
RowSpine
The fields every durable row carries. Which exist depends on when you're looking — absent is honest: a missing field means "the wire can't say", never a placeholder.
Prop
Type
| Field | Present when |
|---|---|
id | Present when the durable row first appears on supported servers. A prefill placeholder exposes it at node start; subsequent turns and repeated items/samples get their own ids. Use key={row.id} for durable rows and row.key for composites. |
nodeId | The producing node. Absent on user turns. |
subgraphPath | Owning subgraph ids, outermost first. Absent at top level. With nodeId forms the absolute path that keeps a reused subgraph's rows apart. |
activation | The node's 1-based visit count when it ran. Absent on user rows and pre-field servers. |
iteration | The 1-based item/sample index on a repeatable node's per-instance rows. Absent outside an iteration; distinct from the node's activation. |
seq | Durable ordinal — replayed rows only (live rows get it at refetch). The loadEarlier anchor. |
turn | Conversational turn — replayed only, like seq. |
The chat projection
ChatSnapshot<TName>
The reducer's published state — what handle.snapshot and onSnapshot hand
you. Generic over TName (the output-name union).
Prop
Type
Field semantics the table can't hold:
pendingToolsis DERIVED, never edited: atool-callrow with no matchingtool-resultrow, recomputed after every change. It answers "what is waiting on the app" — the HITL surface.toolStatesrecords how DELIVERY is going per call (the client side), deliberately separate from the rows (the agent side). A replay never resurrects it.erroris the customer-safe reason —status: "failed"says THAT it failed, this says why. Absent for a run-level failure with no node reason.retryableis the server's own advertisement forPOST …/retry—retry()gates on it rather than re-deriving the rule.hasEarliermeans this is a tail window of the log — earlier rows exist;loadEarlier()pages them in.bindingsis the cross-turn LATEST value per output name; the per-turn production history is the output ROWS (outputsNamed).tablesis the live projection of the agent's declared collections: table name →TableRowView[], most-recent-first, including a row mid-stream (status: "building"). Rehydrated from the persisted working set on reopen — no live run required.subagentBindingskeys inner (un-promoted) outputs by the subagent's absolute path, so two instances of a reused subgraph don't collide.
ChatStatus
The single lifecycle answer — no phase/terminal pair to reconcile.
| Value | Meaning |
|---|---|
idle | Nothing started. |
streaming | A turn is streaming. |
awaitingInput | Paused at a user_turn — the composer is live; the next send resumes. |
awaitingTool | Durably paused on a client tool — survives a refresh; submitToolResult resumes. send is rejected here. |
completed | Finished successfully. |
failed | Ended at a failure (error may say why; retryable may offer retry). |
cancelled | Cancelled, locally or server-side. |
Terminal statuses allow send — but there it starts a fresh
conversation, not a continuation. Drive buttons off the flags, not this
table: canSend ANDs the map with the driver's latches, and canRetry is
computed separately (failed + retryable + nothing in flight — it is not
part of the Capabilities map).
PendingToolCall
One queued client-tool call — the HITL approval surface.
Prop
Type
args is the parsed arguments — what an approval screen renders — parsed
at derivation in both mirrors (Rust + TS, pinned identical by the parity
gate). Absent when the model produced unparseable JSON or the input exceeds
the shared 64KB byte cap (the per-delta re-parse amplification guard); the
raw string is always in argumentsJson. Typed per tool by the generated
<Base>PendingTool union.
ToolCallStatus
The client's delivery lifecycle for one tool call:
"pending" | "submitting" | "submitted" | "failed" | "cancelled".
cancelled is terminal and recorded per call (the queue is derived from
append-only rows, so "cancel forgets the call" must be recorded, not
inferred).
ToolDelivery
{ status: ToolCallStatus; error?: string } — one call's delivery state in
snapshot.toolStates: the handler resolved, the POST is in flight, the POST
failed.
Binding
{ value: unknown; status: BindingStatus; format?: ArtifactFormat } — a
published value: building while streaming, final at settle. What
snapshot.bindings holds per output name, and what useBinding returns
typed (validated at settle when a schema is declared).
BindingStatus
"building" | "final".
ArtifactFormat
"text" | "markdown" | "code" | "html" | "list" — the render hint for a
streamed artifact.
TableRowView
Prop
Type
One row of a live table projection (ChatSnapshot.tables, keyed by the
table's field name). status is building while the row streams
(token-by-token), final once the upsert settles — final for a
non-streamed row. key is the row's collection key (absent for a
single-value table). links is the row's FORWARD links (the document graph
— row-links.md); backlinks are on-demand (memory.peek), not streamed.
value is typed from the table's declared type by the codegen
(<Base>Tables), unknown un-codegen'd.
PublicError
{ kind: PublicErrorKind; message: string } — the customer-safe failure
form. The message is safe BY CONSTRUCTION (app-owned tool text or a curated
server string — never a provider body, a condition expression, or an
internal detail).
PublicErrorKind
"llm" | "tool" | "condition" | "internal" — the coarse class of failure
("misconfigured environment vs LLM outage") for failure UIs.
The run projection
The topology half: which node is doing what, at every subgraph depth. Published whole when it moves (bounded; no changeset), on its own subscription — a run panel re-renders on lifecycle, not on every token.
RunSnapshot
Prop
Type
NodeRunStatus
"running" | "completed" | "failed" | "cancelled".
NodeRun
One node's current activation, keyed by its ABSOLUTE path (a reused subgraph's inner ids repeat; the path keeps them apart).
Prop
Type
FiredEdge
An edge the run traversed, by semantic identity — fromPort says which
branch fired when several wires leave one node.
Prop
Type
TokenUsage
{ promptTokens; completionTokens; reasoningTokens } — raw token
accounting, on NodeRun.usage (per node, cumulative across activations) and
RunSnapshot.totalUsage (the per-node sum). The public run stream carries
tokens only: any client-side cost derived from them is an estimate, not
authoritative billing. Read the billing facade
for server-reported wallet and usage figures. Token usage is absent for a node
that made no LLM calls, or against a server predating the field.
Prop
Type
Subagent
{ path, nodeId, runningCount, doneCount, failedCount } — a subgraph that
ran, with its descendants rolled up at any depth (cancelled counts as
failed). Derived by subagentsOf; the counts are *Count because the
sibling views use the bare names for derived BOOLEANS — a count and a flag
sharing one name is a guaranteed misread.
The views
Derived by joining the two projections — every node, leaf or subgraph, reads the same shape at any depth.
NodeView
What a generated nodes.<name>() returns.
Prop
Type
An unreached node is "idle" — present, not absent — so the whole plan
renders up front. active/done/failed are derived from status
(failed includes cancelled). content/reasoning are the CURRENT
activation's joined text (a container's is its subtree's). Node names
aren't on the wire — the generated client supplies the id→name map.
SubagentChild
Prop
Type
One observed direct child of a subgraph, in run order: an entry in
SubagentView's children roster. kind and status
come from the run projection; kind can be absent when not reported.
SubagentView
Prop
Type
A deliberately DISTINCT shape from NodeView (no active/done/failed
booleans, no content/reasoning): the subgraph node's own status, the
descendant rollup (runningCount/doneCount/failedCount), visitCount,
exitedVia (the generated client narrows it to the declared exit ports),
path, and outputs — the subagent's own un-promoted outputs, validated
and typed by the generated view (dropped entirely for a black-boxed
subgraph).
Conversation, history, session
ConversationRef
{ conversationId, agentId, agentVersion } — identity, captured from the
first opener event.
ConversationStatus
Server vocabulary (snake values by design):
"running" | "completed" | "failed" | "cancelled" | "paused" | "awaiting_client_tool".
ConversationSummary
Prop
Type
Mapped from the snake REST response at the transport boundary; a row with no
string id is dropped, not mapped (absent is honest — String(undefined)
would mint a routable "undefined" id).
ConversationListQuery
{ limit?, offset?, agentId?, status? } — the history list's narrowing
filters; scope (tenant + user) is server-derived from the session and never
sent.
Session
Prop
Type
The browser only ever sees this — never the secret key.
SessionProvider
() => Promise<Session | string> | Session | string — resolves a session on
demand (e.g. a Next.js server action). Called lazily, and again to refresh
after a 401.
SessionSource
Session | string | SessionProvider — what every connection option accepts:
a minted session, a raw JWT, or a provider that mints one on demand.
ToolHandler
(args: unknown) => unknown | Promise<unknown> — a client-side tool
handler: receives the parsed args, returns the result (a string passes
through; anything else is JSON-serialized).
ToolHandlers
Record<string, ToolHandler> — the handler map, keyed by tool name.
ToolHandlersSource
ToolHandlers | (() => ToolHandlers | undefined) — the function form is
read live at each dispatch, so handlers can change across renders without
recreating the conversation.
The wire (stays snake by design)
These two are the JSON the client SENDS — snake, because the wire is the server's shape. Apps almost never build them (the transport does); they're exported for transport implementors.
ExecutionInput
Prop
Type
WireMessage
Mirrors agent-core's Message (OpenAI-shaped):
{ role: "system" | "user" | "assistant" | "tool"; content: string; tool_call_id?: string; name?: string }.
Constants
IDLE_RUN
RunSnapshot — the stable empty run, also the server-render value.
Billing (end-user reads)
The shapes BillingFacade returns. All money is micro-USD
($1 = 1,000,000); the session JWT scopes every read — tenant and end-user
are never sent from the client. Enum-ish strings (source, overage,
status) pass through unvalidated so a newer server reaches your app
instead of being masked by the client.
BillingBalance
Prop
Type
BillingUsage
Prop
Type
BillingPlan
Prop
Type
BillingPlans
Prop
Type
BillingInvoice
Prop
Type
CreditGrantView
Prop
Type
HostedCheckout
{ url?: string } — a hosted purchase surface returned by
billing.topup() and billing.subscribeCheckout(): the Stripe-hosted
URL to redirect the end user to. Card data never touches your or our
servers. The facade throws rather than return a missing URL.