reductool/lib.rs
1use anyhow::Result;
2use futures::future::BoxFuture;
3use linkme::distributed_slice;
4use serde_json::Value;
5
6/// Attribute macro to register a free-standing Rust function as an AI tool.
7///
8/// Usage:
9/// ```rust
10/// use reductool::aitool;
11///
12/// #[aitool]
13/// /// Add two numbers
14/// fn add(a: i32, b: i32) -> i32 { a + b }
15/// ```
16///
17/// Notes:
18/// - Only free functions are supported (no `self` receiver).
19/// - Parameters must be simple identifiers like `arg: T`. Patterns such as `(_: T)`, `(a, b): (T, U)`, or `S { x, y }: S` are rejected.
20/// - Optional parameters are expressed as `Option<T>` and will be omitted from the generated `"required"` list.
21/// - Supported parameter type mappings to JSON Schema are documented in this crate’s README under “Supported parameter types”.
22pub use reductool_proc_macro::aitool;
23
24// Re-export linkme under a stable path for the generated macro code
25pub use linkme as __linkme;
26
27/// Boxed async result produced by an aitool invocation. Resolves to a `serde_json::Value` or an error.
28pub type InvokeFuture = BoxFuture<'static, Result<Value>>;
29
30/// Runtime definition of a registered AI tool.
31///
32/// Values of this type are generated by the `#[aitool]` macro and linked into
33/// the `ALL_TOOLS` distributed slice at compile time.
34#[derive(Clone)]
35pub struct ToolDefinition {
36 /// Tool name (function identifier), used to dispatch the tool, e.g. `"add"`.
37 pub name: &'static str,
38 /// Human-readable description aggregated from the function’s Rustdoc comments.
39 pub description: &'static str,
40 /// JSON string containing the JSON Schema for this tool’s parameters.
41 pub json_schema: &'static str,
42 /// Invoker that deserializes arguments and executes the function, returning its JSON value.
43 pub invoke: fn(Value) -> InvokeFuture,
44}
45
46/// Registry of all tools discovered via the `#[aitool]` attribute.
47///
48/// Implemented as a `linkme` distributed slice so tools can be defined across crates
49/// and automatically linked into a single registry.
50#[distributed_slice]
51pub static ALL_TOOLS: [ToolDefinition] = [..];
52
53/// Produce the aggregated JSON Schema for all registered tools.
54///
55/// Returns:
56/// - A JSON array where each element is the parsed schema of a tool (as `serde_json::Value`).
57///
58/// Panics:
59/// - If a tool’s embedded `json_schema` string fails to parse (should never happen; generated by the macro).
60///
61/// Example:
62/// ```rust
63/// let schema = reductool::tools_to_schema();
64/// assert!(schema.is_array());
65/// ```
66pub fn tools_to_schema() -> Value {
67 Value::Array(
68 ALL_TOOLS
69 .iter()
70 .map(|t| serde_json::from_str(t.json_schema).unwrap())
71 .collect(),
72 )
73}
74
75/// Invoke a registered tool by name with JSON arguments.
76///
77/// Parameters:
78/// - `name`: Tool name as exposed by the annotated function identifier.
79/// - `args`: JSON arguments object matching the tool’s schema.
80///
81/// Returns:
82/// - On success, the function’s return value serialized as `serde_json::Value`.
83///
84/// Errors:
85/// - Returns an error if no tool with the given name exists.
86/// - Returns an error if the tool’s argument deserialization or execution fails.
87///
88/// Example:
89/// ```no_run
90/// async fn run() -> anyhow::Result<()> {
91/// let out = reductool::dispatch_tool("add", serde_json::json!({ "a": 2, "b": 3 })).await?;
92/// assert_eq!(out, serde_json::json!(5));
93/// Ok(())
94/// }
95/// ```
96pub async fn dispatch_tool(name: &str, args: Value) -> Result<Value> {
97 (ALL_TOOLS
98 .iter()
99 .find(|t| t.name == name)
100 .ok_or_else(|| anyhow::anyhow!("Unknown tool: {name}"))?
101 .invoke)(args)
102 .await
103}