zeph_tools/
tool_filter.rs1use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
5use crate::registry::ToolDef;
6
7#[derive(Debug)]
13pub struct ToolFilter<E: ToolExecutor> {
14 inner: E,
15 suppressed: &'static [&'static str],
16}
17
18impl<E: ToolExecutor> ToolFilter<E> {
19 #[must_use]
20 pub fn new(inner: E, suppressed: &'static [&'static str]) -> Self {
21 Self { inner, suppressed }
22 }
23}
24
25impl<E: ToolExecutor> ToolExecutor for ToolFilter<E> {
26 async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
27 self.inner.execute(response).await
28 }
29
30 async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
31 self.inner.execute_confirmed(response).await
32 }
33
34 fn tool_definitions(&self) -> Vec<ToolDef> {
35 self.inner
36 .tool_definitions()
37 .into_iter()
38 .filter(|d| !self.suppressed.contains(&d.id.as_ref()))
39 .collect()
40 }
41
42 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
43 if self.suppressed.contains(&call.tool_id.as_str()) {
44 return Ok(None);
45 }
46 self.inner.execute_tool_call(call).await
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::ToolName;
54
55 #[derive(Debug)]
56 struct StubExecutor;
57 impl ToolExecutor for StubExecutor {
58 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
59 Ok(None)
60 }
61 fn tool_definitions(&self) -> Vec<ToolDef> {
62 vec![
63 ToolDef {
64 id: "read".into(),
65 description: "read a file".into(),
66 schema: schemars::schema_for!(String),
67 invocation: crate::registry::InvocationHint::ToolCall,
68 output_schema: None,
69 },
70 ToolDef {
71 id: "glob".into(),
72 description: "find files".into(),
73 schema: schemars::schema_for!(String),
74 invocation: crate::registry::InvocationHint::ToolCall,
75 output_schema: None,
76 },
77 ToolDef {
78 id: "edit".into(),
79 description: "edit a file".into(),
80 schema: schemars::schema_for!(String),
81 invocation: crate::registry::InvocationHint::ToolCall,
82 output_schema: None,
83 },
84 ]
85 }
86 async fn execute_tool_call(
87 &self,
88 call: &ToolCall,
89 ) -> Result<Option<ToolOutput>, ToolError> {
90 Ok(Some(ToolOutput {
91 tool_name: call.tool_id.clone(),
92 summary: "stub".to_owned(),
93 blocks_executed: 1,
94 filter_stats: None,
95 diff: None,
96 streamed: false,
97 terminal_id: None,
98 locations: None,
99 raw_response: None,
100 claim_source: None,
101 }))
102 }
103 }
104
105 #[test]
106 fn suppressed_tools_hidden_from_definitions() {
107 let filter = ToolFilter::new(StubExecutor, &["read", "glob"]);
108 let defs = filter.tool_definitions();
109 let ids: Vec<&str> = defs.iter().map(|d| d.id.as_ref()).collect();
110 assert!(!ids.contains(&"read"));
111 assert!(!ids.contains(&"glob"));
112 assert!(ids.contains(&"edit"));
113 }
114
115 #[tokio::test]
116 async fn suppressed_tool_call_returns_none() {
117 let filter = ToolFilter::new(StubExecutor, &["read", "glob"]);
118 let call = ToolCall {
119 tool_id: ToolName::new("read"),
120 params: serde_json::Map::new(),
121 caller_id: None,
122 };
123 let result = filter.execute_tool_call(&call).await.unwrap();
124 assert!(result.is_none());
125 }
126
127 #[tokio::test]
128 async fn allowed_tool_call_passes_through() {
129 let filter = ToolFilter::new(StubExecutor, &["read", "glob"]);
130 let call = ToolCall {
131 tool_id: ToolName::new("edit"),
132 params: serde_json::Map::new(),
133 caller_id: None,
134 };
135 let result = filter.execute_tool_call(&call).await.unwrap();
136 assert!(result.is_some());
137 }
138}