Guides
Writing custom tools
Define, register and test a tool before connecting it to an LLM.
A tool exposes a named operation and a JSON input schema to the model. Start with a small deterministic tool so you can verify its behaviour without making a model call.
A complete tool example
Section titled “A complete tool example”Use the project from Installation and add JSON support:
cargo add serde_jsonReplace src/main.rs with:
use polaris_ai::tools::{ToolError, ToolRegistry, tool};
#[tool]/// Add two integers.async fn add( /// First integer. left: i64, /// Second integer. right: i64,) -> Result<String, ToolError> { Ok((left + right).to_string())}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let mut registry = ToolRegistry::new(); registry.register(add()); let result = registry .execute("add", &serde_json::json!({"left": 2, "right": 3})) .await?; assert_eq!(result, serde_json::json!("5")); println!("{}", result.as_str().unwrap()); Ok(())}Run cargo run. It should print 5.
The #[tool] macro derives the input schema from the parameter types and uses the doc comments for the descriptions exposed to the model. add() constructs the generated tool; registry.register(add()) makes it available by name. execute deserialises the arguments and returns JSON.
The example returns a string, so the JSON result is "5". Do not serialise a JSON object into a string unless that is deliberately the output format: it produces a JSON string containing JSON text, not a structured object.
Register through a plugin
Section titled “Register through a plugin”For an agent, register tools during plugin build after ToolsPlugin has supplied the registry. The repository’s FileToolsPlugin is a complete example of dependency declaration, registration and a permission override.
Use #[toolset] when several tools share configuration or state; the file-tools example demonstrates that pattern as well. Writing custom plugins covers the lifecycle.
Connect tools to the model
Section titled “Connect tools to the model”Import LlmRequestBuilderExt and call .with_registry(®istry) on the model request builder to attach definitions. The caller then reads tool calls from the response, executes them and appends the corresponding tool results to the conversation. Follow the ReAct example for the complete loop.
A tool’s permission is metadata for the calling runtime. Direct ToolRegistry::execute does not perform an interactive confirmation. Check the effective permission and handle Allow, Confirm and Deny before dispatching a model-requested action. The CLI example demonstrates this for file writes.
Inputs that need execution context
Section titled “Inputs that need execution context”Tools can also declare context-injected parameters. In that case, call execute_with with a ToolContext containing the required resources. Plain execute supplies an empty context, so it cannot satisfy required context parameters. See the tool reference for the complete parameter and context API.
Test the boundary
Section titled “Test the boundary”Test a valid call, malformed arguments, missing inputs and domain-specific failures before attaching a model. For filesystem or network tools, test the allowed scope as part of the tool’s own implementation. Return actionable errors so the agent can distinguish invalid input from a transient service failure.
Keep each tool focused, name it after its action and describe units, formats and constraints in parameter comments. These descriptions are part of the model’s interface.
Copyright © 2026, Radiant AI Limited.