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