defineTool — your own function as a tool
Rust · crate toolnexus · SPEC §6
pub fn define_tool<F, Fut, E>(name, description, schema: Option<Value>, f: F) -> Toolwhere F: Fn(Value, ToolContext) -> Fut, Fut: Future<Output = Result<String, E>>, E: DisplayWrap a plain function with a name, description and schema — the shortest path from code you have to a tool the LLM can call.
When to use it
Section titled “When to use it”To turn your own async function into a tool (source: "native"): Ok(text) becomes the output, Err(e) becomes an isError result the model can read.
Why this and not the alternative
Section titled “Why this and not the alternative”Use this over a hand-built Tool: it fills the source, the default empty-object schema and the error mapping.
Examples
Section titled “Examples”1. A function and four arguments
Section titled “1. A function and four arguments”The four arguments are the name, the description, the JSON schema (Option<Value>), and an async
closure taking the arguments as a serde_json::Value plus the ToolContext.
use serde_json::json;use toolnexus::{ToolContext, ToolSource, define_tool};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let greet = define_tool( "greet", "Greet someone by name", Some(json!({"type":"object","properties":{"name":{"type":"string"}},"required":["name"]})), |args, _ctx| async move { args["name"].as_str().map(|n| format!("Hello, {n}!")).ok_or("name is required") }, ); assert_eq!(greet.source, ToolSource::Native); let r = greet.call(json!({"name": "Rust"}), ToolContext::default()).await; assert_eq!(r.output, "Hello, Rust!"); println!("ok: {}", r.output); Ok(())}2. Returning an error, and the schema-less case
Section titled “2. Returning an error, and the schema-less case”Err(e) becomes an isError result carrying e.to_string() — the model reads it and can retry.
schema: None advertises the empty-object schema.
use serde_json::json;use toolnexus::{ToolContext, define_tool, empty_schema};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let fail = define_tool("fail", "Always fails", None, |_a, _c| async { Err::<String, _>("boom") }); assert_eq!(fail.input_schema, empty_schema()); assert_eq!(fail.input_schema, json!({"type":"object","properties":{},"additionalProperties":false}));
let r = fail.call(json!({}), ToolContext::default()).await; assert!(r.is_error && r.output == "boom");
// Any E: Display works — including std::io::Error from `?`. let read = define_tool("read_missing", "", None, |_a, _c| async { let s = std::fs::read_to_string("/definitely/not/here.txt")?; Ok::<_, std::io::Error>(s) }); let r2 = read.call(json!({}), ToolContext::default()).await; assert!(r2.is_error && !r2.output.is_empty()); println!("ok: {} | {}", r.output, r2.output); Ok(())}3. Context, cancellation, and registering on a toolkit
Section titled “3. Context, cancellation, and registering on a toolkit”use std::time::Duration;
use serde_json::json;use toolnexus::{BuiltinsConfig, ToolContext, ToolkitOptions, create_toolkit, define_tool};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let wait = define_tool("wait", "Sleep for ms milliseconds", None, |args, ctx| async move { let ms = args["ms"].as_u64().unwrap_or(1000); tokio::select! { _ = ctx.cancel.cancelled() => Err("cancelled".to_string()), _ = tokio::time::sleep(Duration::from_millis(ms)) => Ok(format!("slept {ms}ms")), } }); let tk = create_toolkit(ToolkitOptions { extra_tools: vec![wait], builtins: Some(BuiltinsConfig::off()), ..Default::default() }) .await?; assert!(tk.get("wait").is_some());
let r = tk.execute("wait", json!({"ms": 5}), ToolContext::default()).await; assert_eq!(r.output, "slept 5ms");
let ctx = ToolContext::default(); ctx.cancel.cancel(); let c = tk.execute("wait", json!({"ms": 5000}), ctx).await; assert!(c.is_error && c.output == "cancelled");
// `register` adds tools to a live toolkit (safe mid-turn). tk.register([define_tool("ping", "", None, |_a, _c| async { Ok::<_, String>("pong".into()) })]); assert_eq!(tk.execute("ping", json!({}), ToolContext::default()).await.output, "pong"); println!("ok: {} | {} | {} tools", r.output, c.output, tk.tools().len()); Ok(())}See also
Section titled “See also”define_typed_tool— Derive the schema from the function signature or annotation instead of writing it by hand.