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:
| Member | Type | Notes |
|---|---|---|
nodes.<name>() | () => NodeView | per-node BOUND HOOKS (call during render) — one subscription per accessed node. Routers' exitedVia narrows to the branch labels. |
useNodes() | () => <Base>NodesMap | every 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>Subagent | per-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 shape | Additional kind |
|---|---|
At least one accepted REGIONS descriptor, including supported multi-target Collect groups | FanOutRow |
Subgraphs with generated SUBAGENTS metadata | SubagentRow |
ensemble or for_each_llm nodes | RepeatRow |
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). TypesuseBinding(name).<Base>Tables— collection name → the row's value type; typesuseTable(name). Always emitted, even empty (an empty map makesuseTableuncallable — 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;nullstreams 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"narrowsrow.valueto 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>Argsinterfaces and<Base>ToolHandlers— every handler is required; a missing or mis-named one is a compile error. ReturnToolResult(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.toolNamenarrowspending.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 fromsnapshot.pendingTools(which staysPendingToolCall[]).
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).brancheson every node view —agent.nodes.<name>()anduseNodes()views carry the node's outgoing wires, each labelledtaken/untaken/pendingfrom the run'sedgesFired, 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 viaselectBranches; the per-node views are the front door. Fan-out can use several records sharing onefromPort; aCollectrouter can fire multiple rule ports. See branch labelling for the latest-firing semantics and the distinction from scalarexitedVia. 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.