Skip to main content

pe_tools/
interceptor.rs

1//! Tool call interception — hooks that run before tool execution.
2//!
3//! [`ToolCallInterceptor`] lets users inspect, modify, or log tool calls
4//! before the tool's `execute()` runs. Common uses:
5//! - Input sanitization
6//! - Rate limiting
7//! - Audit logging
8//! - Argument validation or transformation
9
10use pe_core::message::ToolCall;
11
12/// Hook that runs before each tool call inside [`super::ToolNode`].
13///
14/// Can inspect and modify the [`ToolCall`] before execution.
15/// Return the (possibly modified) `ToolCall` to proceed.
16///
17/// # Example
18///
19/// ```ignore
20/// struct LoggingInterceptor;
21///
22/// impl ToolCallInterceptor for LoggingInterceptor {
23///     fn intercept(&self, call: ToolCall) -> ToolCall {
24///         tracing::info!("Tool call: {} with {:?}", call.name, call.args);
25///         call
26///     }
27/// }
28/// ```
29pub trait ToolCallInterceptor: Send + Sync {
30    /// Called before tool execution. Return the (possibly modified) ToolCall.
31    fn intercept(&self, call: ToolCall) -> ToolCall;
32}
33
34/// No-op interceptor — passes tool calls through unchanged.
35pub struct PassthroughInterceptor;
36
37impl ToolCallInterceptor for PassthroughInterceptor {
38    fn intercept(&self, call: ToolCall) -> ToolCall {
39        call
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn passthrough_interceptor_returns_unchanged() {
49        let interceptor = PassthroughInterceptor;
50        let call = ToolCall {
51            id: "tc_1".into(),
52            name: "search".into(),
53            args: serde_json::json!({"query": "test"}),
54        };
55
56        let result = interceptor.intercept(call.clone());
57        assert_eq!(result.id, "tc_1");
58        assert_eq!(result.name, "search");
59        assert_eq!(result.args, serde_json::json!({"query": "test"}));
60    }
61
62    struct PrefixInterceptor {
63        prefix: String,
64    }
65
66    impl ToolCallInterceptor for PrefixInterceptor {
67        fn intercept(&self, mut call: ToolCall) -> ToolCall {
68            call.name = format!("{}_{}", self.prefix, call.name);
69            call
70        }
71    }
72
73    #[test]
74    fn custom_interceptor_modifies_call() {
75        let interceptor = PrefixInterceptor {
76            prefix: "safe".into(),
77        };
78        let call = ToolCall {
79            id: "tc_2".into(),
80            name: "delete".into(),
81            args: serde_json::json!({}),
82        };
83
84        let result = interceptor.intercept(call);
85        assert_eq!(result.name, "safe_delete");
86        assert_eq!(result.id, "tc_2");
87    }
88}