salvor-runtime 0.5.0

The Salvor runtime IO edge: the public RunCtx durability substrate, the Agent builder, budget enforcement, and the built-in agent loop
Documentation

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 against RunCtx (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 public RunCtx surface 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 now observations), 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] returns u64); 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.