An agent run in Reactive Agents behaves like an OS process, not a fire-and-forget function call. Every durable run has an identity (runId), a live control plane (pause / resume / stop / inspect), an on-disk checkpoint history you can fork from, and a graded evidence trail — the trust receipt — attached to its result.
Runnable end-to-end demo: apps/examples/src/advanced/process-model-demo.ts (local Ollama, no API key).
The process model
Section titled “The process model”agent.runStream() returns a RunHandle — an async iterator of stream events that is also the run’s control plane:
const agent = await ReactiveAgents.create() .withProvider("ollama").withModel("qwen3:4b") .withTools({ tools: [calculatorTool] }) .withReasoning() .withDurableRuns({ dir }) // checkpoints + fork/resume need this .build();
const handle = agent.runStream("Compute 137*89, add 4455, divide by 7 — use the calculator for every step.");
handle.status(); // "running" | "paused" | "stopped" | "terminated" | "completed"handle.pause(); // freeze at the next iteration boundaryhandle.resume(); // continue from pausedhandle.stop(); // graceful: synthesize, emit StreamCompletedhandle.inspect(); // live kernel-state snapshot (below)inspect() — live kernel-state introspection
Section titled “inspect() — live kernel-state introspection”handle.inspect() projects the most recent iteration-boundary checkpoint into a small, stable shape — while the run is still going:
const snap = handle.inspect();// {// status: "running",// iteration: 2,// stepsCount: 6,// messagesCount: 5,// lastThought: "The product is 12193, now I need to add 4455…", // ≤500 chars// pendingToolCalls: ["calculator"],// capturedAt: 1751712000000,// }It returns undefined before the first iteration boundary and on non-kernel paths (inspect() requires .withReasoning() — the kernel notes a checkpoint at every iteration boundary). It never throws, and a run that never calls inspect() pays zero serialization cost: the snapshot is a lazy thunk, only invoked when you ask.
fork() — counterfactual restart from a checkpoint
Section titled “fork() — counterfactual restart from a checkpoint”agent.fork(runId, opts?) starts a brand-new run seeded from any checkpoint of a prior durable run:
const result = await agent.fork(runId, { at: 1 }); // restart from iteration ≤ 1// result is a normal AgentResult (fork mirrors resume, not runStream)
const runs = await agent.listRuns();// the fork row: runId "<src>-fork-3f2a", forkedFrom: "<src>", forkedAtIteration: 1Options: at (checkpoint iteration, defaults to the latest), task (override the re-run input), model (override the model for this run only).
Honest scoping — this is a counterfactual restart, not time-travel. The forked run replays nothing: it restores the recorded kernel state at the fork point and then continues with live, fresh LLM calls against the current provider. Same state, new future. Fork requires .withDurableRuns() and the kernel path (.withReasoning()); v1 forks under the same agent instance (same tools and system prompt). Two known caveats:
- A run currently paused awaiting approval/interaction may not have flushed its latest checkpoint — forking it can see a stale or absent checkpoint row.
- The
modeloverride has no effect when.withModelRouting()is enabled (the routing phase recomputes the model independently — known v1 gap). Don’t combine them.
The trust receipt
Section titled “The trust receipt”Every terminal result carries result.receipt — graded evidence about HOW the answer was produced, not a truth certificate. It grades the run’s evidence trail (did the answer come from tool observations, or from the model’s own head?), never the factual correctness of the output.
const result = await agent.run("Compute 137*89 with the calculator.");result.receipt;// {// verdict: "tool-grounded",// method: "heuristic",// confidence: 0.8,// toolsUsed: ["calculator"],// toolCallStats: { ok: 3, failed: 0 },// terminatedBy: "final_answer",// modelId: "qwen3:4b",// computedAt: 1751712000000,// }It is computed from in-memory run data at result assembly — present even with tracing disabled — and attached on both the promise path (result.receipt) and the streaming path (StreamCompleted.receipt, plus a TrustEvent before it; AgentStream.collect() carries it through). Paused runs (awaiting approval/interaction) get no receipt: receipts belong to terminal results only.
Deliverable truth (receipt.deliverables[])
Section titled “Deliverable truth (receipt.deliverables[])”When the run’s compiled contract declared at least one concrete deliverable (a file to write, an answer section, a structured object), the receipt carries a deliverables[] array naming each one as produced or missing:
result.receipt?.deliverables;// [// { spec: "produce the file ./report.md", produced: true },// { spec: "produce the file ./summary.md", produced: false }, // never landed// ]Each entry is { spec: string; produced: boolean }. produced: false names a missing output — so a partial multi-file run reports exactly which deliverables never landed instead of claiming success. The check runs against the run’s append-only evidence ledger (which records artifacts written by the built-in file-write tool as well as by code-execute / shell / MCP tools, each with a content digest). The field is absent for pure Q&A runs that declared no deliverable, keeping those receipts byte-identical to before. Declare deliverables explicitly with .withContract(); the harness also infers them from task phrasing that names files or outputs.
Verdicts
Section titled “Verdicts”Deterministic rules, evaluated in order — first match wins:
| Verdict | Rule | Confidence |
|---|---|---|
abstained | the run ended by declining (terminatedBy: "abstained") — wins over everything | 0.95 |
failed | the run did not succeed | 0.95 |
tool-grounded | ≥1 successful substantive tool call and the goal wasn’t marked unachieved | 0.8 |
partially-grounded | tools were attempted but none succeeded | 0.6 |
ungrounded | zero substantive tool calls — the model answered from itself. Fine for pure-knowledge tasks, and now visible | 0.8 |
confidence is confidence in the verdict itself, not in the answer.
Two honest footnotes:
- “Substantive” tool calls exclude the kernel’s own meta/termination/memory-retrieval tools (
final-answer,task-complete,recall,checkpoint,abstain, …). Every kernel run terminates throughfinal-answer— if it counted,ungroundedwould be unreachable and the receipt would be meaningless. Only real work counts as grounding evidence. toolCallStats.okmeans executor-level success — the tool ran without erroring. It does not grade the semantic quality of what the tool returned.
Signing (optional, Ed25519)
Section titled “Signing (optional, Ed25519)”Configure a key and every receipt is signed:
import { generateReceiptKeyPair, verifyReceipt } from "@reactive-agents/runtime";
const { privateKeyJwk } = await generateReceiptKeyPair();
const agent = await ReactiveAgents.create() /* … */ .withReceiptSigning({ privateKeyJwk }) // or env: RA_RECEIPT_KEY (JWK JSON) .build();
const result = await agent.run("…");await verifyReceipt(result.receipt!); // true — public key is embedded in the signatureThe signature certifies provenance: this receipt, for this run, untampered — the receipt bytes were produced by the holder of the embedded key and haven’t been altered since. It never certifies that the answer is correct, and it doesn’t change what verdict means. Unsigned is the default (zero overhead).
The evidence ledger
Section titled “The evidence ledger”The receipt, the deliverable check, the terminal gate, and the rax diagnose replay view are all projections of one substrate: the run’s append-only evidence ledger. It is the second node of the reasoning meta-loop DAG (Contract → Ledger → Assessment → Control → Actuators → Projector) and the single source of run history — populated on every kernel run (.withReasoning()), no opt-in required.
Each entry is a typed, plain-data fact with a dense, monotonic, append-assigned seq (its stable address) and the iteration it was recorded at. There are twelve fact families:
| Kind | Records |
|---|---|
tool-invocation / tool-result | a tool call issued / returned (with executor-level success) |
artifact | a file/output written — path plus a content digest (by the built-in file-write tool and by code-execute / shell / MCP tools) |
requirement | a contract requirement surfaced for the run |
claim | an evidence claim the model asserted |
verdict | a verifier verdict (grounding, post-conditions, …) |
harness-signal | a control-plane signal, e.g. a mid-run harness recompile |
handoff | a strategy-switch / sub-agent handoff |
contract-amended | a mid-run change to the compiled contract |
compaction-marker | a re-projection of history (see below) |
checkpoint-marker | a durable checkpoint boundary |
deliverable-commit | a declared deliverable committed as produced |
Append-only, never mutated. Appending returns a new ledger; prior entries keep their identity and their seq. This is what makes downstream reads pure functions of the ledger — and therefore replayable.
Honest compaction. When old history is compacted, it is re-projected, not rewritten: compaction is recorded as a new compaction-marker entry rather than editing or deleting the facts it summarizes. History is never silently altered.
Crash-resume. The ledger lives on the kernel state as a plain readonly array of plain-data entries, so the durable kernel codec round-trips it automatically — a run resumed with .withDurableRuns() restores its full evidence trail, not just its message thread.
Run assessment
Section titled “Run assessment”Between the ledger and the loop’s control decisions sits run assessment — one pure function, recomputed each iteration, that answers where does this run stand? from contract × ledger × budget. It is the perception node of the meta-loop DAG. It never mutates state, never appends to the ledger, and never reads loop-control state beyond its three inputs.
The result is cached on the kernel state and emitted every iteration as an AssessmentEmitted event (on the EventBus, and in the trace rax diagnose replay reads). It carries:
| Field | Meaning |
|---|---|
phase | run phase — orient / gather / execute / synthesize / verify |
pace.band | budget-vs-work pace — green / economize / triage / terminal |
pace.burnRatio | fraction of the token/cost/iteration budget consumed |
evidenceDelta | how much new evidence this iteration produced (dedup-aware — reuses the same normalized-args notion of “seen” the gather dedup index uses) |
requirements | contract requirements partitioned into satisfied / outstanding / blocked |
deliverables | declared deliverables partitioned into produced / missing (the same data the receipt’s deliverables[] reports) |
health | windowed run-health signals (repeated failures, stalls) |
Assessment is default-on: it runs on every kernel run that compiled a contract (all of them). What is opt-in is the reaction to it — .withLongHorizon() turns the pace band into budget-aware actions, and .withAdaptiveHarness() recompiles the harness plan from the assessment on a cadence (deepen scaffolding when the run struggles, lean when it flows). Both only read the assessment; the assessment itself is always computed and always traced. .withAdaptiveHarness() remains experimental — its cross-tier ablation was inconclusive, so it is not default-on.
CLI: rax ps and rax attach
Section titled “CLI: rax ps and rax attach”Durable runs live in ~/.reactive-agents/<agentId>/runs.db (or the .withDurableRuns({ dir }) you configured). The CLI reads the same substrate:
rax ps # active (non-terminal) runs across ~/.reactive-agents/*/runs.dbrax ps --all # include completed / failedrax ps --db ./runs.db # scan one specific RunStore dbRuns RUN ID STATUS AGENT TASK 2he5bx8bquo6k-fork-acc1 completed process-model-demo Compute 137*89… [FORKED-FROM 2he5bx8bquo6k@1] 2he5bx8bquo6k completed process-model-demo Compute 137*89…rax attach <runId> tails a run’s status and checkpoint iteration (1s poll) until it reaches a terminal status — Ctrl-C detaches without stopping the run:
Attaching to 2he5bx8bquo6k status: running iteration: 1 iteration: 2 iteration: 3 status: completedExact replay
Section titled “Exact replay”Recorded runs (JSONL traces with llm-exchange events) can be re-executed with zero LLM tokens via makeReplayLLMLayer from @reactive-agents/replay:
import { loadRecordedRun, makeReplayLLMLayer } from "@reactive-agents/replay";
const run = await loadRecordedRun("r-abc123"); // resolves ~/.reactive-agents/traces/r-abc123.jsonlconst llmLayer = makeReplayLLMLayer(run.llmTable); // dispenses recorded LLM responses// provide llmLayer in place of the live provider — the whole run re-executes// from the recording: same thoughts, same tool calls, zero tokens.Honest scoping — this is exact-replay only, not general deterministic re-execution. Responses are keyed on a hash of the exact recorded request (system prompt + messages). Any change that alters the rendered prompt — a model swap, a prompt-template edit, a tool-schema change — produces a different key and misses loudly (the run dies with a descriptive error) rather than silently falling back to a live call. Unchanged prompts and config replay for free; anything else needs a re-recording. Tool-result replay (the replay() API with frozen tool tables and diffing) is documented separately in Snapshot & Replay.
Putting it together
Section titled “Putting it together”The 90-second arc, from the demo script:
runStream()a multi-step tool task on a durable agent.- Call
handle.inspect()while it runs — watchiteration/stepsCountadvance. - On
StreamCompleted, readreceipt—tool-grounded, with the actual tool names as evidence. agent.fork(runId, { at: 1 })— a second, live run continues from iteration 1’s state.agent.listRuns()/rax ps --all— the fork row carriesforkedFromlineage.
bun apps/examples/src/advanced/process-model-demo.ts