Custom Tools
Heartbit agents interact with the world through tools. You can extend agent capabilities by implementing the Tool trait and registering your tools with an agent.
The Tool Trait
Section titled “The Tool Trait”Every tool implements two methods:
definition()— returns aToolDefinitiondescribing the tool’s name, description, and parameters (JSON Schema).execute()— receives the LLM’s input as aserde_json::Valueand returns aToolOutput.
use heartbit::{Tool, ToolDefinition, ToolOutput, Error};use serde_json::Value;use std::pin::Pin;use std::future::Future;
pub struct MyTool;
impl Tool for MyTool { fn definition(&self) -> ToolDefinition { ToolDefinition { name: "my_tool".into(), description: "Does something useful".into(), input_schema: serde_json::json!({ "type": "object", "properties": { "input": { "type": "string", "description": "The input value" } }, "required": ["input"] }), } }
fn execute( &self, input: Value, ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, Error>> + Send + '_>> { Box::pin(async move { let input_str = input["input"].as_str().unwrap_or_default(); Ok(ToolOutput::success(format!("Processed: {input_str}"))) }) }}Registering Tools
Section titled “Registering Tools”Pass tools as Vec<Arc<dyn Tool>> to an agent builder:
use std::sync::Arc;use heartbit::AgentRunner;
let agent = AgentRunner::builder(provider) .tools(vec![Arc::new(MyTool)]) .build()?;Tool Output Types
Section titled “Tool Output Types”ToolOutput supports several output formats:
ToolOutput::success("result")— plain text outputToolOutput::error("something went wrong")— error result returned to the LLM (the agent can adjust and retry)
Tool Definition Schema
Section titled “Tool Definition Schema”ToolDefinition is a struct with three fields: name, description, and input_schema (JSON Schema as serde_json::Value):
ToolDefinition { name: "search".into(), description: "Search the web for information".into(), input_schema: serde_json::json!({ "type": "object", "properties": { "query": { "type": "string", "description": "Search query" }, "max_results": { "type": "number", "description": "Maximum results to return" } }, "required": ["query"] }),}The input_schema follows JSON Schema format. Use "required" to list mandatory parameters.
Async and State
Section titled “Async and State”The execute method returns a pinned boxed future, allowing full async operation. You can hold references to &self across await points:
pub struct DatabaseTool { pool: sqlx::PgPool,}
impl Tool for DatabaseTool { fn definition(&self) -> ToolDefinition { ToolDefinition { name: "query_db".into(), description: "Run a read-only database query".into(), input_schema: serde_json::json!({ "type": "object", "properties": { "sql": { "type": "string", "description": "SQL query" } }, "required": ["sql"] }), } }
fn execute( &self, input: Value, ) -> Pin<Box<dyn Future<Output = Result<ToolOutput, Error>> + Send + '_>> { Box::pin(async move { let sql = input["sql"].as_str().unwrap_or_default(); // Use self.pool across the await point let rows: Vec<_> = sqlx::query(sql) .fetch_all(&self.pool) .await .map_err(|e| Error::Agent(e.to_string()))?; Ok(ToolOutput::success(format!("Returned {} rows", rows.len()))) }) }}MCP Tools
Section titled “MCP Tools”For tools hosted on external MCP servers, you don’t need to implement the Tool trait. Configure MCP server URLs in your agent config and Heartbit discovers tools automatically:
[[agents]]name = "researcher"mcp_servers = ["http://localhost:8000/mcp"]See the MCP Servers guide for details on connecting to MCP tool servers.