Adding Tools
How to add new built-in tools to swarm.
Overview
Tools are the primary way agents interact with the outside world. Each tool implements the Tool trait and is registered in the ToolRegistry. This guide walks through adding a new native tool from scratch.
The Tool Trait
Every tool implements this trait from swarm/src/tools/mod.rs:
#![allow(unused)] fn main() { pub trait Tool: Send + Sync { /// Unique tool name (e.g., "bash", "read", "my_tool") fn name(&self) -> &str; /// Human-readable description for the model fn description(&self) -> &str; /// JSON Schema describing the tool's input parameters fn input_schema(&self) -> Value; /// Whether the tool runs natively or in a sandbox (default: Native) fn execution_mode(&self) -> ExecutionMode { ExecutionMode::Native } /// Execute the tool with the given input and context fn execute<'a>( &'a self, input: Value, ctx: &'a ToolContext, ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>>; } }
The five methods:
| Method | Required | Purpose |
|---|---|---|
name() | Yes | Unique identifier used in tool calls |
description() | Yes | Shown to the model to explain what the tool does |
input_schema() | Yes | JSON Schema the model uses to construct input |
execution_mode() | No (default: Native) | Native or Sandboxed |
execute() | Yes | Performs the actual work |
Step 1: Create the Tool File
Create a new file in swarm/src/tools/:
touch swarm/src/tools/my_tool.rs
Step 2: Implement the Tool Trait
#![allow(unused)] fn main() { use anyhow::Result; use serde_json::Value; use std::future::Future; use std::pin::Pin; use super::{Tool, ToolResult}; use super::context::ToolContext; pub struct MyTool; impl MyTool { pub fn new() -> Self { Self } } impl Tool for MyTool { fn name(&self) -> &str { "my_tool" } fn description(&self) -> &str { "A brief description of what this tool does. \ The model reads this to decide when to use the tool." } fn input_schema(&self) -> Value { serde_json::json!({ "type": "object", "properties": { "query": { "type": "string", "description": "The search query" }, "limit": { "type": "integer", "description": "Maximum number of results", "default": 10 } }, "required": ["query"] }) } fn execute<'a>( &'a self, input: Value, ctx: &'a ToolContext, ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>> { Box::pin(async move { // Parse input let query = input["query"] .as_str() .ok_or_else(|| anyhow::anyhow!("missing required field: query"))?; let limit = input["limit"].as_u64().unwrap_or(10); // Use context for working directory, agent info, etc. let _working_dir = &ctx.working_dir; // Check permissions if needed let decision = ctx.check_permission("my_tool", &input); // Handle decision... // Do the actual work let result = format!("Found results for '{}' (limit: {})", query, limit); Ok(ToolResult::text(result)) }) } } }
Key Points
ToolResult::text(s)— Creates a successful result with text contentToolResult::error(s)— Creates an error result (displayed to the model as an error)ctx.working_dir— The agent's working directory (its worktree)ctx.agent_name— The name of the calling agentctx.session_id— The current session IDctx.is_cancelled()— Check if the agent's session has been cancelledctx.check_permission(tool, input)— Check permission for this operation
Step 3: Register the Module
Add the module to swarm/src/tools/mod.rs:
#![allow(unused)] fn main() { mod my_tool; }
Then register the tool in the default_registry() function in swarm/src/tools/registry.rs:
#![allow(unused)] fn main() { pub fn default_registry() -> ToolRegistry { let mut registry = ToolRegistry::new(); registry.register(Box::new(bash::BashTool::new())); registry.register(Box::new(read::ReadTool::new())); // ... existing tools ... registry.register(Box::new(my_tool::MyTool::new())); registry } }
Step 4: Write Tests
Add tests in the same file or in a separate test module:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use crate::tools::context::ToolContext; use serde_json::json; use std::path::PathBuf; #[tokio::test] async fn test_my_tool_basic() { let tool = MyTool::new(); let ctx = ToolContext::new( PathBuf::from("/tmp/test"), "test-agent".to_string(), "test-session".to_string(), ); let input = json!({ "query": "hello" }); let result = tool.execute(input, &ctx).await.unwrap(); assert!(!result.is_error); } #[tokio::test] async fn test_my_tool_missing_query() { let tool = MyTool::new(); let ctx = ToolContext::new( PathBuf::from("/tmp/test"), "test-agent".to_string(), "test-session".to_string(), ); let input = json!({}); let result = tool.execute(input, &ctx).await; assert!(result.is_err()); } #[test] fn test_schema_is_valid() { let tool = MyTool::new(); let schema = tool.input_schema(); assert_eq!(schema["type"], "object"); assert!(schema["properties"]["query"].is_object()); } } }
Step 5: Run Tests
cargo test --lib tools::my_tool
ToolResult Details
The ToolResult struct supports text and image content:
#![allow(unused)] fn main() { pub struct ToolResult { pub content: Vec<ToolResultContent>, pub is_error: bool, } pub enum ToolResultContent { Text(String), Image { media_type: String, data: String }, } }
Helper constructors:
| Method | Creates |
|---|---|
ToolResult::text("output") | Successful text result |
ToolResult::error("message") | Error result displayed to model |
For richer results, construct ToolResult directly:
#![allow(unused)] fn main() { Ok(ToolResult { content: vec![ ToolResultContent::Text("Found 3 files:".to_string()), ToolResultContent::Text("- src/main.rs\n- src/lib.rs\n- Cargo.toml".to_string()), ], is_error: false, }) }
ToolContext Details
The ToolContext provides execution context:
#![allow(unused)] fn main() { pub struct ToolContext { pub working_dir: PathBuf, // Agent's worktree directory pub agent_name: String, // Name of the calling agent pub session_id: String, // Current session ID pub env_vars: HashMap<String, String>, // Agent environment variables pub cancellation_token: CancellationToken, // Cancellation signal pub permissions: Option<Arc<PermissionEvaluator>>, // Permission checker } }
Use ctx.is_cancelled() to check for cancellation in long-running tools. This is important for tools that perform loops or wait for external resources.
Walkthrough: Existing Tool
Here's how the built-in GlobTool is structured (simplified):
#![allow(unused)] fn main() { pub struct GlobTool; impl Tool for GlobTool { fn name(&self) -> &str { "glob" } fn description(&self) -> &str { "Fast file pattern matching tool that works with any codebase size" } fn input_schema(&self) -> Value { json!({ "type": "object", "properties": { "pattern": { "type": "string", "description": "Glob pattern (e.g., **/*.rs)" }, "path": { "type": "string", "description": "Directory to search in" } }, "required": ["pattern"] }) } fn execute<'a>( &'a self, input: Value, ctx: &'a ToolContext, ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>> { Box::pin(async move { let pattern = input["pattern"].as_str() .ok_or_else(|| anyhow::anyhow!("missing pattern"))?; let base_dir = input["path"].as_str() .map(PathBuf::from) .unwrap_or_else(|| ctx.working_dir.clone()); let full_pattern = base_dir.join(pattern); let matches = glob::glob(full_pattern.to_str().unwrap())?; let files: Vec<String> = matches .filter_map(|entry| entry.ok()) .map(|p| p.display().to_string()) .collect(); Ok(ToolResult::text(files.join("\n"))) }) } } }
Pattern to follow:
- Parse input from JSON, validating required fields
- Use
ctx.working_diras default base path - Perform the operation
- Return
ToolResult::text()for success orToolResult::error()for failures
Checklist
Before submitting your tool:
-
Implements all required
Tooltrait methods -
Input schema has
"type": "object"with documented properties -
Required fields are listed in
"required"array - Description is clear enough for the model to know when to use it
- Tool handles missing/invalid input gracefully
-
Module is declared in
tools/mod.rs -
Tool is registered in
default_registry()intools/registry.rs - Tests cover basic usage and error cases
-
cargo testpasses -
cargo clippyhas no warnings
Related
- Tools Concept — How the tool system works
- WASM Tools — Building sandboxed tools with WASM
- Development — Development setup and workflow