llm-tool
Framework-agnostic Rust tool definitions for LLM agents.
Define strongly-typed tools with the #[llm_tool] attribute macro and register
them in a ToolRegistry. The registry produces ToolDefinition structs
(name + description + JSON Schema) that any LLM framework can consume —
there is zero coupling to a specific SDK or agent runtime.
Quick start
Add llm-tool to your Cargo.toml:
[]
= "0.1"
Defining a tool with #[llm_tool]
The easiest way to create a tool is the #[llm_tool] attribute macro.
It generates a params struct and a RustTool implementation from a plain
function:
use ;
/// Adds two numbers together.
// The macro generates an `Add` struct (PascalCase of the fn name).
let registry = new.with_tool;
let defs = registry.definitions;
assert_eq!;
assert_eq!;
Rules for #[llm_tool] functions:
- Must have a doc comment (becomes the tool description).
- Every parameter must have a doc comment (becomes the JSON Schema description for that field).
- Return type can be
Result<T, E>or a bareT(infallible tools):TisString(auto-wrapped intoToolOutput),ToolOutput(passed through), anyT: Serialize(auto-serialized to JSON), or anyT: Into<ToolOutput>.Eis anyE: Into<ToolError>— built-in conversions exist forToolError,String,std::io::Error,serde_json::Error, andBox<dyn Error + Send + Sync>.
- Can be
async fn— the generatedRustTool::callis always async. &strparameters are accepted — the generated struct storesStringand auto-borrows.Option<T>parameters automatically get#[serde(default)], so they are omitted from the JSON Schemarequiredarray.- A
&ToolContextparameter is recognized as the execution context and forwarded from the registry — it is not included in the params struct.
Returning ToolOutput with metadata
Tools can return a ToolOutput directly, attaching structured metadata
for hooks, policies, and logging pipelines. Metadata is never sent to
the model — only the content string is:
use ;
/// Runs a shell command and returns its stdout.
Similarly, ToolError supports metadata for error diagnostics:
use ;
/// Fetches a URL.
The ? operator — zero-boilerplate error handling
ToolError implements From<std::io::Error>, From<serde_json::Error>,
and From<Box<dyn Error + Send + Sync>>, so the ? operator works
without manual .map_err():
use ;
/// Reads a file from disk.
async
# block_on;
Infallible tools — no Result needed
Tools that can never fail can return a bare type instead of Result:
use ;
/// Returns a friendly greeting.
let registry = new.with_tool;
# block_on;
Returning structured data with ToolOutput::json()
Use ToolOutput::json() to serialize any T: Serialize to JSON:
use ;
use Serialize;
/// Gets the current weather for a city.
Auto-serialized return types
Any T: Serialize returned from a tool is automatically serialized to JSON
by the macro's compile-time dispatch — no ToolOutput::json() call needed:
use ;
use Serialize;
/// Returns metadata about a file.
Json<T> wrapper for infallible serialization
Use Json<T> when your tool is infallible but returns a serializable
struct. It implements Into<ToolOutput>, panicking only if Serialize
is broken (use ToolOutput::json() for fallible serialization):
use ;
use Serialize;
/// Computes statistics.
Custom From<T> for ToolOutput
Implement From<YourType> for ToolOutput for domain types that should
convert directly into tool output, then call .into() in the tool body:
use ;
;
/// Renders documentation as Markdown.
Async tools
Async functions work out of the box. The body can .await freely:
use ;
/// Reads a file from disk.
async
# block_on;
Optional parameters
Option<T> fields are not required in the JSON the model sends:
use ;
/// Greets someone.
Accessing ToolContext
If your tool needs the conversation ID or shared state, accept a
&ToolContext parameter. It is automatically wired by the registry and
excluded from the generated params struct:
use ;
/// Returns the current conversation ID.
Manual RustTool implementation
For full control, implement RustTool directly:
use ;
use Deserialize;
;
ToolRegistry
The registry stores tools and provides two operations:
definitions()— returnsVec<ToolDefinition>with the name, description, and JSON Schema for each tool. Feed these to your framework.dispatch(name, args, ctx)— deserializes JSON args, calls the tool, and returns aToolOutputor aToolError.
use ;
/// Echoes its input.
# block_on;
Plugging into any agent framework
llm-tool is deliberately framework-agnostic. To integrate with a new
framework:
- Register your tools in a
ToolRegistry. - Extract definitions via
registry.definitions()— eachToolDefinitionhas.name,.description, and.parameter_schema(aserde_json::Valuecontaining a JSON Schema object). - Convert
ToolDefinitions into whatever format your framework expects (e.g.OpenAIfunction-calling JSON, Anthropic tool-use blocks, GeminiFunctionDeclarations, etc.). Theparameter_schemais standard JSON Schema (draft 7, with nullable arrays already sanitized to scalar"type"strings for Go genai compatibility). - On tool call, extract the tool name and JSON arguments from your
framework's response, then call
registry.dispatch(name, args, &ctx). - Return the result (or error message) to the model as the tool response.
Minimal integration sketch:
use ;
async
# let registry = new;
# send_definitions_to_model;
# block_on;
Key types
| Type | Description |
|---|---|
RustTool |
Trait for implementing a tool with typed parameters. |
ToolRegistry |
Registry for storing and dispatching tools by name. |
ToolDefinition |
Serializable metadata (name, description, JSON Schema). |
ToolContext |
Execution context with conversation state and shared key-value store. |
ToolOutput |
Structured return value (content + metadata) from tool execution. |
ToolError |
Error type with From impls for io::Error, serde_json::Error. |
Json<T> |
Wrapper for infallible serialization of T: Serialize into output. |
EmptyParams |
Convenience struct for tools that take no parameters. |
#[llm_tool] |
Proc-macro attribute for defining tools from plain functions. |
License
Dual-licensed under Apache-2.0 OR MIT.