Your First Agent with Tools
Build an agent with custom tools using the AgentBuilder and tool macros.
Tools give your agent the ability to take actions -- read files, call APIs, run calculations, or anything else you can express in Rust. This guide walks through defining a tool, registering it with an agent, and letting the LLM call it autonomously.
Define a Tool Input Schema
Tool inputs are plain Rust structs annotated with #[derive(Deserialize, Schema)]. The Schema derive macro generates a JSON Schema that the LLM uses to understand what arguments the tool expects. Use the #[description = "..."] attribute to describe each field:
use appam::prelude::*;
#[derive(Deserialize, Schema)]
struct AddInput {
#[description = "First number"]
a: f64,
#[description = "Second number"]
b: f64,
}Supported field types include String, f64, i64, u32, bool, Vec<T>, and nested structs that also derive Schema.
Implement the Tool
The #[tool] attribute macro turns a function declaration into a full Tool implementation. For struct-based input, the function takes your input struct as its only argument and returns a Result:
#[derive(Serialize)]
struct AddOutput {
sum: f64,
}
#[tool(description = "Add two numbers together")]
fn add(input: AddInput) -> Result<AddOutput> {
Ok(AddOutput {
sum: input.a + input.b,
})
}The macro generates a struct named Add (the function name converted to PascalCase) that implements the Tool trait. It also generates a zero-argument constructor function add() that returns an instance of Add.
What the Macro Generates
For the add function above, the #[tool] macro produces roughly:
- A struct
Addthat implementsappam::tools::Tool. Add::name()returns"add".Add::spec()returns aToolSpecwith the JSON Schema derived fromAddInput.Add::execute()deserializes the JSON arguments intoAddInput, calls your function body, and wraps the result.- A factory function
add() -> Addfor convenient instantiation.
You can override the tool name with #[tool(name = "calculator_add", description = "...")].
Build the Agent
Use AgentBuilder for full control over agent configuration. Register tools with .with_tool():
use appam::prelude::*;
#[derive(Deserialize, Schema)]
struct AddInput {
#[description = "First number"]
a: f64,
#[description = "Second number"]
b: f64,
}
#[derive(Serialize)]
struct AddOutput {
sum: f64,
}
#[tool(description = "Add two numbers together")]
fn add(input: AddInput) -> Result<AddOutput> {
Ok(AddOutput {
sum: input.a + input.b,
})
}
#[tokio::main]
async fn main() -> Result<()> {
let agent = AgentBuilder::new("calculator")
.provider(LlmProvider::Anthropic)
.model("claude-sonnet-4-5")
.system_prompt("You are a calculator assistant. Use the add tool to perform additions.")
.with_tool(Arc::new(add()))
.build()?;
agent
.stream("What is 42 + 58?")
.on_content(|text| print!("{}", text))
.on_tool_call(|name, _args| println!("\n[Calling tool: {}]", name))
.on_tool_result(|name, result| println!("[Tool {} returned: {}]", name, result))
.run()
.await?;
println!();
Ok(())
}The agent will reason about the user's question, decide to call add with {"a": 42, "b": 58}, receive the result, and then respond with the answer.
The Agentic Loop
When you call .stream().run(), Appam runs a tool loop for that one user
prompt:
- Send the conversation (system prompt + user message) to the LLM.
- The LLM streams back text and/or tool calls.
- If the LLM requests a tool call, Appam executes the tool and appends the result to the conversation.
- Steps 1-3 repeat until the LLM responds with only text (no more tool calls).
- The session completes and a
Sessionobject is returned.
This loop is fully automatic. Tool execution is synchronous from the runtime's perspective, and results are fed back to the LLM without any manual intervention.
For a readline-style chat loop across multiple user prompts, keep the returned
Session.messages and pass the updated transcript into
default_run_streaming_with_messages(...) on the next prompt.
Inline Tool Definitions
For tools with simple parameters, you can skip the input struct entirely and annotate parameters directly with #[arg]:
#[tool(description = "Multiply two numbers")]
fn multiply(
#[arg(description = "First number")] a: f64,
#[arg(description = "Second number")] b: f64,
) -> Result<f64> {
Ok(a * b)
}This generates the same Tool implementation. The JSON Schema is built from the parameter types and #[arg] attributes.
Adding Tools Without Arc::new()
The AgentBuilderToolExt trait, re-exported by appam::prelude::*, provides a .tool() method that wraps the tool in Arc for you:
use appam::prelude::*;
let agent = AgentBuilder::new("calculator")
.provider(LlmProvider::Anthropic)
.model("claude-sonnet-4-5")
.system_prompt("You are a calculator.")
.tool(add()) // No Arc::new() needed
.tool(multiply())
.build()?;Both .with_tool(Arc::new(add())) and .tool(add()) are equivalent. If you already have a Vec<Arc<dyn Tool>>, use .with_tools(...) or .tools(...) instead.
Multiple Tools
Agents can have any number of tools. The LLM chooses which tool to call based on the user's request and the tool descriptions:
let agent = AgentBuilder::new("coding-assistant")
.provider(LlmProvider::Anthropic)
.model("claude-sonnet-4-5")
.system_prompt("You are a coding assistant with file and shell access.")
.with_tool(Arc::new(read_file()))
.with_tool(Arc::new(write_file()))
.with_tool(Arc::new(bash()))
.with_tool(Arc::new(list_files()))
.build()?;The LLM may chain multiple tool calls in a single turn or across turns to accomplish complex tasks.
Next Steps
- Core Concepts: Agents -- Understand sessions, continuations, and the
Agenttrait in depth. - Core Concepts: Tools -- Tool registries, closure tools, and Python tool implementations.
- Examples -- Full working example of a coding agent with file and shell tools.