Skip to content

Add #[tool] macro for ergonomic tool creation#24

Merged
irshadnilam merged 9 commits intoagents-sh:mainfrom
grainier:tool-macro
Nov 12, 2025
Merged

Add #[tool] macro for ergonomic tool creation#24
irshadnilam merged 9 commits intoagents-sh:mainfrom
grainier:tool-macro

Conversation

@grainier
Copy link
Copy Markdown
Contributor

@grainier grainier commented Nov 10, 2025

Introduces a procedural macro that simplifies tool definition and refactors tool storage for better ergonomics and performance.

Key Changes

  1. #[tool] macro generates tool structs - Function name becomes the tool name
  2. Storage refactoring - Changed from Arc<dyn BaseTool> to Box<dyn BaseTool> for unique ownership
  3. Borrowed tool references - BaseToolset::get_tools() now returns Vec<&dyn BaseTool> with lifetimes
  4. Generic tool registration - with_tool() accepts any BaseTool implementation

Usage

Before:

let weather_tool = Arc::new(FunctionTool::new(
    "get_weather",
    "Get current weather",
    |args: HashMap<String, Value>, _ctx: &ToolContext| {
        Box::pin(async move {
            let location: String = serde_json::from_value(
                args.get("location").cloned().unwrap_or_default()
            )?;
            ToolResult::success(json!({
                "temperature": 72.5,
                "condition": "sunny",
                "location": location
            }))
        })
    },
));

let worker = LlmWorker::builder(llm)
    .with_tool(weather_tool)
    .build();

After:

#[derive(Deserialize, JsonSchema)]
struct GetWeatherArgs {
    location: String,
}

#[tool(description = "Get current weather")]
async fn get_weather(args: GetWeatherArgs) -> ToolResult {
    ToolResult::success(json!({
        "temperature": 72.5,
        "condition": "sunny",
        "location": args.location
    }))
}

// Use it - function name becomes tool name, struct-based tool
let worker = LlmWorker::builder(llm)
    .with_tool(get_weather)  // Pass struct directly, no Arc wrapper needed
    .build();

Benefits

  • Less boilerplate - No manual FunctionTool::new() or Arc wrapping
  • Type safety - Structured args with serde validation and JSON schema generation
  • Better ergonomics - Tool name derived from function name automatically
  • Clearer ownership - Box for unique ownership, borrowed references for access
  • Efficient sharing - No Arc cloning overhead in toolsets

Implementation Details

The macro generates a zero-sized struct that implements BaseTool:

#[derive(Clone, Copy, Debug)]
struct get_weather;

impl BaseTool for get_weather {
    fn name(&self) -> &str { "get_weather" }
    fn description(&self) -> &str { "Get current weather" }
    // ... schema and execute implementation
}

Toolsets store tools as Vec<Box<dyn BaseTool>> and return borrowed references via get_tools() -> Vec<&dyn BaseTool>, eliminating unnecessary Arc cloning while maintaining efficient access patterns.

@grainier grainier requested a review from irshadnilam November 10, 2025 06:59
This commit simplifies the tool macro implementation and optimizes tool
storage patterns across the codebase.

Key changes:
- Tool macro now generates structs implementing BaseTool directly
- Removed support for custom tool names (function name = tool name)
- Changed tool storage from Arc<dyn BaseTool> to Box<dyn BaseTool>
- BaseToolset.get_tools() returns Vec<&dyn BaseTool> (borrowed refs)
- MCPToolset uses OnceCell for lazy initialization
- OpenApiToolSet updated to match new storage pattern
- RecordingTool made cloneable for testing convenience
- All tests updated and passing

Performance benefits:
- Reduced allocations (references instead of Arc clones)
- Maintained efficient sharing via Arc for internal resources
- Lazy initialization preserved for MCP tools
Integrated upstream changes including:
- React-based dev UI
- WASM compatibility improvements for MCP/OpenAPI
- Streaming delay fixes
- Additional test coverage

All our tool macro changes preserved:
- Struct generation with BaseTool implementation
- Box storage with borrowed references
- Generic with_tool methods
- RecordingTool Clone support

Conflicts resolved in:
- Cargo.lock (regenerated)
- radkit/tests/worker_execution.rs (combined assertions)
@irshadnilam irshadnilam merged commit e5641ff into agents-sh:main Nov 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants