Skip to main content

salvor_runtime/
lib.rs

1//! Salvor runtime: the IO edge where core (replay), store, llm, and tools
2//! meet. Durable, resumable agent runs with hard budgets, in two tiers.
3//!
4//! # The two tiers
5//!
6//! - **The substrate: [`RunCtx`].** The library-first tier. A team that
7//!   owns its control flow writes an ordinary async function against
8//!   `RunCtx` (recorded model calls, recorded tool dispatch, `now`,
9//!   `random`, suspension) and gets durability, crash-exact replay, and the
10//!   suspension protocol with no framework in the way. A runtime that sits
11//!   under your loop is infrastructure; this type is that runtime.
12//! - **The batteries: [`Agent`] + [`Runtime`].** [`Agent::builder`]
13//!   configures model, prompt, tools, budgets, pricing;
14//!   [`Runtime::start`] / [`Runtime::recover`] / [`Runtime::resume`] drive
15//!   the single built-in loop. The loop is implemented entirely against the
16//!   public `RunCtx` surface and doubles as its reference example; it holds
17//!   no private hooks, by design and by test.
18//!
19//! # Where this crate sits
20//!
21//! The agent loop cannot live inside `salvor-core`: `salvor-store` and
22//! `salvor-tools` both depend on
23//! `salvor-core`, and the loop depends on both of them (plus `salvor-llm`),
24//! so a loop in core would be a dependency cycle. This crate is the
25//! resolution: core keeps the pure event model and replay engine with **no
26//! new dependencies**, and `salvor-runtime` sits above all four crates as
27//! the one place allowed to do IO. For the same reason this is the one
28//! crate that may be tokio-bound: it is the IO edge, while core and store
29//! stay executor-agnostic.
30//!
31//! # Constraints this crate honors
32//!
33//! The replay engine fixes the ground rules the runtime builds on:
34//!
35//! - **Budget checks are computed from replayed data** (recorded usage,
36//!   recorded `now` observations), so a crossing re-fires identically at
37//!   the same position on replay ([`Budgets`]).
38//! - **Suspension is `suspend` + `await_resume`**, and budget crossings
39//!   park through the same protocol ([`RunCtx::suspend`],
40//!   [`RunCtx::await_resume`], [`RunCtx::budget_exceeded`]).
41//! - **Divergence detection is always on**: every replayed step is compared
42//!   structurally against the log, and a mismatch is a typed error, never a
43//!   silent drift.
44//! - **Random values are raw recorded bits** ([`RunCtx::random`] returns
45//!   `u64`); richer values derive from the bits deterministically, which is
46//!   how idempotency keys reproduce on replay.
47//! - **Sequence numbers are 0-based and contiguous**, with completions
48//!   correlated to their intent's position; the cursor enforces it and this
49//!   crate persists exactly what the cursor emits, in order.
50//!
51//! # Recorded wire shapes this crate defines
52//!
53//! On top of the core event vocabulary, four shapes are contracts here:
54//! the suspension sentinel, the sleep sentinel, and the structured tool-error
55//! object recorded in tool-call completions (module docs of [`wire`], exported
56//! constants [`SUSPEND_SENTINEL_KEY`] / [`SLEEP_SENTINEL_KEY`] /
57//! [`ERROR_SENTINEL_KEY`]), and the
58//! budget-extension resume input (module docs of [`budgets`]). Error
59//! compaction, what a failed tool call puts into the model's context, is
60//! specified and exported in [`compact`].
61//!
62//! [`wire`]: crate::wire
63//! [`budgets`]: crate::Budgets
64//! [`compact`]: crate::compact_error_message
65
66#![warn(missing_docs)]
67
68mod agent;
69mod compact;
70mod ctx;
71mod driver;
72mod error;
73mod hash;
74mod labels;
75mod model;
76mod progress;
77mod runtime;
78mod validate;
79mod wake;
80mod wire;
81
82pub use agent::{Agent, AgentBuildError, AgentBuilder, DEFAULT_MAX_RESPONSE_TOKENS};
83// The budget declaration and its crossing check. Every input to a check is
84// replayed data, so the rule is pure arithmetic and lives in `salvor-replay`
85// with the rest of the pure half: one implementation the runtime enforces at
86// its IO edge and a browser evaluates client-side. Re-exported unchanged, so
87// `salvor_runtime::Budgets` and the rest keep resolving where they always did.
88pub use compact::{
89    COMPACT_HEAD_CHARS, COMPACT_MESSAGE_CAP, COMPACT_TAIL_CHARS, FailureTracker,
90    compact_error_message,
91};
92pub use ctx::{
93    ClockFn, MAX_TOOL_ATTEMPTS, ModelTurn, RandomFn, Resumption, RunCtx, ToolCallResult, Waking,
94};
95pub use driver::{ANSWER_TOOL, LoopOutcome, drive_loop, drive_loop_structured};
96pub use error::RuntimeError;
97pub use hash::{canonical_json, hash_value, sha256_hex};
98pub use labels::{MAX_LABEL_KEY_LEN, MAX_LABEL_VALUE_LEN, MAX_LABELS, validate_labels};
99pub use model::{clamp_tokens, response_value, usage_of};
100pub use progress::{event_detail, event_kind};
101pub use runtime::{ParkReason, RunOutcome, Runtime};
102pub use salvor_core::{
103    BudgetExtensions, BudgetObservations, Budgets, Pricing, budget_extensions, budget_observations,
104    validate_extension_input,
105};
106pub use validate::validate_against_schema;
107pub use wake::{DueRun, due_runs};
108pub use wire::{
109    ERROR_SENTINEL_KEY, SLEEP_SENTINEL_KEY, SUSPEND_SENTINEL_KEY, ToolFailure, ToolFailureKind,
110    content_string, decode_failure, decode_sleep, decode_suspension, encode_failure, encode_sleep,
111    encode_suspension, error_chain, slept_output,
112};