Skip to main content

Store

Struct Store 

Source
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

Source

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.

Source

pub fn memory() -> Result<Self>

An in-memory store, for tests and throwaway runs.

Source

pub fn record_event(&self, run_id: i64, e: &PolicyEvent) -> Result<()>

Record a policy refusal or a human decision against a run.

Source

pub fn events(&self, run_id: i64) -> Result<Vec<PolicyEvent>>

Every policy event recorded for a run, in order.

Source

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.

Source

pub fn pending(&self, request_id: i64) -> Result<Option<Pending>>

Read a pending action back by request id.

Source

pub fn resolve_pending(&self, request_id: i64, decision: &str) -> Result<()>

Mark a pending action decided, so a resume knows what the human chose.

Source

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.

Source

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.

Source

pub fn record_agent_event(&self, e: &AgentEvent) -> Result<()>

Record a spawn, a spawn refusal, or a budget draw against the tree.

Source

pub fn agent_events(&self, run_id: i64) -> Result<Vec<AgentEvent>>

Every agent event recorded for a run, in order.

Source

pub fn record_sandbox_event(&self, e: &SandboxEvent) -> Result<()>

Record one sandbox lifecycle event against a run.

Source

pub fn record_mcp(&self, run_id: i64, e: &McpEvent) -> Result<()>

Record one MCP event.

Source

pub fn mcp_events(&self, run_id: i64) -> Result<Vec<McpEvent>>

Every MCP event recorded for a run, in order.

Source

pub fn record_context_event(&self, run_id: i64, e: &ContextEvent) -> Result<()>

Record one context-assembly event against a run.

Source

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.

Source

pub fn context_events(&self, run_id: i64) -> Result<Vec<ContextEvent>>

Every context-assembly event recorded for a run, in order.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn sandbox_events(&self, run_id: i64) -> Result<Vec<SandboxEvent>>

Every sandbox event recorded for a run, in order.

Source

pub fn children(&self, run_id: i64) -> Result<Vec<i64>>

The run ids of the direct children of run_id, in spawn order.

Source

pub fn parent(&self, run_id: i64) -> Result<Option<i64>>

The parent run id of run_id, or None for a root run.

Source

pub fn depth(&self, run_id: i64) -> Result<u32>

The nesting depth recorded for a run (0 at the root).

Source

pub fn record(&self, run_id: i64, step: &StepRecord) -> Result<()>

Record one step’s full trace entry.

Source

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.

Source

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.

Source

pub fn checkpoint_events(&self, run_id: i64) -> Result<Vec<CheckpointEvent>>

Every checkpoint-lifecycle event recorded for a run, in order.

Source

pub fn set_status(&self, run_id: i64, status: &str) -> Result<()>

Set the durable run status (running, paused, completed, failed).

Source

pub fn status(&self, run_id: i64) -> Result<Option<String>>

The durable run status, if the run exists.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn set_provider(&self, run_id: i64, provider: &str) -> Result<()>

Record which provider ran this run, for the audit trace.

Source

pub fn provider(&self, run_id: i64) -> Result<Option<String>>

The provider recorded for a run, if any.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn outcome(&self, run_id: i64) -> Result<Option<String>>

The recorded final outcome string of a run, if it has finished.

Source

pub fn run_status(&self, run_id: i64) -> Result<Option<RunStatus>>

The durable run status as a typed RunStatus, if the run exists.

Source

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.

Source

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 stampsruns.started_at, memory.created_at, run_outcomes.finished_at and duration_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 idsAUTOINCREMENT values, 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.

Source

pub fn steps(&self, run_id: i64) -> Result<Vec<StepRecord>>

Source

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.

Source

pub fn memory_list(&self, workspace: &str) -> Result<Vec<MemoryEntry>>

Every entry for workspace, oldest first. Never another workspace’s.

Source

pub fn memory_get( &self, workspace: &str, key: &str, ) -> Result<Option<MemoryEntry>>

One entry of workspace by key, if it holds one.

Source

pub fn memory_delete(&self, workspace: &str, key: &str) -> Result<bool>

Forget one entry of workspace. True when an entry was removed.

Source

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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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 more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

Source§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
Source§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
Source§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
Source§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more