The walk
Many actions as one correlated route — a plan admitted up front, executed without a model call between steps, answered with a manifest.
Why
A model that returns to its orchestrator between every screen action spends a round trip per step re-deciding things it already decided. And a batch that plows ahead blindly is worse: a screen action cannot be rolled back, so "the batch failed" while the screen sits two steps along is a lie of summary. The walk is the middle: admission checks what declarations can prove before anything runs; execution re-derives every step's offer at its own turn; and the answer is a manifest — ran, refused, never-reached, per row — so partial execution is legible instead of summarized.
import { beginWalk, connectAction, createActionRuntime, defineAction } from 'hcifootprint';
const findRun = defineAction('runs.find', {
does: 'Find the most recent zone-redundancy run',
invocation: 'inputless',
produces: { kind: 'run.ref' },
mutate: () => archive.mostRecent('zone-redundancy'),
});
const openRun = defineAction('runs.open', {
does: 'Open one run on screen',
invocation: 'scalar',
inputSchema: { safeParse: () => ({ success: true as const }) },
mutate: (ref: string) => screen.open(ref),
});
const runtime = createActionRuntime();
connectAction(runtime, findRun, { node: 'runs' });
connectAction(runtime, openRun, { node: 'runs' });
const walk = beginWalk(runtime, 'agent');
const manifest = await walk.run([
{ action: findRun },
{ action: openRun, carry: { from: 0 } },
]);
// manifest.rows → [{ status: 'ran', transition }, { status: 'ran', transition }]
// manifest.counts → { planned: 2, ran: 2, refused: 0, neverReached: 0 }The laws, and why each one
- An agent may not pre-plan a decision it does not own. A step whose
decisionOwnerthe walking principal does not hold is refused at plan time — not at step three of a half-executed screen. The same step is admitted for the owner: the law is ownership, not fear of batching. - A carry needs a declared
produces. Chaining is what makes plans worth having — but free references are a dataflow language by the back door. The gate is the declaration: a carried value comes from a declared output, never from whatever a handler happened to return. - Batched is not blind. The plan was made from what was true at step zero, and step one can invalidate step two — so every step re-reads its offers when its turn comes. A guard that stopped holding is a refused row carrying the runtime's own teaching sentence.
- One turn, one walk, many plans. A refused step means the caller
replans under the same walk id;
walk.record()then holds the route actually taken — which is not the route anybody planned, and comparing the two is where the interesting failures live.
Admission failures throw (nothing ran, so an exception is honest). Execution failures never throw — they are manifest rows, because by then something did happen and the caller needs the ledger, not a stack trace.
Breaking the walk
The human has a door, and the reason travels. A batch a person can watch is a batch a person must be able to stop — and a break without a reason is a guard failure wearing a trench coat: the model replans blind, and probably replans the same thing.
const walk = beginWalk(runtime, 'agent');
const running = walk.run(steps, {
onRow: (row) => renderStatus(row), // the FE's live loop — and where the
}); // stop control belongs, beside it
// The person presses stop:
walk.interrupt({ by: 'user', reason: 'wrong array — I meant NORTHWIND-01' });
const manifest = await running;
manifest.interrupted;
// → { by: 'user', reason: 'wrong array — I meant NORTHWIND-01', beforeStep: 1 }The semantics that keep it sound:
- Step-boundary only. The in-flight step finishes and settles normally — an L1 transition is atomic, and tearing one mid-flight would violate settlement law. L2 stops future steps; L1 transitions are never torn.
interruptedis a manifest field, not a row status. Un-run rows staynever-reached, but the manifest says who stopped it and why — because "the plan was wrong" and "the person knows something the plan didn't" must be treated completely differently by whoever replans.- The walk survives. A break ends the plan, not the walk: the model reads the reason and replans under the same walk id. The break is just the most human way a plan ends.
- The intent stands. An interrupt armed before a run is consumed at step zero — pressing stop between plans is still pressing stop.
Bring your own skin
The React hook is sugar, not the seam — write a framework skin of your own against the same primitives, and the four laws it must uphold.
Kind governance
needs and produces name kinds — a vocabulary governed before matching exists, so two teams' meanings of one word collide at connect time, not in production.