State and memory
Durable fields, live tables, and the agent's own memory tools.
An agent's state is a set of declared fields — named values with semantics attached. Outputs and bindings covered the per-run kind; this page is the rest: values that persist, values that stream row-by-row, and the tools the agent itself has for remembering.
A field declares its semantics
Every field is declared on the graph with four attributes:
| Attribute | Values | Default |
|---|---|---|
visibility | private / public | private — declaring is not publishing. Only public fields reach the wire, the API, and codegen. |
durability | run / user / agent | run — per-conversation scratch. |
shape | slot / collection | slot — one value per scope. A collection is a table: many rows per scope. |
merge | replace / sum / append / union | replace — how a run's write-back composes with a durable value. |
Keys a node writes without declaring anything stay legal and mean
private + run — ephemeral working state needs no ceremony.
For a structured ensemble, output_key must name a declared public
field for individual sample decisions to appear in the timeline. Making it
public exposes samples, not just the consensus. A private or undeclared field
still lets the ensemble compute decisions and consensus internally.
Durable state
user and agent fields are durable memory: kept across conversations,
seeded into the run at start, and written back when it ends. user is scoped
to the calling end user (from the signed session claim — never the request
body); agent is shared by the whole agent.
Write-back composes by the field's merge policy, server-side, with deduped
write IDs — so a retried finalize cannot double-apply a counter or a list.
On the client there is nothing new to learn: a public durable slot is a
binding that is already populated when a conversation reopens, before
anything streams. useBinding("preferences") just has a value.
One special case: a field marked core is the agent's always-included memory
— one document per scope, injected into every LLM node's system prompt. It
never appears on the wire as an artifact; it is for the model, not the UI.
Tables
A public collection field appears on the snapshot as a table — read it
with useTable:
function GroceryList() {
const rows = useInlayChat(conn).useTable("grocery_list");
if (!rows) return null;
return (
<ul>
{rows.map((row) => (
<li key={row.id} data-building={row.status === "building"}>
{typeof row.value === "string" ? row.value : JSON.stringify(row.value)}
{row.links.map((link) => (
<small key={`${link.toField}/${link.toKey}`}> {link.label}</small>
))}
</li>
))}
</ul>
);
}What to know:
- Rows stream. A row being written arrives with
status: "building"and itsvaluegrows as it is produced, settling to"final". (A collection never streams as one whole value — its rows stream individually.) - Most-recent-first. New rows land at the top.
keyis the upsert key. A remember with an existing key replaces that row in place; keyless rows insert.linksare the document graph — forward links only ({ label, toField, toKey }). Backlinks are a server-side lookup, never streamed.- Reopen-safe. Tables rehydrate from the persisted working set — no live run required.
- A
buildingrow that never settles is swept when the run ends.
The codegen split guards the two reads: slot fields type useBinding,
collection fields type useTable, and passing a table's name to useBinding
(or the reverse) is a compile error. <Base>Fields carries every public
field's format / durability / streams as data, so generic rendering can
read the contract instead of hardcoding it. See
The generated module.
Two current limits, honestly: a subagent's own tables land in
snapshot.tables under scoped keys but useTable only reads the root tables,
and TableRowView is exported from @inlayai/sdk (the React package does not
re-export it).
Edits after settle
A settled value is not frozen. The agent can edit a published artifact in
place — artifact.edit applies a surgical replacement to the binding, and
artifact.rewrite clears and re-streams it. On a table, an edit names a row
key and only that row repaints.
Your side of this is nothing: useBinding / useTable re-render with the new
value. It is why "the agent revised the plan" does not need a bespoke event in
your UI.
The agent's memory tools
The agent works its own memory through built-in server tools — memory.remember
(write a slot, upsert a table row, optionally born-linked),
memory.recall (semantic search), memory.list / memory.peek (inventory and
read), memory.load / memory.unload (what the working set holds),
memory.append / memory.replace (surgical edits), memory.forget (clear a
slot, delete a row), memory.link / memory.unlink (the document graph).
You never implement these and they never pause the run — they are server
tools, surfaced in your timeline as observability rows with
requiresClientResponse: false. A call touching a private field arrives
redacted: the tool's real name, "{}" for args, an empty result. The full
contract is in Server tools.
There is deliberately no SDK read API for memory — the agent's memory is
the agent's. What your app sees is exactly what the graph declares public,
through the bindings and tables above.
Next
Progress and topology — which node is doing what, at every depth.