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, three shapes are contracts here:
54//! the suspension sentinel and the structured tool-error object recorded in
55//! tool-call completions (module docs of [`wire`], exported constants
56//! [`SUSPEND_SENTINEL_KEY`] / [`ERROR_SENTINEL_KEY`]), and the
57//! budget-extension resume input (module docs of [`budgets`]). Error
58//! compaction, what a failed tool call puts into the model's context, is
59//! specified and exported in [`compact`].
60//!
61//! [`wire`]: crate::wire
62//! [`budgets`]: crate::Budgets
63//! [`compact`]: crate::compact_error_message
64
65#![warn(missing_docs)]
66
67mod agent;
68mod budgets;
69mod compact;
70mod ctx;
71mod driver;
72mod error;
73mod hash;
74mod labels;
75mod model;
76mod progress;
77mod runtime;
78mod validate;
79mod wire;
80
81pub use agent::{Agent, AgentBuildError, AgentBuilder, DEFAULT_MAX_RESPONSE_TOKENS};
82pub use budgets::{
83 BudgetExtensions, BudgetObservations, Budgets, Pricing, validate_extension_input,
84};
85pub use compact::{
86 COMPACT_HEAD_CHARS, COMPACT_MESSAGE_CAP, COMPACT_TAIL_CHARS, FailureTracker,
87 compact_error_message,
88};
89pub use ctx::{
90 ClockFn, MAX_TOOL_ATTEMPTS, ModelTurn, RandomFn, Resumption, RunCtx, ToolCallResult,
91};
92pub use driver::{LoopOutcome, drive_loop};
93pub use error::RuntimeError;
94pub use hash::{canonical_json, hash_value, sha256_hex};
95pub use labels::{MAX_LABEL_KEY_LEN, MAX_LABEL_VALUE_LEN, MAX_LABELS, validate_labels};
96pub use model::{clamp_tokens, response_value, usage_of};
97pub use progress::{event_detail, event_kind};
98pub use runtime::{ParkReason, RunOutcome, Runtime};
99pub use validate::validate_against_schema;
100pub use wire::{
101 ERROR_SENTINEL_KEY, SUSPEND_SENTINEL_KEY, ToolFailure, ToolFailureKind, content_string,
102 decode_failure, decode_suspension, encode_failure, encode_suspension, error_chain,
103};