use crate::tool::ToolResultExtensions;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ToolFailureKind {
InvalidArgs,
Timeout,
Cancelled,
NotFound,
PermissionDenied,
RateLimited,
Provider,
Network,
Other,
}
impl ToolFailureKind {
pub const fn as_str(self) -> &'static str {
match self {
ToolFailureKind::InvalidArgs => "invalid_args",
ToolFailureKind::Timeout => "timeout",
ToolFailureKind::Cancelled => "cancelled",
ToolFailureKind::NotFound => "not_found",
ToolFailureKind::PermissionDenied => "permission_denied",
ToolFailureKind::RateLimited => "rate_limited",
ToolFailureKind::Provider => "provider",
ToolFailureKind::Network => "network",
ToolFailureKind::Other => "other",
}
}
const fn default_retryable(self) -> Option<bool> {
match self {
ToolFailureKind::Timeout | ToolFailureKind::RateLimited | ToolFailureKind::Network => {
Some(true)
}
ToolFailureKind::InvalidArgs
| ToolFailureKind::NotFound
| ToolFailureKind::PermissionDenied
| ToolFailureKind::Cancelled => Some(false),
ToolFailureKind::Provider | ToolFailureKind::Other => None,
}
}
}
impl std::fmt::Display for ToolFailureKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ToolFailure {
pub kind: ToolFailureKind,
pub message: String,
pub retryable: Option<bool>,
pub code: Option<String>,
pub http_status: Option<u16>,
}
impl ToolFailure {
pub fn new(kind: ToolFailureKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
retryable: None,
code: None,
http_status: None,
}
}
pub(crate) fn of_kind(kind: ToolFailureKind, message: impl Into<String>) -> Self {
Self {
retryable: kind.default_retryable(),
..Self::new(kind, message)
}
}
pub fn invalid_args(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::InvalidArgs, message)
}
pub fn timeout(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::Timeout, message)
}
pub fn cancelled(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::Cancelled, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::NotFound, message)
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::PermissionDenied, message)
}
pub fn rate_limited(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::RateLimited, message)
}
pub fn provider(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::Provider, message)
}
pub fn network(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::Network, message)
}
pub fn other(message: impl Into<String>) -> Self {
Self::of_kind(ToolFailureKind::Other, message)
}
pub fn with_retryable(mut self, retryable: bool) -> Self {
self.retryable = Some(retryable);
self
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_http_status(mut self, status: u16) -> Self {
self.http_status = Some(status);
self
}
}
impl std::fmt::Display for ToolFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.kind, self.message)
}
}
impl std::error::Error for ToolFailure {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToolOutcome {
Success,
Error(ToolFailure),
Skipped,
Denied,
}
impl ToolOutcome {
pub const fn as_str(&self) -> &'static str {
match self {
ToolOutcome::Success => "success",
ToolOutcome::Error(_) => "error",
ToolOutcome::Skipped => "skipped",
ToolOutcome::Denied => "denied",
}
}
pub const fn is_success(&self) -> bool {
matches!(self, ToolOutcome::Success)
}
pub const fn is_error(&self) -> bool {
matches!(self, ToolOutcome::Error(_))
}
pub const fn is_skipped(&self) -> bool {
matches!(self, ToolOutcome::Skipped)
}
pub const fn is_denied(&self) -> bool {
matches!(self, ToolOutcome::Denied)
}
pub const fn failure(&self) -> Option<&ToolFailure> {
match self {
ToolOutcome::Error(failure) => Some(failure),
_ => None,
}
}
pub const fn error_kind(&self) -> Option<ToolFailureKind> {
match self {
ToolOutcome::Error(failure) => Some(failure.kind),
_ => None,
}
}
pub fn is_error_kind(&self, kind: ToolFailureKind) -> bool {
self.error_kind() == Some(kind)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToolReturnOutcome {
Success,
Error(ToolFailure),
Denied,
}
impl ToolReturnOutcome {
pub const fn as_str(&self) -> &'static str {
match self {
ToolReturnOutcome::Success => "success",
ToolReturnOutcome::Error(_) => "error",
ToolReturnOutcome::Denied => "denied",
}
}
pub const fn failure(&self) -> Option<&ToolFailure> {
match self {
ToolReturnOutcome::Error(failure) => Some(failure),
_ => None,
}
}
}
impl From<ToolReturnOutcome> for ToolOutcome {
fn from(outcome: ToolReturnOutcome) -> Self {
match outcome {
ToolReturnOutcome::Success => ToolOutcome::Success,
ToolReturnOutcome::Error(failure) => ToolOutcome::Error(failure),
ToolReturnOutcome::Denied => ToolOutcome::Denied,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ToolExecutionResult {
pub(crate) model_output: String,
pub(crate) outcome: ToolOutcome,
pub(crate) extensions: ToolResultExtensions,
}
impl ToolExecutionResult {
pub(crate) fn new(model_output: impl Into<String>, outcome: ToolOutcome) -> Self {
Self {
model_output: model_output.into(),
outcome,
extensions: ToolResultExtensions::new(),
}
}
pub fn success(model_output: impl Into<String>) -> Self {
Self::new(model_output, ToolOutcome::Success)
}
pub fn failed(model_output: impl Into<String>, failure: ToolFailure) -> Self {
Self::new(model_output, ToolOutcome::Error(failure))
}
pub(crate) fn skipped(model_output: impl Into<String>) -> Self {
Self::new(model_output, ToolOutcome::Skipped)
}
pub fn denied(model_output: impl Into<String>) -> Self {
Self::new(model_output, ToolOutcome::Denied)
}
pub fn with_extensions(mut self, extensions: ToolResultExtensions) -> Self {
self.extensions = extensions;
self
}
pub fn with_extension<
E: Clone + crate::wasm_compat::WasmCompatSend + crate::wasm_compat::WasmCompatSync + 'static,
>(
mut self,
extension: E,
) -> Self {
self.extensions.insert(extension);
self
}
pub fn model_output(&self) -> &str {
&self.model_output
}
pub fn outcome(&self) -> &ToolOutcome {
&self.outcome
}
pub fn extensions(&self) -> &ToolResultExtensions {
&self.extensions
}
}
#[derive(Debug, Clone)]
pub struct ToolReturn<T> {
pub output: T,
pub outcome: ToolReturnOutcome,
pub extensions: ToolResultExtensions,
}
impl<T> ToolReturn<T> {
pub fn success(output: T) -> Self {
Self {
output,
outcome: ToolReturnOutcome::Success,
extensions: ToolResultExtensions::new(),
}
}
pub fn new(output: T, outcome: ToolReturnOutcome) -> Self {
Self {
output,
outcome,
extensions: ToolResultExtensions::new(),
}
}
pub fn failed(output: T, failure: ToolFailure) -> Self {
Self::new(output, ToolReturnOutcome::Error(failure))
}
pub fn denied(output: T) -> Self {
Self::new(output, ToolReturnOutcome::Denied)
}
pub fn with_outcome(mut self, outcome: ToolReturnOutcome) -> Self {
self.outcome = outcome;
self
}
pub fn with_extensions(mut self, extensions: ToolResultExtensions) -> Self {
self.extensions = extensions;
self
}
pub fn with_extension<
E: Clone + crate::wasm_compat::WasmCompatSend + crate::wasm_compat::WasmCompatSync + 'static,
>(
mut self,
extension: E,
) -> Self {
self.extensions.insert(extension);
self
}
}
impl<T: Serialize> ToolReturn<T> {
pub(crate) fn into_execution_result(self) -> ToolExecutionResult {
let ToolReturn {
output,
outcome,
extensions,
} = self;
match super::serialize_tool_output(&output) {
Ok(model_output) => ToolExecutionResult {
model_output,
outcome: outcome.into(),
extensions,
},
Err(err) => {
let outcome = match outcome {
ToolReturnOutcome::Success => {
ToolOutcome::Error(ToolFailure::other(err.to_string()))
}
other => other.into(),
};
ToolExecutionResult {
model_output: format!("failed to serialize tool output: {err}"),
outcome,
extensions,
}
}
}
}
}
#[cfg(not(target_family = "wasm"))]
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ToolExecutionResult>();
assert_send_sync::<ToolOutcome>();
assert_send_sync::<ToolFailure>();
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn per_kind_constructors_prefill_retryable() {
assert_eq!(ToolFailure::timeout("t").retryable, Some(true));
assert_eq!(ToolFailure::rate_limited("r").retryable, Some(true));
assert_eq!(ToolFailure::network("n").retryable, Some(true));
assert_eq!(ToolFailure::not_found("nf").retryable, Some(false));
assert_eq!(ToolFailure::permission_denied("p").retryable, Some(false));
assert_eq!(ToolFailure::invalid_args("i").retryable, Some(false));
assert_eq!(ToolFailure::cancelled("c").retryable, Some(false));
assert_eq!(ToolFailure::provider("p").retryable, None);
assert_eq!(ToolFailure::other("o").retryable, None);
assert_eq!(
ToolFailure::new(ToolFailureKind::Timeout, "t").retryable,
None
);
}
#[test]
fn failure_builders_compose() {
let failure = ToolFailure::rate_limited("slow down")
.with_http_status(429)
.with_code("RATE_LIMIT")
.with_retryable(false);
assert_eq!(failure.kind, ToolFailureKind::RateLimited);
assert_eq!(failure.http_status, Some(429));
assert_eq!(failure.code.as_deref(), Some("RATE_LIMIT"));
assert_eq!(failure.retryable, Some(false));
assert_eq!(failure.to_string(), "rate_limited: slow down");
}
#[test]
fn outcome_predicates() {
let ok = ToolOutcome::Success;
assert!(ok.is_success());
assert!(!ok.is_error());
assert_eq!(ok.error_kind(), None);
assert_eq!(ok.as_str(), "success");
let err = ToolOutcome::Error(ToolFailure::not_found("x"));
assert!(err.is_error());
assert!(err.is_error_kind(ToolFailureKind::NotFound));
assert!(!err.is_error_kind(ToolFailureKind::Timeout));
assert_eq!(err.error_kind(), Some(ToolFailureKind::NotFound));
assert_eq!(
err.failure().map(|f| f.kind),
Some(ToolFailureKind::NotFound)
);
assert_eq!(err.as_str(), "error");
assert!(ToolOutcome::Skipped.is_skipped());
assert!(ToolOutcome::Denied.is_denied());
}
#[test]
fn tool_return_success_serializes_verbatim_string() {
let result = ToolReturn::success("hello\nworld".to_string()).into_execution_result();
assert_eq!(result.model_output, "hello\nworld");
assert!(result.outcome.is_success());
}
#[test]
fn tool_return_object_serializes_as_json_and_keeps_outcome() {
let result = ToolReturn::failed(
serde_json::json!({ "status": "missing", "id": 7 }),
ToolFailure::not_found("id 7"),
)
.into_execution_result();
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result.model_output).unwrap(),
serde_json::json!({ "status": "missing", "id": 7 })
);
assert!(result.outcome.is_error_kind(ToolFailureKind::NotFound));
}
#[test]
fn tool_return_extensions_flow_into_execution_result() {
#[derive(Clone, Debug, PartialEq)]
struct ReqId(String);
let result = ToolReturn::success(1u32)
.with_extension(ReqId("abc".into()))
.into_execution_result();
assert_eq!(result.extensions.get::<ReqId>(), Some(&ReqId("abc".into())));
}
#[test]
fn tool_return_outcome_maps_to_observed_outcome() {
assert_eq!(
ToolOutcome::from(ToolReturnOutcome::Success),
ToolOutcome::Success
);
assert_eq!(
ToolOutcome::from(ToolReturnOutcome::Denied),
ToolOutcome::Denied
);
let failure = ToolFailure::not_found("x");
assert_eq!(
ToolOutcome::from(ToolReturnOutcome::Error(failure.clone())),
ToolOutcome::Error(failure)
);
assert_eq!(ToolReturnOutcome::Success.as_str(), "success");
assert_eq!(ToolReturnOutcome::Denied.as_str(), "denied");
assert_eq!(
ToolReturnOutcome::Error(ToolFailure::timeout("t")).as_str(),
"error"
);
assert_eq!(
ToolReturnOutcome::Error(ToolFailure::timeout("t"))
.failure()
.map(|f| f.kind),
Some(ToolFailureKind::Timeout)
);
assert_eq!(ToolReturnOutcome::Denied.failure(), None);
}
#[test]
fn tool_return_denied_surfaces_as_denied_observed_outcome() {
let result = ToolReturn::denied("refused".to_string()).into_execution_result();
assert_eq!(*result.outcome(), ToolOutcome::Denied);
assert_eq!(result.model_output(), "refused");
assert!(result.outcome().is_denied());
assert!(!result.outcome().is_skipped());
}
#[test]
fn serialize_failure_preserves_declared_outcome_and_extensions() {
struct Unserializable;
impl serde::Serialize for Unserializable {
fn serialize<S: serde::Serializer>(&self, _s: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("cannot serialize"))
}
}
#[derive(Clone, Debug, PartialEq)]
struct ReqId(String);
let failed = ToolReturn::failed(Unserializable, ToolFailure::rate_limited("slow"))
.with_extension(ReqId("req-1".into()))
.into_execution_result();
assert!(
failed.outcome().is_error_kind(ToolFailureKind::RateLimited),
"declared failure classification must survive serialize failure; got {:?}",
failed.outcome()
);
assert_eq!(
failed.outcome().failure().and_then(|f| f.retryable),
Some(true),
"the declared failure's retryable hint must survive"
);
assert_eq!(
failed.extensions().get::<ReqId>(),
Some(&ReqId("req-1".into())),
"extensions must survive serialize failure"
);
assert!(failed.model_output().contains("failed to serialize"));
let denied = ToolReturn::denied(Unserializable).into_execution_result();
assert!(
denied.outcome().is_denied(),
"declared denial must survive serialize failure; got {:?}",
denied.outcome()
);
let ok = ToolReturn::success(Unserializable).into_execution_result();
assert!(
ok.outcome().is_error_kind(ToolFailureKind::Other),
"a success whose output cannot serialize becomes Other; got {:?}",
ok.outcome()
);
}
}