Skip to main content

tiny_agent/tools/
mod.rs

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/// 工具到达让步窗口后转为后台任务的执行策略。
17#[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/// 工具注册元信息。旧的 `register(...)` 仍可用;需要配置异步策略时使用
29/// `ToolSpec::new(...).asynchronous(...)` 再交给 `register_tool(...)`。
30#[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/// 工具执行期间可发出的进度通道。异步工具的 progress 会被 `check(id)` 按增量读回;
55/// 同时也会推给 `MessageChannel` 供前端实时展示。
56#[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/// 调用工具时注入的运行时上下文。
91///
92/// 大多数工具用不到它(用 [`ToolRegistry::register`] 注册的 handler 拿不到);
93/// 需要上报工具进度时用 [`ToolRegistry::register_with_ctx`] 注册,并通过
94/// [`ToolCtx::progress`] 发出增量输出。这样实时 UI 与 `check(id)` 看到的内容保持一致。
95#[derive(Clone, Default)]
96pub struct ToolCtx {
97    messages: MessageChannel,
98    session_id: String,
99    tasks: TaskManager,
100    /// 当前这轮 `run` 的取消令牌。工具若自己派生子 agent,应把它(或其 `child_token()`)
101    /// 透传下去,这样用户取消父任务时子 agent 也能**干净地**一并取消(走 Interrupted/存档),
102    /// 而不是被父 future 的 drop 硬掐断。
103    cancellation: CancellationToken,
104    /// 当前工具调用的进度通道。
105    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    /// 当前会话 id。工具需要打标签或组织输出时可读取;进度输出请走 [`ToolCtx::progress`]。
125    pub fn session_id(&self) -> &str {
126        &self.session_id
127    }
128
129    /// 当前这轮 `run` 的取消令牌。派生子 agent 的工具应用它(或 `child_token()`)替代
130    /// `CancellationToken::new()`,以便取消能向下传播。
131    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    /// 是否只读、无副作用。只读工具之间可以并发执行(见 `core::tool_execution` 的分段调度);
161    /// 任何会改动 sandbox 状态的工具都必须保持 `false`,以免与并发读发生 read-write race。
162    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    /// 注册一个工具。`read_only` 标记它是否无副作用 —— 只读工具之间可被并发调度
180    /// (见 `core::tool_execution` 的分段执行),任何会改动 sandbox 的工具都必须传 `false`。
181    /// handler 统一返回 `ToolOutcome`(普通完成用 `Value.into()`,需要用户交互用 `NeedsUserInteraction`)。
182    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    /// 与 [`register`](Self::register) 相同,但 handler 额外收到一个 [`ToolCtx`]。
231    /// 工具需要上报运行进度时应使用 `ctx.progress`,这样 UI 实时输出与 `check(id)`
232    /// 增量读取会保持同源。
233    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    /// 工具是否声明为只读。未知工具按非只读处理(最保守,强制串行)。
266    pub fn is_read_only(&self, name: &str) -> bool {
267        self.tools.get(name).is_some_and(|t| t.read_only)
268    }
269
270    /// 是否注册了某个工具。
271    pub fn contains(&self, name: &str) -> bool {
272        self.tools.contains_key(name)
273    }
274
275    /// 已注册的全部工具名(无序)。
276    pub fn names(&self) -> Vec<String> {
277        self.tools.keys().cloned().collect()
278    }
279
280    /// 派生一个只含给定名字的子集 registry,完整保留各工具的 callable/schema/异步策略。
281    /// 未知名字静默忽略——调用方若需校验可先用 [`contains`](Self::contains)。
282    /// 用于按能力边界裁剪某个(子)agent 可见的工具集。
283    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    /// 按名字调度执行,执行时注入该会话绑定的 sandbox 与 [`ToolCtx`]。
295    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    /// 将工具 struct 转成 `ToolDefinition` 列表
332    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        // bash 的异步策略要随子集一起保留(callable/schema/async 都来自原 entry 的 clone)。
446        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}