hacifootprint
Get Started

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 tool0.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 navigate function and url-gesture edges materialise through it.
  • If your app keeps a live action store, fromLiveStore does 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.

On this page