radkit_macros/lib.rs
1//! Procedural macros for the radkit agent framework.
2//!
3//! This crate provides attribute macros for defining A2A-compliant skills and tools.
4
5#![deny(unsafe_code, unreachable_patterns, unused_must_use)]
6#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
7#![allow(clippy::module_name_repetitions)] // Common pattern in proc macro crates
8
9mod skill;
10mod tool;
11mod validation;
12
13use proc_macro::TokenStream;
14use syn::parse_macro_input;
15
16/// Attribute macro for defining A2A skills with metadata.
17///
18/// This macro generates the `SkillMetadata` and implements the `RegisteredSkill` trait
19/// for the annotated struct, making it usable with the radkit agent builder.
20///
21/// # Required Parameters
22///
23/// - `id`: A unique identifier for the skill (String)
24/// - `name`: A human-readable name for the skill (String)
25/// - `description`: A detailed description of what the skill does (String)
26///
27/// # Optional Parameters
28///
29/// - `tags`: Array of keywords describing the skill's capabilities (default: [])
30/// - `examples`: Array of example prompts or scenarios (default: [])
31/// - `input_modes`: Array of supported input MIME types (default: [])
32/// - `output_modes`: Array of supported output MIME types (default: [])
33///
34/// # MIME Type Validation
35///
36/// The macro validates `input_modes` and `output_modes` against a list of common MIME types.
37/// If an invalid type is provided, a compile error will be generated with suggestions.
38///
39/// # Example
40///
41/// ```ignore
42/// use radkit::prelude::*;
43///
44/// #[skill(
45/// id = "summarize_text",
46/// name = "Text Summarizer",
47/// description = "Summarizes long text documents into concise summaries",
48/// tags = ["text", "summarization", "nlp"],
49/// examples = [
50/// "Summarize this article",
51/// "Give me a brief summary of this document"
52/// ],
53/// input_modes = ["text/plain", "text/markdown"],
54/// output_modes = ["text/plain", "application/json"]
55/// )]
56/// pub struct SummarizeTextSkill;
57///
58/// #[async_trait]
59/// impl SkillHandler for SummarizeTextSkill {
60/// async fn on_request(
61/// &self,
62/// task_context: &mut TaskContext,
63/// context: &Context,
64/// runtime: &dyn Runtime,
65/// content: Content,
66/// ) -> Result<OnRequestResult, AgentError> {
67/// // Implementation here
68/// Ok(OnRequestResult::Completed {
69/// message: Some(Content::text("Summary here")),
70/// artifacts: vec![],
71/// })
72/// }
73/// }
74/// ```
75///
76/// # Generated Code
77///
78/// The macro generates:
79/// 1. A static `SkillMetadata` constant named `{STRUCT_NAME}_METADATA`
80/// 2. An implementation of `RegisteredSkill` trait for the struct
81///
82/// This allows the skill to be registered with an agent using `.with_skill()`:
83///
84/// ```ignore
85/// let agent = AgentBuilder::new()
86/// .with_skill(SummarizeTextSkill)
87/// .build(runtime)?;
88/// ```
89#[proc_macro_attribute]
90pub fn skill(attr: TokenStream, item: TokenStream) -> TokenStream {
91 let args = parse_macro_input!(attr as skill::SkillArgs);
92 let item = proc_macro2::TokenStream::from(item);
93
94 skill::generate_skill_impl(args, item).into()
95}
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]
160pub 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}