ftooling 3.0.0

Tool library for the fiddlesticks agent harness framework
Documentation
# Capability Layer API

`ftooling` provides the tool registration and execution runtime for Fiddlesticks.

It is designed to plug into `fchat` tool loops while staying provider-agnostic by using `fprovider` tool contracts.

## Responsibilities

- Register tools and expose their `ToolDefinition` metadata
- Execute tool calls from model output (`fprovider::ToolCall`)
- Return tool outputs as structured execution results
- Offer runtime hooks and timeout controls for observability and resilience

`ftooling` does **not**:

- Decide when to call tools during a conversation (`fchat` owns that)
- Persist conversation state (`fmemory` owns that)
- Implement model transport/provider adapters (`fprovider` owns that)

## Add dependency

```toml
[dependencies]
ftooling = { path = "../ftooling" }
fprovider = { path = "../fprovider" }
```

## Core types

- `Tool`: trait for executable capabilities
- `ToolRegistry`: registry keyed by tool name
- `ToolRuntime`: runtime contract for tool execution
- `DefaultToolRuntime`: registry-backed runtime implementation
- `ToolExecutionContext`: session/trace metadata passed to tools
- `ToolExecutionResult`: normalized output payload
- `ToolError`: typed error with retryability and optional call metadata

## Easiest registration path

You can register tools without implementing a custom struct.

### Sync closure

```rust
use fprovider::ToolDefinition;
use ftooling::prelude::*;

let mut registry = ToolRegistry::new();
registry.register_sync_fn(
    ToolDefinition {
        name: "echo".to_string(),
        description: "Echo input".to_string(),
        input_schema: "{\"type\":\"string\"}".to_string(),
    },
    |args, _ctx| Ok(args),
);
```

### Async closure

```rust
use fprovider::ToolDefinition;
use ftooling::prelude::*;

let mut registry = ToolRegistry::new();
registry.register_fn(
    ToolDefinition {
        name: "uppercase".to_string(),
        description: "Uppercase input".to_string(),
        input_schema: "{\"type\":\"string\"}".to_string(),
    },
    |args, _ctx| async move { Ok(args.to_uppercase()) },
);
```

## Runtime usage

```rust
use std::sync::Arc;

use fprovider::ToolCall;
use ftooling::prelude::*;

async fn run_once(registry: ToolRegistry) -> Result<ToolExecutionResult, ToolError> {
    let runtime = DefaultToolRuntime::new(Arc::new(registry))
        .with_timeout(std::time::Duration::from_secs(2));

    runtime
        .execute(
            ToolCall {
                id: "call_1".to_string(),
                name: "echo".to_string(),
                arguments: "hello".to_string(),
            },
            ToolExecutionContext::new("session-1").with_trace_id("trace-abc"),
        )
        .await
}
```

## Argument helper utilities

`ftooling` exposes lightweight JSON helpers so tool closures do not need to repeat parsing boilerplate:

- `parse_json_value(args_json)`
- `parse_json_object(args_json)`
- `required_string(&args, key)`

```rust
use ftooling::prelude::*;

let args = parse_json_object("{\"query\":\"rust\"}")?;
let query = required_string(&args, "query")?;
let _ = query;
```

## Hooks and timeout

- `DefaultToolRuntime::with_hooks(...)` attaches runtime lifecycle hooks
- `DefaultToolRuntime::with_timeout(...)` enforces per-call timeout
- Hook events include start/success/failure with elapsed duration
- Hook order contract:
  1) `on_execution_start`
  2) exactly one of `on_execution_success` or `on_execution_failure`

```rust
use std::sync::Arc;
use std::time::Duration;

use ftooling::prelude::*;

let runtime = DefaultToolRuntime::new(Arc::new(ToolRegistry::new()))
    .with_hooks(Arc::new(NoopToolRuntimeHooks))
    .with_timeout(Duration::from_millis(500));

let _ = runtime;
```

## Error model

`ToolErrorKind` variants:

- `NotFound`
- `InvalidArguments`
- `Execution`
- `Timeout`
- `Unauthorized`
- `Other`

`ToolError` includes:

- `retryable` for policy decisions
- optional `tool_name` and `tool_call_id` for richer context
- helper methods: `is_retryable()`, `is_user_error()`

When errors are produced by `DefaultToolRuntime`, `tool_name` and `tool_call_id` are populated automatically.

## Integration with `fchat`

`fchat` can consume `ftooling::ToolRuntime` directly:

- configure on `ChatService` via `.with_tool_runtime(...)`
- cap loops with `.with_max_tool_round_trips(...)`
- `fchat` maps `ToolError` to `ChatErrorKind::Tooling`