InlayDocs
Reference

The generated module

What `inlay codegen` emits for your agent — typed hooks, tools, outputs, nodes, and subagents.

inlay codegen reads your deployed agent and emits one TypeScript module — an optional typed layer over the generic SDK. It bakes the contract in: the agent id and version pin, the tool arg schemas, and types built from YOUR agent's declared outputs, tools, nodes, and subgraphs. Re-run codegen to move to a newer version; anything the new contract changed becomes a compile error rather than a runtime surprise. The typed-client guide covers installation, credentials, and the write-then-check sequence. The CI check requires --version and the same --name and --out as generation.

The module is data + thin typed wrappers: the consts (AGENT, NODES, EDGES, REGIONS, <Base>Fields, TOOL_SCHEMAS, SUBAGENTS), the types, and hooks that delegate to the agent-agnostic machinery in @inlayai/react/internal. That machinery splits per environment via the react-server export condition, so the module works in any component — a generated hook called in a Server Component resolves the frozen idle build (zero React hooks); in a client component it's the live one. See Server and shared components.

Everything below is parameterized on your agent's base name — shown as <Base> (e.g. Kitchen, Recipetto). The SDK's test fixtures continuously compile emitted modules for agent shapes including linear graphs, fan-out, repeat-only graphs, and subgraphs, with typed outputs and client tools where declared.

The agent-agnostic provider + hook-owned tools

// One provider holds the connection for EVERY agent (from `@inlayai/react`):
<InlayProvider session={session}>
  <App />
</InlayProvider>

// The hook owns the tools (typed, required); the connection comes from context:
const agent = useKitchenAgent({ tools });
const history = useKitchenHistory();

The provider is the package's <InlayProvider>agent-agnostic by design. Its props: session (required), baseUrl? (defaults to the production API — an env var is a dev/staging override, not a requirement), fetch?, children. It carries the connection and the history-invalidation channel and nothing that varies per agent, so ONE provider serves any number of agents on the page.

The agent's tools live on the hook, not the provider — they're typed to the agent (<Base>ToolHandlers, required and exhaustive) and evolve with its contract, so a mis-named or missing handler is a compile error where you use the agent. The connection knobs (session, baseUrl, fetch) default to the provider's and override per-option, so useKitchenAgent({ tools, conversationId }) under a provider needs nothing else. Standalone (no provider) you supply at least the session — enforced at runtime, since TS can't see the provider.

Each hook call builds its own client. To share one conversation across components (a sidebar and a composer), lift the handle — build it once in a parent with useKitchenAgent({ tools }) and pass it down (or wrap it in your own context). There is deliberately no context-shared chat handle.

The provider is a client reference: render <InlayProvider session> from a Server Component and the real provider mounts on the client, publishing the connection. Only serializable props (session, baseUrl) cross that boundary — and since tools live on the hook (a client component), they never cross it. Under a provider, a mounted use<Base>History refetches itself when a conversation starts or settles (the channel) — no hand-wired refetch.

The handle — <Base>AgentHandle

Without composite shapes or untyped subgraph bodies, the handle extends InlayChatHandle<TBindings, TTables> directly, using <Base>Bindings and <Base>Tables. With composites or untyped subgraph bodies, it extends Omit<InlayChatHandle<TBindings, TTables>, "snapshot"> and supplies snapshot: <Base>Snapshot instead. All other base members remain, plus these agent-specific accessors:

MemberTypeNotes
nodes.<name>()() => NodeViewper-node BOUND HOOKS (call during render) — one subscription per accessed node. Routers' exitedVia narrows to the branch labels.
useNodes()() => <Base>NodesMapevery node's view at once, as DATA (safe in conditionals/callbacks). A whole-panel view takes this; a single-node card keeps nodes.<name>().
subagents.<name>()() => <Base><Name>Subagentper-subagent bound hooks (emitted when the agent has subgraphs) — status, descendant rollup, exitedVia narrowed to declared exits, typed outputs (validated; dropped for black-boxed subgraphs).

