1use std::collections::BTreeMap;
40use std::fmt;
41
42pub mod cassette;
43pub mod catalogue;
44pub mod events;
45mod interp;
46pub mod price;
47pub mod provider;
48pub mod router;
49pub mod schema;
50pub mod snapshot;
51pub mod tools;
52
53#[cfg(feature = "http")]
56pub mod http;
57
58#[cfg(feature = "anthropic")]
59pub mod anthropic;
60
61#[cfg(feature = "openai")]
62pub mod openai;
63
64#[cfg(feature = "google")]
65pub mod google;
66
67#[cfg(test)]
68mod tests;
69
70pub use cassette::{
71 invocation_digest, load_directory, Cassette, Interaction, RecordingProvider, RecordingTools,
72 ReplayProvider, ReplayToolHost, ScriptedProvider, ToolExchange, CASSETTE_VERSION,
73 SUPPORTED_CASSETTE_VERSIONS,
74};
75pub use catalogue::{ModelConfig, ProviderConfig, ProviderKind};
76pub use events::{Artifact, CollectingSink, EventSink, NullSink, RunEvent, TeeSink, VerifyOutcome};
77pub use interp::{run, AgentRegistry, RunOptions};
78pub use provider::{
79 CompletionRequest, CompletionResponse, ModelProvider, ModelSelection, ProviderError, Usage,
80};
81pub use router::RoutingProvider;
82pub use snapshot::{artifact_digest, Resumption, SnapshotError};
83pub use tools::{
84 ApprovalHandler, ApprovalMode, ApprovalRequest, DenyAllTools, ScriptedApprovals,
85 StaticToolHost, ToolError, ToolHost, ToolInvocation,
86};
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct RunReport {
91 pub agent: String,
92 pub outputs: BTreeMap<String, Artifact>,
93 pub stopped: Option<snapshot::Resumption>,
99 pub memory: BTreeMap<String, serde_json::Value>,
105 pub usage: Usage,
106 pub steps: u32,
107 pub spend: price::Spend,
112}
113
114#[derive(Debug)]
119pub enum RunError {
120 MissingInput { name: String, ty: String },
122 InvalidInput { name: String, reason: String },
124 UnknownInput { name: String, expected: Vec<String> },
126 CapabilityDenied {
128 node: String,
129 effect: String,
130 explicit: bool,
131 },
132 ApprovalDenied { node: String, reason: String },
134 VerificationFailed { node: String, verifier: String },
141 BudgetExceeded {
143 budget: String,
144 limit: String,
145 node: String,
146 },
147 Provider { node: String, source: ProviderError },
149 Tool { node: String, source: ToolError },
151 SubAgent {
153 node: String,
154 agent: String,
155 source: Box<RunError>,
156 },
157 AgentNotAvailable { node: String, agent: String },
159 UnsupportedResponseType {
161 node: String,
162 ty: String,
163 reason: &'static str,
164 },
165 StateNotSet { node: String, field: String },
167 TypeMismatch {
169 node: String,
170 what: String,
171 reason: String,
172 },
173 OutputNotProduced { name: String },
175 InvalidMemory { field: String, reason: String },
177 Snapshot(snapshot::SnapshotError),
179 InputsAfterResume,
181 NotResumable {
183 label: String,
184 nested: bool,
186 available: Vec<String>,
187 },
188 UnknownMemoryField {
190 field: String,
191 expected: Vec<String>,
192 },
193 UnsupportedIrVersion { found: String, supported: String },
195 MalformedIr(String),
197}
198
199impl fmt::Display for RunError {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 match self {
202 RunError::MissingInput { name, ty } => {
203 write!(f, "missing input `{name}` (expected `{ty}`)")
204 }
205 RunError::InvalidInput { name, reason } => {
206 write!(f, "input `{name}` is invalid: {reason}")
207 }
208 RunError::UnknownInput { name, expected } => write!(
209 f,
210 "this agent has no input named `{name}`; it declares: {}",
211 if expected.is_empty() { "none".to_string() } else { expected.join(", ") }
212 ),
213 RunError::CapabilityDenied { node, effect, explicit: true } => write!(
214 f,
215 "node `{node}` needs the `{effect}` effect, which the artifact's policy denies"
216 ),
217 RunError::CapabilityDenied { node, effect, explicit: false } => write!(
218 f,
219 "node `{node}` needs the `{effect}` effect, and the artifact's policy grants no \
220 rule for it (an absent rule is a denial)"
221 ),
222 RunError::ApprovalDenied { node, reason } => {
223 write!(f, "approval was refused at node `{node}`: {reason}")
224 }
225 RunError::VerificationFailed { node, verifier } => write!(
226 f,
227 "the check `{verifier}` did not hold at node `{node}`, so the run stopped there"
228 ),
229 RunError::BudgetExceeded { budget, limit, node } => write!(
230 f,
231 "the `{budget}` budget of {limit} was exhausted at node `{node}`"
232 ),
233 RunError::Provider { node, source } => write!(f, "at node `{node}`: {source}"),
234 RunError::Tool { node, source } => write!(f, "at node `{node}`: {source}"),
235 RunError::SubAgent { node, agent, source } => {
236 write!(f, "at node `{node}`, sub-agent `{agent}` failed: {source}")
237 }
238 RunError::AgentNotAvailable { node, agent } => write!(
239 f,
240 "node `{node}` calls `{agent}`, whose artifact was not supplied to this run"
241 ),
242 RunError::UnsupportedResponseType { node, ty, reason } => {
243 write!(f, "node `{node}` asks for `{ty}`, which cannot be requested: {reason}")
244 }
245 RunError::StateNotSet { node, field } if node.is_empty() => {
246 write!(f, "`state.{field}` was read before it was written")
247 }
248 RunError::StateNotSet { node, field } => {
249 write!(f, "node `{node}` read `state.{field}` before it was written")
250 }
251 RunError::TypeMismatch { node, what, reason } => {
252 write!(f, "at node `{node}`, {what}: {reason}")
253 }
254 RunError::OutputNotProduced { name } => {
255 write!(f, "the run finished without producing the declared output `{name}`")
256 }
257 RunError::InvalidMemory { field, reason } => write!(
258 f,
259 "the stored value for `memory.{field}` does not match its declared type: {reason}"
260 ),
261 RunError::Snapshot(error) => write!(f, "{error}"),
262 RunError::InputsAfterResume => f.write_str(
263 "a resumption already carries the inputs the run started with
264 supplying different ones would let the two halves of one run disagree about what it was given",
265 ),
266 RunError::NotResumable { label, nested, available } => {
267 let why = if *nested {
268 format!(
269 "the checkpoint \"{label}\" is inside a branch or a loop, so a run cannot stop at it
270 resuming into one would mean serialising a continuation, which is not a file anybody could read"
271 )
272 } else {
273 format!("no checkpoint is labelled \"{label}\"")
274 };
275 let offered = if available.is_empty() {
276 "this agent has no resumable checkpoint".to_string()
277 } else {
278 format!("resumable checkpoints: {}", available.join(", "))
279 };
280 write!(f, "{why}
281 {offered}")
282 }
283 RunError::UnknownMemoryField { field, expected } => write!(
284 f,
285 "the store carries `{field}`, which this agent does not declare
286 it declares: {}",
287 if expected.is_empty() { "nothing".to_string() } else { expected.join(", ") }
288 ),
289 RunError::UnsupportedIrVersion { found, supported } => write!(
290 f,
291 "this artifact declares IR version `{found}`; this runtime implements `{supported}`. \
292 Refusing to run it rather than ignoring the parts it does not understand."
293 ),
294 RunError::MalformedIr(message) => write!(f, "the artifact is malformed: {message}"),
295 }
296 }
297}
298
299impl std::error::Error for RunError {}
300
301impl RunError {
302 pub fn is_operator_error(&self) -> bool {
305 matches!(
306 self,
307 RunError::MissingInput { .. }
308 | RunError::InvalidInput { .. }
309 | RunError::UnknownInput { .. }
310 | RunError::ApprovalDenied { .. }
311 | RunError::AgentNotAvailable { .. }
312 | RunError::Tool {
313 source: ToolError::NotAvailable(_),
314 ..
315 }
316 )
317 }
318}