Salvor runtime: the IO edge where core (replay), store, llm, and tools meet. Durable, resumable agent runs with hard budgets, in two tiers.
The two tiers
- The substrate: [
RunCtx]. The library-first tier. A team that owns its control flow writes an ordinary async function againstRunCtx(recorded model calls, recorded tool dispatch,now,random, suspension) and gets durability, crash-exact replay, and the suspension protocol with no framework in the way. A runtime that sits under your loop is infrastructure; this type is that runtime. - The batteries: [
Agent] + [Runtime]. [Agent::builder] configures model, prompt, tools, budgets, pricing; [Runtime::start] / [Runtime::recover] / [Runtime::resume] drive the single built-in loop. The loop is implemented entirely against the publicRunCtxsurface and doubles as its reference example; it holds no private hooks, by design and by test.
Where this crate sits
The agent loop cannot live inside salvor-core: salvor-store and
salvor-tools both depend on
salvor-core, and the loop depends on both of them (plus salvor-llm),
so a loop in core would be a dependency cycle. This crate is the
resolution: core keeps the pure event model and replay engine with no
new dependencies, and salvor-runtime sits above all four crates as
the one place allowed to do IO. For the same reason this is the one
crate that may be tokio-bound: it is the IO edge, while core and store
stay executor-agnostic.
Constraints this crate honors
The replay engine fixes the ground rules the runtime builds on:
- Budget checks are computed from replayed data (recorded usage,
recorded
nowobservations), so a crossing re-fires identically at the same position on replay (Budgets). - Suspension is
suspend+await_resume, and budget crossings park through the same protocol ([RunCtx::suspend], [RunCtx::await_resume], [RunCtx::budget_exceeded]). - Divergence detection is always on: every replayed step is compared structurally against the log, and a mismatch is a typed error, never a silent drift.
- Random values are raw recorded bits ([
RunCtx::random] returnsu64); richer values derive from the bits deterministically, which is how idempotency keys reproduce on replay. - Sequence numbers are 0-based and contiguous, with completions correlated to their intent's position; the cursor enforces it and this crate persists exactly what the cursor emits, in order.
Recorded wire shapes this crate defines
On top of the core event vocabulary, three shapes are contracts here:
the suspension sentinel and the structured tool-error object recorded in
tool-call completions (module docs of wire, exported constants
[SUSPEND_SENTINEL_KEY] / [ERROR_SENTINEL_KEY]), and the
budget-extension resume input (module docs of budgets). Error
compaction, what a failed tool call puts into the model's context, is
specified and exported in compact.