pub struct Store { /* private fields */ }Expand description
A persisted run store. Use Store::open for a file, or Store::memory
for an ephemeral in-memory database.
The store is not a log. It is what makes a run resumable after a crash and auditable afterwards, so which constructor you choose is a decision about whether either matters for this run.
use io_harness::{run, OpenRouter, Store, TaskContract, Verification};
// A file store outlives the process: a run that dies mid-loop is resumable
// from its last committed step, and a second process may read the trace while
// this one is still writing it (WAL, plus `BUSY_TIMEOUT`).
let store = Store::open("runs.sqlite3")?;
let contract = TaskContract::new(
"add a hello function returning 42",
"src/hello.rs",
Verification::FileContains("fn hello".into()),
);
let result = run(&contract, &OpenRouter::from_env()?, &store).await?;
// Everything the run did, read back by id: the trace, the budget draws, the
// policy refusals, and what it cost.
for step in store.steps(result.run_id)? {
println!("{}: {} ({} tokens)", step.step, step.decision, step.tokens);
}
println!("{:?}", store.run_summary(result.run_id)?);Store::memory is the same API with no file: a throwaway run, or a test.
Nothing survives the process, so nothing is resumable — which is the right
trade only when a failed run is cheaper to restart than to continue.
Implementations§
Source§impl Store
impl Store
Sourcepub fn open(path: impl AsRef<Path>) -> Result<Self>
pub fn open(path: impl AsRef<Path>) -> Result<Self>
Open (creating if absent) a store at path and ensure the schema exists.
Sets journal_mode = WAL and a BUSY_TIMEOUT, so a second process may
read the trace while a run is still writing it without either side
blocking or aborting the other. Before 0.12.0 this was a bare
Connection::open, which left every reader to configure the file itself
— reaching around this API to do it, and having to do it before the
harness opened the file at all.
WAL is a persistent property of the database file, not of this connection: a store opened once by 0.12.0 stays in WAL mode afterwards. That is why it is documented as a migration.
Sourcepub fn record_event(&self, run_id: i64, e: &PolicyEvent) -> Result<()>
pub fn record_event(&self, run_id: i64, e: &PolicyEvent) -> Result<()>
Record a policy refusal or a human decision against a run.
Sourcepub fn events(&self, run_id: i64) -> Result<Vec<PolicyEvent>>
pub fn events(&self, run_id: i64) -> Result<Vec<PolicyEvent>>
Every policy event recorded for a run, in order.
Sourcepub fn put_pending(
&self,
run_id: i64,
step: u32,
act: &str,
target: &str,
content: Option<&str>,
) -> Result<i64>
pub fn put_pending( &self, run_id: i64, step: u32, act: &str, target: &str, content: Option<&str>, ) -> Result<i64>
Persist an action awaiting a human decision; returns its request id.
Sourcepub fn pending(&self, request_id: i64) -> Result<Option<Pending>>
pub fn pending(&self, request_id: i64) -> Result<Option<Pending>>
Read a pending action back by request id.
Sourcepub fn resolve_pending(&self, request_id: i64, decision: &str) -> Result<()>
pub fn resolve_pending(&self, request_id: i64, decision: &str) -> Result<()>
Mark a pending action decided, so a resume knows what the human chose.
Sourcepub fn start_run(&self, goal: &str, file: &str) -> Result<i64>
pub fn start_run(&self, goal: &str, file: &str) -> Result<i64>
Start a run row; returns its id. Stamps started_at (UTC, from SQLite’s
clock) so a 24h wall-clock budget survives a restart, and marks the run
running.
Sourcepub fn start_child_run(
&self,
goal: &str,
file: &str,
parent_run_id: i64,
depth: u32,
) -> Result<i64>
pub fn start_child_run( &self, goal: &str, file: &str, parent_run_id: i64, depth: u32, ) -> Result<i64>
Start a child run under parent_run_id at depth, so the tree records
who spawned whom. Returns the child’s run id.
Sourcepub fn record_agent_event(&self, e: &AgentEvent) -> Result<()>
pub fn record_agent_event(&self, e: &AgentEvent) -> Result<()>
Record a spawn, a spawn refusal, or a budget draw against the tree.
Sourcepub fn agent_events(&self, run_id: i64) -> Result<Vec<AgentEvent>>
pub fn agent_events(&self, run_id: i64) -> Result<Vec<AgentEvent>>
Every agent event recorded for a run, in order.
Sourcepub fn record_sandbox_event(&self, e: &SandboxEvent) -> Result<()>
pub fn record_sandbox_event(&self, e: &SandboxEvent) -> Result<()>
Record one sandbox lifecycle event against a run.
Sourcepub fn mcp_events(&self, run_id: i64) -> Result<Vec<McpEvent>>
pub fn mcp_events(&self, run_id: i64) -> Result<Vec<McpEvent>>
Every MCP event recorded for a run, in order.
Sourcepub fn record_context_event(&self, run_id: i64, e: &ContextEvent) -> Result<()>
pub fn record_context_event(&self, run_id: i64, e: &ContextEvent) -> Result<()>
Record one context-assembly event against a run.
Sourcepub fn record_context_reported(
&self,
run_id: i64,
step: u32,
reported: u64,
) -> Result<()>
pub fn record_context_reported( &self, run_id: i64, step: u32, reported: u64, ) -> Result<()>
Fill in what the provider said one turn’s request cost, once the completion has returned. The estimate is left as it was: the pair is the point — one row carries both numbers, so drift is readable.
Sourcepub fn context_events(&self, run_id: i64) -> Result<Vec<ContextEvent>>
pub fn context_events(&self, run_id: i64) -> Result<Vec<ContextEvent>>
Every context-assembly event recorded for a run, in order.
Sourcepub fn record_run_policy(&self, run_id: i64, policy: &Policy) -> Result<()>
pub fn record_run_policy(&self, run_id: i64, policy: &Policy) -> Result<()>
Record the policy a run was started under.
INSERT OR REPLACE, like every other per-run row, so recording twice for
one run — a resume that re-states its boundary — replaces rather than
duplicates or fails.
Sourcepub fn run_policy(&self, run_id: i64) -> Result<Option<Policy>>
pub fn run_policy(&self, run_id: i64) -> Result<Option<Policy>>
The policy a run was started under, or None if none was recorded.
None is not Policy::permissive and must never be read as it: a run
written by 0.12.0 has no row at all, so the honest answer is “nobody
recorded what the boundary was”, not “the caller chose to enforce
nothing”. A caller that needs a policy either way has to decide which to
assume, and it should decide that knowingly.
Unlike the other getters in this file, a failed read is an error rather
than None. They can fold the two together because a missing memory
entry and an unreadable one lead to the same recovery; here they do not.
None is what tells crate::resume the run had no boundary and may be
resumed permissively, so a disk error that read as None would hand a
policy-bearing run an agent with no policy — silently, and by exactly the
route this table exists to close.
Sourcepub fn record_observations(
&self,
run_id: i64,
entries: &[Observation],
) -> Result<()>
pub fn record_observations( &self, run_id: i64, entries: &[Observation], ) -> Result<()>
Append observations to a run’s durable ledger, in one transaction.
Called once at a committed step boundary rather than once per observation: the step is the unit the rest of the checkpoint works in, and an observation belonging to a step that never committed must not survive a crash the step itself did not survive.
Sourcepub fn observations(&self, run_id: i64) -> Result<Vec<Observation>>
pub fn observations(&self, run_id: i64) -> Result<Vec<Observation>>
A run’s durable ledger, in the order it was observed.
Empty for a run that recorded nothing and for a run written before 0.13.0 — the two are the same to a reader, and both mean “there is nothing to restore”, which is 0.12.0’s behaviour and not a lie about it.
Sourcepub fn sandbox_events(&self, run_id: i64) -> Result<Vec<SandboxEvent>>
pub fn sandbox_events(&self, run_id: i64) -> Result<Vec<SandboxEvent>>
Every sandbox event recorded for a run, in order.
Sourcepub fn children(&self, run_id: i64) -> Result<Vec<i64>>
pub fn children(&self, run_id: i64) -> Result<Vec<i64>>
The run ids of the direct children of run_id, in spawn order.
Sourcepub fn parent(&self, run_id: i64) -> Result<Option<i64>>
pub fn parent(&self, run_id: i64) -> Result<Option<i64>>
The parent run id of run_id, or None for a root run.
Sourcepub fn depth(&self, run_id: i64) -> Result<u32>
pub fn depth(&self, run_id: i64) -> Result<u32>
The nesting depth recorded for a run (0 at the root).
Sourcepub fn record(&self, run_id: i64, step: &StepRecord) -> Result<()>
pub fn record(&self, run_id: i64, step: &StepRecord) -> Result<()>
Record one step’s full trace entry.
Sourcepub fn checkpoint_step(&self, run_id: i64, step: &StepRecord) -> Result<()>
pub fn checkpoint_step(&self, run_id: i64, step: &StepRecord) -> Result<()>
Durably checkpoint one completed step: the step’s trace row and its checkpoint event are written in a single transaction, so a crash leaves either both (the step is done) or neither (it replays) — never a torn half. The committed checkpoint is the step’s completion marker.
Sourcepub fn record_checkpoint_event(&self, e: &CheckpointEvent) -> Result<()>
pub fn record_checkpoint_event(&self, e: &CheckpointEvent) -> Result<()>
Record a checkpoint/resume/skipped event on its own (not tied to a step commit) — used for resume and skip markers.
Sourcepub fn checkpoint_events(&self, run_id: i64) -> Result<Vec<CheckpointEvent>>
pub fn checkpoint_events(&self, run_id: i64) -> Result<Vec<CheckpointEvent>>
Every checkpoint-lifecycle event recorded for a run, in order.
Sourcepub fn set_status(&self, run_id: i64, status: &str) -> Result<()>
pub fn set_status(&self, run_id: i64, status: &str) -> Result<()>
Set the durable run status (running, paused, completed, failed).
Sourcepub fn status(&self, run_id: i64) -> Result<Option<String>>
pub fn status(&self, run_id: i64) -> Result<Option<String>>
The durable run status, if the run exists.
Sourcepub fn elapsed_secs(&self, run_id: i64) -> Result<f64>
pub fn elapsed_secs(&self, run_id: i64) -> Result<f64>
Real wall-clock seconds elapsed since the run’s started_at, from the
database clock — so a budget over duration counts time that passed while
the process was down, not just this process’s uptime. Zero if the run has
no start stamp (a pre-0.7.0 run).
Sourcepub fn spent_tokens(&self, run_id: i64) -> Result<u64>
pub fn spent_tokens(&self, run_id: i64) -> Result<u64>
Total tokens recorded across this run’s steps — the durable spend, so a resume restores the token budget instead of restarting it at zero.
Sourcepub fn tree_run_ids(&self, root: i64) -> Result<Vec<i64>>
pub fn tree_run_ids(&self, root: i64) -> Result<Vec<i64>>
Every run id in the tree rooted at root (the root plus all descendants),
via the parent_run_id edge — the set a tree-level resume re-drives.
Sourcepub fn spent_tokens_tree(&self, root: i64) -> Result<u64>
pub fn spent_tokens_tree(&self, root: i64) -> Result<u64>
Total tokens spent across the whole tree rooted at root — the durable
aggregate-ledger spend restored on a tree resume.
Sourcepub fn agent_count_tree(&self, root: i64) -> Result<u32>
pub fn agent_count_tree(&self, root: i64) -> Result<u32>
Number of agents (run rows) in the tree rooted at root — the durable
agent count restored on a tree resume.
Sourcepub fn record_spawn(
&self,
parent_run_id: i64,
step: u32,
child_run_id: i64,
goal: &str,
verify_file: &str,
needle: &str,
max_steps: Option<u32>,
deny_write_json: &str,
) -> Result<()>
pub fn record_spawn( &self, parent_run_id: i64, step: u32, child_run_id: i64, goal: &str, verify_file: &str, needle: &str, max_steps: Option<u32>, deny_write_json: &str, ) -> Result<()>
Persist a spawned child’s contract so a crashed tree can rebuild and resume that exact child on resume instead of spawning a duplicate. Keyed by (parent, step, goal) so a replayed spawn step adopts the existing child.
Sourcepub fn find_spawn(
&self,
parent_run_id: i64,
step: u32,
goal: &str,
) -> Result<Option<SpawnRow>>
pub fn find_spawn( &self, parent_run_id: i64, step: u32, goal: &str, ) -> Result<Option<SpawnRow>>
Find the child spawned by parent_run_id at step for goal, if any —
the adopt-on-resume lookup that makes a replayed spawn step idempotent.
Sourcepub fn check_resumable(&self, run_id: i64) -> Result<()>
pub fn check_resumable(&self, run_id: i64) -> Result<()>
Check a run can be resumed from its checkpoint, or return a typed
Error::Resume. Refuses a store written by a newer checkpoint format
(rather than misreading a layout it does not understand) and a run id that
does not exist. An already-completed run is resumable as a no-op, so it
is not refused here.
Sourcepub fn set_provider(&self, run_id: i64, provider: &str) -> Result<()>
pub fn set_provider(&self, run_id: i64, provider: &str) -> Result<()>
Record which provider ran this run, for the audit trace.
Sourcepub fn provider(&self, run_id: i64) -> Result<Option<String>>
pub fn provider(&self, run_id: i64) -> Result<Option<String>>
The provider recorded for a run, if any.
Sourcepub fn finish_run(&self, run_id: i64, outcome: &str) -> Result<()>
pub fn finish_run(&self, run_id: i64, outcome: &str) -> Result<()>
Record the run’s final outcome, and derive the durable status from it:
success completes the run, awaiting_approval pauses it, any other
terminal outcome completes it (finished, just not with success). A run
that crashed mid-loop never reaches here, so it stays running and is
resumable.
Sourcepub fn run_summary(&self, run_id: i64) -> Result<Option<RunSummary>>
pub fn run_summary(&self, run_id: i64) -> Result<Option<RunSummary>>
What a finished run cost and whether it worked.
None if the run has not finished, is paused awaiting a human, or was
finished by a pre-0.12.0 binary — a missing summary is reported as absent
rather than as a row of zeroes, which would be indistinguishable from a run
that did nothing.
Sourcepub fn runs(&self) -> Result<Vec<i64>>
pub fn runs(&self) -> Result<Vec<i64>>
Every run in this store, newest first.
Exists because an escalation returns Err rather than a
RunResult, so a caller whose run escalated has no
run_id to resume with and therefore no way to reach
RunOutcome::Escalated — the outcome added
for exactly that case. A caller who did not record the id before starting
can find it here.
Sourcepub fn last_run(&self) -> Result<Option<i64>>
pub fn last_run(&self) -> Result<Option<i64>>
The most recently started run, if this store holds one.
A convenience over Store::runs for the common single-run case. With
concurrent runs in one store, “most recent” is by insertion order and a
caller that cares should track its own ids.
Sourcepub fn outcome(&self, run_id: i64) -> Result<Option<String>>
pub fn outcome(&self, run_id: i64) -> Result<Option<String>>
The recorded final outcome string of a run, if it has finished.
Sourcepub fn run_status(&self, run_id: i64) -> Result<Option<RunStatus>>
pub fn run_status(&self, run_id: i64) -> Result<Option<RunStatus>>
The durable run status as a typed RunStatus, if the run exists.
Sourcepub fn last_step(&self, run_id: i64) -> Result<u32>
pub fn last_step(&self, run_id: i64) -> Result<u32>
The highest step number recorded for a run, or 0 if none — the resume
point for crate::resume.
Sourcepub fn canonical_trace(&self, run_id: i64) -> Result<String>
pub fn canonical_trace(&self, run_id: i64) -> Result<String>
Read every step of a run back, in order, as the full trace. The run’s trace reduced to the part that two identical runs must match, as diffable text.
This is the crate’s definition of “the same run twice”, and it exists
because equality could not be row identity: steps has no
UNIQUE(run_id, step) and a retry inserts its own row under the step
number the eventual commit will reuse, so counting or comparing rows
compares trace entries rather than agent behaviour.
§What is compared
Every steps row — step number, decision, result, prompt, tool call and
tokens — and every context_events row’s step, kind and detail. Between
them these are what the agent was shown, what it decided, what it did, and
what that cost.
§What is excluded, and why
Everything whose value is a fact about this execution rather than about the run:
- Wall-clock stamps —
runs.started_at,memory.created_at,run_outcomes.finished_atandduration_ms. Two runs of the same case take different amounts of time; that is not a divergence. mcp_events.millis— a measured duration, for the same reason.sandbox_events.detail— it carries the argv, and the argv carries an ephemeral tempdir path that is different every run by design.- Run and child ids —
AUTOINCREMENTvalues, meaningful only within one store.
Excluding a field is a decision that this crate cannot promise it, not a convenience. Anything added to this list should be added to this doc with its reason, because a comparison that quietly excludes what it cannot match is a comparison that asserts nothing.
§What it assumes
That each run being compared has its own fresh store. Run ids are
excluded from the text, but a child agent’s run id is embedded in the
parent’s composed observation ([child 5 "goal" -> …]), which is real
content the model was shown. In a fresh store those ids start at 1 and are
allocated in spawn order, so they match; in a shared store the second run’s
ids are higher and the traces differ for a reason that has nothing to do
with the agent.
Deterministic replay also requires the provider to answer identically —
see Replay — and the same workspace state to
start from.
pub fn steps(&self, run_id: i64) -> Result<Vec<StepRecord>>
Sourcepub fn memory_put(
&self,
workspace: &str,
key: &str,
value: &str,
run_id: i64,
step: u32,
) -> Result<Vec<String>>
pub fn memory_put( &self, workspace: &str, key: &str, value: &str, run_id: i64, step: u32, ) -> Result<Vec<String>>
Write or replace key for workspace, attributed to the run and step
that wrote it. A value past MEMORY_MAX_ENTRY_CHARS is truncated with
a visible marker rather than refused. Returns the keys evicted to stay
inside the caps, oldest first — the caller records the eviction in the
trace; this never writes a trace row itself.
Sourcepub fn memory_list(&self, workspace: &str) -> Result<Vec<MemoryEntry>>
pub fn memory_list(&self, workspace: &str) -> Result<Vec<MemoryEntry>>
Every entry for workspace, oldest first. Never another workspace’s.
Sourcepub fn memory_get(
&self,
workspace: &str,
key: &str,
) -> Result<Option<MemoryEntry>>
pub fn memory_get( &self, workspace: &str, key: &str, ) -> Result<Option<MemoryEntry>>
One entry of workspace by key, if it holds one.
Sourcepub fn memory_delete(&self, workspace: &str, key: &str) -> Result<bool>
pub fn memory_delete(&self, workspace: &str, key: &str) -> Result<bool>
Forget one entry of workspace. True when an entry was removed.
Sourcepub fn memory_clear(&self, workspace: &str) -> Result<usize>
pub fn memory_clear(&self, workspace: &str) -> Result<usize>
Removes every entry for workspace; returns how many. Other workspaces
keep theirs.
Auto Trait Implementations§
impl !Freeze for Store
impl !RefUnwindSafe for Store
impl !Sync for Store
impl !UnwindSafe for Store
impl Send for Store
impl Unpin for Store
impl UnsafeUnpin for Store
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
fn is_in_subset(&self) -> bool
self is actually part of its subset T (and can be converted to it).Source§fn to_subset_unchecked(&self) -> SS
fn to_subset_unchecked(&self) -> SS
self.to_subset but without any property checks. Always succeeds.Source§fn from_subset(element: &SS) -> SP
fn from_subset(element: &SS) -> SP
self to the equivalent element of its superset.