1#![recursion_limit = "256"]
2use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::{format_ident, quote};
10use syn::{
11 Expr, ItemStruct, Meta, MetaList, Type, parse_macro_input, parse_quote, punctuated::Punctuated,
12 spanned::Spanned, token::Comma,
13};
14
15#[proc_macro_attribute]
16pub fn askit(attr: TokenStream, item: TokenStream) -> TokenStream {
17 askit_agent(attr, item)
18}
19
20#[proc_macro_attribute]
37pub fn askit_agent(attr: TokenStream, item: TokenStream) -> TokenStream {
38 let args = parse_macro_input!(attr with Punctuated<Meta, Comma>::parse_terminated);
39 let item_struct = parse_macro_input!(item as ItemStruct);
40
41 match expand_askit_agent(args, item_struct) {
42 Ok(tokens) => tokens.into(),
43 Err(err) => err.into_compile_error().into(),
44 }
45}
46
47struct AgentArgs {
48 kind: Option<Expr>,
49 name: Option<Expr>,
50 title: Option<Expr>,
51 hide_title: bool,
52 description: Option<Expr>,
53 category: Option<Expr>,
54 inputs: Vec<Expr>,
55 outputs: Vec<Expr>,
56 configs: Vec<ConfigSpec>,
57 global_configs: Vec<ConfigSpec>,
58}
59
60#[derive(Default)]
61struct CommonConfig {
62 name: Option<Expr>,
63 default: Option<Expr>,
64 title: Option<Expr>,
65 description: Option<Expr>,
66 hide_title: bool,
67 hidden: bool,
68 readonly: bool,
69}
70
71struct CustomConfig {
72 name: Expr,
73 default: Expr,
74 type_: Expr,
75 title: Option<Expr>,
76 description: Option<Expr>,
77 hide_title: bool,
78 hidden: bool,
79 readonly: bool,
80}
81
82enum ConfigSpec {
83 Unit(CommonConfig),
84 Boolean(CommonConfig),
85 Integer(CommonConfig),
86 Number(CommonConfig),
87 String(CommonConfig),
88 Text(CommonConfig),
89 Array(CommonConfig),
90 Object(CommonConfig),
91 Custom(CustomConfig),
92}
93
94fn expand_askit_agent(
95 args: Punctuated<Meta, Comma>,
96 item: ItemStruct,
97) -> syn::Result<proc_macro2::TokenStream> {
98 let has_data_field = item.fields.iter().any(|f| match (&f.ident, &f.ty) {
99 (Some(ident), Type::Path(tp)) if ident == "data" => tp
100 .path
101 .segments
102 .last()
103 .map(|seg| seg.ident == "AgentData")
104 .unwrap_or(false),
105 _ => false,
106 });
107
108 if !has_data_field {
109 return Err(syn::Error::new(
110 item.span(),
111 "#[askit_agent] expects the struct to have a `data: AgentData` field",
112 ));
113 }
114
115 let mut parsed = AgentArgs {
116 kind: None,
117 name: None,
118 title: None,
119 hide_title: false,
120 description: None,
121 category: None,
122 inputs: Vec::new(),
123 outputs: Vec::new(),
124 configs: Vec::new(),
125 global_configs: Vec::new(),
126 };
127
128 for meta in args {
129 match meta {
130 Meta::NameValue(nv) if nv.path.is_ident("kind") => {
131 parsed.kind = Some(nv.value);
132 }
133 Meta::NameValue(nv) if nv.path.is_ident("name") => {
134 parsed.name = Some(nv.value);
135 }
136 Meta::NameValue(nv) if nv.path.is_ident("title") => {
137 parsed.title = Some(nv.value);
138 }
139 Meta::Path(p) if p.is_ident("hide_title") => {
140 parsed.hide_title = true;
141 }
142 Meta::NameValue(nv) if nv.path.is_ident("description") => {
143 parsed.description = Some(nv.value);
144 }
145 Meta::NameValue(nv) if nv.path.is_ident("category") => {
146 parsed.category = Some(nv.value);
147 }
148 Meta::NameValue(nv) if nv.path.is_ident("inputs") => {
149 parsed.inputs = parse_expr_array(nv.value)?;
150 }
151 Meta::NameValue(nv) if nv.path.is_ident("outputs") => {
152 parsed.outputs = parse_expr_array(nv.value)?;
153 }
154 Meta::List(ml) if ml.path.is_ident("inputs") => {
155 parsed.inputs = collect_exprs(ml)?;
156 }
157 Meta::List(ml) if ml.path.is_ident("outputs") => {
158 parsed.outputs = collect_exprs(ml)?;
159 }
160 Meta::List(ml) if ml.path.is_ident("string_config") => {
161 parsed
162 .configs
163 .push(ConfigSpec::String(parse_common_config(ml)?));
164 }
165 Meta::List(ml) if ml.path.is_ident("text_config") => {
166 parsed
167 .configs
168 .push(ConfigSpec::Text(parse_common_config(ml)?));
169 }
170 Meta::List(ml) if ml.path.is_ident("array_config") => {
171 parsed
172 .configs
173 .push(ConfigSpec::Array(parse_common_config(ml)?));
174 }
175 Meta::List(ml) if ml.path.is_ident("boolean_config") => {
176 parsed
177 .configs
178 .push(ConfigSpec::Boolean(parse_common_config(ml)?));
179 }
180 Meta::List(ml) if ml.path.is_ident("integer_config") => {
181 parsed
182 .configs
183 .push(ConfigSpec::Integer(parse_common_config(ml)?));
184 }
185 Meta::List(ml) if ml.path.is_ident("number_config") => {
186 parsed
187 .configs
188 .push(ConfigSpec::Number(parse_common_config(ml)?));
189 }
190 Meta::List(ml) if ml.path.is_ident("object_config") => {
191 parsed
192 .configs
193 .push(ConfigSpec::Object(parse_common_config(ml)?));
194 }
195 Meta::List(ml) if ml.path.is_ident("custom_config") => {
196 parsed
197 .configs
198 .push(ConfigSpec::Custom(parse_custom_config(ml)?));
199 }
200 Meta::List(ml) if ml.path.is_ident("unit_config") => {
201 parsed
202 .configs
203 .push(ConfigSpec::Unit(parse_common_config(ml)?));
204 }
205 Meta::List(ml) if ml.path.is_ident("string_global_config") => {
206 parsed
207 .global_configs
208 .push(ConfigSpec::String(parse_common_config(ml)?));
209 }
210 Meta::List(ml) if ml.path.is_ident("text_global_config") => {
211 parsed
212 .global_configs
213 .push(ConfigSpec::Text(parse_common_config(ml)?));
214 }
215 Meta::List(ml) if ml.path.is_ident("boolean_global_config") => {
216 parsed
217 .global_configs
218 .push(ConfigSpec::Boolean(parse_common_config(ml)?));
219 }
220 Meta::List(ml) if ml.path.is_ident("array_global_config") => {
221 parsed
222 .global_configs
223 .push(ConfigSpec::Array(parse_common_config(ml)?));
224 }
225 Meta::List(ml) if ml.path.is_ident("integer_global_config") => {
226 parsed
227 .global_configs
228 .push(ConfigSpec::Integer(parse_common_config(ml)?));
229 }
230 Meta::List(ml) if ml.path.is_ident("number_global_config") => {
231 parsed
232 .global_configs
233 .push(ConfigSpec::Number(parse_common_config(ml)?));
234 }
235 Meta::List(ml) if ml.path.is_ident("object_global_config") => {
236 parsed
237 .global_configs
238 .push(ConfigSpec::Object(parse_common_config(ml)?));
239 }
240 Meta::List(ml) if ml.path.is_ident("custom_global_config") => {
241 parsed
242 .global_configs
243 .push(ConfigSpec::Custom(parse_custom_config(ml)?));
244 }
245 Meta::List(ml) if ml.path.is_ident("unit_global_config") => {
246 parsed
247 .global_configs
248 .push(ConfigSpec::Unit(parse_common_config(ml)?));
249 }
250 other => {
251 return Err(syn::Error::new_spanned(
252 other,
253 "unsupported askit_agent argument",
254 ));
255 }
256 }
257 }
258
259 let ident = &item.ident;
260 let generics = item.generics.clone();
261 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
262 let data_impl = quote! {
263 impl #impl_generics ::agent_stream_kit::HasAgentData for #ident #ty_generics #where_clause {
264 fn data(&self) -> &::agent_stream_kit::AgentData {
265 &self.data
266 }
267
268 fn mut_data(&mut self) -> &mut ::agent_stream_kit::AgentData {
269 &mut self.data
270 }
271 }
272 };
273
274 let kind = parsed.kind.unwrap_or_else(|| parse_quote! { "Agent" });
275 let name_tokens = parsed.name.map(|n| quote! { #n }).unwrap_or_else(|| {
276 quote! { concat!(module_path!(), "::", stringify!(#ident)) }
277 });
278
279 let title = parsed
280 .title
281 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `title`"))?;
282 let category = parsed
283 .category
284 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `category`"))?;
285 let title = quote! { .title(#title) };
286 let hide_title = if parsed.hide_title {
287 quote! { .hide_title() }
288 } else {
289 quote! {}
290 };
291 let description = parsed.description.map(|d| quote! { .description(#d) });
292 let category = quote! { .category(#category) };
293
294 let inputs = if parsed.inputs.is_empty() {
295 quote! {}
296 } else {
297 let values = parsed.inputs;
298 quote! { .inputs(vec![#(#values),*]) }
299 };
300
301 let outputs = if parsed.outputs.is_empty() {
302 quote! {}
303 } else {
304 let values = parsed.outputs;
305 quote! { .outputs(vec![#(#values),*]) }
306 };
307
308 let config_calls = parsed
309 .configs
310 .into_iter()
311 .map(|cfg| match cfg {
312 ConfigSpec::Unit(c) => {
313 let name = c.name.ok_or_else(|| {
314 syn::Error::new(Span::call_site(), "unit_config missing `name`")
315 })?;
316 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
317 let description = c
318 .description
319 .map(|d| quote! { let entry = entry.description(#d); });
320 let hide_title = if c.hide_title {
321 quote! { let entry = entry.hide_title(); }
322 } else {
323 quote! {}
324 };
325 let hidden = if c.hidden {
326 quote! { let entry = entry.hidden(); }
327 } else {
328 quote! {}
329 };
330 let readonly = if c.readonly {
331 quote! { let entry = entry.readonly(); }
332 } else {
333 quote! {}
334 };
335 Ok(quote! {
336 .unit_config_with(#name, |entry| {
337 let entry = entry;
338 #title
339 #description
340 #hide_title
341 #hidden
342 #readonly
343 entry
344 })
345 })
346 }
347 ConfigSpec::Boolean(c) => {
348 let name = c.name.ok_or_else(|| {
349 syn::Error::new(Span::call_site(), "boolean_config missing `name`")
350 })?;
351 let default = c.default.unwrap_or_else(|| parse_quote! { false });
352 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
353 let description = c
354 .description
355 .map(|d| quote! { let entry = entry.description(#d); });
356 let hide_title = if c.hide_title {
357 quote! { let entry = entry.hide_title(); }
358 } else {
359 quote! {}
360 };
361 let hidden = if c.hidden {
362 quote! { let entry = entry.hidden(); }
363 } else {
364 quote! {}
365 };
366 let readonly = if c.readonly {
367 quote! { let entry = entry.readonly(); }
368 } else {
369 quote! {}
370 };
371 Ok(quote! {
372 .boolean_config_with(#name, #default, |entry| {
373 let entry = entry;
374 #title
375 #description
376 #hide_title
377 #hidden
378 #readonly
379 entry
380 })
381 })
382 }
383 ConfigSpec::Integer(c) => {
384 let name = c.name.ok_or_else(|| {
385 syn::Error::new(Span::call_site(), "integer_config missing `name`")
386 })?;
387 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
388 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
389 let description = c
390 .description
391 .map(|d| quote! { let entry = entry.description(#d); });
392 let hide_title = if c.hide_title {
393 quote! { let entry = entry.hide_title(); }
394 } else {
395 quote! {}
396 };
397 let hidden = if c.hidden {
398 quote! { let entry = entry.hidden(); }
399 } else {
400 quote! {}
401 };
402 let readonly = if c.readonly {
403 quote! { let entry = entry.readonly(); }
404 } else {
405 quote! {}
406 };
407 Ok(quote! {
408 .integer_config_with(#name, #default, |entry| {
409 let entry = entry;
410 #title
411 #description
412 #hide_title
413 #hidden
414 #readonly
415 entry
416 })
417 })
418 }
419 ConfigSpec::Number(c) => {
420 let name = c.name.ok_or_else(|| {
421 syn::Error::new(Span::call_site(), "number_config missing `name`")
422 })?;
423 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
424 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
425 let description = c
426 .description
427 .map(|d| quote! { let entry = entry.description(#d); });
428 let hide_title = if c.hide_title {
429 quote! { let entry = entry.hide_title(); }
430 } else {
431 quote! {}
432 };
433 let hidden = if c.hidden {
434 quote! { let entry = entry.hidden(); }
435 } else {
436 quote! {}
437 };
438 let readonly = if c.readonly {
439 quote! { let entry = entry.readonly(); }
440 } else {
441 quote! {}
442 };
443 Ok(quote! {
444 .number_config_with(#name, #default, |entry| {
445 let entry = entry;
446 #title
447 #description
448 #hide_title
449 #hidden
450 #readonly
451 entry
452 })
453 })
454 }
455 ConfigSpec::String(c) => {
456 let name = c.name.ok_or_else(|| {
457 syn::Error::new(Span::call_site(), "string_config missing `name`")
458 })?;
459 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
460 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
461 let description = c
462 .description
463 .map(|d| quote! { let entry = entry.description(#d); });
464 let hide_title = if c.hide_title {
465 quote! { let entry = entry.hide_title(); }
466 } else {
467 quote! {}
468 };
469 let hidden = if c.hidden {
470 quote! { let entry = entry.hidden(); }
471 } else {
472 quote! {}
473 };
474 let readonly = if c.readonly {
475 quote! { let entry = entry.readonly(); }
476 } else {
477 quote! {}
478 };
479 Ok(quote! {
480 .string_config_with(#name, #default, |entry| {
481 let entry = entry;
482 #title
483 #description
484 #hide_title
485 #hidden
486 #readonly
487 entry
488 })
489 })
490 }
491 ConfigSpec::Text(c) => {
492 let name = c.name.ok_or_else(|| {
493 syn::Error::new(Span::call_site(), "text_config missing `name`")
494 })?;
495 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
496 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
497 let description = c
498 .description
499 .map(|d| quote! { let entry = entry.description(#d); });
500 let hide_title = if c.hide_title {
501 quote! { let entry = entry.hide_title(); }
502 } else {
503 quote! {}
504 };
505 let hidden = if c.hidden {
506 quote! { let entry = entry.hidden(); }
507 } else {
508 quote! {}
509 };
510 let readonly = if c.readonly {
511 quote! { let entry = entry.readonly(); }
512 } else {
513 quote! {}
514 };
515 Ok(quote! {
516 .text_config_with(#name, #default, |entry| {
517 let entry = entry;
518 #title
519 #description
520 #hide_title
521 #hidden
522 #readonly
523 entry
524 })
525 })
526 }
527 ConfigSpec::Array(c) => {
528 let name = c.name.ok_or_else(|| {
529 syn::Error::new(Span::call_site(), "array_config missing `name`")
530 })?;
531 let default = c.default.unwrap_or_else(|| {
532 parse_quote! { ::agent_stream_kit::AgentValue::array_default() }
533 });
534 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
535 let description = c
536 .description
537 .map(|d| quote! { let entry = entry.description(#d); });
538 let hide_title = if c.hide_title {
539 quote! { let entry = entry.hide_title(); }
540 } else {
541 quote! {}
542 };
543 let hidden = if c.hidden {
544 quote! { let entry = entry.hidden(); }
545 } else {
546 quote! {}
547 };
548 let readonly = if c.readonly {
549 quote! { let entry = entry.readonly(); }
550 } else {
551 quote! {}
552 };
553 Ok(quote! {
554 .array_config_with(#name, #default, |entry| {
555 let entry = entry;
556 #title
557 #description
558 #hide_title
559 #hidden
560 #readonly
561 entry
562 })
563 })
564 }
565 ConfigSpec::Object(c) => {
566 let name = c.name.ok_or_else(|| {
567 syn::Error::new(Span::call_site(), "object_config missing `name`")
568 })?;
569 let default = c.default.unwrap_or_else(|| {
570 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
571 });
572 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
573 let description = c
574 .description
575 .map(|d| quote! { let entry = entry.description(#d); });
576 let hide_title = if c.hide_title {
577 quote! { let entry = entry.hide_title(); }
578 } else {
579 quote! {}
580 };
581 let hidden = if c.hidden {
582 quote! { let entry = entry.hidden(); }
583 } else {
584 quote! {}
585 };
586 let readonly = if c.readonly {
587 quote! { let entry = entry.readonly(); }
588 } else {
589 quote! {}
590 };
591 Ok(quote! {
592 .object_config_with(#name, #default, |entry| {
593 let entry = entry;
594 #title
595 #description
596 #hide_title
597 #hidden
598 #readonly
599 entry
600 })
601 })
602 }
603 ConfigSpec::Custom(c) => custom_config_call("custom_config_with", c),
604 })
605 .collect::<syn::Result<Vec<_>>>()?;
606
607 let global_config_calls = parsed
608 .global_configs
609 .into_iter()
610 .map(|cfg| match cfg {
611 ConfigSpec::Unit(c) => {
612 let name = c.name.ok_or_else(|| {
613 syn::Error::new(Span::call_site(), "unit_global_config missing `name`")
614 })?;
615 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
616 let description = c
617 .description
618 .map(|d| quote! { let entry = entry.description(#d); });
619 let hide_title = if c.hide_title {
620 quote! { let entry = entry.hide_title(); }
621 } else {
622 quote! {}
623 };
624 let hidden = if c.hidden {
625 quote! { let entry = entry.hidden(); }
626 } else {
627 quote! {}
628 };
629 let readonly = if c.readonly {
630 quote! { let entry = entry.readonly(); }
631 } else {
632 quote! {}
633 };
634 Ok(quote! {
635 .unit_global_config_with(#name, |entry| {
636 let entry = entry;
637 #title
638 #description
639 #hide_title
640 #hidden
641 #readonly
642 entry
643 })
644 })
645 }
646 ConfigSpec::Boolean(c) => {
647 let name = c.name.ok_or_else(|| {
648 syn::Error::new(Span::call_site(), "boolean_global_config missing `name`")
649 })?;
650 let default = c.default.unwrap_or_else(|| parse_quote! { false });
651 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
652 let description = c
653 .description
654 .map(|d| quote! { let entry = entry.description(#d); });
655 let hide_title = if c.hide_title {
656 quote! { let entry = entry.hide_title(); }
657 } else {
658 quote! {}
659 };
660 let hidden = if c.hidden {
661 quote! { let entry = entry.hidden(); }
662 } else {
663 quote! {}
664 };
665 let readonly = if c.readonly {
666 quote! { let entry = entry.readonly(); }
667 } else {
668 quote! {}
669 };
670 Ok(quote! {
671 .boolean_global_config_with(#name, #default, |entry| {
672 let entry = entry;
673 #title
674 #description
675 #hide_title
676 #hidden
677 #readonly
678 entry
679 })
680 })
681 }
682 ConfigSpec::Integer(c) => {
683 let name = c.name.ok_or_else(|| {
684 syn::Error::new(Span::call_site(), "integer_global_config missing `name`")
685 })?;
686 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
687 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
688 let description = c
689 .description
690 .map(|d| quote! { let entry = entry.description(#d); });
691 let hide_title = if c.hide_title {
692 quote! { let entry = entry.hide_title(); }
693 } else {
694 quote! {}
695 };
696 let hidden = if c.hidden {
697 quote! { let entry = entry.hidden(); }
698 } else {
699 quote! {}
700 };
701 let readonly = if c.readonly {
702 quote! { let entry = entry.readonly(); }
703 } else {
704 quote! {}
705 };
706 Ok(quote! {
707 .integer_global_config_with(#name, #default, |entry| {
708 let entry = entry;
709 #title
710 #description
711 #hide_title
712 #hidden
713 #readonly
714 entry
715 })
716 })
717 }
718 ConfigSpec::Number(c) => {
719 let name = c.name.ok_or_else(|| {
720 syn::Error::new(Span::call_site(), "number_global_config missing `name`")
721 })?;
722 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
723 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
724 let description = c
725 .description
726 .map(|d| quote! { let entry = entry.description(#d); });
727 let hide_title = if c.hide_title {
728 quote! { let entry = entry.hide_title(); }
729 } else {
730 quote! {}
731 };
732 let hidden = if c.hidden {
733 quote! { let entry = entry.hidden(); }
734 } else {
735 quote! {}
736 };
737 let readonly = if c.readonly {
738 quote! { let entry = entry.readonly(); }
739 } else {
740 quote! {}
741 };
742 Ok(quote! {
743 .number_global_config_with(#name, #default, |entry| {
744 let entry = entry;
745 #title
746 #description
747 #hide_title
748 #hidden
749 #readonly
750 entry
751 })
752 })
753 }
754 ConfigSpec::String(c) => {
755 let name = c.name.ok_or_else(|| {
756 syn::Error::new(Span::call_site(), "string_global_config missing `name`")
757 })?;
758 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
759 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
760 let description = c
761 .description
762 .map(|d| quote! { let entry = entry.description(#d); });
763 let hide_title = if c.hide_title {
764 quote! { let entry = entry.hide_title(); }
765 } else {
766 quote! {}
767 };
768 let hidden = if c.hidden {
769 quote! { let entry = entry.hidden(); }
770 } else {
771 quote! {}
772 };
773 let readonly = if c.readonly {
774 quote! { let entry = entry.readonly(); }
775 } else {
776 quote! {}
777 };
778 Ok(quote! {
779 .string_global_config_with(#name, #default, |entry| {
780 let entry = entry;
781 #title
782 #description
783 #hide_title
784 #hidden
785 #readonly
786 entry
787 })
788 })
789 }
790 ConfigSpec::Text(c) => {
791 let name = c.name.ok_or_else(|| {
792 syn::Error::new(Span::call_site(), "text_global_config missing `name`")
793 })?;
794 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
795 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
796 let description = c
797 .description
798 .map(|d| quote! { let entry = entry.description(#d); });
799 let hide_title = if c.hide_title {
800 quote! { let entry = entry.hide_title(); }
801 } else {
802 quote! {}
803 };
804 let hidden = if c.hidden {
805 quote! { let entry = entry.hidden(); }
806 } else {
807 quote! {}
808 };
809 let readonly = if c.readonly {
810 quote! { let entry = entry.readonly(); }
811 } else {
812 quote! {}
813 };
814 Ok(quote! {
815 .text_global_config_with(#name, #default, |entry| {
816 let entry = entry;
817 #title
818 #description
819 #hide_title
820 #hidden
821 #readonly
822 entry
823 })
824 })
825 }
826 ConfigSpec::Array(c) => {
827 let name = c.name.ok_or_else(|| {
828 syn::Error::new(Span::call_site(), "array_global_config missing `name`")
829 })?;
830 let default = c.default.unwrap_or_else(|| {
831 parse_quote! { ::agent_stream_kit::AgentValue::array_default() }
832 });
833 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
834 let description = c
835 .description
836 .map(|d| quote! { let entry = entry.description(#d); });
837 let hide_title = if c.hide_title {
838 quote! { let entry = entry.hide_title(); }
839 } else {
840 quote! {}
841 };
842 let hidden = if c.hidden {
843 quote! { let entry = entry.hidden(); }
844 } else {
845 quote! {}
846 };
847 let readonly = if c.readonly {
848 quote! { let entry = entry.readonly(); }
849 } else {
850 quote! {}
851 };
852 Ok(quote! {
853 .array_global_config_with(#name, #default, |entry| {
854 let entry = entry;
855 #title
856 #description
857 #hide_title
858 #hidden
859 #readonly
860 entry
861 })
862 })
863 }
864 ConfigSpec::Object(c) => {
865 let name = c.name.ok_or_else(|| {
866 syn::Error::new(Span::call_site(), "object_global_config missing `name`")
867 })?;
868 let default = c.default.unwrap_or_else(|| {
869 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
870 });
871 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
872 let description = c
873 .description
874 .map(|d| quote! { let entry = entry.description(#d); });
875 let hide_title = if c.hide_title {
876 quote! { let entry = entry.hide_title(); }
877 } else {
878 quote! {}
879 };
880 let hidden = if c.hidden {
881 quote! { let entry = entry.hidden(); }
882 } else {
883 quote! {}
884 };
885 let readonly = if c.readonly {
886 quote! { let entry = entry.readonly(); }
887 } else {
888 quote! {}
889 };
890 Ok(quote! {
891 .object_global_config_with(#name, #default, |entry| {
892 let entry = entry;
893 #title
894 #description
895 #hide_title
896 #hidden
897 #readonly
898 entry
899 })
900 })
901 }
902 ConfigSpec::Custom(c) => custom_config_call("custom_global_config_with", c),
903 })
904 .collect::<syn::Result<Vec<_>>>()?;
905
906 let definition_builder = quote! {
907 ::agent_stream_kit::AgentDefinition::new(
908 #kind,
909 #name_tokens,
910 Some(::agent_stream_kit::new_agent_boxed::<#ident>),
911 )
912 #title
913 #hide_title
914 #description
915 #category
916 #inputs
917 #outputs
918 #(#config_calls)*
919 #(#global_config_calls)*
920 };
921
922 let expanded = quote! {
923 #item
924
925 #data_impl
926
927 impl #impl_generics #ident #ty_generics #where_clause {
928 pub const DEF_NAME: &'static str = #name_tokens;
929
930 pub fn def_name() -> &'static str { Self::DEF_NAME }
931
932 pub fn agent_definition() -> ::agent_stream_kit::AgentDefinition {
933 #definition_builder
934 }
935
936 pub fn register(askit: &::agent_stream_kit::ASKit) {
937 askit.register_agent_definiton(Self::agent_definition());
938 }
939 }
940
941 ::agent_stream_kit::inventory::submit! {
942 ::agent_stream_kit::AgentRegistration {
943 build: || #definition_builder,
944 }
945 }
946 };
947
948 Ok(expanded)
949}
950
951fn parse_name_type_title_description(
952 meta: &Meta,
953 name: &mut Option<Expr>,
954 type_: &mut Option<Expr>,
955 title: &mut Option<Expr>,
956 description: &mut Option<Expr>,
957) -> bool {
958 match meta {
959 Meta::NameValue(nv) if nv.path.is_ident("name") => {
960 *name = Some(nv.value.clone());
961 true
962 }
963 Meta::NameValue(nv) if nv.path.is_ident("type") => {
964 *type_ = Some(nv.value.clone());
965 true
966 }
967 Meta::NameValue(nv) if nv.path.is_ident("type_") => {
968 *type_ = Some(nv.value.clone());
969 true
970 }
971 Meta::NameValue(nv) if nv.path.is_ident("title") => {
972 *title = Some(nv.value.clone());
973 true
974 }
975 Meta::NameValue(nv) if nv.path.is_ident("description") => {
976 *description = Some(nv.value.clone());
977 true
978 }
979 _ => false,
980 }
981}
982
983fn parse_custom_config(list: MetaList) -> syn::Result<CustomConfig> {
984 let mut name = None;
985 let mut default = None;
986 let mut type_ = None;
987 let mut title = None;
988 let mut description = None;
989 let mut hide_title = false;
990 let mut hidden = false;
991 let mut readonly = false;
992 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
993
994 for meta in nested {
995 if parse_name_type_title_description(
996 &meta,
997 &mut name,
998 &mut type_,
999 &mut title,
1000 &mut description,
1001 ) {
1002 continue;
1003 }
1004
1005 match meta {
1006 Meta::NameValue(nv) if nv.path.is_ident("default") => {
1007 default = Some(nv.value.clone());
1008 }
1009 Meta::Path(p) if p.is_ident("hide_title") => {
1010 hide_title = true;
1011 }
1012 Meta::Path(p) if p.is_ident("hidden") => {
1013 hidden = true;
1014 }
1015 Meta::Path(p) if p.is_ident("readonly") => {
1016 readonly = true;
1017 }
1018 other => {
1019 return Err(syn::Error::new_spanned(
1020 other,
1021 "custom_config supports name, default, type/type_, title, description, hide_title, hidden, readonly",
1022 ));
1023 }
1024 }
1025 }
1026
1027 let name = name.ok_or_else(|| syn::Error::new(list.span(), "config missing `name`"))?;
1028 let default =
1029 default.ok_or_else(|| syn::Error::new(list.span(), "config missing `default`"))?;
1030 let type_ = type_.ok_or_else(|| syn::Error::new(list.span(), "config missing `type`"))?;
1031
1032 Ok(CustomConfig {
1033 name,
1034 default,
1035 type_,
1036 title,
1037 description,
1038 hide_title,
1039 hidden,
1040 readonly,
1041 })
1042}
1043
1044fn collect_exprs(list: MetaList) -> syn::Result<Vec<Expr>> {
1045 let values = list.parse_args_with(Punctuated::<Expr, Comma>::parse_terminated)?;
1046 Ok(values.into_iter().collect())
1047}
1048
1049fn parse_expr_array(expr: Expr) -> syn::Result<Vec<Expr>> {
1050 if let Expr::Array(arr) = expr {
1051 Ok(arr.elems.into_iter().collect())
1052 } else {
1053 Err(syn::Error::new_spanned(
1054 expr,
1055 "inputs/outputs expect array expressions",
1056 ))
1057 }
1058}
1059
1060fn parse_common_config(list: MetaList) -> syn::Result<CommonConfig> {
1061 let mut cfg = CommonConfig::default();
1062 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
1063
1064 for meta in nested {
1065 match meta {
1066 Meta::NameValue(nv) if nv.path.is_ident("name") => {
1067 cfg.name = Some(nv.value.clone());
1068 }
1069 Meta::NameValue(nv) if nv.path.is_ident("default") => {
1070 cfg.default = Some(nv.value.clone());
1071 }
1072 Meta::NameValue(nv) if nv.path.is_ident("title") => {
1073 cfg.title = Some(nv.value.clone());
1074 }
1075 Meta::NameValue(nv) if nv.path.is_ident("description") => {
1076 cfg.description = Some(nv.value.clone());
1077 }
1078 Meta::Path(p) if p.is_ident("hide_title") => {
1079 cfg.hide_title = true;
1080 }
1081 Meta::Path(p) if p.is_ident("hidden") => {
1082 cfg.hidden = true;
1083 }
1084 Meta::Path(p) if p.is_ident("readonly") => {
1085 cfg.readonly = true;
1086 }
1087 other => {
1088 return Err(syn::Error::new_spanned(
1089 other,
1090 "config supports name, default, title, description, hide_title, hidden, readonly",
1091 ));
1092 }
1093 }
1094 }
1095
1096 if cfg.name.is_none() {
1097 return Err(syn::Error::new(list.span(), "config missing `name`"));
1098 }
1099 Ok(cfg)
1100}
1101
1102fn custom_config_call(method: &str, cfg: CustomConfig) -> syn::Result<proc_macro2::TokenStream> {
1103 let CustomConfig {
1104 name,
1105 default,
1106 type_,
1107 title,
1108 description,
1109 hide_title,
1110 hidden,
1111 readonly,
1112 } = cfg;
1113 let title = title.map(|t| quote! { let entry = entry.title(#t); });
1114 let description = description.map(|d| quote! { let entry = entry.description(#d); });
1115 let hide_title = if hide_title {
1116 quote! { let entry = entry.hide_title(); }
1117 } else {
1118 quote! {}
1119 };
1120 let hidden = if hidden {
1121 quote! { let entry = entry.hidden(); }
1122 } else {
1123 quote! {}
1124 };
1125 let readonly = if readonly {
1126 quote! { let entry = entry.readonly(); }
1127 } else {
1128 quote! {}
1129 };
1130 let method_ident = format_ident!("{}", method);
1131
1132 Ok(quote! {
1133 .#method_ident(#name, #default, #type_, |entry| {
1134 let entry = entry;
1135 #title
1136 #description
1137 #hide_title
1138 #hidden
1139 #readonly
1140 entry
1141 })
1142 })
1143}