pe-tools 0.1.0

Tool registry and MCP adapter for Potential Expectations — schema-driven tool nodes and protocol bridge
Documentation
//! Tool call interception — hooks that run before tool execution.
//!
//! [`ToolCallInterceptor`] lets users inspect, modify, or log tool calls
//! before the tool's `execute()` runs. Common uses:
//! - Input sanitization
//! - Rate limiting
//! - Audit logging
//! - Argument validation or transformation

use pe_core::message::ToolCall;

/// Hook that runs before each tool call inside [`super::ToolNode`].
///
/// Can inspect and modify the [`ToolCall`] before execution.
/// Return the (possibly modified) `ToolCall` to proceed.
///
/// # Example
///
/// ```ignore
/// struct LoggingInterceptor;
///
/// impl ToolCallInterceptor for LoggingInterceptor {
///     fn intercept(&self, call: ToolCall) -> ToolCall {
///         tracing::info!("Tool call: {} with {:?}", call.name, call.args);
///         call
///     }
/// }
/// ```
pub trait ToolCallInterceptor: Send + Sync {
    /// Called before tool execution. Return the (possibly modified) ToolCall.
    fn intercept(&self, call: ToolCall) -> ToolCall;
}

/// No-op interceptor — passes tool calls through unchanged.
pub struct PassthroughInterceptor;

impl ToolCallInterceptor for PassthroughInterceptor {
    fn intercept(&self, call: ToolCall) -> ToolCall {
        call
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn passthrough_interceptor_returns_unchanged() {
        let interceptor = PassthroughInterceptor;
        let call = ToolCall {
            id: "tc_1".into(),
            name: "search".into(),
            args: serde_json::json!({"query": "test"}),
        };

        let result = interceptor.intercept(call.clone());
        assert_eq!(result.id, "tc_1");
        assert_eq!(result.name, "search");
        assert_eq!(result.args, serde_json::json!({"query": "test"}));
    }

    struct PrefixInterceptor {
        prefix: String,
    }

    impl ToolCallInterceptor for PrefixInterceptor {
        fn intercept(&self, mut call: ToolCall) -> ToolCall {
            call.name = format!("{}_{}", self.prefix, call.name);
            call
        }
    }

    #[test]
    fn custom_interceptor_modifies_call() {
        let interceptor = PrefixInterceptor {
            prefix: "safe".into(),
        };
        let call = ToolCall {
            id: "tc_2".into(),
            name: "delete".into(),
            args: serde_json::json!({}),
        };

        let result = interceptor.intercept(call);
        assert_eq!(result.name, "safe_delete");
        assert_eq!(result.id, "tc_2");
    }
}