synaptic_macros/lib.rs
1//! Procedural macros for the Synaptic framework.
2//!
3//! This crate provides attribute macros that reduce boilerplate when defining
4//! tools, runnable chains, graph entrypoints, tasks, middleware hooks, and
5//! traced functions.
6//!
7//! # Macros
8//!
9//! | Macro | Description |
10//! |-------|-------------|
11//! | [`#[tool]`](macro@tool) | Convert an async fn into a `Tool` implementor |
12//! | [`#[chain]`](macro@chain) | Convert an async fn into a `BoxRunnable` |
13//! | [`#[entrypoint]`](macro@entrypoint) | Define a LangGraph-style workflow entry point |
14//! | [`#[task]`](macro@task) | Define a trackable task inside an entrypoint |
15//! | [`#[before_agent]`](macro@before_agent) | Middleware: before agent loop |
16//! | [`#[before_model]`](macro@before_model) | Middleware: before model call |
17//! | [`#[after_model]`](macro@after_model) | Middleware: after model call |
18//! | [`#[after_agent]`](macro@after_agent) | Middleware: after agent loop |
19//! | [`#[wrap_model_call]`](macro@wrap_model_call) | Middleware: wrap model call |
20//! | [`#[wrap_tool_call]`](macro@wrap_tool_call) | Middleware: wrap tool call |
21//! | [`#[dynamic_prompt]`](macro@dynamic_prompt) | Middleware: dynamic system prompt |
22//! | [`#[traceable]`](macro@traceable) | Add tracing instrumentation |
23
24extern crate proc_macro;
25
26mod chain;
27mod entrypoint;
28mod middleware;
29mod task;
30mod tool;
31mod traceable;
32
33use proc_macro::TokenStream;
34
35/// Convert an async function into a struct that implements `synaptic_core::Tool`.
36///
37/// # Features
38///
39/// - Function name becomes the tool name
40/// - Doc comments become the tool description
41/// - Parameters are mapped to a JSON Schema automatically
42/// - `Option<T>` parameters are optional (not in `required`)
43/// - `#[default = value]` sets a default for a parameter
44/// - `#[inject(state)]`, `#[inject(store)]`, `#[inject(tool_call_id)]` inject
45/// runtime values (the parameter is hidden from the LLM schema); using any
46/// inject attribute switches the generated impl to `RuntimeAwareTool`
47/// - Parameter doc comments become `"description"` in the schema
48///
49/// # Example
50///
51/// ```ignore
52/// use synaptic_macros::tool;
53/// use synaptic_core::SynapticError;
54///
55/// /// Search the web for information.
56/// #[tool]
57/// async fn search(
58/// /// The search query
59/// query: String,
60/// /// Maximum number of results
61/// #[default = 5]
62/// max_results: i64,
63/// ) -> Result<String, SynapticError> {
64/// Ok(format!("Searching for '{}' (max {})", query, max_results))
65/// }
66///
67/// // `search` is now a function returning `Arc<dyn Tool>`
68/// let tool = search();
69/// ```
70///
71/// # Custom name
72///
73/// ```ignore
74/// #[tool(name = "web_search")]
75/// async fn search(query: String) -> Result<String, SynapticError> {
76/// Ok(format!("Searching for '{}'", query))
77/// }
78/// ```
79#[proc_macro_attribute]
80pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
81 tool::expand_tool(attr.into(), item.into())
82 .unwrap_or_else(|e| e.to_compile_error())
83 .into()
84}
85
86/// Convert an async function into a `BoxRunnable<InputType, OutputType>` factory.
87///
88/// The macro generates a public function with the same name that returns a
89/// `BoxRunnable` backed by a `RunnableLambda`.
90///
91/// The output type is inferred from the function signature:
92/// - `Result<Value, _>` → `BoxRunnable<I, Value>` (serializes to Value)
93/// - `Result<String, _>` → `BoxRunnable<I, String>` (direct return)
94/// - `Result<T, _>` → `BoxRunnable<I, T>` (direct return)
95///
96/// # Example
97///
98/// ```ignore
99/// use synaptic_macros::chain;
100/// use synaptic_core::SynapticError;
101/// use serde_json::Value;
102///
103/// #[chain]
104/// async fn uppercase(input: Value) -> Result<Value, SynapticError> {
105/// let s = input.as_str().unwrap_or_default().to_uppercase();
106/// Ok(Value::String(s))
107/// }
108///
109/// // Returns BoxRunnable<Value, Value>
110/// let runnable = uppercase();
111///
112/// // Typed output — returns BoxRunnable<String, String>
113/// #[chain]
114/// async fn to_upper(s: String) -> Result<String, SynapticError> {
115/// Ok(s.to_uppercase())
116/// }
117/// ```
118#[proc_macro_attribute]
119pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
120 chain::expand_chain(attr.into(), item.into())
121 .unwrap_or_else(|e| e.to_compile_error())
122 .into()
123}
124
125/// Define a LangGraph-style workflow entry point.
126///
127/// Converts an async function that takes `serde_json::Value` and returns
128/// `Result<serde_json::Value, SynapticError>` into a factory function that
129/// returns an [`Entrypoint`](::synaptic_core::Entrypoint).
130///
131/// # Attributes
132///
133/// - `name = "..."` — override the entrypoint name (defaults to the function name)
134/// - `checkpointer = "..."` — hint which checkpointer backend to use (e.g. `"memory"`)
135///
136/// # Example
137///
138/// ```ignore
139/// use synaptic_macros::entrypoint;
140/// use synaptic_core::SynapticError;
141/// use serde_json::Value;
142///
143/// #[entrypoint(checkpointer = "memory")]
144/// async fn my_workflow(input: Value) -> Result<Value, SynapticError> {
145/// Ok(input)
146/// }
147///
148/// // `my_workflow` is now a function returning `Entrypoint`
149/// let ep = my_workflow();
150/// ```
151#[proc_macro_attribute]
152pub fn entrypoint(attr: TokenStream, item: TokenStream) -> TokenStream {
153 entrypoint::expand_entrypoint(attr.into(), item.into())
154 .unwrap_or_else(|e| e.to_compile_error())
155 .into()
156}
157
158/// Define a trackable task inside an entrypoint.
159///
160/// Wraps an async function so that it carries a task name for tracing and
161/// streaming identification. The original function body is moved into a
162/// private `{name}_impl` helper and a public wrapper delegates to it.
163///
164/// # Attributes
165///
166/// - `name = "..."` — override the task name (defaults to the function name)
167///
168/// # Example
169///
170/// ```ignore
171/// use synaptic_macros::task;
172/// use synaptic_core::SynapticError;
173///
174/// #[task]
175/// async fn fetch_weather(city: String) -> Result<String, SynapticError> {
176/// Ok(format!("Sunny in {}", city))
177/// }
178///
179/// // `fetch_weather` can be called directly — it forwards to `fetch_weather_impl`
180/// let result = fetch_weather("Paris".into()).await;
181/// ```
182#[proc_macro_attribute]
183pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
184 task::expand_task(attr.into(), item.into())
185 .unwrap_or_else(|e| e.to_compile_error())
186 .into()
187}
188
189/// Middleware: run a hook before the agent loop starts.
190///
191/// The decorated async function must accept `&mut Vec<Message>` and return
192/// `Result<(), SynapticError>`. The macro generates a struct that implements
193/// `AgentMiddleware` with only `before_agent` overridden, plus a factory
194/// function returning `Arc<dyn AgentMiddleware>`.
195///
196/// # Example
197///
198/// ```ignore
199/// use synaptic_macros::before_agent;
200/// use synaptic_core::{Message, SynapticError};
201///
202/// #[before_agent]
203/// async fn setup(messages: &mut Vec<Message>) -> Result<(), SynapticError> {
204/// println!("Agent starting with {} messages", messages.len());
205/// Ok(())
206/// }
207///
208/// let mw = setup(); // Arc<dyn AgentMiddleware>
209/// ```
210#[proc_macro_attribute]
211pub fn before_agent(attr: TokenStream, item: TokenStream) -> TokenStream {
212 middleware::expand_before_agent(attr.into(), item.into())
213 .unwrap_or_else(|e| e.to_compile_error())
214 .into()
215}
216
217/// Middleware: run a hook before each model call.
218///
219/// The decorated async function must accept `&mut ModelRequest` and return
220/// `Result<(), SynapticError>`.
221///
222/// # Example
223///
224/// ```ignore
225/// use synaptic_macros::before_model;
226/// use synaptic_middleware::ModelRequest;
227/// use synaptic_core::SynapticError;
228///
229/// #[before_model]
230/// async fn add_context(request: &mut ModelRequest) -> Result<(), SynapticError> {
231/// request.system_prompt = Some("Be helpful".into());
232/// Ok(())
233/// }
234///
235/// let mw = add_context(); // Arc<dyn AgentMiddleware>
236/// ```
237#[proc_macro_attribute]
238pub fn before_model(attr: TokenStream, item: TokenStream) -> TokenStream {
239 middleware::expand_before_model(attr.into(), item.into())
240 .unwrap_or_else(|e| e.to_compile_error())
241 .into()
242}
243
244/// Middleware: run a hook after each model call.
245///
246/// The decorated async function must accept `&ModelRequest` and
247/// `&mut ModelResponse`, returning `Result<(), SynapticError>`.
248///
249/// # Example
250///
251/// ```ignore
252/// use synaptic_macros::after_model;
253/// use synaptic_middleware::{ModelRequest, ModelResponse};
254/// use synaptic_core::SynapticError;
255///
256/// #[after_model]
257/// async fn log_response(request: &ModelRequest, response: &mut ModelResponse) -> Result<(), SynapticError> {
258/// println!("Model responded: {}", response.message.content());
259/// Ok(())
260/// }
261///
262/// let mw = log_response(); // Arc<dyn AgentMiddleware>
263/// ```
264#[proc_macro_attribute]
265pub fn after_model(attr: TokenStream, item: TokenStream) -> TokenStream {
266 middleware::expand_after_model(attr.into(), item.into())
267 .unwrap_or_else(|e| e.to_compile_error())
268 .into()
269}
270
271/// Middleware: run a hook after the agent loop finishes.
272///
273/// The decorated async function must accept `&mut Vec<Message>` and return
274/// `Result<(), SynapticError>`.
275///
276/// # Example
277///
278/// ```ignore
279/// use synaptic_macros::after_agent;
280/// use synaptic_core::{Message, SynapticError};
281///
282/// #[after_agent]
283/// async fn cleanup(messages: &mut Vec<Message>) -> Result<(), SynapticError> {
284/// println!("Agent done");
285/// Ok(())
286/// }
287///
288/// let mw = cleanup(); // Arc<dyn AgentMiddleware>
289/// ```
290#[proc_macro_attribute]
291pub fn after_agent(attr: TokenStream, item: TokenStream) -> TokenStream {
292 middleware::expand_after_agent(attr.into(), item.into())
293 .unwrap_or_else(|e| e.to_compile_error())
294 .into()
295}
296
297/// Middleware: wrap the model call with custom logic.
298///
299/// The decorated async function must accept `ModelRequest` and
300/// `&dyn ModelCaller`, returning `Result<ModelResponse, SynapticError>`.
301/// This enables retry, fallback, and other wrapping patterns.
302///
303/// # Example
304///
305/// ```ignore
306/// use synaptic_macros::wrap_model_call;
307/// use synaptic_middleware::{ModelRequest, ModelResponse, ModelCaller};
308/// use synaptic_core::SynapticError;
309///
310/// #[wrap_model_call]
311/// async fn retry_model(request: ModelRequest, next: &dyn ModelCaller) -> Result<ModelResponse, SynapticError> {
312/// match next.call(request.clone()).await {
313/// Ok(r) => Ok(r),
314/// Err(_) => next.call(request).await,
315/// }
316/// }
317///
318/// let mw = retry_model(); // Arc<dyn AgentMiddleware>
319/// ```
320#[proc_macro_attribute]
321pub fn wrap_model_call(attr: TokenStream, item: TokenStream) -> TokenStream {
322 middleware::expand_wrap_model_call(attr.into(), item.into())
323 .unwrap_or_else(|e| e.to_compile_error())
324 .into()
325}
326
327/// Middleware: wrap a tool call with custom logic.
328///
329/// The decorated async function must accept `ToolCallRequest` and
330/// `&dyn ToolCaller`, returning `Result<Value, SynapticError>`.
331///
332/// # Example
333///
334/// ```ignore
335/// use synaptic_macros::wrap_tool_call;
336/// use synaptic_middleware::{ToolCallRequest, ToolCaller};
337/// use synaptic_core::SynapticError;
338/// use serde_json::Value;
339///
340/// #[wrap_tool_call]
341/// async fn log_tool(request: ToolCallRequest, next: &dyn ToolCaller) -> Result<Value, SynapticError> {
342/// println!("Calling tool: {}", request.call.name);
343/// next.call(request).await
344/// }
345///
346/// let mw = log_tool(); // Arc<dyn AgentMiddleware>
347/// ```
348#[proc_macro_attribute]
349pub fn wrap_tool_call(attr: TokenStream, item: TokenStream) -> TokenStream {
350 middleware::expand_wrap_tool_call(attr.into(), item.into())
351 .unwrap_or_else(|e| e.to_compile_error())
352 .into()
353}
354
355/// Middleware: dynamically generate a system prompt based on current messages.
356///
357/// The decorated function (non-async) must accept `&[Message]` and return
358/// `String`. The macro generates a middleware whose `before_model` hook
359/// sets `request.system_prompt` to the return value.
360///
361/// # Example
362///
363/// ```ignore
364/// use synaptic_macros::dynamic_prompt;
365/// use synaptic_core::Message;
366///
367/// #[dynamic_prompt]
368/// fn custom_prompt(messages: &[Message]) -> String {
369/// format!("You have {} messages in context", messages.len())
370/// }
371///
372/// let mw = custom_prompt(); // Arc<dyn AgentMiddleware>
373/// ```
374#[proc_macro_attribute]
375pub fn dynamic_prompt(attr: TokenStream, item: TokenStream) -> TokenStream {
376 middleware::expand_dynamic_prompt(attr.into(), item.into())
377 .unwrap_or_else(|e| e.to_compile_error())
378 .into()
379}
380
381/// Add tracing instrumentation to an async or sync function.
382///
383/// Wraps the function body in a `tracing::info_span!` with the function name
384/// and parameter values recorded as span fields. Async functions use
385/// `tracing::Instrument` for correct span propagation.
386///
387/// # Attributes
388///
389/// - `name = "..."` — override the span name (defaults to the function name)
390/// - `skip = "a,b"` — comma-separated list of parameter names to exclude from the span
391///
392/// # Example
393///
394/// ```ignore
395/// use synaptic_macros::traceable;
396///
397/// #[traceable]
398/// async fn process_data(input: String, count: usize) -> String {
399/// format!("{}: {}", input, count)
400/// }
401///
402/// #[traceable(name = "custom_span", skip = "secret")]
403/// async fn with_secret(query: String, secret: String) -> String {
404/// format!("Processing: {}", query)
405/// }
406/// ```
407#[proc_macro_attribute]
408pub fn traceable(attr: TokenStream, item: TokenStream) -> TokenStream {
409 traceable::expand_traceable(attr.into(), item.into())
410 .unwrap_or_else(|e| e.to_compile_error())
411 .into()
412}