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