Appam
Guides

Tool Creation

Create custom tools using the Tool trait, macros, or closures.

Tools are executable functions that agents can invoke during conversations. Appam provides several ways to create tools, from manual trait implementation to zero-boilerplate macros and inline closures.

The Tool Trait

Every tool in Appam implements the Tool trait, which requires three methods:

pub trait Tool: Send + Sync {
    /// Unique name used for routing LLM tool calls.
    fn name(&self) -> &str;

    /// Tool specification with JSON Schema for the LLM.
    fn spec(&self) -> Result<ToolSpec>;

    /// Execute the tool with JSON arguments and return a JSON result.
    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value>;
}

The trait is Send + Sync, so tools can be shared safely across async tasks and threads.

ToolSpec Structure

ToolSpec describes the tool for the LLM:

pub struct ToolSpec {
    pub type_field: String,       // Always "function"
    pub name: String,             // Matches Tool::name()
    pub description: String,      // Human-readable description for the LLM
    pub parameters: serde_json::Value, // JSON Schema for input validation
    pub strict: Option<bool>,     // Optional strict mode
}

Manual Implementation

For full control, implement the Tool trait directly:

use appam::prelude::*;

struct SearchTool;

impl Tool for SearchTool {
    fn name(&self) -> &str {
        "search"
    }

    fn spec(&self) -> Result<ToolSpec> {
        Ok(serde_json::from_value(json!({
            "type": "function",
            "name": "search",
            "description": "Search a knowledge base for relevant documents",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query"
                    },
                    "limit": {
                        "type": "number",
                        "description": "Maximum number of results"
                    }
                },
                "required": ["query"]
            }
        }))?)
    }

    fn execute(&self, args: Value) -> Result<Value> {
        let query = args["query"].as_str().unwrap_or_default();
        let limit = args["limit"].as_u64().unwrap_or(10);

        // Your search logic here
        Ok(json!({
            "output": format!("Found {} results for: {}", limit, query)
        }))
    }
}

The #[tool] attribute macro eliminates boilerplate by generating the Tool implementation from an annotated function. For detailed usage, see The #[tool] Macro.

With Inline Parameters

For tools with simple parameters, annotate them directly with #[arg]:

use appam::prelude::*;

#[tool(description = "Multiply two numbers")]
fn multiply(
    #[arg(description = "First number")] a: f64,
    #[arg(description = "Second number")] b: f64,
) -> Result<f64> {
    Ok(a * b)
}

// Generates `Multiply` struct implementing Tool
// Use: Arc::new(multiply()) or AgentBuilderToolExt::tool(multiply())

With a Typed Input Struct

For complex inputs, use #[derive(Schema, Deserialize)] on a struct:

use appam::prelude::*;

#[derive(Deserialize, Schema)]
struct ReadFileInput {
    #[description = "Path to the file to read"]
    path: String,
}

#[tool(description = "Read the contents of a file from disk")]
fn read_file(input: ReadFileInput) -> Result<String> {
    let content = std::fs::read_to_string(&input.path)?;
    Ok(content)
}

// Generates `ReadFile` struct implementing Tool

Using ClosureTool

For quick inline tools, use ClosureTool::new() without defining a struct:

use appam::prelude::*;
use appam::tools::register::ClosureTool;

let tool = ClosureTool::new(
    "echo",
    serde_json::from_value(json!({
        "type": "function",
        "name": "echo",
        "description": "Echo back the input message",
        "parameters": {
            "type": "object",
            "properties": {
                "message": { "type": "string", "description": "Message to echo" }
            },
            "required": ["message"]
        }
    }))?,
    |args: Value| {
        let msg = args["message"].as_str().unwrap_or_default();
        Ok(json!({ "output": msg }))
    },
);

Async Tools with Managed State

For Rust tools that need runtime metadata, app-scoped state, or session-scoped state, implement the async tool path. App state is registered once with manage(...); session state is registered lazily with session_state::<T>() or session_state_with(...).

use appam::prelude::*;

#[derive(Clone)]
struct GreetingConfig {
    prefix: String,
}

#[derive(Default)]
struct GreetingSession {
    count: u64,
}

#[tool(description = "Greet a user while tracking per-session state")]
async fn greet_user(
    config: State<GreetingConfig>,
    session: SessionState<GreetingSession>,
    #[arg(description = "User name")] name: String,
) -> Result<String> {
    let count = session.update(|state| {
        state.count += 1;
        state.count
    })?;

    Ok(format!("{} {}, visit {}", config.prefix, name, count))
}

let agent = AgentBuilder::new("stateful-agent")
    .system_prompt("Use the greeting tool when helpful.")
    .manage(GreetingConfig {
        prefix: "Hello".to_string(),
    })
    .session_state::<GreetingSession>()
    .async_tool(greet_user())
    .build()?;

Injected parameters such as ToolContext, State<T>, and SessionState<T> are excluded from the JSON schema that the model sees. Only user-facing arguments remain in the tool definition.

Using ToolRegistryExt::register_fn

Register closure-based tools directly on a ToolRegistry:

use appam::prelude::*;

let registry = ToolRegistry::new();
registry.register_fn(
    "greet",
    serde_json::from_value(json!({
        "type": "function",
        "name": "greet",
        "description": "Greet someone by name",
        "parameters": {
            "type": "object",
            "properties": {
                "name": { "type": "string", "description": "Name to greet" }
            },
            "required": ["name"]
        }
    }))?,
    |args: Value| {
        let name = args["name"].as_str().unwrap_or("world");
        Ok(json!({ "output": format!("Hello, {}!", name) }))
    },
);

Registering Tools with Agents

Using AgentBuilder::with_tool

The standard approach wraps the tool in Arc:

let agent = AgentBuilder::new("my-agent")
    .provider(LlmProvider::Anthropic)
    .model("claude-sonnet-4-5")
    .system_prompt("You are a helpful assistant.")
    .with_tool(Arc::new(SearchTool))
    .with_tool(Arc::new(multiply()))
    .build()?;

Using AgentBuilderToolExt::tool (No Arc Needed)

The tool() extension method wraps the tool in Arc automatically:

use appam::prelude::*;

let agent = AgentBuilder::new("my-agent")
    .provider(LlmProvider::Anthropic)
    .model("claude-sonnet-4-5")
    .system_prompt("You are a helpful assistant.")
    .tool(multiply())    // No Arc::new() needed
    .tool(read_file())
    .build()?;

Using with_tools for Multiple Tools at Once

let agent = AgentBuilder::new("my-agent")
    .system_prompt("You are a helpful assistant.")
    .with_tools(vec![
        Arc::new(multiply()),
        Arc::new(read_file()),
        Arc::new(SearchTool),
    ])
    .build()?;

Choosing an Approach

ApproachBest For
#[tool] + #[derive(Schema)]Most tools. Schema derived from Rust types with minimal boilerplate.
#[tool] + #[arg]Simple tools with a few parameters.
Manual impl ToolFull control over spec loading (e.g., from JSON files).
ClosureToolQuick prototyping, one-off tools, or dynamic tool creation.
register_fnAdding tools to an existing ToolRegistry inline.