use std::collections::BTreeMap;
use std::fmt;
pub mod cassette;
pub mod catalogue;
pub mod events;
mod interp;
pub mod price;
pub mod provider;
pub mod router;
pub mod schema;
pub mod snapshot;
pub mod tools;
#[cfg(feature = "http")]
pub mod http;
#[cfg(feature = "anthropic")]
pub mod anthropic;
#[cfg(feature = "openai")]
pub mod openai;
#[cfg(feature = "google")]
pub mod google;
#[cfg(test)]
mod tests;
pub use cassette::{
invocation_digest, load_directory, Cassette, Interaction, RecordingProvider, RecordingTools,
ReplayProvider, ReplayToolHost, ScriptedProvider, ToolExchange, CASSETTE_VERSION,
SUPPORTED_CASSETTE_VERSIONS,
};
pub use catalogue::{ModelConfig, ProviderConfig, ProviderKind};
pub use events::{Artifact, CollectingSink, EventSink, NullSink, RunEvent, TeeSink, VerifyOutcome};
pub use interp::{run, AgentRegistry, RunOptions};
pub use provider::{
CompletionRequest, CompletionResponse, ModelProvider, ModelSelection, ProviderError, Usage,
};
pub use router::RoutingProvider;
pub use snapshot::{artifact_digest, Resumption, SnapshotError};
pub use tools::{
ApprovalHandler, ApprovalMode, ApprovalRequest, DenyAllTools, ScriptedApprovals,
StaticToolHost, ToolError, ToolHost, ToolInvocation,
};
#[derive(Debug, Clone, PartialEq)]
pub struct RunReport {
pub agent: String,
pub outputs: BTreeMap<String, Artifact>,
pub stopped: Option<snapshot::Resumption>,
pub memory: BTreeMap<String, serde_json::Value>,
pub usage: Usage,
pub steps: u32,
pub spend: price::Spend,
}
#[derive(Debug)]
pub enum RunError {
MissingInput { name: String, ty: String },
InvalidInput { name: String, reason: String },
UnknownInput { name: String, expected: Vec<String> },
CapabilityDenied {
node: String,
effect: String,
explicit: bool,
},
ApprovalDenied { node: String, reason: String },
VerificationFailed { node: String, verifier: String },
BudgetExceeded {
budget: String,
limit: String,
node: String,
},
Provider { node: String, source: ProviderError },
Tool { node: String, source: ToolError },
SubAgent {
node: String,
agent: String,
source: Box<RunError>,
},
AgentNotAvailable { node: String, agent: String },
UnsupportedResponseType {
node: String,
ty: String,
reason: &'static str,
},
StateNotSet { node: String, field: String },
TypeMismatch {
node: String,
what: String,
reason: String,
},
OutputNotProduced { name: String },
InvalidMemory { field: String, reason: String },
Snapshot(snapshot::SnapshotError),
InputsAfterResume,
NotResumable {
label: String,
nested: bool,
available: Vec<String>,
},
UnknownMemoryField {
field: String,
expected: Vec<String>,
},
UnsupportedIrVersion { found: String, supported: String },
MalformedIr(String),
}
impl fmt::Display for RunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunError::MissingInput { name, ty } => {
write!(f, "missing input `{name}` (expected `{ty}`)")
}
RunError::InvalidInput { name, reason } => {
write!(f, "input `{name}` is invalid: {reason}")
}
RunError::UnknownInput { name, expected } => write!(
f,
"this agent has no input named `{name}`; it declares: {}",
if expected.is_empty() { "none".to_string() } else { expected.join(", ") }
),
RunError::CapabilityDenied { node, effect, explicit: true } => write!(
f,
"node `{node}` needs the `{effect}` effect, which the artifact's policy denies"
),
RunError::CapabilityDenied { node, effect, explicit: false } => write!(
f,
"node `{node}` needs the `{effect}` effect, and the artifact's policy grants no \
rule for it (an absent rule is a denial)"
),
RunError::ApprovalDenied { node, reason } => {
write!(f, "approval was refused at node `{node}`: {reason}")
}
RunError::VerificationFailed { node, verifier } => write!(
f,
"the check `{verifier}` did not hold at node `{node}`, so the run stopped there"
),
RunError::BudgetExceeded { budget, limit, node } => write!(
f,
"the `{budget}` budget of {limit} was exhausted at node `{node}`"
),
RunError::Provider { node, source } => write!(f, "at node `{node}`: {source}"),
RunError::Tool { node, source } => write!(f, "at node `{node}`: {source}"),
RunError::SubAgent { node, agent, source } => {
write!(f, "at node `{node}`, sub-agent `{agent}` failed: {source}")
}
RunError::AgentNotAvailable { node, agent } => write!(
f,
"node `{node}` calls `{agent}`, whose artifact was not supplied to this run"
),
RunError::UnsupportedResponseType { node, ty, reason } => {
write!(f, "node `{node}` asks for `{ty}`, which cannot be requested: {reason}")
}
RunError::StateNotSet { node, field } if node.is_empty() => {
write!(f, "`state.{field}` was read before it was written")
}
RunError::StateNotSet { node, field } => {
write!(f, "node `{node}` read `state.{field}` before it was written")
}
RunError::TypeMismatch { node, what, reason } => {
write!(f, "at node `{node}`, {what}: {reason}")
}
RunError::OutputNotProduced { name } => {
write!(f, "the run finished without producing the declared output `{name}`")
}
RunError::InvalidMemory { field, reason } => write!(
f,
"the stored value for `memory.{field}` does not match its declared type: {reason}"
),
RunError::Snapshot(error) => write!(f, "{error}"),
RunError::InputsAfterResume => f.write_str(
"a resumption already carries the inputs the run started with
supplying different ones would let the two halves of one run disagree about what it was given",
),
RunError::NotResumable { label, nested, available } => {
let why = if *nested {
format!(
"the checkpoint \"{label}\" is inside a branch or a loop, so a run cannot stop at it
resuming into one would mean serialising a continuation, which is not a file anybody could read"
)
} else {
format!("no checkpoint is labelled \"{label}\"")
};
let offered = if available.is_empty() {
"this agent has no resumable checkpoint".to_string()
} else {
format!("resumable checkpoints: {}", available.join(", "))
};
write!(f, "{why}
{offered}")
}
RunError::UnknownMemoryField { field, expected } => write!(
f,
"the store carries `{field}`, which this agent does not declare
it declares: {}",
if expected.is_empty() { "nothing".to_string() } else { expected.join(", ") }
),
RunError::UnsupportedIrVersion { found, supported } => write!(
f,
"this artifact declares IR version `{found}`; this runtime implements `{supported}`. \
Refusing to run it rather than ignoring the parts it does not understand."
),
RunError::MalformedIr(message) => write!(f, "the artifact is malformed: {message}"),
}
}
}
impl std::error::Error for RunError {}
impl RunError {
pub fn is_operator_error(&self) -> bool {
matches!(
self,
RunError::MissingInput { .. }
| RunError::InvalidInput { .. }
| RunError::UnknownInput { .. }
| RunError::ApprovalDenied { .. }
| RunError::AgentNotAvailable { .. }
| RunError::Tool {
source: ToolError::NotAvailable(_),
..
}
)
}
}