1mod default_tools;
2
3pub use default_tools::{register_default_tools, register_skill_tools, register_spawn_agent};
4
5use super::sandbox::Sandbox;
6use crate::messages::MessageChannel;
7use crate::messages::MessageKind;
8use crate::shared::{ToolDefinition, UserInteraction};
9use crate::tasks::{TaskBuffer, TaskManager};
10use schemars::{JsonSchema, schema_for};
11use serde::de::DeserializeOwned;
12use serde_json::{Map, Value, json};
13use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc, time::Duration};
14use tokio_util::sync::CancellationToken;
15
16#[derive(Clone, Debug)]
18pub struct AsyncToolConfig {
19 pub yield_after: Duration,
20}
21
22impl AsyncToolConfig {
23 pub fn new(yield_after: Duration) -> Self {
24 Self { yield_after }
25 }
26}
27
28#[derive(Clone, Debug)]
31pub struct ToolSpec {
32 pub name: String,
33 pub description: String,
34 pub read_only: bool,
35 pub asynchronous: Option<AsyncToolConfig>,
36}
37
38impl ToolSpec {
39 pub fn new(name: impl Into<String>, description: impl Into<String>, read_only: bool) -> Self {
40 Self {
41 name: name.into(),
42 description: description.into(),
43 read_only,
44 asynchronous: None,
45 }
46 }
47
48 pub fn asynchronous(mut self, config: AsyncToolConfig) -> Self {
49 self.asynchronous = Some(config);
50 self
51 }
52}
53
54#[derive(Clone, Default)]
57pub struct ToolProgress {
58 messages: MessageChannel,
59 session_id: String,
60 buffer: Option<TaskBuffer>,
61}
62
63impl ToolProgress {
64 pub(crate) fn new(
65 messages: MessageChannel,
66 session_id: String,
67 buffer: Option<TaskBuffer>,
68 ) -> Self {
69 Self {
70 messages,
71 session_id,
72 buffer,
73 }
74 }
75
76 pub fn emit_text(&self, chunk: impl AsRef<str>) {
77 let chunk = chunk.as_ref();
78 if let Some(buffer) = &self.buffer {
79 buffer.append(chunk);
80 }
81 self.messages.send(
82 &self.session_id,
83 MessageKind::ToolOutput {
84 chunk: chunk.to_string(),
85 },
86 );
87 }
88}
89
90#[derive(Clone, Default)]
96pub struct ToolCtx {
97 messages: MessageChannel,
98 session_id: String,
99 tasks: TaskManager,
100 cancellation: CancellationToken,
104 pub progress: ToolProgress,
106}
107
108impl ToolCtx {
109 pub(crate) fn new(
110 messages: MessageChannel,
111 session_id: String,
112 tasks: TaskManager,
113 cancellation: CancellationToken,
114 ) -> Self {
115 Self {
116 messages,
117 session_id,
118 tasks,
119 cancellation,
120 progress: ToolProgress::default(),
121 }
122 }
123
124 pub fn session_id(&self) -> &str {
126 &self.session_id
127 }
128
129 pub fn cancellation_token(&self) -> CancellationToken {
132 self.cancellation.clone()
133 }
134
135 pub(crate) fn tasks(&self) -> &TaskManager {
136 &self.tasks
137 }
138}
139
140type BoxFuture = Pin<Box<dyn Future<Output = Result<ToolOutcome, String>> + Send>>;
141type BoxedCall = Arc<dyn Fn(Arc<dyn Sandbox>, Value, ToolCtx) -> BoxFuture + Send + Sync>;
142
143#[derive(Debug, Clone)]
144pub enum ToolOutcome {
145 Completed(Value),
146 NeedsUserInteraction(UserInteraction),
147}
148
149impl From<Value> for ToolOutcome {
150 fn from(value: Value) -> Self {
151 Self::Completed(value)
152 }
153}
154
155#[derive(Clone)]
156pub struct ToolEntry {
157 pub name: String,
158 pub description: String,
159 pub schema: Value,
160 pub read_only: bool,
163 pub asynchronous: Option<AsyncToolConfig>,
164 call: BoxedCall,
165}
166
167#[derive(Clone)]
168pub struct ToolRegistry {
169 tools: HashMap<String, ToolEntry>,
170}
171
172impl ToolRegistry {
173 pub fn new() -> Self {
174 Self {
175 tools: HashMap::new(),
176 }
177 }
178
179 pub fn register<A, F, Fut>(
183 &mut self,
184 name: &str,
185 description: &str,
186 read_only: bool,
187 handler: F,
188 ) where
189 A: DeserializeOwned + JsonSchema + Send + 'static,
190 F: Fn(Arc<dyn Sandbox>, A) -> Fut + Send + Sync + 'static,
191 Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
192 {
193 self.register_tool(ToolSpec::new(name, description, read_only), handler);
194 }
195
196 pub fn register_tool<A, F, Fut>(&mut self, spec: ToolSpec, handler: F)
197 where
198 A: DeserializeOwned + JsonSchema + Send + 'static,
199 F: Fn(Arc<dyn Sandbox>, A) -> Fut + Send + Sync + 'static,
200 Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
201 {
202 let schema = serde_json::to_value(schema_for!(A)).unwrap();
203 let handler = Arc::new(handler);
204 let call: BoxedCall =
205 Arc::new(move |sandbox: Arc<dyn Sandbox>, v: Value, _ctx: ToolCtx| {
206 let handler = handler.clone();
207 Box::pin(async move {
208 let args: A =
209 serde_json::from_value(v).map_err(|e| format!("参数不合法: {e}"))?;
210 handler(sandbox, args).await
211 })
212 });
213 self.insert(spec, schema, call);
214 }
215
216 fn insert(&mut self, spec: ToolSpec, schema: Value, call: BoxedCall) {
217 self.tools.insert(
218 spec.name.clone(),
219 ToolEntry {
220 name: spec.name,
221 description: spec.description,
222 schema,
223 read_only: spec.read_only,
224 asynchronous: spec.asynchronous,
225 call,
226 },
227 );
228 }
229
230 pub fn register_with_ctx<A, F, Fut>(
234 &mut self,
235 name: &str,
236 description: &str,
237 read_only: bool,
238 handler: F,
239 ) where
240 A: DeserializeOwned + JsonSchema + Send + 'static,
241 F: Fn(Arc<dyn Sandbox>, A, ToolCtx) -> Fut + Send + Sync + 'static,
242 Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
243 {
244 self.register_with_ctx_tool(ToolSpec::new(name, description, read_only), handler);
245 }
246
247 pub fn register_with_ctx_tool<A, F, Fut>(&mut self, spec: ToolSpec, handler: F)
248 where
249 A: DeserializeOwned + JsonSchema + Send + 'static,
250 F: Fn(Arc<dyn Sandbox>, A, ToolCtx) -> Fut + Send + Sync + 'static,
251 Fut: Future<Output = Result<ToolOutcome, String>> + Send + 'static,
252 {
253 let schema = serde_json::to_value(schema_for!(A)).unwrap();
254 let handler = Arc::new(handler);
255 let call: BoxedCall = Arc::new(move |sandbox: Arc<dyn Sandbox>, v: Value, ctx: ToolCtx| {
256 let handler = handler.clone();
257 Box::pin(async move {
258 let args: A = serde_json::from_value(v).map_err(|e| format!("参数不合法: {e}"))?;
259 handler(sandbox, args, ctx).await
260 })
261 });
262 self.insert(spec, schema, call);
263 }
264
265 pub fn is_read_only(&self, name: &str) -> bool {
267 self.tools.get(name).is_some_and(|t| t.read_only)
268 }
269
270 pub fn contains(&self, name: &str) -> bool {
272 self.tools.contains_key(name)
273 }
274
275 pub fn names(&self) -> Vec<String> {
277 self.tools.keys().cloned().collect()
278 }
279
280 pub fn subset<S: AsRef<str>>(&self, names: &[S]) -> ToolRegistry {
284 let mut tools = HashMap::new();
285 for name in names {
286 let name = name.as_ref();
287 if let Some(entry) = self.tools.get(name) {
288 tools.insert(name.to_string(), entry.clone());
289 }
290 }
291 ToolRegistry { tools }
292 }
293
294 pub async fn call(
296 &self,
297 name: &str,
298 args: Value,
299 sandbox: Arc<dyn Sandbox>,
300 ctx: ToolCtx,
301 ) -> Result<ToolOutcome, String> {
302 let tool = self
303 .tools
304 .get(name)
305 .ok_or_else(|| format!("工具不存在: {name}"))?;
306 let mut ctx = ctx;
307 ctx.progress = ToolProgress::new(ctx.messages.clone(), ctx.session_id.clone(), None);
308
309 let Some(config) = tool.asynchronous.clone() else {
310 return (tool.call)(sandbox, args, ctx).await;
311 };
312
313 let buffer = TaskBuffer::new();
314 ctx.progress = ToolProgress::new(
315 ctx.messages.clone(),
316 ctx.session_id.clone(),
317 Some(buffer.clone()),
318 );
319 call_asynchronous_tool(
320 tool.name.clone(),
321 tool.call.clone(),
322 sandbox,
323 args,
324 ctx,
325 buffer,
326 config,
327 )
328 .await
329 }
330
331 pub fn definitions(&self) -> Vec<ToolDefinition> {
333 self.tools
334 .values()
335 .map(|t| ToolDefinition {
336 name: t.name.clone(),
337 desc: t.description.clone(),
338 arguments: t.schema.clone(),
339 })
340 .collect()
341 }
342}
343
344async fn call_asynchronous_tool(
345 tool_name: String,
346 call: BoxedCall,
347 sandbox: Arc<dyn Sandbox>,
348 args: Value,
349 ctx: ToolCtx,
350 buffer: TaskBuffer,
351 config: AsyncToolConfig,
352) -> Result<ToolOutcome, String> {
353 let tasks = ctx.tasks.clone();
354 let mut handle = tokio::spawn({
355 let buffer = buffer.clone();
356 async move {
357 let result = match (call)(sandbox, args, ctx).await {
358 Ok(ToolOutcome::Completed(value)) => Ok(value),
359 Ok(ToolOutcome::NeedsUserInteraction(_)) => Err(
360 "asynchronous tools cannot request user interaction after yielding".to_string(),
361 ),
362 Err(error) => Err(error),
363 };
364 buffer.finish(result);
365 }
366 });
367
368 tokio::select! {
369 join_result = &mut handle => {
370 if let Err(err) = join_result {
371 return Err(format!("tool task failed: {err}"));
372 }
373 let output = buffer.drain_new();
374 let (_finished, result, error) = buffer.status_parts();
375 if let Some(error) = error {
376 return Err(error);
377 }
378 Ok(merge_tool_result(
379 "completed",
380 output,
381 result.unwrap_or_else(|| json!({})),
382 ).into())
383 }
384 _ = tokio::time::sleep(config.yield_after) => {
385 let output = buffer.drain_new();
386 let id = tasks.register(tool_name.clone(), handle, buffer);
387 Ok(json!({
388 "id": id,
389 "tool_name": tool_name,
390 "status": "running",
391 "output": output,
392 "note": "工具仍在后台运行;用 check 工具按 id 回来查看新增输出与最终状态,用 kill 终止。",
393 }).into())
394 }
395 }
396}
397
398pub(crate) fn merge_tool_result(status: &str, output: String, result: Value) -> Value {
399 match result {
400 Value::Object(mut object) => {
401 object
402 .entry("status".to_string())
403 .or_insert_with(|| Value::String(status.to_string()));
404 object
405 .entry("output".to_string())
406 .or_insert_with(|| Value::String(output));
407 Value::Object(object)
408 }
409 other => {
410 let mut object = Map::new();
411 object.insert("status".to_string(), Value::String(status.to_string()));
412 object.insert("output".to_string(), Value::String(output));
413 object.insert("result".to_string(), other);
414 Value::Object(object)
415 }
416 }
417}
418
419impl Default for ToolRegistry {
420 fn default() -> Self {
421 Self::new()
422 }
423}
424
425#[cfg(test)]
426mod subset_tests {
427 use super::*;
428
429 fn full() -> ToolRegistry {
430 let mut r = ToolRegistry::new();
431 register_default_tools(&mut r);
432 r
433 }
434
435 #[test]
436 fn subset_keeps_only_named_tools_and_preserves_callable() {
437 let r = full();
438 let sub = r.subset(&["read_file", "bash"]);
439 assert!(sub.contains("read_file"));
440 assert!(sub.contains("bash"));
441 assert!(!sub.contains("write_file"));
442 let mut names = sub.names();
443 names.sort();
444 assert_eq!(names, vec!["bash".to_string(), "read_file".to_string()]);
445 assert!(sub.definitions().iter().any(|d| d.name == "bash"));
447 }
448
449 #[test]
450 fn subset_ignores_unknown_names() {
451 let sub = full().subset(&["read_file", "does_not_exist"]);
452 assert!(sub.contains("read_file"));
453 assert!(!sub.contains("does_not_exist"));
454 assert_eq!(sub.names().len(), 1);
455 }
456}