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)]
136#[serde(from = "ToolExecutionErrorRepr", into = "ToolExecutionErrorRepr")]
137pub struct ToolExecutionError {
138 kind: ToolErrorKind,
139 message: String,
140 model_output: ToolOutput,
141 retryable: Option<bool>,
142 code: Option<String>,
143 http_status: Option<u16>,
144 refusal: bool,
145 #[cfg(not(target_family = "wasm"))]
149 source: Option<Arc<dyn Error + Send + Sync + 'static>>,
150}
151
152#[derive(serde::Serialize, serde::Deserialize)]
156struct ToolExecutionErrorRepr {
157 kind: ToolErrorKind,
158 message: String,
159 model_output: ToolOutput,
160 retryable: Option<bool>,
161 code: Option<String>,
162 http_status: Option<u16>,
163 refusal: bool,
164}
165
166impl From<ToolExecutionError> for ToolExecutionErrorRepr {
167 fn from(error: ToolExecutionError) -> Self {
168 Self {
169 kind: error.kind,
170 message: error.message,
171 model_output: error.model_output,
172 retryable: error.retryable,
173 code: error.code,
174 http_status: error.http_status,
175 refusal: error.refusal,
176 }
177 }
178}
179
180impl From<ToolExecutionErrorRepr> for ToolExecutionError {
181 fn from(repr: ToolExecutionErrorRepr) -> Self {
182 Self {
183 kind: repr.kind,
184 message: repr.message,
185 model_output: repr.model_output,
186 retryable: repr.retryable,
187 code: repr.code,
188 http_status: repr.http_status,
189 refusal: repr.refusal,
190 #[cfg(not(target_family = "wasm"))]
191 source: None,
192 }
193 }
194}
195
196impl ToolExecutionError {
197 pub fn new(kind: ToolErrorKind, message: impl Into<String>) -> Self {
199 let message = message.into();
200 Self {
201 kind,
202 model_output: ToolOutput::text(message.clone()),
203 message,
204 retryable: kind.default_retryable(),
205 code: None,
206 http_status: None,
207 refusal: false,
208 #[cfg(not(target_family = "wasm"))]
209 source: None,
210 }
211 }
212
213 pub fn refused(message: impl Into<String>) -> Self {
218 let mut error = Self::new(ToolErrorKind::PermissionDenied, message);
219 error.refusal = true;
220 error
221 }
222
223 pub fn from_error<E>(error: E) -> Self
230 where
231 E: Error + WasmCompatSend + WasmCompatSync + 'static,
232 {
233 #[cfg(not(target_family = "wasm"))]
234 {
235 let source: Box<dyn Error + Send + Sync + 'static> = Box::new(error);
236 return match source.downcast::<Self>() {
237 Ok(error) => *error,
238 Err(source) => {
239 let message = source.to_string();
240 let mut error = Self::other(message).redact_model_feedback();
241 error.source = Some(Arc::from(source));
242 error
243 }
244 };
245 }
246 #[cfg(target_family = "wasm")]
247 {
248 let source: Box<dyn Error + 'static> = Box::new(error);
249 match source.downcast::<Self>() {
250 Ok(error) => *error,
251 Err(source) => Self::other(source.to_string()).redact_model_feedback(),
252 }
253 }
254 }
255
256 pub fn with_model_feedback(mut self, feedback: impl Into<String>) -> Self {
258 self.model_output = ToolOutput::text(feedback);
259 self
260 }
261
262 pub fn with_model_output(mut self, output: ToolOutput) -> Self {
265 self.model_output = output;
266 self
267 }
268
269 pub(crate) fn redact_model_feedback(mut self) -> Self {
277 self.model_output = ToolOutput::text(self.kind.default_model_feedback());
278 self
279 }
280
281 pub fn with_retryable(mut self, retryable: bool) -> Self {
283 self.retryable = Some(retryable);
284 self
285 }
286
287 pub fn with_code(mut self, code: impl Into<String>) -> Self {
289 self.code = Some(code.into());
290 self
291 }
292
293 pub fn with_http_status(mut self, status: u16) -> Self {
295 self.http_status = Some(status);
296 self
297 }
298
299 #[cfg_attr(target_family = "wasm", allow(unused_mut))]
304 pub fn with_source<E>(mut self, source: E) -> Self
305 where
306 E: Error + WasmCompatSend + WasmCompatSync + 'static,
307 {
308 #[cfg(not(target_family = "wasm"))]
309 {
310 self.source = Some(Arc::new(source));
311 }
312 #[cfg(target_family = "wasm")]
313 {
314 let _ = source;
315 }
316 self
317 }
318
319 pub const fn kind(&self) -> ToolErrorKind {
321 self.kind
322 }
323
324 pub fn message(&self) -> &str {
326 &self.message
327 }
328
329 pub fn model_feedback(&self) -> Option<&str> {
334 self.model_output.as_text()
335 }
336
337 pub fn model_output(&self) -> &ToolOutput {
339 &self.model_output
340 }
341
342 pub const fn retryable(&self) -> Option<bool> {
344 self.retryable
345 }
346
347 pub fn code(&self) -> Option<&str> {
349 self.code.as_deref()
350 }
351
352 pub const fn http_status(&self) -> Option<u16> {
354 self.http_status
355 }
356
357 pub const fn is_refusal(&self) -> bool {
359 self.refusal
360 }
361
362 pub fn downcast_ref<E>(&self) -> Option<&E>
364 where
365 E: Error + WasmCompatSend + WasmCompatSync + 'static,
366 {
367 #[cfg(not(target_family = "wasm"))]
368 {
369 self.source.as_ref()?.downcast_ref::<E>()
370 }
371 #[cfg(target_family = "wasm")]
372 {
373 None
374 }
375 }
376
377 pub fn is<E>(&self) -> bool
379 where
380 E: Error + WasmCompatSend + WasmCompatSync + 'static,
381 {
382 self.downcast_ref::<E>().is_some()
383 }
384}
385
386impl std::fmt::Display for ToolExecutionError {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.write_str(&self.message)
389 }
390}
391
392impl std::fmt::Debug for ToolExecutionError {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.debug_struct("ToolExecutionError")
395 .field("kind", &self.kind)
396 .field("retryable", &self.retryable)
397 .field("code", &self.code)
398 .field("http_status", &self.http_status)
399 .field("refusal", &self.refusal)
400 .field("model_output", &"<redacted>")
401 .field("source_configured", &self.has_source())
402 .finish()
403 }
404}
405
406impl ToolExecutionError {
407 fn has_source(&self) -> bool {
408 #[cfg(not(target_family = "wasm"))]
409 {
410 self.source.is_some()
411 }
412 #[cfg(target_family = "wasm")]
413 {
414 false
415 }
416 }
417}
418
419impl Error for ToolExecutionError {
420 fn source(&self) -> Option<&(dyn Error + 'static)> {
421 #[cfg(not(target_family = "wasm"))]
422 {
423 self.source
424 .as_deref()
425 .map(|source| source as &(dyn Error + 'static))
426 }
427 #[cfg(target_family = "wasm")]
428 {
429 None
430 }
431 }
432}
433
434#[derive(Clone, serde::Serialize, serde::Deserialize)]
436#[serde(tag = "status", content = "value", rename_all = "snake_case")]
437enum ToolDisposition {
438 Success(ToolOutput),
439 Error(ToolExecutionError),
440 Refused(ToolExecutionError),
441 Skipped(ToolOutput),
442}
443
444#[derive(Clone, serde::Serialize, serde::Deserialize)]
450#[serde(transparent)]
451pub struct ToolResult {
452 disposition: ToolDisposition,
453}
454
455impl std::fmt::Debug for ToolResult {
456 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 let error = match &self.disposition {
458 ToolDisposition::Error(error) | ToolDisposition::Refused(error) => Some(error),
459 ToolDisposition::Success(_) | ToolDisposition::Skipped(_) => None,
460 };
461 formatter
462 .debug_struct("ToolResult")
463 .field("status", &self.status_name())
464 .field("error_kind", &error.map(ToolExecutionError::kind))
465 .field("retryable", &error.and_then(ToolExecutionError::retryable))
466 .field("code", &error.and_then(ToolExecutionError::code))
467 .field(
468 "http_status",
469 &error.and_then(ToolExecutionError::http_status),
470 )
471 .finish()
472 }
473}
474
475impl ToolResult {
476 pub fn success(output: ToolOutput) -> Self {
478 Self {
479 disposition: ToolDisposition::Success(output),
480 }
481 }
482
483 pub fn failed(error: ToolExecutionError) -> Self {
485 let disposition = if error.is_refusal() {
486 ToolDisposition::Refused(error)
487 } else {
488 ToolDisposition::Error(error)
489 };
490 Self { disposition }
491 }
492
493 pub fn skipped(reason: impl Into<String>) -> Self {
495 Self {
496 disposition: ToolDisposition::Skipped(ToolOutput::text(reason)),
497 }
498 }
499
500 pub fn with_output(self, output: ToolOutput) -> Self {
504 let disposition = match self.disposition {
505 ToolDisposition::Success(_) => ToolDisposition::Success(output),
506 ToolDisposition::Skipped(_) => ToolDisposition::Skipped(output),
507 ToolDisposition::Error(error) => {
508 ToolDisposition::Error(error.with_model_output(output))
509 }
510 ToolDisposition::Refused(error) => {
511 ToolDisposition::Refused(error.with_model_output(output))
512 }
513 };
514 Self { disposition }
515 }
516
517 pub fn output(&self) -> &ToolOutput {
519 match &self.disposition {
520 ToolDisposition::Success(output) | ToolDisposition::Skipped(output) => output,
521 ToolDisposition::Error(error) | ToolDisposition::Refused(error) => error.model_output(),
522 }
523 }
524
525 pub fn error(&self) -> Option<&ToolExecutionError> {
529 match &self.disposition {
530 ToolDisposition::Error(error) => Some(error),
531 ToolDisposition::Success(_)
532 | ToolDisposition::Refused(_)
533 | ToolDisposition::Skipped(_) => None,
534 }
535 }
536
537 pub fn refusal(&self) -> Option<&ToolExecutionError> {
541 match &self.disposition {
542 ToolDisposition::Refused(error) => Some(error),
543 ToolDisposition::Success(_)
544 | ToolDisposition::Error(_)
545 | ToolDisposition::Skipped(_) => None,
546 }
547 }
548
549 pub fn is_success(&self) -> bool {
551 matches!(&self.disposition, ToolDisposition::Success(_))
552 }
553
554 pub fn is_error(&self) -> bool {
559 matches!(&self.disposition, ToolDisposition::Error(_))
560 }
561
562 pub fn is_skipped(&self) -> bool {
564 matches!(&self.disposition, ToolDisposition::Skipped(_))
565 }
566
567 pub fn is_refused(&self) -> bool {
569 matches!(&self.disposition, ToolDisposition::Refused(_))
570 }
571
572 pub fn is_error_kind(&self, kind: ToolErrorKind) -> bool {
577 self.error().is_some_and(|error| error.kind == kind)
578 }
579
580 pub fn into_result(self) -> Result<ToolOutput, ToolExecutionError> {
583 match self.disposition {
584 ToolDisposition::Success(output) | ToolDisposition::Skipped(output) => Ok(output),
585 ToolDisposition::Error(error) | ToolDisposition::Refused(error) => Err(error),
586 }
587 }
588
589 pub fn status_name(&self) -> &'static str {
591 match &self.disposition {
592 ToolDisposition::Success(_) => "success",
593 ToolDisposition::Error(_) => "error",
594 ToolDisposition::Refused(_) => "denied",
595 ToolDisposition::Skipped(_) => "skipped",
596 }
597 }
598}
599
600#[cfg(not(target_family = "wasm"))]
601const _: fn() = || {
602 fn assert_send_sync<T: Send + Sync>() {}
603 assert_send_sync::<ToolExecutionError>();
604 assert_send_sync::<ToolResult>();
605};
606
607#[cfg(test)]
608mod tests;
609
610#[cfg(test)]
611mod migrated_tests;