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 ApprovalRequest, ConsultError, ConsultRequest, DenyAllTools, HumanChannel, Interlocutor,
85 ScriptedAnswers, ScriptedApprovals, 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 ConsultFailed {
139 node: String,
140 question: String,
141 reason: String,
142 },
143 VerificationFailed { node: String, verifier: String },
150 BudgetExceeded {
152 budget: String,
153 limit: String,
154 node: String,
155 },
156 Provider { node: String, source: ProviderError },
158 Tool { node: String, source: ToolError },
160 SubAgent {
162 node: String,
163 agent: String,
164 source: Box<RunError>,
165 },
166 AgentNotAvailable { node: String, agent: String },
168 UnsupportedResponseType {
170 node: String,
171 ty: String,
172 reason: &'static str,
173 },
174 StateNotSet { node: String, field: String },
176 TypeMismatch {
178 node: String,
179 what: String,
180 reason: String,
181 },
182 OutputNotProduced { name: String },
184 InvalidMemory { field: String, reason: String },
186 Snapshot(snapshot::SnapshotError),
188 InputsAfterResume,
190 NotResumable {
192 label: String,
193 nested: bool,
195 available: Vec<String>,
196 },
197 UnknownMemoryField {
199 field: String,
200 expected: Vec<String>,
201 },
202 UnsupportedIrVersion { found: String, supported: String },
204 MalformedIr(String),
206}
207
208impl fmt::Display for RunError {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 RunError::MissingInput { name, ty } => {
212 write!(f, "missing input `{name}` (expected `{ty}`)")
213 }
214 RunError::InvalidInput { name, reason } => {
215 write!(f, "input `{name}` is invalid: {reason}")
216 }
217 RunError::UnknownInput { name, expected } => write!(
218 f,
219 "this agent has no input named `{name}`; it declares: {}",
220 if expected.is_empty() { "none".to_string() } else { expected.join(", ") }
221 ),
222 RunError::CapabilityDenied { node, effect, explicit: true } => write!(
223 f,
224 "node `{node}` needs the `{effect}` effect, which the artifact's policy denies"
225 ),
226 RunError::CapabilityDenied { node, effect, explicit: false } => write!(
227 f,
228 "node `{node}` needs the `{effect}` effect, and the artifact's policy grants no \
229 rule for it (an absent rule is a denial)"
230 ),
231 RunError::ApprovalDenied { node, reason } => {
232 write!(f, "approval was refused at node `{node}`: {reason}")
233 }
234 RunError::ConsultFailed {
235 node,
236 question,
237 reason,
238 } => write!(
239 f,
240 "the question at node `{node}` was not answered: {reason}
241 asked: {question}"
242 ),
243 RunError::VerificationFailed { node, verifier } => write!(
244 f,
245 "the check `{verifier}` did not hold at node `{node}`, so the run stopped there"
246 ),
247 RunError::BudgetExceeded { budget, limit, node } => write!(
248 f,
249 "the `{budget}` budget of {limit} was exhausted at node `{node}`"
250 ),
251 RunError::Provider { node, source } => write!(f, "at node `{node}`: {source}"),
252 RunError::Tool { node, source } => write!(f, "at node `{node}`: {source}"),
253 RunError::SubAgent { node, agent, source } => {
254 write!(f, "at node `{node}`, sub-agent `{agent}` failed: {source}")
255 }
256 RunError::AgentNotAvailable { node, agent } => write!(
257 f,
258 "node `{node}` calls `{agent}`, whose artifact was not supplied to this run"
259 ),
260 RunError::UnsupportedResponseType { node, ty, reason } => {
261 write!(f, "node `{node}` asks for `{ty}`, which cannot be requested: {reason}")
262 }
263 RunError::StateNotSet { node, field } if node.is_empty() => {
264 write!(f, "`state.{field}` was read before it was written")
265 }
266 RunError::StateNotSet { node, field } => {
267 write!(f, "node `{node}` read `state.{field}` before it was written")
268 }
269 RunError::TypeMismatch { node, what, reason } => {
270 write!(f, "at node `{node}`, {what}: {reason}")
271 }
272 RunError::OutputNotProduced { name } => {
273 write!(f, "the run finished without producing the declared output `{name}`")
274 }
275 RunError::InvalidMemory { field, reason } => write!(
276 f,
277 "the stored value for `memory.{field}` does not match its declared type: {reason}"
278 ),
279 RunError::Snapshot(error) => write!(f, "{error}"),
280 RunError::InputsAfterResume => f.write_str(
281 "a resumption already carries the inputs the run started with
282 supplying different ones would let the two halves of one run disagree about what it was given",
283 ),
284 RunError::NotResumable { label, nested, available } => {
285 let why = if *nested {
286 format!(
287 "the checkpoint \"{label}\" is inside a branch or a loop, so a run cannot stop at it
288 resuming into one would mean serialising a continuation, which is not a file anybody could read"
289 )
290 } else {
291 format!("no checkpoint is labelled \"{label}\"")
292 };
293 let offered = if available.is_empty() {
294 "this agent has no resumable checkpoint".to_string()
295 } else {
296 format!("resumable checkpoints: {}", available.join(", "))
297 };
298 write!(f, "{why}
299 {offered}")
300 }
301 RunError::UnknownMemoryField { field, expected } => write!(
302 f,
303 "the store carries `{field}`, which this agent does not declare
304 it declares: {}",
305 if expected.is_empty() { "nothing".to_string() } else { expected.join(", ") }
306 ),
307 RunError::UnsupportedIrVersion { found, supported } => write!(
308 f,
309 "this artifact declares IR version `{found}`; this runtime implements `{supported}`. \
310 Refusing to run it rather than ignoring the parts it does not understand."
311 ),
312 RunError::MalformedIr(message) => write!(f, "the artifact is malformed: {message}"),
313 }
314 }
315}
316
317impl std::error::Error for RunError {}
318
319impl RunError {
320 pub fn is_operator_error(&self) -> bool {
323 matches!(
324 self,
325 RunError::MissingInput { .. }
326 | RunError::InvalidInput { .. }
327 | RunError::UnknownInput { .. }
328 | RunError::ApprovalDenied { .. }
329 | RunError::ConsultFailed { .. }
330 | RunError::AgentNotAvailable { .. }
331 | RunError::Tool {
332 source: ToolError::NotAvailable(_),
333 ..
334 }
335 )
336 }
337}