Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Add a Tool

Use this when you need to expose a custom capability to the agent by implementing the Tool trait.

Prerequisites

  • awaken crate added to Cargo.toml
  • async-trait and serde_json available

Steps

  1. Implement the Tool trait.
#![allow(unused)]
fn main() {
use async_trait::async_trait;
use serde_json::{Value, json};
use awaken::contract::tool::{Tool, ToolCallContext, ToolDescriptor, ToolError, ToolResult, ToolOutput};

async fn fetch_weather(_city: &str) -> Result<String, ToolError> {
    Ok("Sunny, 22°C".to_string())
}

pub struct WeatherTool;

#[async_trait]
impl Tool for WeatherTool {
    fn descriptor(&self) -> ToolDescriptor {
        ToolDescriptor::new("get_weather", "Get Weather", "Fetch current weather for a city")
            .with_parameters(json!({
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name"
                    }
                },
                "required": ["city"]
            }))
    }

    async fn execute(&self, args: Value, _ctx: &ToolCallContext) -> Result<ToolOutput, ToolError> {
        let city = args["city"]
            .as_str()
            .ok_or_else(|| ToolError::InvalidArguments("Missing 'city'".into()))?;

        let weather = fetch_weather(city).await?;

        Ok(ToolResult::success("get_weather", json!({ "forecast": weather })).into())
    }
}
}
  1. Optionally override argument validation.
fn validate_args(&self, args: &Value) -> Result<(), ToolError> {
    if !args.get("city").and_then(|v| v.as_str()).is_some_and(|s| !s.is_empty()) {
        return Err(ToolError::InvalidArguments("'city' must be a non-empty string".into()));
    }
    Ok(())
}

validate_args runs before execute and lets you reject malformed input early.

  1. Register the tool with the builder.
use std::sync::Arc;
use awaken::AgentRuntimeBuilder;

let runtime = AgentRuntimeBuilder::new()
    .with_tool("get_weather", Arc::new(WeatherTool))
    .with_agent_spec(spec)
    .with_provider("anthropic", Arc::new(provider))
    .build()?;

The string ID passed to with_tool must match the id in ToolDescriptor::new.

  1. Register via a plugin (alternative).

    Tools can also be registered inside a Plugin::register method through the PluginRegistrar:

fn register(&self, registrar: &mut PluginRegistrar) -> Result<(), StateError> {
    registrar.register_tool("get_weather", Arc::new(WeatherTool))?;
    Ok(())
}

Plugin-registered tools are scoped to agents that activate that plugin.

Verify

Send a message that should trigger the tool. Inspect the run result to confirm the tool was called and returned the expected output.

Common Errors

ErrorCauseFix
ToolError::InvalidArgumentsThe LLM passed malformed JSONTighten the JSON Schema in with_parameters to guide the model
Tool never calledDescriptor id does not match the registered IDEnsure the ID in ToolDescriptor::new and with_tool are identical
ToolError::ExecutionFailedRuntime error inside executeReturn a descriptive error; the agent will see it and may retry

examples/src/research/tools.rsSearchTool and WriteReportTool implementations.

Key Files

  • crates/awaken-contract/src/contract/tool.rsTool trait, ToolDescriptor, ToolResult, ToolError
  • crates/awaken-runtime/src/builder.rswith_tool registration