Snapshot and feed types

In the generated React module, <Base>FeedRow is emitted only for graphs with composite shapes. It starts with Row<keyof <Base>Bindings & string> and adds the applicable kinds. If an untyped subgraph body can contribute public output rows, its durable arm is the unrestricted Row instead:

Graph shapeAdditional kind
At least one accepted REGIONS descriptor, including supported multi-target Collect groupsFanOutRow
Subgraphs with generated SUBAGENTS metadataSubagentRow
ensemble or for_each_llm nodesRepeatRow

A repeat-only graph gets RepeatRow even without fan-out or subgraphs. These types describe possible row shapes, not a promise that every run contains a card of each kind. The fan-out union arm comes from accepted descriptors, not an independent guess from multi-target wiring. REGIONS = [] is authoritative: it adds no FanOutRow case.

<Base>Snapshot is emitted when there are public outputs, composite shapes, or untyped subgraph bodies. For a non-composite graph without an untyped subgraph it aliases ChatSnapshot<keyof <Base>Bindings & string>. For a composite graph it omits that snapshot's rows member and replaces it with rows: <Base>FeedRow[]. A graph without public outputs, composite shapes, or untyped subgraph bodies emits neither alias.

The actual use<Base>Agent() return type preserves this feed union: narrowing row.type works directly on agent.snapshot.rows, not just on a separately imported alias. For agent-specific component props, keep <Base>AgentHandle, <Base>AgentHandle["snapshot"], or <Base>Snapshot when emitted, rather than annotating a composite snapshot as the flat ChatSnapshot. A renderer reused across agents should accept the generic FeedRow; composite tracks and inner rows also use that generic union. See Keys for durable id versus composite key.

Codegen supplies node kind and ensemble samples in NODES, alongside region descriptors and subgraph metadata. The generated hook normalizes these into FeedConfig automatically; there is no manual feed-config setup or metadata copying to do.

Subgraph node metadata also keeps the full-row path active when the CLI cannot emit an accessor for its body, such as an unresolved by-reference subgraph. Public inner rows without a known container remain flat, even when REGIONS is empty. The node kind alone does not create a SubagentRow or infer fan-out membership; server-side hidden events remain hidden.

These full snapshots allow output name: string and value: unknown: a child can publish a name that the parent never declared, or reuse a parent's name with a different schema. The parent's useBinding names remain typed to its own declarations. The generated isOutputFor guard excludes inner rows in this case; check it before treating a row as a typed parent output. With no composite shapes, this wider <Base>Snapshot aliases ChatSnapshot.

Feed topology

Every React-target module exports REGIONS, including an empty array for graphs with no supported fan-out regions, and passes it as CONFIG.regions. Its emitted annotation, NonNullable<AgentMachineryConfig["regions"]>, is readonly FeedRegion[]. The framework-free types target does not emit this React machinery. See FeedRegion for the descriptor members and absolute-path encoding.

The shared Rust analyzer owns join selection and execution-region analysis. Its conservative feed projection omits branch cycles, overlapping claims, staged joins, ambiguous outside ingress, and downstream flow that re-enters the source, a branch, or the selected join. A cycle confined after the join is allowed. These are presentation restrictions, not new execution validation: backend-valid post-join loops remain valid even when their feed regions are omitted.

For Collect, the descriptor groups eligible rule targets across ports, excluding else/error fallback targets, but retains the analyzer-selected join for the full backend region. A fallback that can enter a rule branch makes attribution ambiguous, so that region stays flat. Projection warnings also participate in inlay codegen --strict.

REGIONS is the source of fan-out membership; NODES and EDGES do not trigger runtime graph analysis. EDGES remains the adjacency surface for selectBranches, independently of whether a fan-out card can be rendered. Pacing, row-based firing-boundary matching, and reference stability remain client-side; descriptors add no runtime firing provenance.

