Skip to content

Commit e5641ff

Browse files
authored
Merge pull request #24 from grainier/tool-macro
Add `#[tool]` macro for ergonomic tool creation
2 parents e7c0015 + 047a6a9 commit e5641ff

34 files changed

Lines changed: 3110 additions & 235 deletions

Cargo.lock

Lines changed: 85 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

a2a-client/src/client.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,13 +623,12 @@ impl A2AClient {
623623
if let JsonRpcResponse::Success {
624624
id: Some(resp_id), ..
625625
} = &json_response
626+
&& resp_id != &request_id
626627
{
627-
if resp_id != &request_id {
628-
eprintln!(
629-
"WARNING: RPC response ID mismatch for method {}. Expected {:?}, got {:?}",
630-
method, request_id, resp_id
631-
);
632-
}
628+
eprintln!(
629+
"WARNING: RPC response ID mismatch for method {}. Expected {:?}, got {:?}",
630+
method, request_id, resp_id
631+
);
633632
}
634633

635634
Ok(json_response)

radkit-macros/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ proc-macro2 = "1.0"
1818
radkit = { path = "../radkit" }
1919
async-trait = "0.1"
2020
tokio = { version = "1", features = ["macros", "rt"] }
21+
serde = { version = "1.0", features = ["derive"] }
22+
schemars = "1.0.4"
23+
serde_json = "1.0"
24+
trybuild = "1.0"

radkit-macros/src/lib.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
//! Procedural macros for the radkit agent framework.
22
//!
3-
//! This crate provides the `#[skill]` attribute macro for defining A2A-compliant skills.
3+
//! This crate provides attribute macros for defining A2A-compliant skills and tools.
44
55
#![deny(unsafe_code, unreachable_patterns, unused_must_use)]
66
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
77
#![allow(clippy::module_name_repetitions)] // Common pattern in proc macro crates
88

99
mod skill;
10+
mod tool;
1011
mod validation;
1112

1213
use proc_macro::TokenStream;
@@ -92,3 +93,73 @@ pub fn skill(attr: TokenStream, item: TokenStream) -> TokenStream {
9293

9394
skill::generate_skill_impl(args, item).into()
9495
}
96+
97+
/// Attribute macro for defining tools with automatic parameter extraction.
98+
///
99+
/// This macro generates a zero-sized struct with the function name and implements
100+
/// the `BaseTool` trait directly, eliminating manual parameter extraction and JSON schema construction.
101+
///
102+
/// The function name is used as the tool name, so choose function names that accurately
103+
/// describe the tool's purpose.
104+
///
105+
/// # Required Parameters
106+
///
107+
/// - `description`: A detailed description of what the tool does (String)
108+
///
109+
/// # Example
110+
///
111+
/// ```ignore
112+
/// use radkit::tools::{ToolResult, ToolContext};
113+
/// use radkit_macros::tool;
114+
/// use serde::{Deserialize};
115+
/// use schemars::JsonSchema;
116+
/// use serde_json::json;
117+
///
118+
/// #[derive(Deserialize, JsonSchema)]
119+
/// struct AddArgs {
120+
/// a: i64,
121+
/// b: i64,
122+
/// }
123+
///
124+
/// #[tool(description = "Add two numbers")]
125+
/// async fn add(args: AddArgs) -> ToolResult {
126+
/// ToolResult::success(json!({"sum": args.a + args.b}))
127+
/// }
128+
///
129+
/// // With ToolContext
130+
/// #[derive(Deserialize, JsonSchema)]
131+
/// struct SaveArgs {
132+
/// key: String,
133+
/// value: String,
134+
/// }
135+
///
136+
/// #[tool(description = "Save state")]
137+
/// async fn save_state(args: SaveArgs, ctx: &ToolContext<'_>) -> ToolResult {
138+
/// ctx.state().set_state(&args.key, json!(args.value));
139+
/// ToolResult::success(json!({"saved": true}))
140+
/// }
141+
/// ```
142+
///
143+
/// # Generated Code
144+
///
145+
/// The macro transforms the async function into a zero-sized struct that implements
146+
/// `BaseTool`. Parameters are automatically deserialized using serde and the JSON
147+
/// schema is generated using schemars. The function name becomes both the struct
148+
/// name and the tool name visible to the LLM.
149+
///
150+
/// # Usage
151+
///
152+
/// ```ignore
153+
/// // Pass the tool struct directly to with_tool() - no function call!
154+
/// let worker = LlmWorker::builder(llm)
155+
/// .with_tool(add) // ← Not add()
156+
/// .with_tool(save_state) // ← Not save_state()
157+
/// .build();
158+
/// ```
159+
#[proc_macro_attribute]
160+
pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
161+
let args = parse_macro_input!(attr as tool::ToolArgs);
162+
let item = proc_macro2::TokenStream::from(item);
163+
164+
tool::generate_tool_impl(args, item).into()
165+
}

0 commit comments

Comments
 (0)