use mentra::runtime::{EarlyEnd, RunOptions};
use super::Bound;
pub(super) enum Ended<'a> {
Answered(Option<String>),
Failed(&'a mentra::error::RuntimeError),
Mismatched(&'a serde_json::Error),
}
pub(super) fn ended_on(
observed: &RunOptions,
error: Option<&mentra::error::RuntimeError>,
) -> Option<Bound> {
match observed.ended_early() {
Some(EarlyEnd::TokenBudget) => Some(Bound::TokenBudget),
_ => error.and_then(tripped_bound),
}
}
pub(super) fn tripped_bound(error: &mentra::error::RuntimeError) -> Option<Bound> {
match error {
mentra::error::RuntimeError::DeadlineExceeded => Some(Bound::Deadline),
mentra::error::RuntimeError::ToolBudgetExceeded(_) => Some(Bound::ToolBudget),
_ => None,
}
}
pub(super) fn chain_message(error: &dyn std::error::Error) -> String {
let mut message = error.to_string();
let mut cause = error.source();
while let Some(source) = cause {
let text = source.to_string();
if !message.contains(&text) {
message.push_str(": ");
message.push_str(&text);
}
cause = source.source();
}
message
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_tripped_bound_is_told_apart_from_a_failed_run() {
use mentra::error::RuntimeError;
assert_eq!(
tripped_bound(&RuntimeError::DeadlineExceeded),
Some(Bound::Deadline)
);
assert_eq!(
tripped_bound(&RuntimeError::ToolBudgetExceeded(40)),
Some(Bound::ToolBudget)
);
assert_eq!(tripped_bound(&RuntimeError::EmptyAssistantResponse), None);
assert_eq!(tripped_bound(&RuntimeError::Cancelled), None);
}
fn recorded(end: EarlyEnd) -> RunOptions {
let slot = std::sync::OnceLock::new();
let _ = slot.set(end);
RunOptions {
early_end: std::sync::Arc::new(slot),
..RunOptions::default()
}
}
#[test]
fn a_run_that_answered_still_names_the_budget_that_ended_it() {
assert_eq!(
ended_on(&recorded(EarlyEnd::TokenBudget), None),
Some(Bound::TokenBudget)
);
}
#[test]
fn a_budget_that_ends_a_run_owing_an_answer_is_not_read_as_a_provider_failure() {
use mentra::error::RuntimeError;
assert_eq!(
ended_on(
&recorded(EarlyEnd::TokenBudget),
Some(&RuntimeError::EmptyAssistantResponse)
),
Some(Bound::TokenBudget)
);
}
#[test]
fn a_graceful_stop_is_not_reported_as_a_bound() {
use mentra::error::RuntimeError;
assert_eq!(ended_on(&recorded(EarlyEnd::StopRequested), None), None);
assert_eq!(
ended_on(
&recorded(EarlyEnd::StopRequested),
Some(&RuntimeError::EmptyAssistantResponse)
),
None
);
}
#[test]
fn a_run_that_recorded_nothing_is_classified_by_its_failure_alone() {
use mentra::error::RuntimeError;
assert_eq!(ended_on(&RunOptions::default(), None), None);
assert_eq!(
ended_on(
&RunOptions::default(),
Some(&RuntimeError::DeadlineExceeded)
),
Some(Bound::Deadline)
);
}
#[derive(Debug)]
struct Leaf(&'static str);
impl std::fmt::Display for Leaf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for Leaf {}
#[derive(Debug)]
struct Opaque {
own_text: &'static str,
source: Leaf,
}
impl std::fmt::Display for Opaque {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.own_text)
}
}
impl std::error::Error for Opaque {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[derive(Debug)]
struct Interpolated {
source: Leaf,
}
impl std::fmt::Display for Interpolated {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "wrapper failed: {}", self.source)
}
}
impl std::error::Error for Interpolated {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[test]
fn a_leaf_error_is_left_exactly_as_its_own_display_wrote_it() {
let error = Leaf("no providers are registered");
assert_eq!(chain_message(&error), "no providers are registered");
}
#[test]
fn a_source_a_wrappers_display_never_mentions_is_appended() {
let error = Opaque {
own_text: "error sending request for url (http://127.0.0.1:1/)",
source: Leaf("connection refused (os error 61)"),
};
assert_eq!(
chain_message(&error),
"error sending request for url (http://127.0.0.1:1/): connection refused (os error 61)"
);
}
#[test]
fn a_source_a_wrappers_display_already_quotes_is_not_repeated() {
let error = Interpolated {
source: Leaf("disk quota exceeded"),
};
assert_eq!(
chain_message(&error),
"wrapper failed: disk quota exceeded",
"the source's text must appear once, not twice"
);
}
#[test]
fn a_chain_three_levels_deep_still_reaches_its_root_cause() {
struct Middle {
source: Opaque,
}
impl std::fmt::Debug for Middle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Middle").finish()
}
}
impl std::fmt::Display for Middle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failed to send provider request: {}", self.source)
}
}
impl std::error::Error for Middle {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
let error = Middle {
source: Opaque {
own_text: "provider transport error: error sending request for url (http://127.0.0.1:1/)",
source: Leaf("connection refused (os error 61)"),
},
};
assert_eq!(
chain_message(&error),
"failed to send provider request: provider transport error: error sending request for url (http://127.0.0.1:1/): connection refused (os error 61)"
);
}
#[test]
fn a_real_runtime_errors_already_complete_message_is_unchanged() {
use mentra::error::RuntimeError;
let parse_error = serde_json::from_str::<serde_json::Value>("{")
.expect_err("truncated JSON does not parse");
let error = RuntimeError::FailedToSerializeTasks(parse_error);
assert_eq!(chain_message(&error), error.to_string());
}
}