1use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::quote;
9use syn::{
10 spanned::Spanned,
11 parse_macro_input, parse_quote,
12 punctuated::Punctuated,
13 Expr, ItemStruct, Lit, Meta, MetaList, Type, token::Comma,
14};
15
16#[proc_macro_attribute]
17pub fn askit(attr: TokenStream, item: TokenStream) -> TokenStream {
18 askit_agent(attr, item)
19}
20
21#[proc_macro_attribute]
40pub fn askit_agent(attr: TokenStream, item: TokenStream) -> TokenStream {
41 let args = parse_macro_input!(attr with Punctuated<Meta, Comma>::parse_terminated);
42 let item_struct = parse_macro_input!(item as ItemStruct);
43
44 match expand_askit_agent(args, item_struct) {
45 Ok(tokens) => tokens.into(),
46 Err(err) => err.into_compile_error().into(),
47 }
48}
49
50struct AgentArgs {
51 kind: Option<Expr>,
52 name: Option<Expr>,
53 title: Option<Expr>,
54 description: Option<Expr>,
55 category: Option<Expr>,
56 inputs: Vec<Expr>,
57 outputs: Vec<Expr>,
58 configs: Vec<ConfigSpec>,
59 global_configs: Vec<ConfigSpec>,
60 displays: Vec<DisplaySpec>,
61}
62
63#[derive(Default)]
64struct CommonConfig {
65 name: Option<Expr>,
66 default: Option<Expr>,
67 title: Option<Expr>,
68 description: Option<Expr>,
69}
70
71enum ConfigSpec {
72 Unit(CommonConfig),
73 Boolean(CommonConfig),
74 Integer(CommonConfig),
75 Number(CommonConfig),
76 String(CommonConfig),
77 Text(CommonConfig),
78 Object(CommonConfig),
79}
80
81enum DisplaySpec {
82 Unit(CommonDisplay),
83 Boolean(CommonDisplay),
84 Integer(CommonDisplay),
85 Number(CommonDisplay),
86 String(CommonDisplay),
87 Text(CommonDisplay),
88 Object(CommonDisplay),
89 Any(CommonDisplay),
90}
91
92#[derive(Default)]
93struct CommonDisplay {
94 name: Option<Expr>,
95 title: Option<Expr>,
96 description: Option<Expr>,
97 hide_title: bool,
98}
99
100fn expand_askit_agent(
101 args: Punctuated<Meta, Comma>,
102 item: ItemStruct,
103) -> syn::Result<proc_macro2::TokenStream> {
104 let has_data_field = item.fields.iter().any(|f| match (&f.ident, &f.ty) {
105 (Some(ident), Type::Path(tp)) if ident == "data" => {
106 tp.path
107 .segments
108 .last()
109 .map(|seg| seg.ident == "AsAgentData")
110 .unwrap_or(false)
111 }
112 _ => false,
113 });
114
115 if !has_data_field {
116 return Err(syn::Error::new(
117 item.span(),
118 "#[askit_agent] expects the struct to have a `data: AsAgentData` field",
119 ));
120 }
121
122 let mut parsed = AgentArgs {
123 kind: None,
124 name: None,
125 title: None,
126 description: None,
127 category: None,
128 inputs: Vec::new(),
129 outputs: Vec::new(),
130 configs: Vec::new(),
131 global_configs: Vec::new(),
132 displays: Vec::new(),
133 };
134
135 for meta in args {
136 match meta {
137 Meta::NameValue(nv) if nv.path.is_ident("kind") => {
138 parsed.kind = Some(nv.value);
139 }
140 Meta::NameValue(nv) if nv.path.is_ident("name") => {
141 parsed.name = Some(nv.value);
142 }
143 Meta::NameValue(nv) if nv.path.is_ident("title") => {
144 parsed.title = Some(nv.value);
145 }
146 Meta::NameValue(nv) if nv.path.is_ident("description") => {
147 parsed.description = Some(nv.value);
148 }
149 Meta::NameValue(nv) if nv.path.is_ident("category") => {
150 parsed.category = Some(nv.value);
151 }
152 Meta::NameValue(nv) if nv.path.is_ident("inputs") => {
153 parsed.inputs = parse_expr_array(nv.value)?;
154 }
155 Meta::NameValue(nv) if nv.path.is_ident("outputs") => {
156 parsed.outputs = parse_expr_array(nv.value)?;
157 }
158 Meta::List(ml) if ml.path.is_ident("inputs") => {
159 parsed.inputs = collect_exprs(ml)?;
160 }
161 Meta::List(ml) if ml.path.is_ident("outputs") => {
162 parsed.outputs = collect_exprs(ml)?;
163 }
164 Meta::List(ml) if ml.path.is_ident("string_config") => {
165 parsed.configs.push(ConfigSpec::String(parse_common_config(ml)?));
166 }
167 Meta::List(ml) if ml.path.is_ident("text_config") => {
168 parsed.configs.push(ConfigSpec::Text(parse_common_config(ml)?));
169 }
170 Meta::List(ml) if ml.path.is_ident("boolean_config") => {
171 parsed.configs.push(ConfigSpec::Boolean(parse_common_config(ml)?));
172 }
173 Meta::List(ml) if ml.path.is_ident("integer_config") => {
174 parsed.configs.push(ConfigSpec::Integer(parse_common_config(ml)?));
175 }
176 Meta::List(ml) if ml.path.is_ident("number_config") => {
177 parsed.configs.push(ConfigSpec::Number(parse_common_config(ml)?));
178 }
179 Meta::List(ml) if ml.path.is_ident("object_config") => {
180 parsed.configs.push(ConfigSpec::Object(parse_common_config(ml)?));
181 }
182 Meta::List(ml) if ml.path.is_ident("unit_config") => {
183 parsed.configs.push(ConfigSpec::Unit(parse_common_config(ml)?));
184 }
185 Meta::List(ml) if ml.path.is_ident("string_global_config") => {
186 parsed.global_configs.push(ConfigSpec::String(parse_common_config(ml)?));
187 }
188 Meta::List(ml) if ml.path.is_ident("text_global_config") => {
189 parsed.global_configs.push(ConfigSpec::Text(parse_common_config(ml)?));
190 }
191 Meta::List(ml) if ml.path.is_ident("boolean_global_config") => {
192 parsed.global_configs.push(ConfigSpec::Boolean(parse_common_config(ml)?));
193 }
194 Meta::List(ml) if ml.path.is_ident("integer_global_config") => {
195 parsed.global_configs.push(ConfigSpec::Integer(parse_common_config(ml)?));
196 }
197 Meta::List(ml) if ml.path.is_ident("number_global_config") => {
198 parsed.global_configs.push(ConfigSpec::Number(parse_common_config(ml)?));
199 }
200 Meta::List(ml) if ml.path.is_ident("object_global_config") => {
201 parsed.global_configs.push(ConfigSpec::Object(parse_common_config(ml)?));
202 }
203 Meta::List(ml) if ml.path.is_ident("unit_global_config") => {
204 parsed.global_configs.push(ConfigSpec::Unit(parse_common_config(ml)?));
205 }
206 Meta::List(ml) if ml.path.is_ident("unit_display") => {
207 parsed.displays.push(DisplaySpec::Unit(parse_common_display(ml)?));
208 }
209 Meta::List(ml) if ml.path.is_ident("boolean_display") => {
210 parsed.displays.push(DisplaySpec::Boolean(parse_common_display(ml)?));
211 }
212 Meta::List(ml) if ml.path.is_ident("integer_display") => {
213 parsed.displays.push(DisplaySpec::Integer(parse_common_display(ml)?));
214 }
215 Meta::List(ml) if ml.path.is_ident("number_display") => {
216 parsed.displays.push(DisplaySpec::Number(parse_common_display(ml)?));
217 }
218 Meta::List(ml) if ml.path.is_ident("string_display") => {
219 parsed.displays.push(DisplaySpec::String(parse_common_display(ml)?));
220 }
221 Meta::List(ml) if ml.path.is_ident("text_display") => {
222 parsed.displays.push(DisplaySpec::Text(parse_common_display(ml)?));
223 }
224 Meta::List(ml) if ml.path.is_ident("object_display") => {
225 parsed.displays.push(DisplaySpec::Object(parse_common_display(ml)?));
226 }
227 Meta::List(ml) if ml.path.is_ident("any_display") => {
228 parsed.displays.push(DisplaySpec::Any(parse_common_display(ml)?));
229 }
230 other => {
231 return Err(syn::Error::new_spanned(
232 other,
233 "unsupported askit_agent argument",
234 ));
235 }
236 }
237 }
238
239 let ident = &item.ident;
240 let generics = item.generics.clone();
241 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
242 let data_impl = quote! {
243 impl #impl_generics ::agent_stream_kit::HasAgentData for #ident #ty_generics #where_clause {
244 fn data(&self) -> &::agent_stream_kit::AsAgentData {
245 &self.data
246 }
247
248 fn mut_data(&mut self) -> &mut ::agent_stream_kit::AsAgentData {
249 &mut self.data
250 }
251 }
252 };
253
254 let kind = parsed.kind.unwrap_or_else(|| parse_quote! { "Agent" });
255 let name_tokens = parsed.name.map(|n| quote! { #n }).unwrap_or_else(|| {
256 quote! { concat!(module_path!(), "::", stringify!(#ident)) }
257 });
258
259 let title = parsed
260 .title
261 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `title`"))?;
262 let category = parsed
263 .category
264 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `category`"))?;
265 let title = quote! { .title(#title) };
266 let description = parsed.description.map(|d| quote! { .description(#d) });
267 let category = quote! { .category(#category) };
268
269 let inputs = if parsed.inputs.is_empty() {
270 quote! {}
271 } else {
272 let values = parsed.inputs;
273 quote! { .inputs(vec![#(#values),*]) }
274 };
275
276 let outputs = if parsed.outputs.is_empty() {
277 quote! {}
278 } else {
279 let values = parsed.outputs;
280 quote! { .outputs(vec![#(#values),*]) }
281 };
282
283 let config_calls = parsed
284 .configs
285 .into_iter()
286 .map(|cfg| match cfg {
287 ConfigSpec::Unit(c) => {
288 let name = c.name.ok_or_else(|| {
289 syn::Error::new(Span::call_site(), "unit_config missing `name`")
290 })?;
291 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
292 let description = c
293 .description
294 .map(|d| quote! { let entry = entry.description(#d); });
295 Ok(quote! {
296 .unit_config_with(#name, |entry| {
297 let entry = entry;
298 #title
299 #description
300 entry
301 })
302 })
303 }
304 ConfigSpec::Boolean(c) => {
305 let name = c.name.ok_or_else(|| {
306 syn::Error::new(Span::call_site(), "boolean_config missing `name`")
307 })?;
308 let default = c.default.unwrap_or_else(|| parse_quote! { false });
309 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
310 let description = c
311 .description
312 .map(|d| quote! { let entry = entry.description(#d); });
313 Ok(quote! {
314 .boolean_config_with(#name, #default, |entry| {
315 let entry = entry;
316 #title
317 #description
318 entry
319 })
320 })
321 }
322 ConfigSpec::Integer(c) => {
323 let name = c.name.ok_or_else(|| {
324 syn::Error::new(Span::call_site(), "integer_config missing `name`")
325 })?;
326 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
327 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
328 let description = c
329 .description
330 .map(|d| quote! { let entry = entry.description(#d); });
331 Ok(quote! {
332 .integer_config_with(#name, #default, |entry| {
333 let entry = entry;
334 #title
335 #description
336 entry
337 })
338 })
339 }
340 ConfigSpec::Number(c) => {
341 let name = c.name.ok_or_else(|| {
342 syn::Error::new(Span::call_site(), "number_config missing `name`")
343 })?;
344 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
345 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
346 let description = c
347 .description
348 .map(|d| quote! { let entry = entry.description(#d); });
349 Ok(quote! {
350 .number_config_with(#name, #default, |entry| {
351 let entry = entry;
352 #title
353 #description
354 entry
355 })
356 })
357 }
358 ConfigSpec::String(c) => {
359 let name = c.name.ok_or_else(|| {
360 syn::Error::new(Span::call_site(), "string_config missing `name`")
361 })?;
362 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
363 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
364 let description = c
365 .description
366 .map(|d| quote! { let entry = entry.description(#d); });
367 Ok(quote! {
368 .string_config_with(#name, #default, |entry| {
369 let entry = entry;
370 #title
371 #description
372 entry
373 })
374 })
375 }
376 ConfigSpec::Text(c) => {
377 let name = c.name.ok_or_else(|| {
378 syn::Error::new(Span::call_site(), "text_config missing `name`")
379 })?;
380 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
381 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
382 let description = c
383 .description
384 .map(|d| quote! { let entry = entry.description(#d); });
385 Ok(quote! {
386 .text_config_with(#name, #default, |entry| {
387 let entry = entry;
388 #title
389 #description
390 entry
391 })
392 })
393 }
394 ConfigSpec::Object(c) => {
395 let name = c.name.ok_or_else(|| {
396 syn::Error::new(Span::call_site(), "object_config missing `name`")
397 })?;
398 let default = c.default.unwrap_or_else(|| {
399 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
400 });
401 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
402 let description = c
403 .description
404 .map(|d| quote! { let entry = entry.description(#d); });
405 Ok(quote! {
406 .object_config_with(#name, #default, |entry| {
407 let entry = entry;
408 #title
409 #description
410 entry
411 })
412 })
413 }
414 })
415 .collect::<syn::Result<Vec<_>>>()?;
416
417 let global_config_calls = parsed
418 .global_configs
419 .into_iter()
420 .map(|cfg| match cfg {
421 ConfigSpec::Unit(c) => {
422 let name = c.name.ok_or_else(|| {
423 syn::Error::new(Span::call_site(), "unit_global_config missing `name`")
424 })?;
425 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
426 let description = c
427 .description
428 .map(|d| quote! { let entry = entry.description(#d); });
429 Ok(quote! {
430 .unit_global_config_with(#name, |entry| {
431 let entry = entry;
432 #title
433 #description
434 entry
435 })
436 })
437 }
438 ConfigSpec::Boolean(c) => {
439 let name = c.name.ok_or_else(|| {
440 syn::Error::new(Span::call_site(), "boolean_global_config missing `name`")
441 })?;
442 let default = c.default.unwrap_or_else(|| parse_quote! { false });
443 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
444 let description = c
445 .description
446 .map(|d| quote! { let entry = entry.description(#d); });
447 Ok(quote! {
448 .boolean_global_config_with(#name, #default, |entry| {
449 let entry = entry;
450 #title
451 #description
452 entry
453 })
454 })
455 }
456 ConfigSpec::Integer(c) => {
457 let name = c.name.ok_or_else(|| {
458 syn::Error::new(Span::call_site(), "integer_global_config missing `name`")
459 })?;
460 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
461 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
462 let description = c
463 .description
464 .map(|d| quote! { let entry = entry.description(#d); });
465 Ok(quote! {
466 .integer_global_config_with(#name, #default, |entry| {
467 let entry = entry;
468 #title
469 #description
470 entry
471 })
472 })
473 }
474 ConfigSpec::Number(c) => {
475 let name = c.name.ok_or_else(|| {
476 syn::Error::new(Span::call_site(), "number_global_config missing `name`")
477 })?;
478 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
479 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
480 let description = c
481 .description
482 .map(|d| quote! { let entry = entry.description(#d); });
483 Ok(quote! {
484 .number_global_config_with(#name, #default, |entry| {
485 let entry = entry;
486 #title
487 #description
488 entry
489 })
490 })
491 }
492 ConfigSpec::String(c) => {
493 let name = c.name.ok_or_else(|| {
494 syn::Error::new(Span::call_site(), "string_global_config missing `name`")
495 })?;
496 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
497 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
498 let description = c
499 .description
500 .map(|d| quote! { let entry = entry.description(#d); });
501 Ok(quote! {
502 .string_global_config_with(#name, #default, |entry| {
503 let entry = entry;
504 #title
505 #description
506 entry
507 })
508 })
509 }
510 ConfigSpec::Text(c) => {
511 let name = c.name.ok_or_else(|| {
512 syn::Error::new(Span::call_site(), "text_global_config missing `name`")
513 })?;
514 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
515 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
516 let description = c
517 .description
518 .map(|d| quote! { let entry = entry.description(#d); });
519 Ok(quote! {
520 .text_global_config_with(#name, #default, |entry| {
521 let entry = entry;
522 #title
523 #description
524 entry
525 })
526 })
527 }
528 ConfigSpec::Object(c) => {
529 let name = c.name.ok_or_else(|| {
530 syn::Error::new(Span::call_site(), "object_global_config missing `name`")
531 })?;
532 let default = c.default.unwrap_or_else(|| {
533 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
534 });
535 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
536 let description = c
537 .description
538 .map(|d| quote! { let entry = entry.description(#d); });
539 Ok(quote! {
540 .object_global_config_with(#name, #default, |entry| {
541 let entry = entry;
542 #title
543 #description
544 entry
545 })
546 })
547 }
548 })
549 .collect::<syn::Result<Vec<_>>>()?;
550
551 let display_calls = parsed
552 .displays
553 .into_iter()
554 .map(|disp| match disp {
555 DisplaySpec::Unit(c) => display_call("unit", c),
556 DisplaySpec::Boolean(c) => display_call("boolean", c),
557 DisplaySpec::Integer(c) => display_call("integer", c),
558 DisplaySpec::Number(c) => display_call("number", c),
559 DisplaySpec::String(c) => display_call("string", c),
560 DisplaySpec::Text(c) => display_call("text", c),
561 DisplaySpec::Object(c) => display_call("object", c),
562 DisplaySpec::Any(c) => display_call("*", c),
563 })
564 .collect::<syn::Result<Vec<_>>>()?;
565
566 let definition_builder = quote! {
567 ::agent_stream_kit::AgentDefinition::new(
568 #kind,
569 #name_tokens,
570 Some(::agent_stream_kit::new_agent_boxed::<#ident>),
571 )
572 #title
573 #description
574 #category
575 #inputs
576 #outputs
577 #(#config_calls)*
578 #(#global_config_calls)*
579 #(#display_calls)*
580 };
581
582 let expanded = quote! {
583 #item
584
585 #data_impl
586
587 impl #impl_generics #ident #ty_generics #where_clause {
588 pub fn agent_definition() -> ::agent_stream_kit::AgentDefinition {
589 #definition_builder
590 }
591
592 pub fn register(askit: &::agent_stream_kit::ASKit) {
593 askit.register_agent(Self::agent_definition());
594 }
595 }
596
597 ::agent_stream_kit::inventory::submit! {
598 ::agent_stream_kit::AgentRegistration {
599 build: || #definition_builder,
600 }
601 }
602 };
603
604 Ok(expanded)
605}
606
607fn collect_exprs(list: MetaList) -> syn::Result<Vec<Expr>> {
608 let values = list.parse_args_with(Punctuated::<Expr, Comma>::parse_terminated)?;
609 Ok(values.into_iter().collect())
610}
611
612fn parse_expr_array(expr: Expr) -> syn::Result<Vec<Expr>> {
613 if let Expr::Array(arr) = expr {
614 Ok(arr.elems.into_iter().collect())
615 } else {
616 Err(syn::Error::new_spanned(
617 expr,
618 "inputs/outputs expect array expressions",
619 ))
620 }
621}
622
623fn parse_common_config(list: MetaList) -> syn::Result<CommonConfig> {
624 let mut cfg = CommonConfig::default();
625 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
626
627 for meta in nested {
628 match meta {
629 Meta::NameValue(nv) if nv.path.is_ident("name") => {
630 cfg.name = Some(match &nv.value {
631 Expr::Lit(expr_lit) => match &expr_lit.lit {
632 Lit::Str(s) => syn::parse_str::<Expr>(&s.value())?,
633 _ => nv.value.clone(),
634 },
635 _ => nv.value.clone(),
636 });
637 }
638 Meta::NameValue(nv) if nv.path.is_ident("default") => {
639 cfg.default = Some(nv.value.clone());
640 }
641 Meta::NameValue(nv) if nv.path.is_ident("title") => {
642 cfg.title = Some(nv.value.clone());
643 }
644 Meta::NameValue(nv) if nv.path.is_ident("description") => {
645 cfg.description = Some(nv.value.clone());
646 }
647 other => {
648 return Err(syn::Error::new_spanned(
649 other,
650 "config supports name, default, title, description",
651 ));
652 }
653 }
654 }
655
656 if cfg.name.is_none() {
657 return Err(syn::Error::new(
658 list.span(),
659 "config missing `name`",
660 ));
661 }
662 Ok(cfg)
663}
664
665fn parse_common_display(list: MetaList) -> syn::Result<CommonDisplay> {
666 let mut cfg = CommonDisplay::default();
667 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
668
669 for meta in nested {
670 match meta {
671 Meta::NameValue(nv) if nv.path.is_ident("name") => {
672 cfg.name = Some(match &nv.value {
673 Expr::Lit(expr_lit) => match &expr_lit.lit {
674 Lit::Str(s) => syn::parse_str::<Expr>(&s.value())?,
675 _ => nv.value.clone(),
676 },
677 _ => nv.value.clone(),
678 });
679 }
680 Meta::NameValue(nv) if nv.path.is_ident("title") => {
681 cfg.title = Some(nv.value.clone());
682 }
683 Meta::NameValue(nv) if nv.path.is_ident("description") => {
684 cfg.description = Some(nv.value.clone());
685 }
686 Meta::Path(p) if p.is_ident("hide_title") => {
687 cfg.hide_title = true;
688 }
689 other => {
690 return Err(syn::Error::new_spanned(
691 other,
692 "display supports name, title, description, hide_title",
693 ));
694 }
695 }
696 }
697
698 if cfg.name.is_none() {
699 return Err(syn::Error::new(list.span(), "display missing `name`"));
700 }
701 Ok(cfg)
702}
703
704fn display_call(type_name: &str, cfg: CommonDisplay) -> syn::Result<proc_macro2::TokenStream> {
705 let name = cfg
706 .name
707 .ok_or_else(|| syn::Error::new(Span::call_site(), "display missing `name`"))?;
708 let title = cfg.title.map(|t| quote! { let entry = entry.title(#t); });
709 let description = cfg
710 .description
711 .map(|d| quote! { let entry = entry.description(#d); });
712 let hide_title = if cfg.hide_title {
713 quote! { let entry = entry.hide_title(); }
714 } else {
715 quote! {}
716 };
717
718 Ok(quote! {
719 .custom_display_config_with(#name, #type_name, |entry| {
720 let entry = entry;
721 #title
722 #description
723 #hide_title
724 entry
725 })
726 })
727}