InlayDocs

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:

AttributeValuesDefault
visibilityprivate / publicprivatedeclaring is not publishing. Only public fields reach the wire, the API, and codegen.
durabilityrun / user / agentrun — per-conversation scratch.
shapeslot / collectionslot — one value per scope. A collection is a table: many rows per scope.
mergereplace / sum / append / unionreplace — 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 its value grows 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.
  • key is the upsert key. A remember with an existing key replaces that row in place; keyless rows insert.
  • links are 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 building row 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.

On this page