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 compact;
69mod ctx;
70mod driver;
71mod error;
72mod hash;
73mod labels;
74mod model;
75mod progress;
76mod runtime;
77mod validate;
78mod wire;
79
80pub use agent::{Agent, AgentBuildError, AgentBuilder, DEFAULT_MAX_RESPONSE_TOKENS};
81// The budget declaration and its crossing check. Every input to a check is
82// replayed data, so the rule is pure arithmetic and lives in `salvor-replay`
83// with the rest of the pure half: one implementation the runtime enforces at
84// its IO edge and a browser evaluates client-side. Re-exported unchanged, so
85// `salvor_runtime::Budgets` and the rest keep resolving where they always did.
86pub use compact::{
87 COMPACT_HEAD_CHARS, COMPACT_MESSAGE_CAP, COMPACT_TAIL_CHARS, FailureTracker,
88 compact_error_message,
89};
90pub use ctx::{
91 ClockFn, MAX_TOOL_ATTEMPTS, ModelTurn, RandomFn, Resumption, RunCtx, ToolCallResult,
92};
93pub use driver::{LoopOutcome, drive_loop};
94pub use error::RuntimeError;
95pub use hash::{canonical_json, hash_value, sha256_hex};
96pub use labels::{MAX_LABEL_KEY_LEN, MAX_LABEL_VALUE_LEN, MAX_LABELS, validate_labels};
97pub use model::{clamp_tokens, response_value, usage_of};
98pub use progress::{event_detail, event_kind};
99pub use runtime::{ParkReason, RunOutcome, Runtime};
100pub use salvor_core::{
101 BudgetExtensions, BudgetObservations, Budgets, Pricing, budget_extensions, budget_observations,
102 validate_extension_input,
103};
104pub use validate::validate_against_schema;
105pub use wire::{
106 ERROR_SENTINEL_KEY, SUSPEND_SENTINEL_KEY, ToolFailure, ToolFailureKind, content_string,
107 decode_failure, decode_suspension, encode_failure, encode_suspension, error_chain,
108};