Skip to content

Add a Tool

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

  • awaken crate added to Cargo.toml
  • async-trait and serde_json available
  1. Implement the Tool trait.
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.
use async_trait::async_trait;
use serde_json::{Value, json};
use awaken::contract::tool::{Tool, ToolCallContext, ToolDescriptor, ToolError, ToolOutput, ToolResult};
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"]
}))
}
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(())
}
async fn execute(&self, _args: Value, _ctx: &ToolCallContext) -> Result<ToolOutput, ToolError> {
Ok(ToolResult::success("get_weather", json!({})).into())
}
}

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

  1. Register the tool with the builder.
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use awaken::engine::GenaiExecutor;
use awaken::registry_spec::ModelSpec;
use awaken::{AgentRuntimeBuilder, AgentSpec};
use awaken::contract::tool::{Tool, ToolCallContext, ToolDescriptor, ToolError, ToolOutput, ToolResult};
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": {}}))
}
async fn execute(&self, _args: Value, _ctx: &ToolCallContext) -> Result<ToolOutput, ToolError> {
Ok(ToolResult::success("get_weather", json!({})).into())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let spec = AgentSpec::new("assistant").with_model_id("claude-sonnet");
let runtime = AgentRuntimeBuilder::new()
.with_tool("get_weather", Arc::new(WeatherTool))
.with_agent_spec(spec)
.with_provider("anthropic", Arc::new(GenaiExecutor::new()))
.with_model(ModelSpec::new("claude-sonnet", "anthropic", "claude-sonnet-4-20250514"))
.build()?;
let _runtime = runtime;
Ok(())
}

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:

use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use awaken::{Plugin, PluginDescriptor, PluginRegistrar, StateError};
use awaken::contract::tool::{Tool, ToolCallContext, ToolDescriptor, ToolError, ToolOutput, ToolResult};
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": {}}))
}
async fn execute(&self, _args: Value, _ctx: &ToolCallContext) -> Result<ToolOutput, ToolError> {
Ok(ToolResult::success("get_weather", json!({})).into())
}
}
pub struct WeatherPlugin;
impl Plugin for WeatherPlugin {
fn descriptor(&self) -> PluginDescriptor {
PluginDescriptor { name: "weather" }
}
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.

with_tool puts the tool in the runtime registry — every running agent can potentially call it. Which tools a given agent is allowed to call is config, not Rust:

Terminal window
curl -sS -X PUT http://localhost:3000/v1/config/agents/assistant \
-H 'content-type: application/json' \
-d '{
"id": "assistant",
"model_id": "gpt-4o-mini",
"system_prompt": "You help with weather questions.",
"allowed_tools": ["get_weather"],
"excluded_tools": []
}'

allowed_tools whitelists; excluded_tools blacklists. Both apply on the next run — no rebuild, no restart. Add a tool in code once; gate it per agent via config.

For finer per-call control (allow/deny/ask on argument shape, not just tool name), use the Permission plugin.

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

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.

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