Value Checkpoints — chidori.step(name, fn)
chidori.stepbounds resume cost on long histories by memoizing expensive deterministic computation into the durable call log. This doc covers thechidori.*host API every agent gets; the lower-level engine-crate counterpart (durableSteponchidori_js::ReplayRuntime,crates/chidori-js/src/replay.rs) is covered in §5. Related:docs/architecture.md,docs/signals.md.
1. Summary (TL;DR)
Chidori's durability model is deterministic replay: resume re-executes the
agent's JavaScript from the top while every recorded host effect (prompt,
tool, http, …) is served from the journal instead of re-performed. Host
effects are therefore cheap on resume — but pure JS compute between effects
is re-executed every time. For agents that do heavy deterministic work
(parsing large documents, building plans, transforming corpora), a long-lived
run gets progressively more expensive to resume.
chidori.step(name, fn) fixes that:
const plan = await chidori.step("plan", () => buildPlan(input)); // expensive, pureLive, fn runs once and its JSON-serializable result is recorded as a step
call-log record. On every subsequent replay — crash recovery, input() /
approval / signal resume, chidori resume, POST /sessions/{id}/replay — the
recorded value is returned (or the recorded error re-thrown) without
re-running fn. Resume cost becomes proportional to the
un-wrapped code, not the total compute the run has ever done.
2. The contract: pure, synchronous compute
A skipped callback must be skippable: if fn had observable effects, replay
(which never runs it) would lose them — state would silently diverge, or the
journal's sequence numbers would desynchronize. So step callbacks must be
pure, synchronous computation, and the runtime enforces the classes of
violation it can see, loudly:
| inside a step callback | behavior |
|---|---|
any chidori.* effect (log, prompt, tool, nested step, …) | throws chidori.<effect> is not allowed inside chidori.step(...) |
captured randomness (node:crypto randomBytes, crypto.getRandomValues) | throws (it would write a crypto.random record) |
VFS writes (node:fs write/append/mkdir/rm/rename) | throws (the mutation would be lost on replay) |
timer / microtask scheduling (setTimeout, setInterval, queueMicrotask) | throws (the scheduled callback would never exist on replay) |
an async callback / returned Promise | throws chidori.step callback must return synchronously |
Allowed: everything deterministic and recordless — plain compute, JSON,
Math.random/Date (deterministic by engine policy), crypto hashing, and
VFS reads (read-only, and the memoized result keeps replay exact
regardless). The result must be JSON-serializable; it is JSON round-tripped on
the live path too, so live and replayed runs observe byte-identical values.
What cannot be policed at reasonable cost: leaking work out of the callback by closure mutation plus deferred promise reactions. Don't do that — the contract is "compute a value from your inputs and return it".
3. Semantics and mechanics
A step is one CallRecord — function: "step", args: {name}, the
result (or error) holding the outcome — at one sequence number. The
implementation is a two-phase protocol because the callback is a JS value that
cannot cross the JSON host boundary:
step_begin {name}(host_core::execute_step_begin): allocates the seq and checks the replay log.- Replay hit (
try_replay_checked(seq, "step")): returns the recorded value or error. The recordednamemust match the call's name, else the code was edited before the resume frontier — fail-loud divergence, same contract as every other host effect. - Miss: marks the step live on the
RuntimeContext(begin_step(seq, name)) and the engine binding runs the callback. While the step is live, the effect dispatchers (HostBindingBackend::dispatch, the__chidori_*sync natives) refuse the calls in the table above.
- Replay hit (
step_end {name, value | error}(host_core::execute_step_end): takes the live-step marker back and writes thesteprecord at the reserved seq.
The engine binding lives in chidori-js's install_chidori_effects
(crates/chidori-js/src/lib.rs): probe, vm.call(fn) only on a miss, report.
A thrown callback records the error message and replays as the same throw.
No pending host operation, no pause. A step cannot suspend (everything
suspendable is refused inside it), so it needs no PendingHostOperation /
host-promise entry. A crash between begin and end simply re-runs the
(deterministic) callback on resume — memoization is an optimization, never a
correctness dependency.
4. Determinism analysis
- Match key is
(seq, "step")plus thenamecarried in args.seqcomes from the deterministicnext_seq()walk, so a replayed run reaches the same step at the same seq (inductively: the prefix is deterministic). A renamed or moved step fails loudly instead of silently mis-replaying. - The journal cannot gap. Because every record-producing or
state-mutating operation is refused while a step is live, the step's record
at seq
Nis always immediately followed by the next effect atN+1— in the live log and in every replayed log. Skipping the callback can therefore never desynchronize sequence numbers (the failure mode that would otherwise make memoize-and-skip unsound). - Errors replay as errors. A failed step records
errorand re-throws on replay, so atry/catcharound a step takes the same branch every run. - Edit-and-resume composes. Editing a step's body after the resume frontier takes effect on the next fresh run; editing it before the frontier is invisible (the recorded value wins) — which is exactly the modify-and-resume contract everywhere else in the journal. Renaming a pre-frontier step is detected as divergence.
5. Relation to neighbors
chidori.mark(label, data)records an explicit marker you compute yourself; it doesn't skip anything.stepis the memoizing version: the runtime decides record-vs-replay and the callback body is the thing being saved.durableStep(fn)(ReplayRuntime::install_memo) is the lower-level engine-crate counterpart with the same record/replay semantics, keyed by invocation index only.chidori.stepis the agent-facing surface: named, divergence-checked, recorded in the real call log, enforced pure, visible in traces andchidori traceoutput as asteprecord.- Provider prompt caching (
docs/context-management.md) bounds token re-billing;stepbounds CPU re-execution. Both are live-only optimizations layered under the same source of truth, the call log.
6. Limitations
- Un-wrapped code still replays linearly. There is no periodic value
snapshot of agent-declared state ("restore from the last checkpoint, skip
the prefix entirely"); that would bound even the un-wrapped replay cost, at
the price of a checkpoint-aware programming model for loops.
stepis the composable primitive that doesn't change the model. - Step bodies must be synchronous. Async callbacks (even ones awaiting only pure promises) throw rather than being drained inside the binding.
- Traces do not report replay time saved per step.