tools-rs 0.3.0

Core functionality for the tools-rs tool collection system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
# Tools-rs - Tool Collection and Execution Framework
*It's pronounced tools-r-us!!*

[![Crates.io](https://img.shields.io/crates/v/tools-rs.svg)](https://crates.io/crates/tools-rs)
[![Documentation](https://docs.rs/tools-rs/badge.svg)](https://docs.rs/tools-rs)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Tools-rs is a framework for building, registering, and executing tools with automatic JSON schema generation for Large Language Model (LLM) integration.

## Features

- **Automatic Registration** - Use `#[tool]` to automatically register functions with compile-time discovery
- **JSON Schema Generation** - Automatic schema generation for LLM integration with full type information
- **Type Safety** - Full type safety with JSON serialization at boundaries, compile-time parameter validation
- **Async Support** - Built for async/await from the ground up with `tokio` integration
- **Error Handling** - Comprehensive error types with context and proper error chaining
- **LLM Integration** - Export function declarations for LLM function calling APIs (OpenAI, Anthropic, etc.)
- **Manual Registration** - Programmatic tool registration for dynamic scenarios
- **Inventory System** - Link-time tool collection using the `inventory` crate for zero-runtime-cost discovery
- **Typed Metadata** - Attach `#[tool(key = value)]` attributes to tools and read them through a user-defined `M` type on `ToolCollection<M>` (see [Tool Metadata]#tool-metadata)
- **Shared Context** - Inject shared resources (`Arc<T>`) into tools via `ctx` first parameter and `ToolCollection::builder().with_context(...)` (see [Shared Context]#shared-context)

## Quick Start

```rust
use serde_json::json;
use tools_rs::{collect_tools, FunctionCall, tool};

#[tool]
/// Adds two numbers.
async fn add(pair: (i32, i32)) -> i32 {
    pair.0 + pair.1
}

#[tool]
/// Greets a person.
async fn greet(name: String) -> String {
    format!("Hello, {name}!")
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tools = collect_tools();

    let sum = tools
        .call(FunctionCall::new(
            "add".into(),
            json!({ "pair": [3, 4] }),
        ))
        .await?.result;
    println!("add → {sum}");  // Outputs: "add → 7"

    let hi = tools
        .call(FunctionCall::new(
            "greet".into(),
            json!({ "name": "Alice" }),
        ))
        .await?.result;
    println!("greet → {hi}");  // Outputs: "greet → \"Hello, Alice!\""

    // Export function declarations for LLM APIs
    let declarations = tools.json()?;
    println!("Function declarations: {}", serde_json::to_string_pretty(&declarations)?);

    Ok(())
}
```

## Installation

Add the following to your `Cargo.toml`:

```toml
[dependencies]
tools-rs = "0.2.0"
tokio = { version = "1.45", features = ["macros", "rt-multi-thread"] }
serde_json = "1.0"
```

## Crate Structure

The tools-rs system is organized as a Rust workspace with three main crates:

- **tools-rs**: Main entry point, re-exports the most commonly used items
- **tools_core**: Core runtime implementation including:
  - Tool collection and execution (`ToolCollection`)
  - JSON schema generation (`ToolSchema` trait)
  - Error handling (`ToolError`, `DeserializationError`)
  - Core data structures (`FunctionCall`, `ToolRegistration`, etc.)
- **tools_macros**: Procedural macros for tool registration:
  - `#[tool]` attribute macro for automatic registration
  - `#[derive(ToolSchema)]` for automatic schema generation
- **examples**: Comprehensive examples demonstrating different use cases

For more details about the codebase organization, see [CODE_ORGANIZATION.md](CODE_ORGANIZATION.md).

## Compatibility

### Rust Version Support

Tools-rs requires **Rust 1.85** or later and supports:
- Automatically generate JSON schemas for LLM consumption
- Execute tools safely with full type checking
- Handle errors gracefully with detailed context

## Function Declarations for LLMs

Tools-rs can automatically generate function declarations suitable for LLM APIs:

```rust
use tools_rs::{function_declarations, tool};

#[tool]
/// Return the current date in ISO-8601 format.
async fn today() -> String {
    chrono::Utc::now().date_naive().to_string()
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Generate function declarations for an LLM
    let declarations = function_declarations()?;

    // Use in API request
    let llm_request = serde_json::json!({
        "model": "gpt-4o",
        "messages": [/* ... */],
        "tools": declarations
    });

    Ok(())
}
```

The generated declarations follow proper JSON Schema format:

```json
[
  {
    "description": "Return the current date in ISO-8601 format.",
    "name": "today",
    "parameters": {
      "properties": {},
      "required": [],
      "type": "object"
    }
  }
]
```

## Manual Registration

While the `#[tool]` macro provides the most convenient way to register tools, you can also register tools manually for more dynamic scenarios:

```rust
use tools_rs::ToolCollection;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut tools: ToolCollection = ToolCollection::new();

    // Register a simple tool manually. Pass `()` as the metadata when the
    // collection uses the default `NoMeta`; pass a real `M` value when the
    // collection is typed (e.g. `ToolCollection::<MyPolicy>::new()`).
    tools.register(
        "multiply",
        "Multiplies two numbers",
        |pair: (i64, i64)| async move { pair.0 * pair.1 },
        (),
    )?;

    // Call the manually registered tool
    let result = tools.call(tools_rs::FunctionCall {
        id: None, // Refers to the call ID
        name: "multiply".to_string(),
        arguments: json!({"a": 6, "b": 7}),
    }).await?;

    println!("6 * 7 = {}", result);
    Ok(())
}
```

### Advanced Manual Registration

For complex scenarios with custom types:

```rust
use tools_rs::{ToolCollection, ToolSchema};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, ToolSchema)]
struct Calculator {
    operation: String,
    operands: Vec<f64>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut tools: ToolCollection = ToolCollection::new();

    tools.register(
        "calculate",
        "Performs arithmetic operations on a list of numbers",
        |input: Calculator| async move {
            match input.operation.as_str() {
                "sum" => input.operands.iter().sum::<f64>(),
                "product" => input.operands.iter().product::<f64>(),
                "mean" => input.operands.iter().sum::<f64>() / input.operands.len() as f64,
                _ => f64::NAN,
            }
        },
        (),
    )?;

    Ok(())
}
```

## Tool Metadata

`#[tool(...)]` accepts flat `key = value` attributes that get stored on each
tool and read back through a user-defined metadata type. This lets a single
tool declaration feed multiple collections, each typed to the schema *that*
collection cares about — useful for HITL approval gates, cost tiering, and
similar policy concerns that don't belong in the function body.

```rust
use serde::Deserialize;
use tools_rs::{tool, ToolCollection};

#[derive(Debug, Default, Deserialize, Clone, Copy)]
#[serde(default)]
struct Policy {
    requires_approval: bool,
    cost_tier: u8,
}

#[tool(requires_approval = true, cost_tier = 3)]
/// Deletes a file.
async fn delete_file(path: String) -> String { format!("deleted {path}") }

#[tool]
/// Reads a file (no metadata declared — fields default).
async fn read_file(path: String) -> String { format!("read {path}") }

fn main() -> Result<(), tools_rs::ToolError> {
    let tools = ToolCollection::<Policy>::collect_tools()?;
    let entry = tools.get("delete_file").unwrap();
    if entry.meta.requires_approval {
        // gate the call behind your approval flow
    }
    Ok(())
}
```

### Default behavior

`ToolCollection` defaults to `ToolCollection<NoMeta>`. `NoMeta` is an empty
struct that swallows any attributes a tool declared, so existing
`collect_tools()` callers see no behavioral change. Opt into typed metadata
by writing `ToolCollection::<MyMeta>::collect_tools()` instead.

### Validation

`ToolCollection::<M>::collect_tools()` fails fast on the first tool whose
attributes don't match `M`. For CI, two helpers accumulate every failure
across the inventory before returning:

```rust
use tools_core::{validate_tool_attrs, validate_tool_attrs_for};
# #[derive(serde::Deserialize)] struct Policy;

#[test]
fn every_tool_conforms_to_policy() {
    validate_tool_attrs::<Policy>().unwrap();
}

#[test]
fn destructive_tools_have_approval_metadata() {
    // Subset gating: only check the named tools, error if any name doesn't
    // match a registered tool (typos in the test list are caught too).
    validate_tool_attrs_for::<Policy>(&["delete_file", "drop_table"]).unwrap();
}
```

### Attribute syntax

- `#[tool(key = "value")]` — string
- `#[tool(key = 42)]` — integer (negative literals are accepted: `-3`)
- `#[tool(key = 1.5)]` — float
- `#[tool(key = true)]` — boolean
- `#[tool(flag)]` — bare flag, equivalent to `flag = true`
- Multiple attributes are comma-separated: `#[tool(a = 1, b = "x", flag)]`

Attributes are flat-only — nested structures (`#[tool(policy = { ... })]`)
are not supported. Use richer types in runtime metadata, not at the
attribute site. The keys `name` and `description` are reserved (the
function name and doc comment supply them).

### Programmatic registration with metadata

`ToolCollection::register` takes a metadata argument. For untyped
collections, pass `()`; passing `()` to a typed collection is a compile
error.

```rust
# use tools_rs::ToolCollection;
# #[derive(Default)] struct Policy { requires_approval: bool }
let mut tools: ToolCollection<Policy> = ToolCollection::new();
tools.register(
    "noop",
    "does nothing",
    |_: ()| async {},
    Policy { requires_approval: false },
)?;
# Ok::<(), tools_rs::ToolError>(())
```

## Shared Context

Tools often need access to shared resources — database connections, HTTP
clients, caches. If a tool's **first parameter is named `ctx`**, the macro
treats it as a shared context that the collection injects at call time.
Callers only pass the "real" arguments; `ctx` never appears in the JSON
schema.

```rust
use std::sync::Arc;
use tools_rs::{tool, ToolCollection, FunctionCall};
use tools_core::NoMeta;

struct Db {
    url: String,
}

#[tool]
/// Look up a user by name.
async fn find_user(ctx: Db, name: String) -> String {
    format!("[{}] SELECT * FROM users WHERE name = '{name}'", ctx.url)
}

#[tool]
/// A plain tool — no context needed.
async fn ping() -> String { "pong".into() }

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Arc::new(Db { url: "postgres://localhost/app".into() });

    let tools = ToolCollection::<NoMeta>::builder()
        .with_context(db)
        .collect()?;

    // Caller never mentions ctx — only the "real" args:
    let resp = tools.call(FunctionCall::new(
        "find_user".into(),
        serde_json::json!({ "name": "Alice" }),
    )).await?;
    println!("{}", resp.result);
    Ok(())
}
```

### How it works

1. **Macro detection** — if the first parameter is named `ctx`, it is
   excluded from the JSON schema wrapper struct. The macro rewrites the
   emitted function signature from `ctx: T` to `ctx: Arc<T>` so that field
   access and method calls work via `Deref`.
2. **Builder**`ToolCollection::builder().with_context(arc).collect()`
   stores the `Arc<T>` and validates at startup that every context-requiring
   tool expects the same type (via `TypeId` comparison). A mismatch produces
   a clear `CtxTypeMismatch` error.
3. **Call-time injection**`collection.call(...)` passes the stored
   context into the closure. Non-context tools ignore it.

### Important: write `ctx: T`, not `ctx: Arc<T>`

The macro wraps the type in `Arc` automatically. Writing `ctx: Arc<T>`
produces a compile error to prevent accidental double-wrapping.

### Interior mutability

`Arc<T>` is immutable, but you can wrap an interior-mutable type:

```rust
use std::sync::Mutex;
# use tools_rs::tool;

struct Cache { entries: Vec<String> }

#[tool]
/// Record a cache hit.
async fn record(ctx: Mutex<Cache>, key: String) -> String {
    ctx.lock().unwrap().entries.push(key.clone());
    format!("recorded {key}")
}
```

The builder receives `Arc::new(Mutex::new(cache))`. The tool sees
`Arc<Mutex<Cache>>` via Deref, so `ctx.lock()` works directly.

### Context + metadata

Context and metadata are orthogonal. Combine them freely:

```rust
# use std::sync::Arc;
# use serde::Deserialize;
# use tools_rs::{tool, ToolCollection};
# struct Db { url: String }
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct Policy { requires_approval: bool }

#[tool(requires_approval = true)]
/// Drop a table — requires approval.
async fn drop_table(ctx: Db, table: String) -> String {
    format!("[{}] DROP TABLE {table}", ctx.url)
}

# fn example() -> Result<(), tools_rs::ToolError> {
let tools = ToolCollection::<Policy>::builder()
    .with_context(Arc::new(Db { url: "pg://localhost".into() }))
    .collect()?;

let meta = tools.meta("drop_table").unwrap();
assert!(meta.requires_approval);
# Ok(())
# }
```

### Without context

`collect_tools()` and `ToolCollection::collect_tools()` still work for
collections that don't need context. If any tool in the inventory requires
context and you call `collect_tools()` without the builder, it fails with
`ToolError::MissingCtx` at startup.

## Examples

Check out the [examples directory](examples/) for comprehensive sample code:

```bash
# Run the basic example - simple tool registration and calling
cargo run --example basic

# Run the function declarations example - LLM integration demo
cargo run --example function_declarations

# Run the schema example - complex type schemas and validation
cargo run --example schema

# Run the newtype demo - custom type wrapping examples
cargo run --example newtype_demo

# Run the HITL example - human-in-the-loop approval gating with metadata
cargo run --example hitl

# Run the context example - shared context injection
cargo run --example context
```

Each example demonstrates different aspects of the framework:

- **basic**: Simple tool registration with `#[tool]` and basic function calls
- **function_declarations**: Complete LLM integration workflow with JSON schema generation
- **schema**: Advanced schema generation for complex nested types and collections
- **newtype_demo**: Working with custom wrapper types and serialization patterns
- **hitl**: Human-in-the-loop approval gating using typed metadata
- **context**: Shared context injection via `ToolCollection::builder().with_context(...)`

## API Reference

### Core Functions

- `collect_tools()` - Discover all tools registered via `#[tool]` macro
- `function_declarations()` - Generate JSON schema declarations for LLMs
- `call_tool(name, args)` - Execute a tool by name with JSON arguments
- `call_tool_with(name, typed_args)` - Execute a tool with typed arguments
- `call_tool_by_name(collection, name, args)` - Execute tool on specific collection
- `list_tool_names(collection)` - List all available tool names

### Core Types

- `ToolCollection<M>` - Container for registered tools, generic over metadata type `M` (defaults to `NoMeta`)
- `CollectionBuilder<M>` - Builder for constructing collections with shared context
- `ToolEntry<M>` - A single tool entry with its function, declaration, and metadata
- `FunctionCall` - Represents a tool invocation with id, name, and arguments
- `FunctionResponse` - Represents the response of a tool invocation with matching id to call, name, and result
- `ToolError` - Comprehensive error type for tool operations
- `NoMeta` - Default metadata type that ignores all `#[tool(...)]` attributes
- `ToolSchema` - Trait for automatic JSON schema generation
- `ToolRegistration` - Internal representation of registered tools
- `FunctionDecl` - LLM-compatible function declaration structure

### Macros

- `#[tool]` - Attribute macro for automatic tool registration. Accepts flat `key = value` attributes for metadata. Detects `ctx` as a reserved first parameter for shared context injection.
- `#[derive(ToolSchema)]` - Derive macro for automatic schema generation

## Error Handling

Tools-rs provides comprehensive error handling with detailed context:

```rust
use tools_rs::{ToolError, collect_tools, FunctionCall};
use serde_json::json;

#[tokio::main]
async fn main() {
    let tools = collect_tools();

    match tools.call(FunctionCall::new(
        "nonexistent".to_string(),
        json!({}),
    )).await {
        Ok(response) => println!("Result: {}", response.result),
        Err(ToolError::FunctionNotFound { name }) => {
            println!("Tool '{}' not found", name);
        },
        Err(ToolError::Deserialize(err)) => {
            println!("Deserialization error: {}", err.source);
        },
        Err(e) => println!("Other error: {}", e),
    }
}
```

## Performance Considerations

### Schema Caching
- JSON schemas are generated once and cached.
- Schema generation has minimal runtime overhead after first access
- Primitive types use pre-computed static schemas for optimal performance

### Tool Discovery
- Tool registration happens at compile-time via the `inventory` crate
- Runtime tool collection (`collect_tools()`) is a zero-cost operation
- Tools are stored in efficient hash maps for O(1) lookup by name

### Execution Performance
- Tool calls have minimal overhead beyond JSON serialization/deserialization
- Async execution allows for concurrent tool invocation
- Error handling uses `Result` types to avoid exceptions and maintain performance

### Memory Usage
- Tool metadata is stored statically with minimal heap allocation
- JSON schemas are shared across all instances of the same type
- Function declarations are generated on-demand and can be cached by the application

### Optimization Tips

```rust
// Reuse ToolCollection instances to avoid repeated discovery
let tools = collect_tools(); // Call once, reuse multiple times

// Generate function declarations once for LLM integration
let declarations = function_declarations()?;
// Cache and reuse declarations across multiple LLM requests

// Use typed parameters to avoid repeated JSON parsing
use tools_rs::call_tool_with;
let result = call_tool_with("my_tool", &my_typed_args).await?.result;
```

## Troubleshooting

### Common Issues

**Tool not found at runtime**
- Ensure the `#[tool]` macro is applied to your function
- Verify the function is in a module that gets compiled (not behind unused feature flags)
- Check that `inventory` is properly collecting tools with `collect_tools()`

**Schema generation errors**
- Ensure all parameter and return types implement `ToolSchema`
- For custom types, add `#[derive(ToolSchema)]` to struct definitions
- Complex generic types may need manual `ToolSchema` implementations

**Deserialization failures**
- Verify JSON arguments match the expected parameter structure
- Check that argument names match function parameter names exactly
- Use `serde` attributes like `#[serde(rename = "...")]` for custom field names

**Async execution issues**
- All tool functions must be `async fn` when using `#[tool]`
- Ensure you're using `tokio` runtime for async execution
- Tool execution is inherently async - use `.await` when calling tools

### Debugging Tips

```rust
// Enable debug logging to see tool registration and execution
use tools_rs::{collect_tools, list_tool_names};

let tools = collect_tools();
println!("Registered tools: {:?}", list_tool_names(&tools));

// Inspect generated schemas
let declarations = tools.json()?;
println!("Function declarations: {}", serde_json::to_string_pretty(&declarations)?);
```

## Contributing

We welcome contributions!

### Development Setup

```bash
# Clone the repository
git clone https://github.com/EggerMarc/tools-rs
cd tools-rs 

# Run tests
cargo test

# Run examples
cargo run --example basic
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.