Skip to main content

agent_sdk/
builtin_tools.rs

1//! Bundled registration for the full set of built-in SDK tools.
2//!
3//! The SDK ships many ready-to-use tools (primitives, todo tracking,
4//! web search and fetch, user-interaction).  Hosts that want to run
5//! these through the v2 durable tool runtime need them registered in a
6//! shared [`ToolRegistry<Ctx>`] so `agent-service-host`'s
7//! `RegistryToolExecutor` can dispatch them by name.
8//!
9//! This module centralises the registration plumbing so hosts don't
10//! have to know the constructor shape of every tool family.  Each
11//! family is exposed as its own focused helper and a top-level
12//! [`register_builtin_tools`] wires the common bundle.
13//!
14//! # Example
15//!
16//! ```no_run
17//! use std::sync::Arc;
18//! use tokio::sync::RwLock;
19//! use agent_sdk::builtin_tools::{register_builtin_tools, BuiltinToolsConfig};
20//! use agent_sdk::todo::TodoState;
21//! use agent_sdk::{AgentCapabilities, InMemoryFileSystem, ToolRegistry};
22//!
23//! let fs = Arc::new(InMemoryFileSystem::new("/workspace"));
24//! let mut registry = ToolRegistry::<()>::new();
25//! register_builtin_tools(&mut registry, BuiltinToolsConfig {
26//!     environment: fs,
27//!     capabilities: AgentCapabilities::full_access(),
28//!     todo_state: Some(Arc::new(RwLock::new(TodoState::new()))),
29//!     link_fetch: true,
30//! });
31//! ```
32
33use std::sync::Arc;
34use tokio::sync::RwLock;
35
36use agent_sdk_tools::tools::ToolRegistry;
37
38use crate::primitive_tools::{BashTool, EditTool, GlobTool, GrepTool, ReadTool, WriteTool};
39use crate::todo::{TodoReadTool, TodoState, TodoWriteTool};
40#[cfg(feature = "web")]
41use crate::web::{LinkFetchTool, SearchProvider, WebSearchTool};
42use crate::{AgentCapabilities, Environment};
43
44/// Configuration for [`register_builtin_tools`].
45///
46/// Every field except `environment` and `capabilities` is optional
47/// because the corresponding tool family has side effects or
48/// dependencies that must be provisioned by the host.
49///
50/// The tool families with more complex constructors — in particular
51/// [`AskUserQuestionTool`](crate::user_interaction::AskUserQuestionTool)
52/// (requires a pair of mpsc channels) and
53/// [`WebSearchTool`] (requires a typed
54/// [`SearchProvider`]) — are **not** wired through this config to
55/// avoid over-coupling it to transport or provider choices.  Register
56/// them with [`register_web_search`] or directly against the registry.
57pub struct BuiltinToolsConfig<E: Environment + 'static> {
58    /// Filesystem environment used by the primitive tools.
59    pub environment: Arc<E>,
60    /// Capability policy enforced by the primitive tools.
61    pub capabilities: AgentCapabilities,
62    /// Shared todo state.  When `Some`, registers `TodoRead` / `TodoWrite`.
63    pub todo_state: Option<Arc<RwLock<TodoState>>>,
64    /// When `true`, registers [`LinkFetchTool`] with default settings.
65    ///
66    /// Only present when the `web` feature is enabled.
67    #[cfg(feature = "web")]
68    pub link_fetch: bool,
69}
70
71/// Register the full bundle of built-in tools described by `config`.
72///
73/// This is a convenience wrapper that delegates to the per-family
74/// helpers ([`register_primitives`], [`register_todo_tools`],
75/// [`register_link_fetch`]).  Hosts that need finer control can call
76/// the helpers directly.
77pub fn register_builtin_tools<Ctx, E>(
78    registry: &mut ToolRegistry<Ctx>,
79    config: BuiltinToolsConfig<E>,
80) where
81    Ctx: Send + Sync + 'static,
82    E: Environment + 'static,
83{
84    register_primitives(registry, config.environment, config.capabilities);
85
86    if let Some(state) = config.todo_state {
87        register_todo_tools(registry, state);
88    }
89
90    #[cfg(feature = "web")]
91    if config.link_fetch {
92        register_link_fetch(registry);
93    }
94}
95
96/// Register the six filesystem / shell primitives
97/// (`Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`).
98pub fn register_primitives<Ctx, E>(
99    registry: &mut ToolRegistry<Ctx>,
100    environment: Arc<E>,
101    capabilities: AgentCapabilities,
102) where
103    Ctx: Send + Sync + 'static,
104    E: Environment + 'static,
105{
106    registry.register(ReadTool::new(
107        Arc::clone(&environment),
108        capabilities.clone(),
109    ));
110    registry.register(WriteTool::new(
111        Arc::clone(&environment),
112        capabilities.clone(),
113    ));
114    registry.register(EditTool::new(
115        Arc::clone(&environment),
116        capabilities.clone(),
117    ));
118    registry.register(BashTool::new(
119        Arc::clone(&environment),
120        capabilities.clone(),
121    ));
122    registry.register(GlobTool::new(
123        Arc::clone(&environment),
124        capabilities.clone(),
125    ));
126    registry.register(GrepTool::new(environment, capabilities));
127}
128
129/// Register [`TodoReadTool`] and [`TodoWriteTool`] sharing `state`.
130pub fn register_todo_tools<Ctx>(registry: &mut ToolRegistry<Ctx>, state: Arc<RwLock<TodoState>>)
131where
132    Ctx: Send + Sync + 'static,
133{
134    registry.register(TodoReadTool::new(Arc::clone(&state)));
135    registry.register(TodoWriteTool::new(state));
136}
137
138/// Register [`LinkFetchTool`] with default HTTP client and
139/// SSRF-protecting [`UrlValidator`](crate::web::security::UrlValidator).
140#[cfg(feature = "web")]
141pub fn register_link_fetch<Ctx>(registry: &mut ToolRegistry<Ctx>)
142where
143    Ctx: Send + Sync + 'static,
144{
145    registry.register(LinkFetchTool::new());
146}
147
148/// Register [`WebSearchTool`] backed by `provider`.
149///
150/// Kept separate from [`BuiltinToolsConfig`] so hosts can pick a
151/// concrete [`SearchProvider`] (Brave, custom, etc.) without that
152/// choice leaking into every call site of [`register_builtin_tools`].
153#[cfg(feature = "web")]
154pub fn register_web_search<Ctx, P>(registry: &mut ToolRegistry<Ctx>, provider: P)
155where
156    Ctx: Send + Sync + 'static,
157    P: SearchProvider + 'static,
158{
159    registry.register(WebSearchTool::new(provider));
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::InMemoryFileSystem;
166
167    #[cfg(feature = "web")]
168    #[test]
169    fn register_builtin_tools_wires_primitives_and_todo_and_fetch() {
170        let fs = Arc::new(InMemoryFileSystem::new("/workspace"));
171        let todo = Arc::new(RwLock::new(TodoState::new()));
172        let mut registry = ToolRegistry::<()>::new();
173
174        register_builtin_tools(
175            &mut registry,
176            BuiltinToolsConfig {
177                environment: fs,
178                capabilities: AgentCapabilities::full_access(),
179                todo_state: Some(todo),
180                link_fetch: true,
181            },
182        );
183
184        for expected in [
185            "read",
186            "write",
187            "edit",
188            "bash",
189            "glob",
190            "grep",
191            "todo_read",
192            "todo_write",
193            "link_fetch",
194        ] {
195            assert!(
196                registry.get(expected).is_some(),
197                "expected '{expected}' registered",
198            );
199        }
200    }
201
202    #[test]
203    fn register_primitives_registers_exactly_six_tools() {
204        let fs = Arc::new(InMemoryFileSystem::new("/workspace"));
205        let mut registry = ToolRegistry::<()>::new();
206        register_primitives(&mut registry, fs, AgentCapabilities::read_only());
207
208        for expected in ["read", "write", "edit", "bash", "glob", "grep"] {
209            assert!(
210                registry.get(expected).is_some(),
211                "expected '{expected}' registered",
212            );
213        }
214    }
215
216    #[test]
217    fn builtin_tools_config_skips_optional_families_when_unset() {
218        let fs = Arc::new(InMemoryFileSystem::new("/workspace"));
219        let mut registry = ToolRegistry::<()>::new();
220        register_builtin_tools(
221            &mut registry,
222            BuiltinToolsConfig {
223                environment: fs,
224                capabilities: AgentCapabilities::full_access(),
225                todo_state: None,
226                #[cfg(feature = "web")]
227                link_fetch: false,
228            },
229        );
230
231        assert!(registry.get("read").is_some());
232        assert!(registry.get("todo_read").is_none());
233        assert!(registry.get("link_fetch").is_none());
234    }
235}