Skip to content

Custom Tool

This example shows how to implement a custom tool by defining the Tool trait for a PriceLookupTool. The tool is combined with Heartbit’s built-in tools and given to an agent that can look up product prices.

  • ANTHROPIC_API_KEY environment variable set with a valid API key
Terminal window
export ANTHROPIC_API_KEY="sk-..."
cargo run -p heartbit --example custom_tool
use std::pin::Pin;
use std::sync::Arc;
use heartbit::{
AgentRunner, AnthropicProvider, BuiltinToolsConfig, Tool, ToolDefinition, ToolOutput,
builtin_tools,
};
use serde_json::json;
/// A domain-specific tool that looks up product prices.
struct PriceLookupTool;
impl Tool for PriceLookupTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "price_lookup".into(),
description: "Look up the price of a product by name. Returns the price in USD.".into(),
input_schema: json!({
"type": "object",
"properties": {
"product": {
"type": "string",
"description": "The product name to look up"
}
},
"required": ["product"]
}),
}
}
fn execute<'a>(
&'a self,
input: serde_json::Value,
) -> Pin<Box<dyn std::future::Future<Output = Result<ToolOutput, heartbit::Error>> + Send + 'a>>
{
Box::pin(async move {
let product = input
.get("product")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
// Simulate a price database lookup.
let price = match product.to_lowercase().as_str() {
"widget" => 9.99,
"gadget" => 24.99,
"thingamajig" => 14.50,
_ => return Ok(ToolOutput::error(format!("Product '{product}' not found"))),
};
Ok(ToolOutput::success(format!(
"Product: {product}\nPrice: ${price:.2}"
)))
})
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key =
std::env::var("ANTHROPIC_API_KEY").expect("set ANTHROPIC_API_KEY environment variable");
let provider = Arc::new(AnthropicProvider::new(&api_key, "claude-sonnet-4-20250514"));
// Combine built-in tools with our custom tool.
let mut tools = builtin_tools(BuiltinToolsConfig::default());
tools.push(Arc::new(PriceLookupTool));
let runner = AgentRunner::builder(provider)
.name("quoter")
.system_prompt(
"You are a sales assistant. Use the price_lookup tool to find product prices \
when asked. Be concise and helpful.",
)
.tools(tools)
.max_turns(5)
.max_total_tokens(50000)
.build()?;
let output = runner
.execute("How much does a widget cost? And a gadget?")
.await?;
println!("{}", output.result);
if let Some(cost) = output.estimated_cost_usd {
eprintln!("[estimated cost: ${cost:.4}]");
}
Ok(())
}

The Tool trait — Every tool implements two methods:

  • definition() — returns a ToolDefinition with the tool’s name, description, and JSON Schema for its input parameters. The LLM uses this schema to know how to call the tool.
  • execute(input) — receives the JSON input from the LLM and returns a ToolOutput. The return type uses Pin<Box<dyn Future>> to support async execution.

Tool output — Tools return either ToolOutput::success(text) for successful results or ToolOutput::error(text) for graceful failures. Error outputs are sent back to the LLM so it can adjust its approach.

Combining toolsbuiltin_tools(BuiltinToolsConfig::default()) returns Heartbit’s built-in tools (file operations, shell, etc.). Custom tools are appended to this vector with tools.push(Arc::new(PriceLookupTool)).

Token budgetmax_total_tokens(50000) sets an overall token budget for the entire execution. The agent stops if it exceeds this limit, preventing runaway costs.

Cost estimationoutput.estimated_cost_usd provides an approximate dollar cost for the execution based on the model’s pricing.

The agent calls price_lookup twice (once for “widget”, once for “gadget”) and returns a summary:

A widget costs $9.99 and a gadget costs $24.99.
[estimated cost: $0.0042]