Regenerate existing modules with the updated CLI, even if the agent version pin has not changed. Older NODES/EDGES-only metadata has no runtime fallback that reconstructs REGIONS; absent descriptors mean no fan-out grouping. Use the same agent, version, name, and output options, then review the generated diff and compile your renderer. See Staying current. With a feed config, composition also requires the conversation's actual agent id and positive version to match the pin; otherwise the identity/version guard preserves the full paced feed flat, including inner rows.

Typed outputs

interface RecipettoBindings { recipe: string; grocery_list: unknown[] }
interface RecipettoTables { /* collection fields, by name → row value type */ }

const RecipettoFields = {
  recipe: { format: "markdown", durability: "run", streams: true },
  grocery_list: { format: "list", durability: "run", streams: true },
} as const;
  • <Base>Bindings — slot output name → value type (schema-typed when declared, else the format's natural type). Types useBinding(name).
  • <Base>Tables — collection name → the row's value type; types useTable(name). Always emitted, even empty (an empty map makes useTable uncallable — the correct answer for a table-less agent). The bindings/tables split is the type-level guard against reading a table as a slot or vice versa.
  • <Base>Fields — every declared public field as data: format (the render hint; null streams as plain text), durability ("run", or "user"/"agent" for durable memory that persists across conversations), streams (a node fills it progressively). Drive generic rendering off this instead of hardcoding per-field behavior.
  • <Base>OutputRow — the output row as a NAME-discriminated union: row.name === "recipe" narrows row.value to the declared type. This is the surface you render from (the timeline's output rows).
  • isOutputFor(row, name) — the guard producing that narrowing, so the check and the type can't disagree.
  • OUTPUT_SCHEMAS (only when outputs declare schemas) — settled binding values are validated before they're handed back typed: a schema'd precise type is a checked cast, not an assertion over model output.

Typed tools (when the agent declares them)

  • <X>Args interfaces and <Base>ToolHandlers — every handler is required; a missing or mis-named one is a compile error. Return ToolResult (any JSON value; a string passes through raw).
  • TOOL_SCHEMAS — the runtime half: LLM-produced args are validated before your handler runs (the server doesn't validate client-tool args).
  • isToolCallFor(row, name) — narrows a row to one tool's call, preserving the spine.
  • <Base>PendingTool — the queued call NAME-discriminated: pending.toolName narrows pending.args, so an approval screen renders the arguments typed. Args are PARSED at derivation (absent when unparseable), typed by the same trusted cast as the handler's args — NOT validated here (validation runs at handler invocation, after the human decides; render defensively for consequential actions).
  • isPendingToolFor(pending, name) — the guard producing that narrowing from snapshot.pendingTools (which stays PendingToolCall[]).

A tool-less agent emits none of these — no empty interfaces, no tools prop anywhere.

Nodes and names

  • AGENT{ id, version } as const: the pin. Every run sends the version, so publishing a newer one can't move the graph under a deployed app.
  • NODES / <Base>NodeName / NODE_NAMES / nodeNameOf(row) — the id↔name tables and the row→name resolver (undefined-safe: a conversation can outlive the graph it ran under, so an unknown id must not throw).
  • branches on every node viewagent.nodes.<name>() and useNodes() views carry the node's outgoing wires, each labelled taken / untaken / pending from the run's edgesFired, so a flow diagram greys the branches a run didn't take. EDGES (when the graph has wires) is the raw adjacency const underneath — source node name → { fromPort, to } records — exported for custom joins via selectBranches; the per-node views are the front door. Fan-out can use several records sharing one fromPort; a Collect router can fire multiple rule ports. See branch labelling for the latest-firing semantics and the distinction from scalar exitedVia. Interiors of black-boxed or by-reference subgraphs are absent.
  • <Base>NodesMap / <Base>NodeAccessors — the two node-read shapes (views-as-data vs bound hooks).
  • SUBAGENTS (when subgraphs exist) — name → { path, exits, outputSchemas, blackBox }.

Session glue

Pair with @inlayai/next: the agent-agnostic provider's session prop takes createSessionProvider() directly.

On this page