Skip to main content

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//! and the `include_skill!` function macro for embedding `AgentSkills` at compile time.
5
6#![deny(unsafe_code, unreachable_patterns, unused_must_use)]
7#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
8#![allow(clippy::module_name_repetitions)] // Common pattern in proc macro crates
9
10mod agentskill;
11mod skill;
12mod tool;
13mod validation;
14
15use proc_macro::TokenStream;
16use syn::parse_macro_input;
17
18/// Attribute macro for defining A2A skills with metadata.
19///
20/// This macro generates the `SkillMetadata` and implements the `RegisteredSkill` trait
21/// for the annotated struct, making it usable with the radkit agent builder.
22///
23/// # Required Parameters
24///
25/// - `id`: A unique identifier for the skill (String)
26/// - `name`: A human-readable name for the skill (String)
27/// - `description`: A detailed description of what the skill does (String)
28///
29/// # Optional Parameters
30///
31/// - `tags`: Array of keywords describing the skill's capabilities (default: [])
32/// - `examples`: Array of example prompts or scenarios (default: [])
33/// - `input_modes`: Array of supported input MIME types (default: [])
34/// - `output_modes`: Array of supported output MIME types (default: [])
35///
36/// # MIME Type Validation
37///
38/// The macro validates `input_modes` and `output_modes` against a list of common MIME types.
39/// If an invalid type is provided, a compile error will be generated with suggestions.
40///
41/// # Example
42///
43/// ```ignore
44/// use radkit::prelude::*;
45///
46/// #[skill(
47///     id = "summarize_text",
48///     name = "Text Summarizer",
49///     description = "Summarizes long text documents into concise summaries",
50///     tags = ["text", "summarization", "nlp"],
51///     examples = [
52///         "Summarize this article",
53///         "Give me a brief summary of this document"
54///     ],
55///     input_modes = ["text/plain", "text/markdown"],
56///     output_modes = ["text/plain", "application/json"]
57/// )]
58/// pub struct SummarizeTextSkill;
59///
60/// #[async_trait]
61/// impl SkillHandler for SummarizeTextSkill {
62///     async fn on_request(
63///         &self,
64///         task_context: &mut TaskContext,
65///         context: &Context,
66///         runtime: &dyn Runtime,
67///         content: Content,
68///     ) -> Result<OnRequestResult, AgentError> {
69///         // Implementation here
70///         Ok(OnRequestResult::Completed {
71///             message: Some(Content::text("Summary here")),
72///             artifacts: vec![],
73///         })
74///     }
75/// }
76/// ```
77///
78/// # Generated Code
79///
80/// The macro generates:
81/// 1. A static `SkillMetadata` constant named `{STRUCT_NAME}_METADATA`
82/// 2. An implementation of `RegisteredSkill` trait for the struct
83///
84/// This allows the skill to be registered with an agent using `.with_skill()`:
85///
86/// ```ignore
87/// let agent = AgentBuilder::new()
88///     .with_skill(SummarizeTextSkill)
89///     .build(runtime)?;
90/// ```
91#[proc_macro_attribute]
92pub fn skill(attr: TokenStream, item: TokenStream) -> TokenStream {
93    let args = parse_macro_input!(attr as skill::SkillArgs);
94    let item = proc_macro2::TokenStream::from(item);
95
96    skill::generate_skill_impl(args, item).into()
97}
98
99/// Attribute macro for defining tools with automatic parameter extraction.
100///
101/// This macro generates a zero-sized struct with the function name and implements
102/// the `BaseTool` trait directly, eliminating manual parameter extraction and JSON schema construction.
103///
104/// The function name is used as the tool name, so choose function names that accurately
105/// describe the tool's purpose.
106///
107/// # Required Parameters
108///
109/// - `description`: A detailed description of what the tool does (String)
110///
111/// # Example
112///
113/// ```ignore
114/// use radkit::tools::{ToolResult, ToolContext};
115/// use radkit_macros::tool;
116/// use serde::{Deserialize};
117/// use schemars::JsonSchema;
118/// use serde_json::json;
119///
120/// #[derive(Deserialize, JsonSchema)]
121/// struct AddArgs {
122///     a: i64,
123///     b: i64,
124/// }
125///
126/// #[tool(description = "Add two numbers")]
127/// async fn add(args: AddArgs) -> ToolResult {
128///     ToolResult::success(json!({"sum": args.a + args.b}))
129/// }
130///
131/// // With ToolContext
132/// #[derive(Deserialize, JsonSchema)]
133/// struct SaveArgs {
134///     key: String,
135///     value: String,
136/// }
137///
138/// #[tool(description = "Save state")]
139/// async fn save_state(args: SaveArgs, ctx: &ToolContext<'_>) -> ToolResult {
140///     ctx.state().set_state(&args.key, json!(args.value));
141///     ToolResult::success(json!({"saved": true}))
142/// }
143/// ```
144///
145/// # Generated Code
146///
147/// The macro transforms the async function into a zero-sized struct that implements
148/// `BaseTool`. Parameters are automatically deserialized using serde and the JSON
149/// schema is generated using schemars. The function name becomes both the struct
150/// name and the tool name visible to the LLM.
151///
152/// # Usage
153///
154/// ```ignore
155/// // Pass the tool struct directly to with_tool() - no function call!
156/// let worker = LlmWorker::builder(llm)
157///     .with_tool(add)         // ← Not add()
158///     .with_tool(save_state)  // ← Not save_state()
159///     .build();
160/// ```
161#[proc_macro_attribute]
162pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
163    let args = parse_macro_input!(attr as tool::ToolArgs);
164    let item = proc_macro2::TokenStream::from(item);
165
166    tool::generate_tool_impl(args, item).into()
167}
168
169/// Embeds an `AgentSkill` directory into the binary at compile time.
170///
171/// Reads the `SKILL.md` file from the given path (relative to the crate root),
172/// validates its frontmatter structure, and returns an [`AgentSkillDef`] value
173/// that can be passed to [`AgentBuilder::with_skill_def`].
174///
175/// Because `include_str!` is used internally, the `SKILL.md` content is compiled
176/// into the binary — no filesystem I/O happens at runtime, and this works on
177/// WASM targets.
178///
179/// [`AgentSkillDef`]: radkit::agent::AgentSkillDef
180/// [`AgentBuilder::with_skill_def`]: radkit::agent::AgentBuilder::with_skill_def
181///
182/// # Panics (at startup, not compile time)
183///
184/// If the embedded SKILL.md fails full validation (e.g. the `name` field is
185/// invalid), the process will panic at startup with a clear message.
186///
187/// # Compile errors
188///
189/// - `SKILL.md` does not exist at the given path
190/// - `SKILL.md` does not begin with `---`
191/// - `SKILL.md` frontmatter is not closed with `---`
192///
193/// # Examples
194///
195/// ```ignore
196/// use radkit::{agent::Agent, include_skill};
197///
198/// let agent = Agent::builder()
199///     .with_name("My Agent")
200///     .with_skill_def(include_skill!("./skills/pdf-processing"))
201///     .build();
202/// ```
203#[proc_macro]
204pub fn include_skill(input: TokenStream) -> TokenStream {
205    agentskill::generate_include_skill(input)
206}