1use std::error::Error;
12#[cfg(not(target_family = "wasm"))]
13use std::sync::Arc;
14
15use crate::{
16 tool::ToolOutput,
17 wasm_compat::{WasmCompatSend, WasmCompatSync},
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum ToolErrorKind {
24 InvalidArgs,
26 Timeout,
28 Cancelled,
30 NotFound,
32 PermissionDenied,
35 RateLimited,
37 Provider,
39 Network,
41 Other,
43}
44
45macro_rules! kind_defaults {
49 ($($variant:ident => ($name:literal, $retryable:expr, $feedback:literal)),+ $(,)?) => {
50 impl ToolErrorKind {
51 pub const fn as_str(self) -> &'static str {
53 match self { $(Self::$variant => $name,)+ }
54 }
55
56 pub const fn default_retryable(self) -> Option<bool> {
59 match self { $(Self::$variant => $retryable,)+ }
60 }
61
62 const fn default_model_feedback(self) -> &'static str {
63 match self { $(Self::$variant => $feedback,)+ }
64 }
65 }
66 };
67}
68
69kind_defaults! {
70 InvalidArgs => ("invalid_args", Some(false), "tool arguments were invalid"),
71 Timeout => ("timeout", Some(true), "tool execution timed out"),
72 Cancelled => ("cancelled", Some(false), "tool execution was cancelled"),
73 NotFound => ("not_found", Some(false), "the requested tool or resource was not found"),
74 PermissionDenied => ("permission_denied", Some(false), "the tool denied the request"),
75 RateLimited => ("rate_limited", Some(true), "the tool was rate limited; try again later"),
76 Provider => ("provider", None, "the tool provider failed"),
77 Network => ("network", Some(true), "the tool could not reach its upstream service"),
78 Other => ("other", None, "the tool failed"),
79}
80
81macro_rules! kind_ctors {
85 ($($(#[$doc:meta])* $ctor:ident => $variant:ident),+ $(,)?) => {
86 impl ToolExecutionError {
87 $($(#[$doc])*
88 pub fn $ctor(message: impl Into<String>) -> Self {
89 Self::new(ToolErrorKind::$variant, message)
90 })+
91 }
92 };
93}
94
95kind_ctors! {
96 invalid_args => InvalidArgs,
98 timeout => Timeout,
100 cancelled => Cancelled,
102 not_found => NotFound,
104 permission_denied => PermissionDenied,
110 rate_limited => RateLimited,
112 provider => Provider,
114 network => Network,
116 other => Other,
118}
119
120impl std::fmt::Display for ToolErrorKind {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 f.write_str(self.as_str())
123 }
124}
125
126#[derive(Clone, serde::Serialize, serde::Deserialize)]
136pub struct ToolExecutionError {
137 kind: ToolErrorKind,
138 message: String,
139 model_output: ToolOutput,
140 retryable: Option<bool>,
141 code: Option<String>,
142 http_status: Option<u16>,
143 refusal: bool,
144 #[cfg(not(target_family = "wasm"))]
150 #[serde(skip)]
151 source: Option<Arc<dyn Error + Send + Sync + 'static>>,
152}
153
154impl ToolExecutionError {
155 pub fn new(kind: ToolErrorKind, message: impl Into<String>) -> Self {
157 let message = message.into();
158 Self {
159 kind,
160 model_output: ToolOutput::text(message.clone()),
161 message,
162 retryable: kind.default_retryable(),
163 code: None,
164 http_status: None,
165 refusal: false,
166 #[cfg(not(target_family = "wasm"))]
167 source: None,
168 }
169 }
170
171 pub fn refused(message: impl Into<String>) -> Self {
176 let mut error = Self::new(ToolErrorKind::PermissionDenied, message);
177 error.refusal = true;
178 error
179 }
180
181 pub fn from_error<E>(error: E) -> Self
188 where
189 E: Error + WasmCompatSend + WasmCompatSync + 'static,
190 {
191 #[cfg(not(target_family = "wasm"))]
192 {
193 let source: Box<dyn Error + Send + Sync + 'static> = Box::new(error);
194 return match source.downcast::<Self>() {
195 Ok(error) => *error,
196 Err(source) => {
197 let message = source.to_string();
198 let mut error = Self::other(message).redact_model_feedback();
199 error.source = Some(Arc::from(source));
200 error
201 }
202 };
203 }
204 #[cfg(target_family = "wasm")]
205 {
206 let source: Box<dyn Error + 'static> = Box::new(error);
207 match source.downcast::<Self>() {
208 Ok(error) => *error,
209 Err(source) => Self::other(source.to_string()).redact_model_feedback(),
210 }
211 }
212 }
213
214 pub fn with_model_feedback(mut self, feedback: impl Into<String>) -> Self {
216 self.model_output = ToolOutput::text(feedback);
217 self
218 }
219
220 pub fn with_model_output(mut self, output: ToolOutput) -> Self {
223 self.model_output = output;
224 self
225 }
226
227 pub(crate) fn redact_model_feedback(mut self) -> Self {
235 self.model_output = ToolOutput::text(self.kind.default_model_feedback());
236 self
237 }
238
239 pub fn with_retryable(mut self, retryable: bool) -> Self {
241 self.retryable = Some(retryable);
242 self
243 }
244
245 pub fn with_code(mut self, code: impl Into<String>) -> Self {
247 self.code = Some(code.into());
248 self
249 }
250
251 pub fn with_http_status(mut self, status: u16) -> Self {
253 self.http_status = Some(status);
254 self
255 }
256
257 #[cfg_attr(target_family = "wasm", allow(unused_mut))]
262 pub fn with_source<E>(mut self, source: E) -> Self
263 where
264 E: Error + WasmCompatSend + WasmCompatSync + 'static,
265 {
266 #[cfg(not(target_family = "wasm"))]
267 {
268 self.source = Some(Arc::new(source));
269 }
270 #[cfg(target_family = "wasm")]
271 {
272 let _ = source;
273 }
274 self
275 }
276
277 pub const fn kind(&self) -> ToolErrorKind {
279 self.kind
280 }
281
282 pub fn message(&self) -> &str {
284 &self.message
285 }
286
287 pub fn model_feedback(&self) -> Option<&str> {
292 self.model_output.as_text()
293 }
294
295 pub fn model_output(&self) -> &ToolOutput {
297 &self.model_output
298 }
299
300 pub const fn retryable(&self) -> Option<bool> {
302 self.retryable
303 }
304
305 pub fn code(&self) -> Option<&str> {
307 self.code.as_deref()
308 }
309
310 pub const fn http_status(&self) -> Option<u16> {
312 self.http_status
313 }
314
315 pub const fn is_refusal(&self) -> bool {
317 self.refusal
318 }
319
320 pub fn downcast_ref<E>(&self) -> Option<&E>
322 where
323 E: Error + WasmCompatSend + WasmCompatSync + 'static,
324 {
325 #[cfg(not(target_family = "wasm"))]
326 {
327 self.source.as_ref()?.downcast_ref::<E>()
328 }
329 #[cfg(target_family = "wasm")]
330 {
331 None
332 }
333 }
334
335 pub fn is<E>(&self) -> bool
337 where
338 E: Error + WasmCompatSend + WasmCompatSync + 'static,
339 {
340 self.downcast_ref::<E>().is_some()
341 }
342}
343
344impl std::fmt::Display for ToolExecutionError {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.write_str(&self.message)
347 }
348}
349
350impl std::fmt::Debug for ToolExecutionError {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 f.debug_struct("ToolExecutionError")
353 .field("kind", &self.kind)
354 .field("retryable", &self.retryable)
355 .field("code", &self.code)
356 .field("http_status", &self.http_status)
357 .field("refusal", &self.refusal)
358 .field("model_output", &"<redacted>")
359 .field("source_configured", &self.has_source())
360 .finish()
361 }
362}
363
364impl ToolExecutionError {
365 fn has_source(&self) -> bool {
366 #[cfg(not(target_family = "wasm"))]
367 {
368 self.source.is_some()
369 }
370 #[cfg(target_family = "wasm")]
371 {
372 false
373 }
374 }
375}
376
377impl Error for ToolExecutionError {
378 fn source(&self) -> Option<&(dyn Error + 'static)> {
379 #[cfg(not(target_family = "wasm"))]
380 {
381 self.source
382 .as_deref()
383 .map(|source| source as &(dyn Error + 'static))
384 }
385 #[cfg(target_family = "wasm")]
386 {
387 None
388 }
389 }
390}
391
392#[derive(Clone, serde::Serialize, serde::Deserialize)]
394#[serde(tag = "status", content = "value", rename_all = "snake_case")]
395enum ToolDisposition {
396 Success(ToolOutput),
397 Error(ToolExecutionError),
398 Refused(ToolExecutionError),
399 Skipped(ToolOutput),
400}
401
402#[derive(Clone, serde::Serialize, serde::Deserialize)]
408#[serde(transparent)]
409pub struct ToolResult {
410 disposition: ToolDisposition,
411}
412
413impl std::fmt::Debug for ToolResult {
414 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415 let error = match &self.disposition {
416 ToolDisposition::Error(error) | ToolDisposition::Refused(error) => Some(error),
417 ToolDisposition::Success(_) | ToolDisposition::Skipped(_) => None,
418 };
419 formatter
420 .debug_struct("ToolResult")
421 .field("status", &self.status_name())
422 .field("error_kind", &error.map(ToolExecutionError::kind))
423 .field("retryable", &error.and_then(ToolExecutionError::retryable))
424 .field("code", &error.and_then(ToolExecutionError::code))
425 .field(
426 "http_status",
427 &error.and_then(ToolExecutionError::http_status),
428 )
429 .finish()
430 }
431}
432
433impl ToolResult {
434 pub fn success(output: ToolOutput) -> Self {
436 Self {
437 disposition: ToolDisposition::Success(output),
438 }
439 }
440
441 pub fn failed(error: ToolExecutionError) -> Self {
443 let disposition = if error.is_refusal() {
444 ToolDisposition::Refused(error)
445 } else {
446 ToolDisposition::Error(error)
447 };
448 Self { disposition }
449 }
450
451 pub fn skipped(reason: impl Into<String>) -> Self {
453 Self {
454 disposition: ToolDisposition::Skipped(ToolOutput::text(reason)),
455 }
456 }
457
458 pub fn with_output(self, output: ToolOutput) -> Self {
462 let disposition = match self.disposition {
463 ToolDisposition::Success(_) => ToolDisposition::Success(output),
464 ToolDisposition::Skipped(_) => ToolDisposition::Skipped(output),
465 ToolDisposition::Error(error) => {
466 ToolDisposition::Error(error.with_model_output(output))
467 }
468 ToolDisposition::Refused(error) => {
469 ToolDisposition::Refused(error.with_model_output(output))
470 }
471 };
472 Self { disposition }
473 }
474
475 pub fn output(&self) -> &ToolOutput {
477 match &self.disposition {
478 ToolDisposition::Success(output) | ToolDisposition::Skipped(output) => output,
479 ToolDisposition::Error(error) | ToolDisposition::Refused(error) => error.model_output(),
480 }
481 }
482
483 pub fn error(&self) -> Option<&ToolExecutionError> {
487 match &self.disposition {
488 ToolDisposition::Error(error) => Some(error),
489 ToolDisposition::Success(_)
490 | ToolDisposition::Refused(_)
491 | ToolDisposition::Skipped(_) => None,
492 }
493 }
494
495 pub fn refusal(&self) -> Option<&ToolExecutionError> {
499 match &self.disposition {
500 ToolDisposition::Refused(error) => Some(error),
501 ToolDisposition::Success(_)
502 | ToolDisposition::Error(_)
503 | ToolDisposition::Skipped(_) => None,
504 }
505 }
506
507 pub fn is_success(&self) -> bool {
509 matches!(&self.disposition, ToolDisposition::Success(_))
510 }
511
512 pub fn is_error(&self) -> bool {
517 matches!(&self.disposition, ToolDisposition::Error(_))
518 }
519
520 pub fn is_skipped(&self) -> bool {
522 matches!(&self.disposition, ToolDisposition::Skipped(_))
523 }
524
525 pub fn is_refused(&self) -> bool {
527 matches!(&self.disposition, ToolDisposition::Refused(_))
528 }
529
530 pub fn is_error_kind(&self, kind: ToolErrorKind) -> bool {
535 self.error().is_some_and(|error| error.kind == kind)
536 }
537
538 pub fn into_result(self) -> Result<ToolOutput, ToolExecutionError> {
541 match self.disposition {
542 ToolDisposition::Success(output) | ToolDisposition::Skipped(output) => Ok(output),
543 ToolDisposition::Error(error) | ToolDisposition::Refused(error) => Err(error),
544 }
545 }
546
547 pub fn status_name(&self) -> &'static str {
549 match &self.disposition {
550 ToolDisposition::Success(_) => "success",
551 ToolDisposition::Error(_) => "error",
552 ToolDisposition::Refused(_) => "denied",
553 ToolDisposition::Skipped(_) => "skipped",
554 }
555 }
556}
557
558#[cfg(not(target_family = "wasm"))]
559const _: fn() = || {
560 fn assert_send_sync<T: Send + Sync>() {}
561 assert_send_sync::<ToolExecutionError>();
562 assert_send_sync::<ToolResult>();
563};
564
565#[cfg(test)]
566mod tests;