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 let mut search_from = 0;
399 while let Some(rel) = rest[search_from..].find(&marker) {
400 let start = search_from + rel;
401 let after = &rest[start + marker_len..];
402 let boundary_ok = after
406 .chars()
407 .next()
408 .is_none_or(|c| !c.is_alphanumeric() && c != '_' && c != '-');
409 if !boundary_ok {
410 search_from = start + marker_len;
411 continue;
412 }
413 if let Some(end) = after.find("```") {
414 blocks.push(after[..end].trim());
415 rest = &after[end + 3..];
416 search_from = 0;
417 } else {
418 break;
419 }
420 }
421
422 blocks
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn tool_output_display() {
431 let output = ToolOutput {
432 tool_name: "bash".to_owned(),
433 summary: "$ echo hello\nhello".to_owned(),
434 blocks_executed: 1,
435 filter_stats: None,
436 diff: None,
437 streamed: false,
438 terminal_id: None,
439 locations: None,
440 raw_response: None,
441 };
442 assert_eq!(output.to_string(), "$ echo hello\nhello");
443 }
444
445 #[test]
446 fn tool_error_blocked_display() {
447 let err = ToolError::Blocked {
448 command: "rm -rf /".to_owned(),
449 };
450 assert_eq!(err.to_string(), "command blocked by policy: rm -rf /");
451 }
452
453 #[test]
454 fn tool_error_sandbox_violation_display() {
455 let err = ToolError::SandboxViolation {
456 path: "/etc/shadow".to_owned(),
457 };
458 assert_eq!(err.to_string(), "path not allowed by sandbox: /etc/shadow");
459 }
460
461 #[test]
462 fn tool_error_confirmation_required_display() {
463 let err = ToolError::ConfirmationRequired {
464 command: "rm -rf /tmp".to_owned(),
465 };
466 assert_eq!(
467 err.to_string(),
468 "command requires confirmation: rm -rf /tmp"
469 );
470 }
471
472 #[test]
473 fn tool_error_timeout_display() {
474 let err = ToolError::Timeout { timeout_secs: 30 };
475 assert_eq!(err.to_string(), "command timed out after 30s");
476 }
477
478 #[test]
479 fn tool_error_invalid_params_display() {
480 let err = ToolError::InvalidParams {
481 message: "missing field `command`".to_owned(),
482 };
483 assert_eq!(
484 err.to_string(),
485 "invalid tool parameters: missing field `command`"
486 );
487 }
488
489 #[test]
490 fn deserialize_params_valid() {
491 #[derive(Debug, serde::Deserialize, PartialEq)]
492 struct P {
493 name: String,
494 count: u32,
495 }
496 let mut map = serde_json::Map::new();
497 map.insert("name".to_owned(), serde_json::json!("test"));
498 map.insert("count".to_owned(), serde_json::json!(42));
499 let p: P = deserialize_params(&map).unwrap();
500 assert_eq!(
501 p,
502 P {
503 name: "test".to_owned(),
504 count: 42
505 }
506 );
507 }
508
509 #[test]
510 fn deserialize_params_missing_required_field() {
511 #[derive(Debug, serde::Deserialize)]
512 #[allow(dead_code)]
513 struct P {
514 name: String,
515 }
516 let map = serde_json::Map::new();
517 let err = deserialize_params::<P>(&map).unwrap_err();
518 assert!(matches!(err, ToolError::InvalidParams { .. }));
519 }
520
521 #[test]
522 fn deserialize_params_wrong_type() {
523 #[derive(Debug, serde::Deserialize)]
524 #[allow(dead_code)]
525 struct P {
526 count: u32,
527 }
528 let mut map = serde_json::Map::new();
529 map.insert("count".to_owned(), serde_json::json!("not a number"));
530 let err = deserialize_params::<P>(&map).unwrap_err();
531 assert!(matches!(err, ToolError::InvalidParams { .. }));
532 }
533
534 #[test]
535 fn deserialize_params_all_optional_empty() {
536 #[derive(Debug, serde::Deserialize, PartialEq)]
537 struct P {
538 name: Option<String>,
539 }
540 let map = serde_json::Map::new();
541 let p: P = deserialize_params(&map).unwrap();
542 assert_eq!(p, P { name: None });
543 }
544
545 #[test]
546 fn deserialize_params_ignores_extra_fields() {
547 #[derive(Debug, serde::Deserialize, PartialEq)]
548 struct P {
549 name: String,
550 }
551 let mut map = serde_json::Map::new();
552 map.insert("name".to_owned(), serde_json::json!("test"));
553 map.insert("extra".to_owned(), serde_json::json!(true));
554 let p: P = deserialize_params(&map).unwrap();
555 assert_eq!(
556 p,
557 P {
558 name: "test".to_owned()
559 }
560 );
561 }
562
563 #[test]
564 fn tool_error_execution_display() {
565 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "bash not found");
566 let err = ToolError::Execution(io_err);
567 assert!(err.to_string().starts_with("execution failed:"));
568 assert!(err.to_string().contains("bash not found"));
569 }
570
571 #[test]
573 fn error_kind_timeout_is_transient() {
574 let err = ToolError::Timeout { timeout_secs: 30 };
575 assert_eq!(err.kind(), ErrorKind::Transient);
576 }
577
578 #[test]
579 fn error_kind_blocked_is_permanent() {
580 let err = ToolError::Blocked {
581 command: "rm -rf /".to_owned(),
582 };
583 assert_eq!(err.kind(), ErrorKind::Permanent);
584 }
585
586 #[test]
587 fn error_kind_sandbox_violation_is_permanent() {
588 let err = ToolError::SandboxViolation {
589 path: "/etc/shadow".to_owned(),
590 };
591 assert_eq!(err.kind(), ErrorKind::Permanent);
592 }
593
594 #[test]
595 fn error_kind_cancelled_is_permanent() {
596 assert_eq!(ToolError::Cancelled.kind(), ErrorKind::Permanent);
597 }
598
599 #[test]
600 fn error_kind_invalid_params_is_permanent() {
601 let err = ToolError::InvalidParams {
602 message: "bad arg".to_owned(),
603 };
604 assert_eq!(err.kind(), ErrorKind::Permanent);
605 }
606
607 #[test]
608 fn error_kind_confirmation_required_is_permanent() {
609 let err = ToolError::ConfirmationRequired {
610 command: "rm /tmp/x".to_owned(),
611 };
612 assert_eq!(err.kind(), ErrorKind::Permanent);
613 }
614
615 #[test]
616 fn error_kind_execution_timed_out_is_transient() {
617 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
618 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
619 }
620
621 #[test]
622 fn error_kind_execution_interrupted_is_transient() {
623 let io_err = std::io::Error::new(std::io::ErrorKind::Interrupted, "interrupted");
624 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
625 }
626
627 #[test]
628 fn error_kind_execution_connection_reset_is_transient() {
629 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
630 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
631 }
632
633 #[test]
634 fn error_kind_execution_broken_pipe_is_transient() {
635 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken");
636 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
637 }
638
639 #[test]
640 fn error_kind_execution_would_block_is_transient() {
641 let io_err = std::io::Error::new(std::io::ErrorKind::WouldBlock, "would block");
642 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
643 }
644
645 #[test]
646 fn error_kind_execution_connection_aborted_is_transient() {
647 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "aborted");
648 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
649 }
650
651 #[test]
652 fn error_kind_execution_not_found_is_permanent() {
653 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
654 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
655 }
656
657 #[test]
658 fn error_kind_execution_permission_denied_is_permanent() {
659 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
660 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
661 }
662
663 #[test]
664 fn error_kind_execution_other_is_permanent() {
665 let io_err = std::io::Error::new(std::io::ErrorKind::Other, "some other error");
666 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
667 }
668
669 #[test]
670 fn error_kind_execution_already_exists_is_permanent() {
671 let io_err = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "exists");
672 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
673 }
674
675 #[test]
676 fn error_kind_display() {
677 assert_eq!(ErrorKind::Transient.to_string(), "transient");
678 assert_eq!(ErrorKind::Permanent.to_string(), "permanent");
679 }
680
681 #[test]
682 fn truncate_tool_output_short_passthrough() {
683 let short = "hello world";
684 assert_eq!(truncate_tool_output(short), short);
685 }
686
687 #[test]
688 fn truncate_tool_output_exact_limit() {
689 let exact = "a".repeat(MAX_TOOL_OUTPUT_CHARS);
690 assert_eq!(truncate_tool_output(&exact), exact);
691 }
692
693 #[test]
694 fn truncate_tool_output_long_split() {
695 let long = "x".repeat(MAX_TOOL_OUTPUT_CHARS + 1000);
696 let result = truncate_tool_output(&long);
697 assert!(result.contains("truncated"));
698 assert!(result.len() < long.len());
699 }
700
701 #[test]
702 fn truncate_tool_output_notice_contains_count() {
703 let long = "y".repeat(MAX_TOOL_OUTPUT_CHARS + 2000);
704 let result = truncate_tool_output(&long);
705 assert!(result.contains("truncated"));
706 assert!(result.contains("chars"));
707 }
708
709 #[derive(Debug)]
710 struct DefaultExecutor;
711 impl ToolExecutor for DefaultExecutor {
712 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
713 Ok(None)
714 }
715 }
716
717 #[tokio::test]
718 async fn execute_tool_call_default_returns_none() {
719 let exec = DefaultExecutor;
720 let call = ToolCall {
721 tool_id: "anything".to_owned(),
722 params: serde_json::Map::new(),
723 };
724 let result = exec.execute_tool_call(&call).await.unwrap();
725 assert!(result.is_none());
726 }
727
728 #[test]
729 fn filter_stats_savings_pct() {
730 let fs = FilterStats {
731 raw_chars: 1000,
732 filtered_chars: 200,
733 ..Default::default()
734 };
735 assert!((fs.savings_pct() - 80.0).abs() < 0.01);
736 }
737
738 #[test]
739 fn filter_stats_savings_pct_zero() {
740 let fs = FilterStats::default();
741 assert!((fs.savings_pct()).abs() < 0.01);
742 }
743
744 #[test]
745 fn filter_stats_estimated_tokens_saved() {
746 let fs = FilterStats {
747 raw_chars: 1000,
748 filtered_chars: 200,
749 ..Default::default()
750 };
751 assert_eq!(fs.estimated_tokens_saved(), 200); }
753
754 #[test]
755 fn filter_stats_format_inline() {
756 let fs = FilterStats {
757 raw_chars: 1000,
758 filtered_chars: 200,
759 raw_lines: 342,
760 filtered_lines: 28,
761 ..Default::default()
762 };
763 let line = fs.format_inline("shell");
764 assert_eq!(line, "[shell] 342 lines \u{2192} 28 lines, 80.0% filtered");
765 }
766
767 #[test]
768 fn filter_stats_format_inline_zero() {
769 let fs = FilterStats::default();
770 let line = fs.format_inline("bash");
771 assert_eq!(line, "[bash] 0 lines \u{2192} 0 lines, 0.0% filtered");
772 }
773
774 struct FixedExecutor {
777 tool_id: &'static str,
778 output: &'static str,
779 }
780
781 impl ToolExecutor for FixedExecutor {
782 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
783 Ok(Some(ToolOutput {
784 tool_name: self.tool_id.to_owned(),
785 summary: self.output.to_owned(),
786 blocks_executed: 1,
787 filter_stats: None,
788 diff: None,
789 streamed: false,
790 terminal_id: None,
791 locations: None,
792 raw_response: None,
793 }))
794 }
795
796 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
797 vec![]
798 }
799
800 async fn execute_tool_call(
801 &self,
802 _call: &ToolCall,
803 ) -> Result<Option<ToolOutput>, ToolError> {
804 Ok(Some(ToolOutput {
805 tool_name: self.tool_id.to_owned(),
806 summary: self.output.to_owned(),
807 blocks_executed: 1,
808 filter_stats: None,
809 diff: None,
810 streamed: false,
811 terminal_id: None,
812 locations: None,
813 raw_response: None,
814 }))
815 }
816 }
817
818 #[tokio::test]
819 async fn dyn_executor_execute_delegates() {
820 let inner = std::sync::Arc::new(FixedExecutor {
821 tool_id: "bash",
822 output: "hello",
823 });
824 let exec = DynExecutor(inner);
825 let result = exec.execute("```bash\necho hello\n```").await.unwrap();
826 assert!(result.is_some());
827 assert_eq!(result.unwrap().summary, "hello");
828 }
829
830 #[tokio::test]
831 async fn dyn_executor_execute_confirmed_delegates() {
832 let inner = std::sync::Arc::new(FixedExecutor {
833 tool_id: "bash",
834 output: "confirmed",
835 });
836 let exec = DynExecutor(inner);
837 let result = exec.execute_confirmed("...").await.unwrap();
838 assert!(result.is_some());
839 assert_eq!(result.unwrap().summary, "confirmed");
840 }
841
842 #[test]
843 fn dyn_executor_tool_definitions_delegates() {
844 let inner = std::sync::Arc::new(FixedExecutor {
845 tool_id: "my_tool",
846 output: "",
847 });
848 let exec = DynExecutor(inner);
849 let defs = exec.tool_definitions();
851 assert!(defs.is_empty());
852 }
853
854 #[tokio::test]
855 async fn dyn_executor_execute_tool_call_delegates() {
856 let inner = std::sync::Arc::new(FixedExecutor {
857 tool_id: "bash",
858 output: "tool_call_result",
859 });
860 let exec = DynExecutor(inner);
861 let call = ToolCall {
862 tool_id: "bash".to_owned(),
863 params: serde_json::Map::new(),
864 };
865 let result = exec.execute_tool_call(&call).await.unwrap();
866 assert!(result.is_some());
867 assert_eq!(result.unwrap().summary, "tool_call_result");
868 }
869
870 #[test]
871 fn dyn_executor_set_effective_trust_delegates() {
872 use std::sync::atomic::{AtomicU8, Ordering};
873
874 struct TrustCapture(AtomicU8);
875 impl ToolExecutor for TrustCapture {
876 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
877 Ok(None)
878 }
879 fn set_effective_trust(&self, level: crate::TrustLevel) {
880 let v = match level {
882 crate::TrustLevel::Trusted => 0u8,
883 crate::TrustLevel::Verified => 1,
884 crate::TrustLevel::Quarantined => 2,
885 crate::TrustLevel::Blocked => 3,
886 };
887 self.0.store(v, Ordering::Relaxed);
888 }
889 }
890
891 let inner = std::sync::Arc::new(TrustCapture(AtomicU8::new(0)));
892 let exec =
893 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
894 ToolExecutor::set_effective_trust(&exec, crate::TrustLevel::Quarantined);
895 assert_eq!(inner.0.load(Ordering::Relaxed), 2);
896
897 ToolExecutor::set_effective_trust(&exec, crate::TrustLevel::Blocked);
898 assert_eq!(inner.0.load(Ordering::Relaxed), 3);
899 }
900
901 #[test]
902 fn extract_fenced_blocks_no_prefix_match() {
903 assert!(extract_fenced_blocks("```bashrc\nfoo\n```", "bash").is_empty());
905 assert_eq!(
907 extract_fenced_blocks("```bash\nfoo\n```", "bash"),
908 vec!["foo"]
909 );
910 assert_eq!(
912 extract_fenced_blocks("```bash \nfoo\n```", "bash"),
913 vec!["foo"]
914 );
915 }
916}