Skip to main content

tea_tools/
result.rs

1use serde_json::Value;
2use tea_protocol::{ContentBlock, ProtocolMetadata, ToolPresentation, Usage};
3use thiserror::Error;
4
5/// Stable tool execution failure code.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ToolExecutionFailureCode {
8    /// Executor reported an expected operation failure.
9    ExecutionFailed,
10    /// Invocation was cooperatively cancelled.
11    Cancelled,
12    /// Executor violated its output contract.
13    InvalidOutput,
14    /// Executor failed internally.
15    Internal,
16}
17
18/// Bounded machine-readable tool execution failure.
19#[derive(Debug, Clone, PartialEq)]
20pub struct ToolExecutionFailure {
21    code: ToolExecutionFailureCode,
22    message: String,
23    details: ProtocolMetadata,
24}
25
26impl ToolExecutionFailure {
27    /// Creates an execution failure.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error when the technical message is invalid.
32    pub fn execution(message: impl Into<String>) -> Result<Self, ToolResultError> {
33        Self::new(ToolExecutionFailureCode::ExecutionFailed, message)
34    }
35    /// Creates a cancellation failure.
36    #[must_use]
37    pub fn cancelled() -> Self {
38        Self {
39            code: ToolExecutionFailureCode::Cancelled,
40            message: "tool execution was cancelled".to_owned(),
41            details: ProtocolMetadata::default(),
42        }
43    }
44    /// Creates an invalid-output contract failure.
45    #[must_use]
46    pub fn invalid_output() -> Self {
47        Self {
48            code: ToolExecutionFailureCode::InvalidOutput,
49            message: "tool executor returned invalid output".to_owned(),
50            details: ProtocolMetadata::default(),
51        }
52    }
53    /// Creates a fixed internal executor-contract failure.
54    #[must_use]
55    pub fn internal_contract() -> Self {
56        Self {
57            code: ToolExecutionFailureCode::Internal,
58            message: "tool executor stream ended without a terminal result".to_owned(),
59            details: ProtocolMetadata::default(),
60        }
61    }
62    fn new(
63        code: ToolExecutionFailureCode,
64        message: impl Into<String>,
65    ) -> Result<Self, ToolResultError> {
66        let message = message.into();
67        if message.is_empty() || message.len() > 4096 || message.contains('\0') {
68            return Err(ToolResultError::InvalidFailureMessage);
69        }
70        Ok(Self {
71            code,
72            message,
73            details: ProtocolMetadata::default(),
74        })
75    }
76    /// Adds bounded namespaced safe details.
77    #[must_use]
78    pub fn with_details(mut self, details: ProtocolMetadata) -> Self {
79        self.details = details;
80        self
81    }
82    /// Returns failure code.
83    #[must_use]
84    pub const fn code(&self) -> ToolExecutionFailureCode {
85        self.code
86    }
87    /// Returns English technical message.
88    #[must_use]
89    pub fn message(&self) -> &str {
90        &self.message
91    }
92    /// Returns safe details.
93    #[must_use]
94    pub const fn details(&self) -> &ProtocolMetadata {
95        &self.details
96    }
97}
98
99/// Successful terminal tool result.
100#[derive(Debug, Clone, PartialEq)]
101pub struct ToolResult {
102    content: Vec<ContentBlock>,
103    output: Value,
104    details: ProtocolMetadata,
105    presentation: Option<ToolPresentation>,
106    usage: Option<Usage>,
107}
108
109impl ToolResult {
110    /// Creates a result with model-visible content and machine output.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error for empty/invalid content or non-object output.
115    pub fn new(content: Vec<ContentBlock>, output: Value) -> Result<Self, ToolResultError> {
116        if content.is_empty()
117            || content.len() > 256
118            || !output.is_object()
119            || serde_json::to_vec(&output)
120                .map_err(|_| ToolResultError::InvalidResult)?
121                .len()
122                > 256 * 1024
123            || json_depth(&output) > 32
124        {
125            return Err(ToolResultError::InvalidResult);
126        }
127        if content.iter().any(|block| {
128            !matches!(
129                block,
130                ContentBlock::Text { .. } | ContentBlock::Image { .. }
131            )
132        }) || content
133            .iter()
134            .any(|block| serde_json::to_value(block).is_err())
135        {
136            return Err(ToolResultError::InvalidResult);
137        }
138        Ok(Self {
139            content,
140            output,
141            details: ProtocolMetadata::default(),
142            presentation: None,
143            usage: None,
144        })
145    }
146    /// Adds safe details.
147    #[must_use]
148    pub fn with_details(mut self, details: ProtocolMetadata) -> Self {
149        self.details = details;
150        self
151    }
152    /// Adds a bounded durable UI presentation kept out of model-visible content.
153    #[must_use]
154    pub fn with_presentation(mut self, presentation: ToolPresentation) -> Self {
155        self.presentation = Some(presentation);
156        self
157    }
158    /// Adds tool-specific usage.
159    #[must_use]
160    pub fn with_usage(mut self, usage: Usage) -> Self {
161        self.usage = Some(usage);
162        self
163    }
164    /// Returns model-visible content.
165    #[must_use]
166    pub fn content(&self) -> &[ContentBlock] {
167        &self.content
168    }
169    /// Returns machine output.
170    #[must_use]
171    pub const fn output(&self) -> &Value {
172        &self.output
173    }
174    /// Returns safe details.
175    #[must_use]
176    pub const fn details(&self) -> &ProtocolMetadata {
177        &self.details
178    }
179    /// Returns the optional durable UI presentation.
180    #[must_use]
181    pub const fn presentation(&self) -> Option<&ToolPresentation> {
182        self.presentation.as_ref()
183    }
184    /// Returns tool-specific usage.
185    #[must_use]
186    pub const fn usage(&self) -> Option<&Usage> {
187        self.usage.as_ref()
188    }
189}
190
191/// Error constructing tool results/failures.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
193pub enum ToolResultError {
194    /// Result content or output is invalid.
195    #[error("tool result is invalid")]
196    InvalidResult,
197    /// Failure message is invalid.
198    #[error("tool failure message is invalid")]
199    InvalidFailureMessage,
200}
201
202fn json_depth(value: &Value) -> usize {
203    match value {
204        Value::Array(values) => 1 + values.iter().map(json_depth).max().unwrap_or(0),
205        Value::Object(values) => 1 + values.values().map(json_depth).max().unwrap_or(0),
206        _ => 1,
207    }
208}