返回 CodeWhale
parity_tools.rs
根目录 / crates / tools / tests / parity_tools.rs
1 use std::sync::{Arc, OnceLock};
2 use std::time::Duration;
3
4 use async_trait::async_trait;
5 use codewhale_protocol::{ToolKind, ToolOutput, ToolPayload};
6 use codewhale_tools::{
7 ToolCall, ToolCallSource, ToolDescriptor, ToolHandler, ToolInvocation, ToolRegistry,
8 };
9 use serde_json::json;
10 use tokio::sync::Notify;
11
12 struct EchoHandler;
13
14 #[async_trait]
15 impl ToolHandler for EchoHandler {
16 fn kind(&self) -> ToolKind {
17 ToolKind::Function
18 }
19
20 fn is_mutating(&self) -> bool {
21 false
22 }
23
24 async fn handle(
25 &self,
26 invocation: ToolInvocation,
27 ) -> std::result::Result<ToolOutput, codewhale_tools::FunctionCallError> {
28 Ok(ToolOutput::Function {
29 body: Some(json!({
30 "tool": invocation.tool_name,
31 "call_id": invocation.call_id
32 })),
33 success: true,
34 })
35 }
36 }
37
38 struct BlockingHandler {
39 started: Arc<Notify>,
40 release: Arc<Notify>,
41 }
42
43 #[async_trait]
44 impl ToolHandler for BlockingHandler {
45 fn kind(&self) -> ToolKind {
46 ToolKind::Function
47 }
48
49 async fn handle(
50 &self,
51 invocation: ToolInvocation,
52 ) -> std::result::Result<ToolOutput, codewhale_tools::FunctionCallError> {
53 self.started.notify_waiters();
54 self.release.notified().await;
55 Ok(ToolOutput::Function {
56 body: Some(json!({
57 "tool": invocation.tool_name,
58 "call_id": invocation.call_id
59 })),
60 success: true,
61 })
62 }
63 }
64
65 struct ReentrantHandler {
66 registry: Arc<OnceLock<Arc<ToolRegistry>>>,
67 }
68
69 #[async_trait]
70 impl ToolHandler for ReentrantHandler {
71 fn kind(&self) -> ToolKind {
72 ToolKind::Function
73 }
74
75 async fn handle(
76 &self,
77 _invocation: ToolInvocation,
78 ) -> std::result::Result<ToolOutput, codewhale_tools::FunctionCallError> {
79 let registry = self.registry.get().expect("registry initialized").clone();
80 registry
81 .dispatch(
82 ToolCall {
83 name: "inner".to_string(),
84 payload: ToolPayload::Function {
85 arguments: "{}".to_string(),
86 },
87 source: ToolCallSource::Direct,
88 raw_tool_call_id: Some("inner-call".to_string()),
89 },
90 true,
91 )
92 .await
93 }
94 }
95
96 #[tokio::test]
97 async fn dispatches_function_tool_with_parallel_flag() {
98 let mut registry = ToolRegistry::default();
99 registry
100 .register(
101 ToolDescriptor {
102 name: "echo".to_string(),
103 input_schema: json!({"type":"object"}),
104 output_schema: json!({"type":"object"}),
105 supports_parallel_tool_calls: true,
106 timeout_ms: Some(1000),
107 },
108 Arc::new(EchoHandler),
109 )
110 .expect("register tool");
111
112 let output = registry
113 .dispatch(
114 ToolCall {
115 name: "echo".to_string(),
116 payload: ToolPayload::Function {
117 arguments: "{\"message\":\"hi\"}".to_string(),
118 },
119 source: ToolCallSource::Direct,
120 raw_tool_call_id: Some("call-1".to_string()),
121 },
122 true,
123 )
124 .await
125 .expect("dispatch tool");
126 match output {
127 ToolOutput::Function { success, .. } => assert!(success),
128 other => panic!("unexpected output: {other:?}"),
129 }
130 }
131
132 #[tokio::test]
133 async fn serial_tool_waits_for_running_parallel_tool() {
134 let started = Arc::new(Notify::new());
135 let release = Arc::new(Notify::new());
136 let mut registry = ToolRegistry::default();
137 registry
138 .register(
139 ToolDescriptor {
140 name: "slow_read".to_string(),
141 input_schema: json!({"type":"object"}),
142 output_schema: json!({"type":"object"}),
143 supports_parallel_tool_calls: true,
144 timeout_ms: Some(1000),
145 },
146 Arc::new(BlockingHandler {
147 started: started.clone(),
148 release: release.clone(),
149 }),
150 )
151 .expect("register slow read");
152 registry
153 .register(
154 ToolDescriptor {
155 name: "serial".to_string(),
156 input_schema: json!({"type":"object"}),
157 output_schema: json!({"type":"object"}),
158 supports_parallel_tool_calls: false,
159 timeout_ms: Some(1000),
160 },
161 Arc::new(EchoHandler),
162 )
163 .expect("register serial");
164
165 let registry = Arc::new(registry);
166 let started_wait = started.notified();
167 let parallel_registry = registry.clone();
168 let parallel = tokio::spawn(async move {
169 parallel_registry
170 .dispatch(
171 ToolCall {
172 name: "slow_read".to_string(),
173 payload: ToolPayload::Function {
174 arguments: "{}".to_string(),
175 },
176 source: ToolCallSource::Direct,
177 raw_tool_call_id: Some("parallel-call".to_string()),
178 },
179 true,
180 )
181 .await
182 });
183 tokio::time::timeout(Duration::from_secs(1), started_wait)
184 .await
185 .expect("parallel tool started");
186
187 let serial_registry = registry.clone();
188 let mut serial = tokio::spawn(async move {
189 serial_registry
190 .dispatch(
191 ToolCall {
192 name: "serial".to_string(),
193 payload: ToolPayload::Function {
194 arguments: "{}".to_string(),
195 },
196 source: ToolCallSource::Direct,
197 raw_tool_call_id: Some("serial-call".to_string()),
198 },
199 true,
200 )
201 .await
202 });
203
204 tokio::select! {
205 _ = &mut serial => panic!("serial tool overlapped a running parallel tool"),
206 () = tokio::time::sleep(Duration::from_millis(50)) => {}
207 }
208
209 release.notify_waiters();
210 serial
211 .await
212 .expect("serial task panicked")
213 .expect("serial ran");
214 parallel
215 .await
216 .expect("parallel task panicked")
217 .expect("parallel ran");
218 }
219
220 #[tokio::test]
221 async fn serial_tool_can_reenter_registry_without_deadlock() {
222 let registry_cell = Arc::new(OnceLock::new());
223 let mut registry = ToolRegistry::default();
224 registry
225 .register(
226 ToolDescriptor {
227 name: "outer".to_string(),
228 input_schema: json!({"type":"object"}),
229 output_schema: json!({"type":"object"}),
230 supports_parallel_tool_calls: false,
231 timeout_ms: Some(1000),
232 },
233 Arc::new(ReentrantHandler {
234 registry: registry_cell.clone(),
235 }),
236 )
237 .expect("register outer");
238 registry
239 .register(
240 ToolDescriptor {
241 name: "inner".to_string(),
242 input_schema: json!({"type":"object"}),
243 output_schema: json!({"type":"object"}),
244 supports_parallel_tool_calls: false,
245 timeout_ms: Some(1000),
246 },
247 Arc::new(EchoHandler),
248 )
249 .expect("register inner");
250
251 let registry = Arc::new(registry);
252 assert!(registry_cell.set(registry.clone()).is_ok());
253
254 let output = tokio::time::timeout(
255 Duration::from_secs(1),
256 registry.dispatch(
257 ToolCall {
258 name: "outer".to_string(),
259 payload: ToolPayload::Function {
260 arguments: "{}".to_string(),
261 },
262 source: ToolCallSource::Direct,
263 raw_tool_call_id: Some("outer-call".to_string()),
264 },
265 true,
266 ),
267 )
268 .await
269 .expect("outer dispatch timed out")
270 .expect("outer dispatch failed");
271
272 match output {
273 ToolOutput::Function { success, .. } => assert!(success),
274 other => panic!("unexpected output: {other:?}"),
275 }
276 }
277
277 lines RUST