Skip to main content

forge_agent/workflow/
tools.rs

1//! Tool registry for external tool integration with process lifecycle management.
2//!
3//! The tools module provides a centralized registry for external tools (magellan, cargo, splice, etc.)
4//! that workflows can invoke by name. Tools are registered with their executable paths and default
5//! arguments, and can be invoked with additional arguments via ToolInvocation.
6//!
7//! # Process Guards
8//!
9//! The module implements RAII-based process lifecycle management through ProcessGuard, which
10//! automatically terminates spawned processes when dropped. This ensures proper cleanup even
11//! if errors occur during workflow execution.
12//!
13//! # Example
14//!
15//! ```ignore
16//! use forge_agent::workflow::tools::{Tool, ToolRegistry, ToolInvocation};
17//!
18//! let mut registry = ToolRegistry::new();
19//!
20//! // Register a tool
21//! let magellan = Tool::new(
22//!     "magellan",
23//!     "/usr/bin/magellan",
24//!     vec!["--db".to_string(), ".forge/graph.db".to_string()]
25//! );
26//! registry.register(magellan)?;
27//!
28//! // Invoke the tool
29//! let invocation = ToolInvocation::new("magellan")
30//!     .args(vec!["find".to_string(), "--name".to_string(), "symbol".to_string()]);
31//! let result = registry.invoke(&invocation).await?;
32//! ```
33
34use crate::workflow::rollback::ToolCompensation;
35use crate::workflow::task::{TaskContext, TaskError, TaskResult};
36use async_trait::async_trait;
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39use std::fmt;
40use std::path::PathBuf;
41use std::process::Command;
42use std::sync::atomic::{AtomicBool, Ordering};
43use std::sync::Arc;
44use std::time::Duration;
45
46/// A registered external tool.
47///
48/// Tools are registered with their executable path, default arguments, and description.
49/// When invoked, default arguments are combined with invocation-specific arguments.
50#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51pub struct Tool {
52    /// Tool identifier (e.g., "magellan", "cargo", "splice")
53    pub name: String,
54    /// Path to the executable
55    pub executable: PathBuf,
56    /// Default arguments passed to every invocation
57    pub default_args: Vec<String>,
58    /// Human-readable description of the tool
59    pub description: String,
60}
61
62impl Tool {
63    /// Creates a new Tool with the given name and executable.
64    ///
65    /// # Arguments
66    ///
67    /// * `name` - Tool identifier
68    /// * `executable` - Path to the executable (can be relative or absolute)
69    ///
70    /// # Example
71    ///
72    /// ```
73    /// use forge_agent::workflow::tools::Tool;
74    /// use std::path::PathBuf;
75    ///
76    /// let tool = Tool::new("magellan", PathBuf::from("/usr/bin/magellan"));
77    /// ```
78    pub fn new(name: impl Into<String>, executable: impl Into<PathBuf>) -> Self {
79        Self {
80            name: name.into(),
81            executable: executable.into(),
82            default_args: Vec::new(),
83            description: String::new(),
84        }
85    }
86
87    /// Sets the default arguments for the tool.
88    ///
89    /// # Arguments
90    ///
91    /// * `args` - Vector of default argument strings
92    ///
93    /// # Returns
94    ///
95    /// Self for builder pattern chaining
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use forge_agent::workflow::tools::Tool;
101    ///
102    /// let tool = Tool::new("magellan", "/usr/bin/magellan")
103    ///     .default_args(vec!["--db".to_string(), ".forge/graph.db".to_string()]);
104    /// ```
105    pub fn default_args(mut self, args: Vec<String>) -> Self {
106        self.default_args = args;
107        self
108    }
109
110    /// Sets the description for the tool.
111    ///
112    /// # Arguments
113    ///
114    /// * `description` - Human-readable description
115    ///
116    /// # Returns
117    ///
118    /// Self for builder pattern chaining
119    pub fn description(mut self, description: impl Into<String>) -> Self {
120        self.description = description.into();
121        self
122    }
123}
124
125/// A specific tool invocation request.
126///
127/// ToolInvocation specifies which tool to invoke, additional arguments,
128/// and optional execution context (working directory, environment variables).
129#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
130pub struct ToolInvocation {
131    /// Name of the tool to invoke
132    pub tool_name: String,
133    /// Additional arguments beyond tool defaults
134    pub args: Vec<String>,
135    /// Optional working directory for execution
136    pub working_dir: Option<PathBuf>,
137    /// Optional environment variables
138    #[serde(default)]
139    pub env: HashMap<String, String>,
140}
141
142impl ToolInvocation {
143    /// Creates a new ToolInvocation for the specified tool.
144    ///
145    /// # Arguments
146    ///
147    /// * `tool_name` - Name of the registered tool to invoke
148    ///
149    /// # Example
150    ///
151    /// ```
152    /// use forge_agent::workflow::tools::ToolInvocation;
153    ///
154    /// let invocation = ToolInvocation::new("magellan");
155    /// ```
156    pub fn new(tool_name: impl Into<String>) -> Self {
157        Self {
158            tool_name: tool_name.into(),
159            args: Vec::new(),
160            working_dir: None,
161            env: HashMap::new(),
162        }
163    }
164
165    /// Sets the arguments for this invocation.
166    ///
167    /// # Arguments
168    ///
169    /// * `args` - Vector of argument strings
170    ///
171    /// # Returns
172    ///
173    /// Self for builder pattern chaining
174    pub fn args(mut self, args: Vec<String>) -> Self {
175        self.args = args;
176        self
177    }
178
179    /// Sets the working directory for this invocation.
180    ///
181    /// # Arguments
182    ///
183    /// * `path` - Working directory path
184    ///
185    /// # Returns
186    ///
187    /// Self for builder pattern chaining
188    pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
189        self.working_dir = Some(path.into());
190        self
191    }
192
193    /// Adds an environment variable for this invocation.
194    ///
195    /// # Arguments
196    ///
197    /// * `key` - Environment variable name
198    /// * `value` - Environment variable value
199    ///
200    /// # Returns
201    ///
202    /// Self for builder pattern chaining
203    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
204        self.env.insert(key.into(), value.into());
205        self
206    }
207}
208
209impl fmt::Display for ToolInvocation {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "{}", self.tool_name)?;
212        for arg in &self.args {
213            write!(f, " {}", arg)?;
214        }
215        Ok(())
216    }
217}
218
219/// Result of a tool invocation.
220///
221/// Contains the exit code, captured output, and success status.
222#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
223pub struct ToolResult {
224    /// Process exit code (None if process didn't terminate)
225    pub exit_code: Option<i32>,
226    /// Captured standard output
227    pub stdout: String,
228    /// Captured standard error
229    pub stderr: String,
230    /// True if exit code was 0
231    pub success: bool,
232}
233
234impl ToolResult {
235    /// Creates a new ToolResult from execution output.
236    ///
237    /// # Arguments
238    ///
239    /// * `exit_code` - Process exit code
240    /// * `stdout` - Standard output
241    /// * `stderr` - Standard error
242    pub fn new(exit_code: Option<i32>, stdout: String, stderr: String) -> Self {
243        let success = exit_code.map_or(false, |code| code == 0);
244        Self {
245            exit_code,
246            stdout,
247            stderr,
248            success,
249        }
250    }
251
252    /// Creates a successful ToolResult.
253    ///
254    /// # Arguments
255    ///
256    /// * `stdout` - Standard output
257    pub fn success(stdout: String) -> Self {
258        Self {
259            exit_code: Some(0),
260            stdout,
261            stderr: String::new(),
262            success: true,
263        }
264    }
265
266    /// Creates a failed ToolResult.
267    ///
268    /// # Arguments
269    ///
270    /// * `exit_code` - Exit code
271    /// * `stderr` - Standard error
272    pub fn failure(exit_code: i32, stderr: String) -> Self {
273        Self {
274            exit_code: Some(exit_code),
275            stdout: String::new(),
276            stderr,
277            success: false,
278        }
279    }
280}
281
282/// Errors that can occur during tool operations.
283#[derive(Clone, Debug, PartialEq, thiserror::Error)]
284pub enum ToolError {
285    /// Tool not found in registry
286    #[error("Tool not registered: {0}")]
287    ToolNotFound(String),
288
289    /// Process execution failed
290    #[error("Execution failed: {0}")]
291    ExecutionFailed(String),
292
293    /// Tool execution timed out
294    #[error("Tool timed out: {0}")]
295    Timeout(String),
296
297    /// Process termination error
298    #[error("Failed to terminate process: {0}")]
299    TerminationFailed(String),
300
301    /// Tool already registered
302    #[error("Tool already registered: {0}")]
303    AlreadyRegistered(String),
304}
305
306/// Result of a fallback handler operation.
307///
308/// Fallback handlers can retry with modified invocation, skip with a result,
309/// or fail with the original error.
310#[derive(Clone, Debug)]
311pub enum FallbackResult {
312    /// Retry the tool with the same or modified invocation
313    Retry(ToolInvocation),
314    /// Skip the tool and return a result
315    Skip(TaskResult),
316    /// Fail with the original error
317    Fail(ToolError),
318}
319
320/// Handler for tool execution failures.
321///
322/// FallbackHandler allows workflows to recover from tool failures using
323/// configurable strategies (retry, skip, custom handlers).
324#[async_trait]
325pub trait FallbackHandler: Send + Sync {
326    /// Handles a tool execution error.
327    ///
328    /// # Arguments
329    ///
330    /// * `error` - The error that occurred during tool execution
331    /// * `invocation` - The invocation that caused the error
332    ///
333    /// # Returns
334    ///
335    /// A FallbackResult indicating whether to retry, skip, or fail
336    async fn handle(&self, error: &ToolError, invocation: &ToolInvocation) -> FallbackResult;
337}
338
339/// Retry fallback handler with exponential backoff.
340///
341/// Retries tool execution on transient errors using exponential backoff.
342/// Useful for network timeouts or temporary resource issues.
343///
344/// # Example
345///
346/// ```
347/// use forge_agent::workflow::tools::RetryFallback;
348///
349/// // Retry up to 3 times with 100ms base backoff
350/// let fallback = RetryFallback::new(3, 100);
351/// ```
352#[derive(Clone)]
353pub struct RetryFallback {
354    /// Maximum number of retry attempts
355    max_attempts: u32,
356    /// Base backoff duration in milliseconds
357    backoff_ms: u64,
358}
359
360impl RetryFallback {
361    /// Creates a new RetryFallback.
362    ///
363    /// # Arguments
364    ///
365    /// * `max_attempts` - Maximum number of retry attempts (including initial attempt)
366    /// * `backoff_ms` - Base backoff duration in milliseconds (exponential: backoff_ms * 2^attempt)
367    ///
368    /// # Example
369    ///
370    /// ```
371    /// use forge_agent::workflow::tools::RetryFallback;
372    ///
373    /// let fallback = RetryFallback::new(3, 100);
374    /// ```
375    pub fn new(max_attempts: u32, backoff_ms: u64) -> Self {
376        Self {
377            max_attempts,
378            backoff_ms,
379        }
380    }
381}
382
383#[async_trait]
384impl FallbackHandler for RetryFallback {
385    async fn handle(&self, error: &ToolError, invocation: &ToolInvocation) -> FallbackResult {
386        // Extract current attempt from invocation metadata (if available)
387        // For now, we'll always retry unless it's a ToolNotFound error
388        match error {
389            ToolError::ToolNotFound(_) => {
390                // Don't retry if tool is not found
391                FallbackResult::Fail(error.clone())
392            }
393            ToolError::Timeout(_) | ToolError::ExecutionFailed(_) => {
394                // Retry transient errors
395                FallbackResult::Retry(invocation.clone())
396            }
397            ToolError::AlreadyRegistered(_) | ToolError::TerminationFailed(_) => {
398                // Don't retry registration or termination errors
399                FallbackResult::Fail(error.clone())
400            }
401        }
402    }
403}
404
405impl fmt::Debug for RetryFallback {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        f.debug_struct("RetryFallback")
408            .field("max_attempts", &self.max_attempts)
409            .field("backoff_ms", &self.backoff_ms)
410            .finish()
411    }
412}
413
414/// Skip fallback handler that returns a fixed result.
415///
416/// Always skips tool execution and returns a pre-configured result.
417/// Useful for optional tools or graceful degradation scenarios.
418///
419/// # Example
420///
421/// ```
422/// use forge_agent::workflow::tasks::TaskResult;
423/// use forge_agent::workflow::tools::SkipFallback;
424///
425/// // Skip with success result
426/// let fallback = SkipFallback::success();
427///
428/// // Skip with custom result
429/// let fallback = SkipFallback::new(TaskResult::Skipped);
430/// ```
431#[derive(Clone)]
432pub struct SkipFallback {
433    /// Result to return when skipping
434    result: TaskResult,
435}
436
437impl SkipFallback {
438    /// Creates a new SkipFallback with the given result.
439    ///
440    /// # Arguments
441    ///
442    /// * `result` - The result to return when skipping
443    ///
444    /// # Example
445    ///
446    /// ```
447    /// use forge_agent::workflow::tasks::TaskResult;
448    /// use forge_agent::workflow::tools::SkipFallback;
449    ///
450    /// let fallback = SkipFallback::new(TaskResult::Skipped);
451    /// ```
452    pub fn new(result: TaskResult) -> Self {
453        Self { result }
454    }
455
456    /// Creates a SkipFallback that returns Success.
457    ///
458    /// # Example
459    ///
460    /// ```
461    /// use forge_agent::workflow::tools::SkipFallback;
462    ///
463    /// let fallback = SkipFallback::success();
464    /// ```
465    pub fn success() -> Self {
466        Self {
467            result: TaskResult::Success,
468        }
469    }
470
471    /// Creates a SkipFallback that returns Skipped.
472    ///
473    /// # Example
474    ///
475    /// ```
476    /// use forge_agent::workflow::tools::SkipFallback;
477    ///
478    /// let fallback = SkipFallback::skip();
479    /// ```
480    pub fn skip() -> Self {
481        Self {
482            result: TaskResult::Skipped,
483        }
484    }
485}
486
487#[async_trait]
488impl FallbackHandler for SkipFallback {
489    async fn handle(&self, _error: &ToolError, _invocation: &ToolInvocation) -> FallbackResult {
490        FallbackResult::Skip(self.result.clone())
491    }
492}
493
494impl fmt::Debug for SkipFallback {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        f.debug_struct("SkipFallback")
497            .field("result", &self.result)
498            .finish()
499    }
500}
501
502/// Chain fallback handler that tries multiple handlers in sequence.
503///
504/// Tries each handler in order until one returns a non-Fail result.
505/// If all handlers fail, returns the last Fail result.
506///
507/// # Example
508///
509/// ```
510/// use forge_agent::workflow::tools::{ChainFallback, RetryFallback, SkipFallback};
511///
512/// let fallback = ChainFallback::new()
513///     .add(RetryFallback::new(3, 100))
514///     .add(SkipFallback::skip());
515/// ```
516#[derive(Clone)]
517pub struct ChainFallback {
518    /// Chain of handlers to try in sequence
519    handlers: Vec<Arc<dyn FallbackHandler>>,
520}
521
522impl ChainFallback {
523    /// Creates a new ChainFallback.
524    ///
525    /// # Example
526    ///
527    /// ```
528    /// use forge_agent::workflow::tools::ChainFallback;
529    ///
530    /// let fallback = ChainFallback::new();
531    /// ```
532    pub fn new() -> Self {
533        Self {
534            handlers: Vec::new(),
535        }
536    }
537
538    /// Adds a handler to the chain.
539    ///
540    /// # Arguments
541    ///
542    /// * `handler` - Handler to add to the chain
543    ///
544    /// # Returns
545    ///
546    /// Self for builder pattern chaining
547    ///
548    /// # Example
549    ///
550    /// ```
551    /// use forge_agent::workflow::tools::{ChainFallback, RetryFallback, SkipFallback};
552    ///
553    /// let fallback = ChainFallback::new()
554    ///     .add(RetryFallback::new(3, 100))
555    ///     .add(SkipFallback::skip());
556    /// ```
557    pub fn add(mut self, handler: impl FallbackHandler + 'static) -> Self {
558        self.handlers.push(Arc::new(handler));
559        self
560    }
561}
562
563impl Default for ChainFallback {
564    fn default() -> Self {
565        Self::new()
566    }
567}
568
569#[async_trait]
570impl FallbackHandler for ChainFallback {
571    async fn handle(&self, error: &ToolError, invocation: &ToolInvocation) -> FallbackResult {
572        let mut last_fail = None;
573
574        for handler in &self.handlers {
575            match handler.handle(error, invocation).await {
576                FallbackResult::Fail(err) => {
577                    last_fail = Some(err);
578                }
579                result => return result,
580            }
581        }
582
583        // All handlers failed
584        FallbackResult::Fail(last_fail.unwrap_or_else(|| error.clone()))
585    }
586}
587
588impl fmt::Debug for ChainFallback {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        f.debug_struct("ChainFallback")
591            .field("handlers", &self.handlers.len())
592            .finish()
593    }
594}
595
596/// RAII guard for process lifecycle management.
597///
598/// ProcessGuard automatically terminates the spawned process when dropped.
599/// This ensures proper cleanup even if errors occur during workflow execution.
600///
601/// The guard uses a shared boolean flag to track whether the process has already
602/// been terminated manually, preventing double-termination in Drop.
603#[derive(Clone, Debug)]
604pub struct ProcessGuard {
605    /// Process ID being guarded
606    pid: u32,
607    /// Name of the tool (for logging)
608    tool_name: String,
609    /// Shared flag to track termination status
610    terminated: Arc<AtomicBool>,
611}
612
613impl ProcessGuard {
614    /// Creates a new ProcessGuard for the given process.
615    ///
616    /// # Arguments
617    ///
618    /// * `pid` - Process ID to guard
619    /// * `tool_name` - Name of the tool (for logging)
620    ///
621    /// # Example
622    ///
623    /// ```
624    /// use forge_agent::workflow::tools::ProcessGuard;
625    ///
626    /// let guard = ProcessGuard::new(12345, "magellan");
627    /// ```
628    pub fn new(pid: u32, tool_name: impl Into<String>) -> Self {
629        Self {
630            pid,
631            tool_name: tool_name.into(),
632            terminated: Arc::new(AtomicBool::new(false)),
633        }
634    }
635
636    /// Manually terminates the guarded process.
637    ///
638    /// Sets the terminated flag to prevent double-termination in Drop.
639    ///
640    /// # Returns
641    ///
642    /// - `Ok(())` if termination succeeded
643    /// - `Err(ToolError)` if termination failed
644    ///
645    /// # Example
646    ///
647    /// ```
648    /// use forge_agent::workflow::tools::ProcessGuard;
649    ///
650    /// let guard = ProcessGuard::new(12345, "magellan");
651    /// guard.terminate()?;
652    /// ```
653    pub fn terminate(&self) -> Result<(), ToolError> {
654        // Check if already terminated
655        if self.terminated.load(Ordering::SeqCst) {
656            return Ok(());
657        }
658
659        // Try to kill the process gracefully
660        #[cfg(unix)]
661        {
662            use std::process::Command;
663            let result = Command::new("kill")
664                .arg("-TERM")
665                .arg(self.pid.to_string())
666                .output();
667
668            match result {
669                Ok(output) => {
670                    if output.status.success() {
671                        self.terminated.store(true, Ordering::SeqCst);
672                        Ok(())
673                    } else {
674                        Err(ToolError::TerminationFailed(format!(
675                            "kill command failed for process {}",
676                            self.pid
677                        )))
678                    }
679                }
680                Err(e) => Err(ToolError::TerminationFailed(format!(
681                    "Failed to execute kill command: {}",
682                    e
683                ))),
684            }
685        }
686
687        #[cfg(not(unix))]
688        {
689            Err(ToolError::TerminationFailed(
690                "Process termination not supported on this platform".to_string(),
691            ))
692        }
693    }
694
695    /// Returns the process ID being guarded.
696    ///
697    /// # Example
698    ///
699    /// ```
700    /// use forge_agent::workflow::tools::ProcessGuard;
701    ///
702    /// let guard = ProcessGuard::new(12345, "magellan");
703    /// assert_eq!(guard.pid(), 12345);
704    /// ```
705    pub fn pid(&self) -> u32 {
706        self.pid
707    }
708
709    /// Returns true if the process was terminated.
710    ///
711    /// # Example
712    ///
713    /// ```
714    /// use forge_agent::workflow::tools::ProcessGuard;
715    ///
716    /// let guard = ProcessGuard::new(12345, "magellan");
717    /// assert!(!guard.is_terminated());
718    /// ```
719    pub fn is_terminated(&self) -> bool {
720        self.terminated.load(Ordering::SeqCst)
721    }
722}
723
724impl fmt::Display for ProcessGuard {
725    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
726        write!(
727            f,
728            "ProcessGuard(pid={}, tool={}, terminated={})",
729            self.pid,
730            self.tool_name,
731            self.is_terminated()
732        )
733    }
734}
735
736impl Drop for ProcessGuard {
737    fn drop(&mut self) {
738        // Only terminate if not already terminated
739        if !self.is_terminated() {
740            if let Err(e) = self.terminate() {
741                // Log the error but don't panic in Drop
742                eprintln!("ProcessGuard drop error: {}", e);
743            }
744        }
745    }
746}
747
748impl From<ProcessGuard> for ToolCompensation {
749    fn from(guard: ProcessGuard) -> Self {
750        ToolCompensation::new(
751            format!("Terminate process: {} ({})", guard.tool_name, guard.pid),
752            move |_context| {
753                // Try to terminate the process
754                if guard.terminate().is_ok() {
755                    Ok(TaskResult::Success)
756                } else {
757                    // Termination failed, but don't fail rollback
758                    Ok(TaskResult::Skipped)
759                }
760            },
761        )
762    }
763}
764
765/// Wrapper for tool invocation results with optional process guard.
766///
767/// ToolInvocationResult contains both the result of the tool execution
768/// and an optional RAII guard for long-running processes.
769#[derive(Clone, Debug)]
770pub struct ToolInvocationResult {
771    /// Result of the tool invocation
772    pub result: ToolResult,
773    /// Optional process guard (None for simple commands that complete immediately)
774    pub guard: Option<ProcessGuard>,
775}
776
777impl ToolInvocationResult {
778    /// Creates a new ToolInvocationResult.
779    ///
780    /// # Arguments
781    ///
782    /// * `result` - Tool execution result
783    /// * `guard` - Optional process guard
784    pub fn new(result: ToolResult, guard: Option<ProcessGuard>) -> Self {
785        Self { result, guard }
786    }
787
788    /// Creates a result without a process guard (for completed commands).
789    ///
790    /// # Arguments
791    ///
792    /// * `result` - Tool execution result
793    pub fn completed(result: ToolResult) -> Self {
794        Self {
795            result,
796            guard: None,
797        }
798    }
799}
800
801/// Registry for external tools.
802///
803/// ToolRegistry stores registered tools and provides methods for invoking them
804/// with proper process lifecycle management.
805///
806/// # Example
807///
808/// ```ignore
809/// use forge_agent::workflow::tools::{Tool, ToolRegistry, ToolInvocation};
810///
811/// let mut registry = ToolRegistry::new();
812///
813/// // Register magellan
814/// let magellan = Tool::new("magellan", "/usr/bin/magellan")
815///     .default_args(vec!["--db".to_string(), ".forge/graph.db".to_string()])
816///     .description("Graph-based code indexer");
817/// registry.register(magellan)?;
818///
819/// // Check registration
820/// assert!(registry.is_registered("magellan"));
821///
822/// // List all tools
823/// let tools = registry.list_tools();
824/// assert!(tools.contains(&"magellan"));
825/// ```
826#[derive(Clone)]
827pub struct ToolRegistry {
828    /// Registered tools indexed by name
829    tools: HashMap<String, Tool>,
830}
831
832impl ToolRegistry {
833    /// Creates a new empty ToolRegistry.
834    ///
835    /// # Example
836    ///
837    /// ```
838    /// use forge_agent::workflow::tools::ToolRegistry;
839    ///
840    /// let registry = ToolRegistry::new();
841    /// assert_eq!(registry.len(), 0);
842    /// ```
843    pub fn new() -> Self {
844        Self {
845            tools: HashMap::new(),
846        }
847    }
848
849    /// Registers a tool in the registry.
850    ///
851    /// # Arguments
852    ///
853    /// * `tool` - Tool to register
854    ///
855    /// # Returns
856    ///
857    /// - `Ok(())` if registration succeeded
858    /// - `Err(ToolError::AlreadyRegistered)` if tool with same name exists
859    ///
860    /// # Example
861    ///
862    /// ```
863    /// use forge_agent::workflow::tools::{Tool, ToolRegistry};
864    ///
865    /// let mut registry = ToolRegistry::new();
866    /// let tool = Tool::new("magellan", "/usr/bin/magellan");
867    /// registry.register(tool)?;
868    /// ```
869    pub fn register(&mut self, tool: Tool) -> Result<(), ToolError> {
870        if self.tools.contains_key(&tool.name) {
871            return Err(ToolError::AlreadyRegistered(tool.name.clone()));
872        }
873        self.tools.insert(tool.name.clone(), tool);
874        Ok(())
875    }
876
877    /// Gets a tool by name.
878    ///
879    /// # Arguments
880    ///
881    /// * `name` - Tool name to look up
882    ///
883    /// # Returns
884    ///
885    /// - `Some(&Tool)` if tool exists
886    /// - `None` if tool not found
887    ///
888    /// # Example
889    ///
890    /// ```
891    /// use forge_agent::workflow::tools::{Tool, ToolRegistry};
892    ///
893    /// let mut registry = ToolRegistry::new();
894    /// registry.register(Tool::new("magellan", "/usr/bin/magellan")).unwrap();
895    ///
896    /// let tool = registry.get("magellan");
897    /// assert!(tool.is_some());
898    /// assert_eq!(tool.unwrap().name, "magellan");
899    /// ```
900    pub fn get(&self, name: &str) -> Option<&Tool> {
901        self.tools.get(name)
902    }
903
904    /// Invokes a tool with the given invocation parameters.
905    ///
906    /// # Arguments
907    ///
908    /// * `invocation` - Tool invocation request
909    ///
910    /// # Returns
911    ///
912    /// - `Ok(ToolInvocationResult)` with result and optional process guard
913    /// - `Err(ToolError)` if tool not found or execution fails
914    ///
915    /// # Example
916    ///
917    /// ```ignore
918    /// use forge_agent::workflow::tools::{ToolRegistry, ToolInvocation, Tool};
919    ///
920    /// let mut registry = ToolRegistry::new();
921    /// registry.register(Tool::new("echo", "/bin/echo")).unwrap();
922    ///
923    /// let invocation = ToolInvocation::new("echo")
924    ///     .args(vec!["hello".to_string(), "world".to_string()]);
925    ///
926    /// let result = registry.invoke(&invocation).await?;
927    /// assert!(result.result.success);
928    /// ```
929    pub async fn invoke(
930        &self,
931        invocation: &ToolInvocation,
932    ) -> Result<ToolInvocationResult, ToolError> {
933        // Look up the tool
934        let tool = self
935            .get(&invocation.tool_name)
936            .ok_or_else(|| ToolError::ToolNotFound(invocation.tool_name.clone()))?;
937
938        // Build full command: executable + default_args + invocation.args
939        let mut cmd = tokio::process::Command::new(&tool.executable);
940        cmd.args(&tool.default_args);
941        cmd.args(&invocation.args);
942
943        // Apply working directory if specified
944        if let Some(ref working_dir) = invocation.working_dir {
945            cmd.current_dir(working_dir);
946        }
947
948        // Apply environment variables
949        for (key, value) in &invocation.env {
950            cmd.env(key, value);
951        }
952
953        // Ensure stdout and stderr are captured
954        cmd.stdout(std::process::Stdio::piped());
955        cmd.stderr(std::process::Stdio::piped());
956
957        // Spawn the process
958        let child = cmd
959            .spawn()
960            .map_err(|e| ToolError::ExecutionFailed(format!("Failed to spawn: {}", e)))?;
961
962        // Get the process ID
963        let pid = child.id().ok_or_else(|| {
964            ToolError::ExecutionFailed("Failed to get process ID".to_string())
965        })?;
966
967        // Create a process guard immediately
968        let guard = ProcessGuard::new(pid, &tool.name);
969
970        // Wait for the process to complete (with timeout)
971        // For now, use a default timeout of 30 seconds
972        let timeout_duration = Duration::from_secs(30);
973
974        let output = match tokio::time::timeout(timeout_duration, child.wait_with_output()).await {
975            Ok(Ok(output)) => output,
976            Ok(Err(e)) => {
977                return Err(ToolError::ExecutionFailed(format!(
978                    "Failed to wait for process: {}",
979                    e
980                )))
981            }
982            Err(_) => {
983                // Timeout - terminate the process
984                let _ = guard.terminate();
985                return Err(ToolError::Timeout(format!(
986                    "Tool {} timed out after {:?}",
987                    invocation.tool_name, timeout_duration
988                )));
989            }
990        };
991
992        // Mark the process as terminated (it completed normally)
993        guard.terminated.store(true, std::sync::atomic::Ordering::SeqCst);
994
995        // Parse the result
996        let exit_code = output.status.code();
997        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
998        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
999
1000        let result = ToolResult::new(exit_code, stdout, stderr);
1001
1002        // Always return completed result (process already terminated)
1003        Ok(ToolInvocationResult::completed(result))
1004    }
1005
1006    /// Lists all registered tool names.
1007    ///
1008    /// # Returns
1009    ///
1010    /// Vector of tool names
1011    ///
1012    /// # Example
1013    ///
1014    /// ```
1015    /// use forge_agent::workflow::tools::{Tool, ToolRegistry};
1016    ///
1017    /// let mut registry = ToolRegistry::new();
1018    /// registry.register(Tool::new("magellan", "/usr/bin/magellan")).unwrap();
1019    /// registry.register(Tool::new("cargo", "/usr/bin/cargo")).unwrap();
1020    ///
1021    /// let tools = registry.list_tools();
1022    /// assert_eq!(tools.len(), 2);
1023    /// ```
1024    pub fn list_tools(&self) -> Vec<&str> {
1025        self.tools.keys().map(|k| k.as_str()).collect()
1026    }
1027
1028    /// Checks if a tool is registered.
1029    ///
1030    /// # Arguments
1031    ///
1032    /// * `name` - Tool name to check
1033    ///
1034    /// # Returns
1035    ///
1036    /// - `true` if tool is registered
1037    /// - `false` if tool is not registered
1038    ///
1039    /// # Example
1040    ///
1041    /// ```
1042    /// use forge_agent::workflow::tools::{Tool, ToolRegistry};
1043    ///
1044    /// let mut registry = ToolRegistry::new();
1045    /// registry.register(Tool::new("magellan", "/usr/bin/magellan")).unwrap();
1046    ///
1047    /// assert!(registry.is_registered("magellan"));
1048    /// assert!(!registry.is_registered("cargo"));
1049    /// ```
1050    pub fn is_registered(&self, name: &str) -> bool {
1051        self.tools.contains_key(name)
1052    }
1053
1054    /// Returns the number of registered tools.
1055    ///
1056    /// # Example
1057    ///
1058    /// ```
1059    /// use forge_agent::workflow::tools::{Tool, ToolRegistry};
1060    ///
1061    /// let mut registry = ToolRegistry::new();
1062    /// assert_eq!(registry.len(), 0);
1063    ///
1064    /// registry.register(Tool::new("magellan", "/usr/bin/magellan")).unwrap();
1065    /// assert_eq!(registry.len(), 1);
1066    /// ```
1067    pub fn len(&self) -> usize {
1068        self.tools.len()
1069    }
1070
1071    /// Returns true if the registry is empty.
1072    ///
1073    /// # Example
1074    ///
1075    /// ```
1076    /// use forge_agent::workflow::tools::ToolRegistry;
1077    ///
1078    /// let registry = ToolRegistry::new();
1079    /// assert!(registry.is_empty());
1080    /// ```
1081    pub fn is_empty(&self) -> bool {
1082        self.tools.is_empty()
1083    }
1084
1085    /// Creates a ToolRegistry with standard tools pre-registered.
1086    ///
1087    /// This method attempts to discover and register commonly-used tools:
1088    /// - magellan (graph-based code indexer)
1089    /// - cargo (Rust package manager)
1090    /// - splice (precision code editor)
1091    ///
1092    /// Tools that are not found are logged but don't cause failure (graceful degradation).
1093    ///
1094    /// # Returns
1095    ///
1096    /// A ToolRegistry with discovered tools registered
1097    ///
1098    /// # Example
1099    ///
1100    /// ```
1101    /// use forge_agent::workflow::tools::ToolRegistry;
1102    ///
1103    /// let registry = ToolRegistry::with_standard_tools();
1104    /// // registry may have magellan, cargo, splice if they were found
1105    /// ```
1106    pub fn with_standard_tools() -> Self {
1107        let mut registry = Self::new();
1108
1109        // Helper function to find a tool in PATH
1110        let find_tool = |name: &str| -> Option<PathBuf> {
1111            match Command::new("which").arg(name).output() {
1112                Ok(output) => {
1113                    if output.status.success() {
1114                        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
1115                        Some(PathBuf::from(path))
1116                    } else {
1117                        None
1118                    }
1119                }
1120                Err(_) => None,
1121            }
1122        };
1123
1124        // Register magellan if found
1125        if let Some(path) = find_tool("magellan") {
1126            let tool = Tool::new("magellan", path)
1127                .description("Graph-based code indexer");
1128            if registry.register(tool).is_ok() {
1129                eprintln!("Registered standard tool: magellan");
1130            }
1131        } else {
1132            eprintln!("Warning: magellan not found in PATH");
1133        }
1134
1135        // Register cargo if found
1136        if let Some(path) = find_tool("cargo") {
1137            let tool = Tool::new("cargo", path)
1138                .description("Rust package manager");
1139            if registry.register(tool).is_ok() {
1140                eprintln!("Registered standard tool: cargo");
1141            }
1142        } else {
1143            eprintln!("Warning: cargo not found in PATH");
1144        }
1145
1146        // Register splice if found
1147        if let Some(path) = find_tool("splice") {
1148            let tool = Tool::new("splice", path)
1149                .description("Precision code editor");
1150            if registry.register(tool).is_ok() {
1151                eprintln!("Registered standard tool: splice");
1152            }
1153        } else {
1154            eprintln!("Warning: splice not found in PATH");
1155        }
1156
1157        registry
1158    }
1159}
1160
1161impl Default for ToolRegistry {
1162    fn default() -> Self {
1163        Self::with_standard_tools()
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170
1171    // ============== FallbackHandler Tests ==============
1172
1173    #[tokio::test]
1174    async fn test_retry_fallback_retries_transient_errors() {
1175        let fallback = RetryFallback::new(3, 100);
1176        let invocation = ToolInvocation::new("test_tool").args(vec!["arg1".to_string()]);
1177
1178        // Test timeout error (should retry)
1179        let error = ToolError::Timeout("Test timeout".to_string());
1180        let result = fallback.handle(&error, &invocation).await;
1181
1182        assert!(matches!(result, FallbackResult::Retry(_)));
1183    }
1184
1185    #[tokio::test]
1186    async fn test_retry_fallback_fails_on_tool_not_found() {
1187        let fallback = RetryFallback::new(3, 100);
1188        let invocation = ToolInvocation::new("nonexistent_tool");
1189
1190        // Test tool not found (should fail)
1191        let error = ToolError::ToolNotFound("nonexistent_tool".to_string());
1192        let result = fallback.handle(&error, &invocation).await;
1193
1194        assert!(matches!(result, FallbackResult::Fail(_)));
1195    }
1196
1197    #[tokio::test]
1198    async fn test_skip_fallback_success() {
1199        let fallback = SkipFallback::success();
1200        let invocation = ToolInvocation::new("test_tool");
1201        let error = ToolError::ToolNotFound("test_tool".to_string());
1202
1203        let result = fallback.handle(&error, &invocation).await;
1204
1205        assert!(matches!(result, FallbackResult::Skip(TaskResult::Success)));
1206    }
1207
1208    #[tokio::test]
1209    async fn test_skip_fallback_skip() {
1210        let fallback = SkipFallback::skip();
1211        let invocation = ToolInvocation::new("test_tool");
1212        let error = ToolError::ToolNotFound("test_tool".to_string());
1213
1214        let result = fallback.handle(&error, &invocation).await;
1215
1216        assert!(matches!(result, FallbackResult::Skip(TaskResult::Skipped)));
1217    }
1218
1219    #[tokio::test]
1220    async fn test_skip_fallback_custom_result() {
1221        let fallback = SkipFallback::new(TaskResult::Failed("Custom failure".to_string()));
1222        let invocation = ToolInvocation::new("test_tool");
1223        let error = ToolError::ToolNotFound("test_tool".to_string());
1224
1225        let result = fallback.handle(&error, &invocation).await;
1226
1227        assert!(matches!(result, FallbackResult::Skip(TaskResult::Failed(_))));
1228        if let FallbackResult::Skip(TaskResult::Failed(msg)) = result {
1229            assert_eq!(msg, "Custom failure");
1230        }
1231    }
1232
1233    #[tokio::test]
1234    async fn test_chain_fallback_tries_handlers_in_sequence() {
1235        let invocation = ToolInvocation::new("test_tool");
1236        let error = ToolError::Timeout("Test timeout".to_string());
1237
1238        // Create chain with retry (fails) then skip (succeeds)
1239        let fallback = ChainFallback::new()
1240            .add(SkipFallback::skip())
1241            .add(SkipFallback::success());
1242
1243        let result = fallback.handle(&error, &invocation).await;
1244
1245        // First handler (skip) should be used
1246        assert!(matches!(result, FallbackResult::Skip(TaskResult::Skipped)));
1247    }
1248
1249    #[tokio::test]
1250    async fn test_chain_fallback_all_handlers_fail() {
1251        let invocation = ToolInvocation::new("test_tool");
1252        let error = ToolError::Timeout("Test timeout".to_string());
1253
1254        // Create chain with custom handler that always fails
1255        #[derive(Clone)]
1256        struct AlwaysFail;
1257        #[async_trait]
1258        impl FallbackHandler for AlwaysFail {
1259            async fn handle(&self, error: &ToolError, _invocation: &ToolInvocation) -> FallbackResult {
1260                FallbackResult::Fail(error.clone())
1261            }
1262        }
1263
1264        let fallback = ChainFallback::new()
1265            .add(AlwaysFail)
1266            .add(AlwaysFail);
1267
1268        let result = fallback.handle(&error, &invocation).await;
1269
1270        assert!(matches!(result, FallbackResult::Fail(_)));
1271    }
1272
1273    #[tokio::test]
1274    async fn test_chain_fallback_empty_chain() {
1275        let invocation = ToolInvocation::new("test_tool");
1276        let error = ToolError::Timeout("Test timeout".to_string());
1277
1278        let fallback = ChainFallback::new();
1279        let result = fallback.handle(&error, &invocation).await;
1280
1281        // Empty chain should return the original error
1282        assert!(matches!(result, FallbackResult::Fail(_)));
1283    }
1284
1285    // ============== Tool Tests ==============
1286
1287    #[test]
1288    fn test_tool_creation() {
1289        let tool = Tool::new("magellan", "/usr/bin/magellan");
1290
1291        assert_eq!(tool.name, "magellan");
1292        assert_eq!(tool.executable, PathBuf::from("/usr/bin/magellan"));
1293        assert!(tool.default_args.is_empty());
1294        assert!(tool.description.is_empty());
1295    }
1296
1297    #[test]
1298    fn test_tool_with_default_args() {
1299        let tool = Tool::new("magellan", "/usr/bin/magellan").default_args(vec![
1300            "--db".to_string(),
1301            ".forge/graph.db".to_string(),
1302        ]);
1303
1304        assert_eq!(tool.default_args.len(), 2);
1305        assert_eq!(tool.default_args[0], "--db");
1306        assert_eq!(tool.default_args[1], ".forge/graph.db");
1307    }
1308
1309    #[test]
1310    fn test_tool_with_description() {
1311        let tool = Tool::new("magellan", "/usr/bin/magellan")
1312            .description("Graph-based code indexer");
1313
1314        assert_eq!(tool.description, "Graph-based code indexer");
1315    }
1316
1317    #[test]
1318    fn test_tool_builder_pattern() {
1319        let tool = Tool::new("magellan", "/usr/bin/magellan")
1320            .default_args(vec!["--db".to_string(), ".forge/graph.db".to_string()])
1321            .description("Graph-based code indexer");
1322
1323        assert_eq!(tool.name, "magellan");
1324        assert_eq!(tool.default_args.len(), 2);
1325        assert_eq!(tool.description, "Graph-based code indexer");
1326    }
1327
1328    // ============== ToolInvocation Tests ==============
1329
1330    #[test]
1331    fn test_tool_invocation_creation() {
1332        let invocation = ToolInvocation::new("magellan");
1333
1334        assert_eq!(invocation.tool_name, "magellan");
1335        assert!(invocation.args.is_empty());
1336        assert!(invocation.working_dir.is_none());
1337        assert!(invocation.env.is_empty());
1338    }
1339
1340    #[test]
1341    fn test_tool_invocation_with_args() {
1342        let invocation = ToolInvocation::new("magellan").args(vec![
1343            "find".to_string(),
1344            "--name".to_string(),
1345            "symbol".to_string(),
1346        ]);
1347
1348        assert_eq!(invocation.args.len(), 3);
1349        assert_eq!(invocation.args[0], "find");
1350    }
1351
1352    #[test]
1353    fn test_tool_invocation_with_working_dir() {
1354        let invocation = ToolInvocation::new("magellan")
1355            .working_dir("/home/user/project");
1356
1357        assert_eq!(
1358            invocation.working_dir,
1359            Some(PathBuf::from("/home/user/project"))
1360        );
1361    }
1362
1363    #[test]
1364    fn test_tool_invocation_with_env() {
1365        let invocation = ToolInvocation::new("magellan")
1366            .env("RUST_LOG", "debug");
1367
1368        assert_eq!(invocation.env.len(), 1);
1369        assert_eq!(invocation.env.get("RUST_LOG"), Some(&"debug".to_string()));
1370    }
1371
1372    #[test]
1373    fn test_tool_invocation_display() {
1374        let invocation = ToolInvocation::new("magellan")
1375            .args(vec!["find".to_string(), "--name".to_string()]);
1376
1377        let display = format!("{}", invocation);
1378        assert!(display.contains("magellan"));
1379        assert!(display.contains("find"));
1380    }
1381
1382    // ============== ToolResult Tests ==============
1383
1384    #[test]
1385    fn test_tool_result_success() {
1386        let result = ToolResult::success("output".to_string());
1387
1388        assert_eq!(result.exit_code, Some(0));
1389        assert_eq!(result.stdout, "output");
1390        assert!(result.stderr.is_empty());
1391        assert!(result.success);
1392    }
1393
1394    #[test]
1395    fn test_tool_result_failure() {
1396        let result = ToolResult::failure(1, "error".to_string());
1397
1398        assert_eq!(result.exit_code, Some(1));
1399        assert!(result.stdout.is_empty());
1400        assert_eq!(result.stderr, "error");
1401        assert!(!result.success);
1402    }
1403
1404    #[test]
1405    fn test_tool_result_new() {
1406        let result = ToolResult::new(Some(0), "stdout".to_string(), "stderr".to_string());
1407
1408        assert_eq!(result.exit_code, Some(0));
1409        assert_eq!(result.stdout, "stdout");
1410        assert_eq!(result.stderr, "stderr");
1411        assert!(result.success);
1412    }
1413
1414    #[test]
1415    fn test_tool_result_none_exit_code() {
1416        let result = ToolResult::new(None, "stdout".to_string(), "stderr".to_string());
1417
1418        assert_eq!(result.exit_code, None);
1419        assert!(!result.success);
1420    }
1421
1422    // ============== ToolRegistry Tests ==============
1423
1424    #[test]
1425    fn test_tool_registry_new() {
1426        let registry = ToolRegistry::new();
1427
1428        assert!(registry.is_empty());
1429        assert_eq!(registry.len(), 0);
1430    }
1431
1432    #[test]
1433    fn test_register_tool() {
1434        let mut registry = ToolRegistry::new();
1435        let tool = Tool::new("magellan", "/usr/bin/magellan");
1436
1437        registry.register(tool).unwrap();
1438
1439        assert_eq!(registry.len(), 1);
1440        assert!(registry.is_registered("magellan"));
1441    }
1442
1443    #[test]
1444    fn test_register_duplicate_tool() {
1445        let mut registry = ToolRegistry::new();
1446
1447        registry
1448            .register(Tool::new("magellan", "/usr/bin/magellan"))
1449            .unwrap();
1450
1451        let result = registry.register(Tool::new("magellan", "/usr/bin/magellan"));
1452
1453        assert!(result.is_err());
1454        assert_eq!(result.unwrap_err(), ToolError::AlreadyRegistered("magellan".to_string()));
1455    }
1456
1457    #[test]
1458    fn test_get_tool() {
1459        let mut registry = ToolRegistry::new();
1460
1461        registry
1462            .register(Tool::new("magellan", "/usr/bin/magellan"))
1463            .unwrap();
1464
1465        let tool = registry.get("magellan");
1466        assert!(tool.is_some());
1467        assert_eq!(tool.unwrap().name, "magellan");
1468    }
1469
1470    #[test]
1471    fn test_get_nonexistent_tool() {
1472        let registry = ToolRegistry::new();
1473
1474        let tool = registry.get("magellan");
1475        assert!(tool.is_none());
1476    }
1477
1478    #[test]
1479    fn test_list_tools() {
1480        let mut registry = ToolRegistry::new();
1481
1482        registry
1483            .register(Tool::new("magellan", "/usr/bin/magellan"))
1484            .unwrap();
1485        registry
1486            .register(Tool::new("cargo", "/usr/bin/cargo"))
1487            .unwrap();
1488
1489        let tools = registry.list_tools();
1490        assert_eq!(tools.len(), 2);
1491        assert!(tools.contains(&"magellan"));
1492        assert!(tools.contains(&"cargo"));
1493    }
1494
1495    #[test]
1496    fn test_is_registered() {
1497        let mut registry = ToolRegistry::new();
1498
1499        registry
1500            .register(Tool::new("magellan", "/usr/bin/magellan"))
1501            .unwrap();
1502
1503        assert!(registry.is_registered("magellan"));
1504        assert!(!registry.is_registered("cargo"));
1505    }
1506
1507    #[test]
1508    fn test_tool_registry_default() {
1509        let registry = ToolRegistry::default();
1510
1511        // default() now calls with_standard_tools() which pre-registers tools
1512        // The registry should not be empty (may have magellan, cargo, splice if found)
1513        // Just verify it was created successfully
1514        assert!(registry.len() >= 0);
1515    }
1516
1517    #[tokio::test]
1518    async fn test_invoke_basic_tool() {
1519        let mut registry = ToolRegistry::new();
1520
1521        // Register echo as a test tool
1522        registry
1523            .register(Tool::new("echo", "echo"))
1524            .unwrap();
1525
1526        // Invoke echo
1527        let invocation = ToolInvocation::new("echo").args(vec!["hello".to_string()]);
1528
1529        let result = registry.invoke(&invocation).await.unwrap();
1530
1531        assert!(result.result.success);
1532        // echo adds newline, so check for "hello\n" or just "hello"
1533        let trimmed = result.result.stdout.trim();
1534        assert_eq!(trimmed, "hello", "Expected 'hello', got '{}'", trimmed);
1535    }
1536
1537    #[tokio::test]
1538    async fn test_invoke_with_default_args() {
1539        let mut registry = ToolRegistry::new();
1540
1541        // Register echo with default argument
1542        registry
1543            .register(
1544                Tool::new("echo", "/bin/echo").default_args(vec!["-n".to_string()]),
1545            )
1546            .unwrap();
1547
1548        // Invoke echo
1549        let invocation = ToolInvocation::new("echo").args(vec!["test".to_string()]);
1550
1551        let result = registry.invoke(&invocation).await.unwrap();
1552
1553        assert!(result.result.success);
1554    }
1555
1556    #[tokio::test]
1557    async fn test_invoke_nonexistent_tool() {
1558        let registry = ToolRegistry::new();
1559
1560        let invocation = ToolInvocation::new("nonexistent").args(vec!["arg".to_string()]);
1561
1562        let result = registry.invoke(&invocation).await;
1563
1564        assert!(result.is_err());
1565        assert_eq!(
1566            result.unwrap_err(),
1567            ToolError::ToolNotFound("nonexistent".to_string())
1568        );
1569    }
1570
1571    // ============== ProcessGuard Tests ==============
1572
1573    #[test]
1574    fn test_process_guard_creation() {
1575        let guard = ProcessGuard::new(12345, "test_tool");
1576
1577        assert_eq!(guard.pid(), 12345);
1578        assert!(!guard.is_terminated());
1579    }
1580
1581    #[test]
1582    fn test_process_guard_display() {
1583        let guard = ProcessGuard::new(12345, "test_tool");
1584
1585        let display = format!("{}", guard);
1586        assert!(display.contains("12345"));
1587        assert!(display.contains("test_tool"));
1588    }
1589
1590    #[test]
1591    fn test_process_guard_clone() {
1592        let guard1 = ProcessGuard::new(12345, "test_tool");
1593        let guard2 = guard1.clone();
1594
1595        assert_eq!(guard1.pid(), guard2.pid());
1596        assert_eq!(guard1.tool_name, guard2.tool_name);
1597
1598        // Both guards share the same termination flag
1599        assert_eq!(guard1.is_terminated(), guard2.is_terminated());
1600    }
1601
1602    #[test]
1603    fn test_process_guard_into_tool_compensation() {
1604        let guard = ProcessGuard::new(12345, "test_tool");
1605
1606        let compensation: ToolCompensation = guard.into();
1607
1608        assert!(compensation.description.contains("12345"));
1609        assert!(compensation.description.contains("test_tool"));
1610    }
1611
1612    #[tokio::test]
1613    async fn test_tool_invocation_result_completed() {
1614        let result = ToolResult::success("output".to_string());
1615        let invocation_result = ToolInvocationResult::completed(result);
1616
1617        assert!(invocation_result.guard.is_none());
1618        assert!(invocation_result.result.success);
1619    }
1620
1621    #[tokio::test]
1622    async fn test_tool_invocation_result_with_guard() {
1623        let result = ToolResult::failure(1, "error".to_string());
1624        let guard = ProcessGuard::new(12345, "test_tool");
1625        let invocation_result = ToolInvocationResult::new(result, Some(guard));
1626
1627        assert!(invocation_result.guard.is_some());
1628        assert!(!invocation_result.result.success);
1629    }
1630}