1use std::fmt;
5
6#[derive(Debug, Clone)]
8pub struct DiffData {
9 pub file_path: String,
10 pub old_content: String,
11 pub new_content: String,
12}
13
14#[derive(Debug, Clone)]
16pub struct ToolCall {
17 pub tool_id: String,
18 pub params: serde_json::Map<String, serde_json::Value>,
19}
20
21#[derive(Debug, Clone, Default)]
23pub struct FilterStats {
24 pub raw_chars: usize,
25 pub filtered_chars: usize,
26 pub raw_lines: usize,
27 pub filtered_lines: usize,
28 pub confidence: Option<crate::FilterConfidence>,
29 pub command: Option<String>,
30 pub kept_lines: Vec<usize>,
31}
32
33impl FilterStats {
34 #[must_use]
35 #[allow(clippy::cast_precision_loss)]
36 pub fn savings_pct(&self) -> f64 {
37 if self.raw_chars == 0 {
38 return 0.0;
39 }
40 (1.0 - self.filtered_chars as f64 / self.raw_chars as f64) * 100.0
41 }
42
43 #[must_use]
44 pub fn estimated_tokens_saved(&self) -> usize {
45 self.raw_chars.saturating_sub(self.filtered_chars) / 4
46 }
47
48 #[must_use]
49 pub fn format_inline(&self, tool_name: &str) -> String {
50 let cmd_label = self
51 .command
52 .as_deref()
53 .map(|c| {
54 let trimmed = c.trim();
55 if trimmed.len() > 60 {
56 format!(" `{}…`", &trimmed[..57])
57 } else {
58 format!(" `{trimmed}`")
59 }
60 })
61 .unwrap_or_default();
62 format!(
63 "[{tool_name}]{cmd_label} {} lines \u{2192} {} lines, {:.1}% filtered",
64 self.raw_lines,
65 self.filtered_lines,
66 self.savings_pct()
67 )
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct ToolOutput {
74 pub tool_name: String,
75 pub summary: String,
76 pub blocks_executed: u32,
77 pub filter_stats: Option<FilterStats>,
78 pub diff: Option<DiffData>,
79 pub streamed: bool,
81 pub terminal_id: Option<String>,
83 pub locations: Option<Vec<String>>,
85 pub raw_response: Option<serde_json::Value>,
87}
88
89impl fmt::Display for ToolOutput {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(&self.summary)
92 }
93}
94
95pub const MAX_TOOL_OUTPUT_CHARS: usize = 30_000;
96
97#[must_use]
99pub fn truncate_tool_output(output: &str) -> String {
100 truncate_tool_output_at(output, MAX_TOOL_OUTPUT_CHARS)
101}
102
103#[must_use]
105pub fn truncate_tool_output_at(output: &str, max_chars: usize) -> String {
106 if output.len() <= max_chars {
107 return output.to_string();
108 }
109
110 let half = max_chars / 2;
111 let head_end = output.floor_char_boundary(half);
112 let tail_start = output.ceil_char_boundary(output.len() - half);
113 let head = &output[..head_end];
114 let tail = &output[tail_start..];
115 let truncated = output.len() - head_end - (output.len() - tail_start);
116
117 format!(
118 "{head}\n\n... [truncated {truncated} chars, showing first and last ~{half} chars] ...\n\n{tail}"
119 )
120}
121
122#[derive(Debug, Clone)]
124pub enum ToolEvent {
125 Started {
126 tool_name: String,
127 command: String,
128 },
129 OutputChunk {
130 tool_name: String,
131 command: String,
132 chunk: String,
133 },
134 Completed {
135 tool_name: String,
136 command: String,
137 output: String,
138 success: bool,
139 filter_stats: Option<FilterStats>,
140 diff: Option<DiffData>,
141 },
142}
143
144pub type ToolEventTx = tokio::sync::mpsc::UnboundedSender<ToolEvent>;
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
151pub enum ErrorKind {
152 Transient,
153 Permanent,
154}
155
156impl std::fmt::Display for ErrorKind {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 Self::Transient => f.write_str("transient"),
160 Self::Permanent => f.write_str("permanent"),
161 }
162 }
163}
164
165#[derive(Debug, thiserror::Error)]
167pub enum ToolError {
168 #[error("command blocked by policy: {command}")]
169 Blocked { command: String },
170
171 #[error("path not allowed by sandbox: {path}")]
172 SandboxViolation { path: String },
173
174 #[error("command requires confirmation: {command}")]
175 ConfirmationRequired { command: String },
176
177 #[error("command timed out after {timeout_secs}s")]
178 Timeout { timeout_secs: u64 },
179
180 #[error("operation cancelled")]
181 Cancelled,
182
183 #[error("invalid tool parameters: {message}")]
184 InvalidParams { message: String },
185
186 #[error("execution failed: {0}")]
187 Execution(#[from] std::io::Error),
188}
189
190impl ToolError {
191 #[must_use]
199 pub fn kind(&self) -> ErrorKind {
200 match self {
201 Self::Timeout { .. } => ErrorKind::Transient,
202 Self::Execution(io_err) => match io_err.kind() {
203 std::io::ErrorKind::TimedOut
204 | std::io::ErrorKind::WouldBlock
205 | std::io::ErrorKind::Interrupted
206 | std::io::ErrorKind::ConnectionReset
207 | std::io::ErrorKind::ConnectionAborted
208 | std::io::ErrorKind::BrokenPipe => ErrorKind::Transient,
209 _ => ErrorKind::Permanent,
211 },
212 Self::Blocked { .. }
213 | Self::SandboxViolation { .. }
214 | Self::ConfirmationRequired { .. }
215 | Self::Cancelled
216 | Self::InvalidParams { .. } => ErrorKind::Permanent,
217 }
218 }
219}
220
221pub fn deserialize_params<T: serde::de::DeserializeOwned>(
227 params: &serde_json::Map<String, serde_json::Value>,
228) -> Result<T, ToolError> {
229 let obj = serde_json::Value::Object(params.clone());
230 serde_json::from_value(obj).map_err(|e| ToolError::InvalidParams {
231 message: e.to_string(),
232 })
233}
234
235pub trait ToolExecutor: Send + Sync {
240 fn execute(
241 &self,
242 response: &str,
243 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send;
244
245 fn execute_confirmed(
248 &self,
249 response: &str,
250 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
251 self.execute(response)
252 }
253
254 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
256 vec![]
257 }
258
259 fn execute_tool_call(
261 &self,
262 _call: &ToolCall,
263 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
264 std::future::ready(Ok(None))
265 }
266
267 fn set_skill_env(&self, _env: Option<std::collections::HashMap<String, String>>) {}
269
270 fn set_effective_trust(&self, _level: crate::TrustLevel) {}
272}
273
274pub trait ErasedToolExecutor: Send + Sync {
279 fn execute_erased<'a>(
280 &'a self,
281 response: &'a str,
282 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
283
284 fn execute_confirmed_erased<'a>(
285 &'a self,
286 response: &'a str,
287 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
288
289 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef>;
290
291 fn execute_tool_call_erased<'a>(
292 &'a self,
293 call: &'a ToolCall,
294 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
295
296 fn set_skill_env(&self, _env: Option<std::collections::HashMap<String, String>>) {}
298
299 fn set_effective_trust(&self, _level: crate::TrustLevel) {}
301}
302
303impl<T: ToolExecutor> ErasedToolExecutor for T {
304 fn execute_erased<'a>(
305 &'a self,
306 response: &'a str,
307 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
308 {
309 Box::pin(self.execute(response))
310 }
311
312 fn execute_confirmed_erased<'a>(
313 &'a self,
314 response: &'a str,
315 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
316 {
317 Box::pin(self.execute_confirmed(response))
318 }
319
320 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef> {
321 self.tool_definitions()
322 }
323
324 fn execute_tool_call_erased<'a>(
325 &'a self,
326 call: &'a ToolCall,
327 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
328 {
329 Box::pin(self.execute_tool_call(call))
330 }
331
332 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
333 ToolExecutor::set_skill_env(self, env);
334 }
335
336 fn set_effective_trust(&self, level: crate::TrustLevel) {
337 ToolExecutor::set_effective_trust(self, level);
338 }
339}
340
341pub struct DynExecutor(pub std::sync::Arc<dyn ErasedToolExecutor>);
345
346impl ToolExecutor for DynExecutor {
347 fn execute(
348 &self,
349 response: &str,
350 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
351 let inner = std::sync::Arc::clone(&self.0);
353 let response = response.to_owned();
354 async move { inner.execute_erased(&response).await }
355 }
356
357 fn execute_confirmed(
358 &self,
359 response: &str,
360 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
361 let inner = std::sync::Arc::clone(&self.0);
362 let response = response.to_owned();
363 async move { inner.execute_confirmed_erased(&response).await }
364 }
365
366 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
367 self.0.tool_definitions_erased()
368 }
369
370 fn execute_tool_call(
371 &self,
372 call: &ToolCall,
373 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
374 let inner = std::sync::Arc::clone(&self.0);
375 let call = call.clone();
376 async move { inner.execute_tool_call_erased(&call).await }
377 }
378
379 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
380 ErasedToolExecutor::set_skill_env(self.0.as_ref(), env);
381 }
382
383 fn set_effective_trust(&self, level: crate::TrustLevel) {
384 ErasedToolExecutor::set_effective_trust(self.0.as_ref(), level);
385 }
386}
387
388#[must_use]
392pub fn extract_fenced_blocks<'a>(text: &'a str, lang: &str) -> Vec<&'a str> {
393 let marker = format!("```{lang}");
394 let marker_len = marker.len();
395 let mut blocks = Vec::new();
396 let mut rest = text;
397
398 while let Some(start) = rest.find(&marker) {
399 let after = &rest[start + marker_len..];
400 if let Some(end) = after.find("```") {
401 blocks.push(after[..end].trim());
402 rest = &after[end + 3..];
403 } else {
404 break;
405 }
406 }
407
408 blocks
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn tool_output_display() {
417 let output = ToolOutput {
418 tool_name: "bash".to_owned(),
419 summary: "$ echo hello\nhello".to_owned(),
420 blocks_executed: 1,
421 filter_stats: None,
422 diff: None,
423 streamed: false,
424 terminal_id: None,
425 locations: None,
426 raw_response: None,
427 };
428 assert_eq!(output.to_string(), "$ echo hello\nhello");
429 }
430
431 #[test]
432 fn tool_error_blocked_display() {
433 let err = ToolError::Blocked {
434 command: "rm -rf /".to_owned(),
435 };
436 assert_eq!(err.to_string(), "command blocked by policy: rm -rf /");
437 }
438
439 #[test]
440 fn tool_error_sandbox_violation_display() {
441 let err = ToolError::SandboxViolation {
442 path: "/etc/shadow".to_owned(),
443 };
444 assert_eq!(err.to_string(), "path not allowed by sandbox: /etc/shadow");
445 }
446
447 #[test]
448 fn tool_error_confirmation_required_display() {
449 let err = ToolError::ConfirmationRequired {
450 command: "rm -rf /tmp".to_owned(),
451 };
452 assert_eq!(
453 err.to_string(),
454 "command requires confirmation: rm -rf /tmp"
455 );
456 }
457
458 #[test]
459 fn tool_error_timeout_display() {
460 let err = ToolError::Timeout { timeout_secs: 30 };
461 assert_eq!(err.to_string(), "command timed out after 30s");
462 }
463
464 #[test]
465 fn tool_error_invalid_params_display() {
466 let err = ToolError::InvalidParams {
467 message: "missing field `command`".to_owned(),
468 };
469 assert_eq!(
470 err.to_string(),
471 "invalid tool parameters: missing field `command`"
472 );
473 }
474
475 #[test]
476 fn deserialize_params_valid() {
477 #[derive(Debug, serde::Deserialize, PartialEq)]
478 struct P {
479 name: String,
480 count: u32,
481 }
482 let mut map = serde_json::Map::new();
483 map.insert("name".to_owned(), serde_json::json!("test"));
484 map.insert("count".to_owned(), serde_json::json!(42));
485 let p: P = deserialize_params(&map).unwrap();
486 assert_eq!(
487 p,
488 P {
489 name: "test".to_owned(),
490 count: 42
491 }
492 );
493 }
494
495 #[test]
496 fn deserialize_params_missing_required_field() {
497 #[derive(Debug, serde::Deserialize)]
498 #[allow(dead_code)]
499 struct P {
500 name: String,
501 }
502 let map = serde_json::Map::new();
503 let err = deserialize_params::<P>(&map).unwrap_err();
504 assert!(matches!(err, ToolError::InvalidParams { .. }));
505 }
506
507 #[test]
508 fn deserialize_params_wrong_type() {
509 #[derive(Debug, serde::Deserialize)]
510 #[allow(dead_code)]
511 struct P {
512 count: u32,
513 }
514 let mut map = serde_json::Map::new();
515 map.insert("count".to_owned(), serde_json::json!("not a number"));
516 let err = deserialize_params::<P>(&map).unwrap_err();
517 assert!(matches!(err, ToolError::InvalidParams { .. }));
518 }
519
520 #[test]
521 fn deserialize_params_all_optional_empty() {
522 #[derive(Debug, serde::Deserialize, PartialEq)]
523 struct P {
524 name: Option<String>,
525 }
526 let map = serde_json::Map::new();
527 let p: P = deserialize_params(&map).unwrap();
528 assert_eq!(p, P { name: None });
529 }
530
531 #[test]
532 fn deserialize_params_ignores_extra_fields() {
533 #[derive(Debug, serde::Deserialize, PartialEq)]
534 struct P {
535 name: String,
536 }
537 let mut map = serde_json::Map::new();
538 map.insert("name".to_owned(), serde_json::json!("test"));
539 map.insert("extra".to_owned(), serde_json::json!(true));
540 let p: P = deserialize_params(&map).unwrap();
541 assert_eq!(
542 p,
543 P {
544 name: "test".to_owned()
545 }
546 );
547 }
548
549 #[test]
550 fn tool_error_execution_display() {
551 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "bash not found");
552 let err = ToolError::Execution(io_err);
553 assert!(err.to_string().starts_with("execution failed:"));
554 assert!(err.to_string().contains("bash not found"));
555 }
556
557 #[test]
559 fn error_kind_timeout_is_transient() {
560 let err = ToolError::Timeout { timeout_secs: 30 };
561 assert_eq!(err.kind(), ErrorKind::Transient);
562 }
563
564 #[test]
565 fn error_kind_blocked_is_permanent() {
566 let err = ToolError::Blocked {
567 command: "rm -rf /".to_owned(),
568 };
569 assert_eq!(err.kind(), ErrorKind::Permanent);
570 }
571
572 #[test]
573 fn error_kind_sandbox_violation_is_permanent() {
574 let err = ToolError::SandboxViolation {
575 path: "/etc/shadow".to_owned(),
576 };
577 assert_eq!(err.kind(), ErrorKind::Permanent);
578 }
579
580 #[test]
581 fn error_kind_cancelled_is_permanent() {
582 assert_eq!(ToolError::Cancelled.kind(), ErrorKind::Permanent);
583 }
584
585 #[test]
586 fn error_kind_invalid_params_is_permanent() {
587 let err = ToolError::InvalidParams {
588 message: "bad arg".to_owned(),
589 };
590 assert_eq!(err.kind(), ErrorKind::Permanent);
591 }
592
593 #[test]
594 fn error_kind_confirmation_required_is_permanent() {
595 let err = ToolError::ConfirmationRequired {
596 command: "rm /tmp/x".to_owned(),
597 };
598 assert_eq!(err.kind(), ErrorKind::Permanent);
599 }
600
601 #[test]
602 fn error_kind_execution_timed_out_is_transient() {
603 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
604 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
605 }
606
607 #[test]
608 fn error_kind_execution_interrupted_is_transient() {
609 let io_err = std::io::Error::new(std::io::ErrorKind::Interrupted, "interrupted");
610 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
611 }
612
613 #[test]
614 fn error_kind_execution_connection_reset_is_transient() {
615 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
616 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
617 }
618
619 #[test]
620 fn error_kind_execution_broken_pipe_is_transient() {
621 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken");
622 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
623 }
624
625 #[test]
626 fn error_kind_execution_would_block_is_transient() {
627 let io_err = std::io::Error::new(std::io::ErrorKind::WouldBlock, "would block");
628 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
629 }
630
631 #[test]
632 fn error_kind_execution_connection_aborted_is_transient() {
633 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "aborted");
634 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
635 }
636
637 #[test]
638 fn error_kind_execution_not_found_is_permanent() {
639 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
640 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
641 }
642
643 #[test]
644 fn error_kind_execution_permission_denied_is_permanent() {
645 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
646 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
647 }
648
649 #[test]
650 fn error_kind_execution_other_is_permanent() {
651 let io_err = std::io::Error::new(std::io::ErrorKind::Other, "some other error");
652 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
653 }
654
655 #[test]
656 fn error_kind_execution_already_exists_is_permanent() {
657 let io_err = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "exists");
658 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
659 }
660
661 #[test]
662 fn error_kind_display() {
663 assert_eq!(ErrorKind::Transient.to_string(), "transient");
664 assert_eq!(ErrorKind::Permanent.to_string(), "permanent");
665 }
666
667 #[test]
668 fn truncate_tool_output_short_passthrough() {
669 let short = "hello world";
670 assert_eq!(truncate_tool_output(short), short);
671 }
672
673 #[test]
674 fn truncate_tool_output_exact_limit() {
675 let exact = "a".repeat(MAX_TOOL_OUTPUT_CHARS);
676 assert_eq!(truncate_tool_output(&exact), exact);
677 }
678
679 #[test]
680 fn truncate_tool_output_long_split() {
681 let long = "x".repeat(MAX_TOOL_OUTPUT_CHARS + 1000);
682 let result = truncate_tool_output(&long);
683 assert!(result.contains("truncated"));
684 assert!(result.len() < long.len());
685 }
686
687 #[test]
688 fn truncate_tool_output_notice_contains_count() {
689 let long = "y".repeat(MAX_TOOL_OUTPUT_CHARS + 2000);
690 let result = truncate_tool_output(&long);
691 assert!(result.contains("truncated"));
692 assert!(result.contains("chars"));
693 }
694
695 #[derive(Debug)]
696 struct DefaultExecutor;
697 impl ToolExecutor for DefaultExecutor {
698 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
699 Ok(None)
700 }
701 }
702
703 #[tokio::test]
704 async fn execute_tool_call_default_returns_none() {
705 let exec = DefaultExecutor;
706 let call = ToolCall {
707 tool_id: "anything".to_owned(),
708 params: serde_json::Map::new(),
709 };
710 let result = exec.execute_tool_call(&call).await.unwrap();
711 assert!(result.is_none());
712 }
713
714 #[test]
715 fn filter_stats_savings_pct() {
716 let fs = FilterStats {
717 raw_chars: 1000,
718 filtered_chars: 200,
719 ..Default::default()
720 };
721 assert!((fs.savings_pct() - 80.0).abs() < 0.01);
722 }
723
724 #[test]
725 fn filter_stats_savings_pct_zero() {
726 let fs = FilterStats::default();
727 assert!((fs.savings_pct()).abs() < 0.01);
728 }
729
730 #[test]
731 fn filter_stats_estimated_tokens_saved() {
732 let fs = FilterStats {
733 raw_chars: 1000,
734 filtered_chars: 200,
735 ..Default::default()
736 };
737 assert_eq!(fs.estimated_tokens_saved(), 200); }
739
740 #[test]
741 fn filter_stats_format_inline() {
742 let fs = FilterStats {
743 raw_chars: 1000,
744 filtered_chars: 200,
745 raw_lines: 342,
746 filtered_lines: 28,
747 ..Default::default()
748 };
749 let line = fs.format_inline("shell");
750 assert_eq!(line, "[shell] 342 lines \u{2192} 28 lines, 80.0% filtered");
751 }
752
753 #[test]
754 fn filter_stats_format_inline_zero() {
755 let fs = FilterStats::default();
756 let line = fs.format_inline("bash");
757 assert_eq!(line, "[bash] 0 lines \u{2192} 0 lines, 0.0% filtered");
758 }
759
760 struct FixedExecutor {
763 tool_id: &'static str,
764 output: &'static str,
765 }
766
767 impl ToolExecutor for FixedExecutor {
768 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
769 Ok(Some(ToolOutput {
770 tool_name: self.tool_id.to_owned(),
771 summary: self.output.to_owned(),
772 blocks_executed: 1,
773 filter_stats: None,
774 diff: None,
775 streamed: false,
776 terminal_id: None,
777 locations: None,
778 raw_response: None,
779 }))
780 }
781
782 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
783 vec![]
784 }
785
786 async fn execute_tool_call(
787 &self,
788 _call: &ToolCall,
789 ) -> Result<Option<ToolOutput>, ToolError> {
790 Ok(Some(ToolOutput {
791 tool_name: self.tool_id.to_owned(),
792 summary: self.output.to_owned(),
793 blocks_executed: 1,
794 filter_stats: None,
795 diff: None,
796 streamed: false,
797 terminal_id: None,
798 locations: None,
799 raw_response: None,
800 }))
801 }
802 }
803
804 #[tokio::test]
805 async fn dyn_executor_execute_delegates() {
806 let inner = std::sync::Arc::new(FixedExecutor {
807 tool_id: "bash",
808 output: "hello",
809 });
810 let exec = DynExecutor(inner);
811 let result = exec.execute("```bash\necho hello\n```").await.unwrap();
812 assert!(result.is_some());
813 assert_eq!(result.unwrap().summary, "hello");
814 }
815
816 #[tokio::test]
817 async fn dyn_executor_execute_confirmed_delegates() {
818 let inner = std::sync::Arc::new(FixedExecutor {
819 tool_id: "bash",
820 output: "confirmed",
821 });
822 let exec = DynExecutor(inner);
823 let result = exec.execute_confirmed("...").await.unwrap();
824 assert!(result.is_some());
825 assert_eq!(result.unwrap().summary, "confirmed");
826 }
827
828 #[test]
829 fn dyn_executor_tool_definitions_delegates() {
830 let inner = std::sync::Arc::new(FixedExecutor {
831 tool_id: "my_tool",
832 output: "",
833 });
834 let exec = DynExecutor(inner);
835 let defs = exec.tool_definitions();
837 assert!(defs.is_empty());
838 }
839
840 #[tokio::test]
841 async fn dyn_executor_execute_tool_call_delegates() {
842 let inner = std::sync::Arc::new(FixedExecutor {
843 tool_id: "bash",
844 output: "tool_call_result",
845 });
846 let exec = DynExecutor(inner);
847 let call = ToolCall {
848 tool_id: "bash".to_owned(),
849 params: serde_json::Map::new(),
850 };
851 let result = exec.execute_tool_call(&call).await.unwrap();
852 assert!(result.is_some());
853 assert_eq!(result.unwrap().summary, "tool_call_result");
854 }
855
856 #[test]
857 fn dyn_executor_set_effective_trust_delegates() {
858 use std::sync::atomic::{AtomicU8, Ordering};
859
860 struct TrustCapture(AtomicU8);
861 impl ToolExecutor for TrustCapture {
862 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
863 Ok(None)
864 }
865 fn set_effective_trust(&self, level: crate::TrustLevel) {
866 let v = match level {
868 crate::TrustLevel::Trusted => 0u8,
869 crate::TrustLevel::Verified => 1,
870 crate::TrustLevel::Quarantined => 2,
871 crate::TrustLevel::Blocked => 3,
872 };
873 self.0.store(v, Ordering::Relaxed);
874 }
875 }
876
877 let inner = std::sync::Arc::new(TrustCapture(AtomicU8::new(0)));
878 let exec =
879 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
880 ToolExecutor::set_effective_trust(&exec, crate::TrustLevel::Quarantined);
881 assert_eq!(inner.0.load(Ordering::Relaxed), 2);
882
883 ToolExecutor::set_effective_trust(&exec, crate::TrustLevel::Blocked);
884 assert_eq!(inner.0.load(Ordering::Relaxed), 3);
885 }
886}