The adoption ladder
Start in read-only guide mode with zero handlers, then register handlers, then serve an agent — one authored graph carries you the whole way.
You don't wire everything at once. Adoption is a ladder, and the first rung needs no handlers at all.
Phase 0 — guide mode (read-only)
Wire only the two reporting calls: sync() when the router moves and updateState() when
your store changes. Register nothing. The agent can now read the position and plan over the
declared action space (whats_here / available()), but it acts on nothing — with no
handlers bound, every offered edge is plannable-only. This rung is zero-risk: it cannot
touch your app.
The one rule used to be never fire() an unregistered tool — 0.3.0 enforces it for
you. An agent-sourced fire of a tool nothing is bound to is a typed NOT_MATERIALIZED
rejection: it would execute nothing, so it is refused rather than returning a success-shaped
no-op the model reads as "it worked". Since the actuation work landed, the refusal also
carries the declared gesture — "this is a click on the checkout button", not "nothing is
bound". See Actuation & materialisation.
Your app reporting its own motion — fire(id, { source: 'user' }), source: 'system', or
the record-only invoke: false sensor — is never gated: that motion really happened.
Want the agent to walk the graph anyway — a tour, a plan preview, a guided demo? Opt in:
const session = graph.createSession({ node: 'catalog', allowUnmaterializedFires: true });Fires then proceed as honest no-ops: the result carries executed: false and
materialized: false, every served edge is stamped materialized: false before it is even
offered, and each one lands an unmaterialized-fire row in the gap ledger — the binding your
team has yet to build. Navigation claims still move the cursor (that is the tour) and say so
with toNodeClaimed: true.
Phase 1 — register handlers
As components mount, registerActions(path, { handlers }) binds your existing functions by
reference, so firing runs the app's own code. Edges now report materialized (false = still
declared-only, true = wired), and you watch the surface light up.
A page where nothing can act now says so. Once anything is wired, the session watches the
room as well as the action: land the cursor on a page where every served action would refuse
NOT_MATERIALIZED and you get a dead-end gap row and one dev warning naming the three
fixes — before an agent discovers it by looping on a true-but-useless list of things it cannot
do. It is an observation, not a verdict: a guard-closed action is wired and never counts, and
a mount that fixes the page ends the rows. See
Dead-end.
Two shortcuts on this rung:
- Pure navigations need no handler at all — hand the session your router's own
navigatefunction and url-gesture edges materialise through it. - If your app keeps a live action store,
fromLiveStoredoes the subscribe-and-register bookkeeping for you.
Phase 2 — serve an agent (Mode B)
serveToAgent(session) hands your host a fixed MCP tool set; the agent plans over journeys
and acts through your handlers. One authored graph carried you the whole way — no rung forced
you to maintain a second, stripped-down copy. See Journeys as fixed tools.
The three contexts
Map, traversal, actions — the three questions an agent asks about your app, each answered in the same three parts: what you declare, what you wire, and what the agent gets.
Demos
Two runnable demo apps in the repo plus the three-commit dress shop — every one runs with no API key, no network, and behaves identically every time.