askit_macros/
lib.rs

1#![recursion_limit = "256"]
2//! Procedural macros for agent-stream-kit.
3//!
4//! Provides an attribute to declare agent metadata alongside the agent type and
5//! generate the registration boilerplate.
6
7use 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/// Declare agent metadata and generate `agent_definition` / `register` helpers.
21///
22/// Example:
23/// ```rust,ignore
24/// #[askit_agent(
25///     title = "Add Int",
26///     category = "Utils",
27///     inputs = ["int"],
28///     outputs = ["int"],
29///     integer_config(
30///         name = "n",
31///         default = 1,
32///     )
33/// )]
34/// struct AdderAgent { /* ... */ }
35/// ```
36#[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}