Skip to main content

modular_agent_core/
definition.rs

1use std::ops::Not;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::agent::Agent;
7use crate::config::AgentConfigs;
8use crate::error::AgentError;
9use crate::id::new_id;
10use crate::modular_agent::ModularAgent;
11use crate::spec::AgentSpec;
12use crate::value::AgentValue;
13use crate::FnvIndexMap;
14
15/// A map of agent definition names to their definitions.
16pub type AgentDefinitions = FnvIndexMap<String, AgentDefinition>;
17
18/// The definition (blueprint) of an agent type.
19///
20/// An agent definition describes the metadata and capabilities of an agent type,
21/// including its ports, configuration options, and factory function.
22/// Multiple agent instances can be created from a single definition.
23#[derive(Debug, Default, Serialize, Deserialize, Clone)]
24pub struct AgentDefinition {
25    /// The kind/category identifier for this agent type (e.g., "Agent", "Board").
26    pub kind: String,
27
28    /// Unique name of this agent definition.
29    pub name: String,
30
31    /// Human-readable title for display in UI.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub title: Option<String>,
34
35    /// Whether to hide the title in UI.
36    #[serde(default, skip_serializing_if = "<&bool>::not")]
37    pub hide_title: bool,
38
39    /// Description of what this agent does.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub description: Option<String>,
42
43    /// Category path for organizing agents (e.g., "Flow/Control").
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub category: Option<String>,
46
47    /// Default input port names.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub inputs: Option<Vec<String>>,
50
51    /// Default output port names.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub outputs: Option<Vec<String>>,
54
55    /// Configuration specifications for this agent type.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub configs: Option<AgentConfigSpecs>,
58
59    /// Global configuration specifications (shared across instances).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub global_configs: Option<AgentGlobalConfigSpecs>,
62
63    /// Hint metadata for UI presentation (e.g., color, size).
64    #[serde(default, skip_serializing_if = "FnvIndexMap::is_empty")]
65    pub hints: FnvIndexMap<String, Value>,
66
67    /// Whether to run this agent on a native OS thread instead of the async runtime.
68    #[serde(default, skip_serializing_if = "<&bool>::not")]
69    pub native_thread: bool,
70
71    /// Factory function to create new agent instances.
72    #[serde(skip)]
73    pub new_boxed: Option<AgentNewBoxedFn>,
74}
75
76/// A map of configuration keys to their specifications.
77pub type AgentConfigSpecs = FnvIndexMap<String, AgentConfigSpec>;
78
79/// A map of global configuration keys to their specifications.
80pub type AgentGlobalConfigSpecs = FnvIndexMap<String, AgentConfigSpec>;
81
82/// Specification for a configuration entry.
83///
84/// Defines the metadata for a configuration option, including its default value,
85/// type, display properties, and access control.
86#[derive(Debug, Default, Serialize, Deserialize, Clone)]
87pub struct AgentConfigSpec {
88    /// Default value for this configuration.
89    pub value: AgentValue,
90
91    /// Type of this configuration (e.g., "string", "integer", "boolean").
92    #[serde(rename = "type")]
93    pub type_: Option<String>,
94
95    /// Human-readable title for display in UI.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub title: Option<String>,
98
99    /// Whether to hide the title in UI.
100    #[serde(default, skip_serializing_if = "<&bool>::not")]
101    pub hide_title: bool,
102
103    /// Description of this configuration option.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub description: Option<String>,
106
107    /// Whether this configuration entry should be hidden from the user interface.
108    #[serde(default, skip_serializing_if = "<&bool>::not")]
109    pub hidden: bool,
110
111    /// Whether this configuration entry is read-only.
112    #[serde(default, skip_serializing_if = "<&bool>::not")]
113    pub readonly: bool,
114
115    /// Whether this configuration entry should only be shown in the detail view.
116    #[serde(default, skip_serializing_if = "<&bool>::not")]
117    pub detail: bool,
118}
119
120/// Factory function type for creating new agent instances.
121///
122/// Takes a `ModularAgent` orchestrator, agent ID, and spec, and returns
123/// a boxed `Agent` trait object or an error.
124pub type AgentNewBoxedFn =
125    fn(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Box<dyn Agent>, AgentError>;
126
127impl AgentDefinition {
128    /// Creates a new agent definition.
129    ///
130    /// # Arguments
131    ///
132    /// * `kind` - The kind/category identifier (e.g., "std", "llm")
133    /// * `name` - Unique name for this agent definition
134    /// * `new_boxed` - Optional factory function to create agent instances
135    pub fn new(
136        kind: impl Into<String>,
137        name: impl Into<String>,
138        new_boxed: Option<AgentNewBoxedFn>,
139    ) -> Self {
140        Self {
141            kind: kind.into(),
142            name: name.into(),
143            new_boxed,
144            ..Default::default()
145        }
146    }
147
148    /// Sets the display title. Returns self for method chaining.
149    pub fn title(mut self, title: &str) -> Self {
150        self.title = Some(title.into());
151        self
152    }
153
154    /// Hides the title in UI. Returns self for method chaining.
155    pub fn hide_title(mut self) -> Self {
156        self.hide_title = true;
157        self
158    }
159
160    /// Sets the description. Returns self for method chaining.
161    pub fn description(mut self, description: &str) -> Self {
162        self.description = Some(description.into());
163        self
164    }
165
166    /// Sets the category path. Returns self for method chaining.
167    pub fn category(mut self, category: &str) -> Self {
168        self.category = Some(category.into());
169        self
170    }
171
172    /// Sets the input port names. Returns self for method chaining.
173    pub fn inputs(mut self, inputs: Vec<&str>) -> Self {
174        self.inputs = Some(inputs.into_iter().map(|x| x.into()).collect());
175        self
176    }
177
178    /// Sets the output port names. Returns self for method chaining.
179    pub fn outputs(mut self, outputs: Vec<&str>) -> Self {
180        self.outputs = Some(outputs.into_iter().map(|x| x.into()).collect());
181        self
182    }
183
184    // Config Spec
185
186    /// Sets all configuration specifications at once.
187    pub fn configs(mut self, configs: Vec<(&str, AgentConfigSpec)>) -> Self {
188        self.configs = Some(configs.into_iter().map(|(k, v)| (k.into(), v)).collect());
189        self
190    }
191
192    /// Adds a unit (trigger/signal) configuration.
193    pub fn unit_config(self, key: &str) -> Self {
194        self.unit_config_with(key, |entry| entry)
195    }
196
197    /// Adds a unit configuration with customization callback.
198    pub fn unit_config_with<F>(self, key: &str, f: F) -> Self
199    where
200        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
201    {
202        self.config_type_with(key, (), "unit", f)
203    }
204
205    /// Adds a boolean configuration with a default value.
206    pub fn boolean_config(self, key: &str, default: bool) -> Self {
207        self.boolean_config_with(key, default, |entry| entry)
208    }
209
210    /// Adds a boolean configuration with customization callback.
211    pub fn boolean_config_with<F>(self, key: &str, default: bool, f: F) -> Self
212    where
213        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
214    {
215        self.config_type_with(key, default, "boolean", f)
216    }
217
218    /// Adds a boolean configuration with default value `false`.
219    pub fn boolean_config_default(self, key: &str) -> Self {
220        self.boolean_config(key, false)
221    }
222
223    /// Adds an integer configuration with a default value.
224    pub fn integer_config(self, key: &str, default: i64) -> Self {
225        self.integer_config_with(key, default, |entry| entry)
226    }
227
228    /// Adds an integer configuration with customization callback.
229    pub fn integer_config_with<F>(self, key: &str, default: i64, f: F) -> Self
230    where
231        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
232    {
233        self.config_type_with(key, default, "integer", f)
234    }
235
236    /// Adds an integer configuration with default value `0`.
237    pub fn integer_config_default(self, key: &str) -> Self {
238        self.integer_config(key, 0)
239    }
240
241    /// Adds a number (f64) configuration with a default value.
242    pub fn number_config(self, key: &str, default: f64) -> Self {
243        self.number_config_with(key, default, |entry| entry)
244    }
245
246    /// Adds a number configuration with customization callback.
247    pub fn number_config_with<F>(self, key: &str, default: f64, f: F) -> Self
248    where
249        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
250    {
251        self.config_type_with(key, default, "number", f)
252    }
253
254    /// Adds a number configuration with default value `0.0`.
255    pub fn number_config_default(self, key: &str) -> Self {
256        self.number_config(key, 0.0)
257    }
258
259    /// Adds a string configuration with a default value.
260    pub fn string_config(self, key: &str, default: impl Into<String>) -> Self {
261        self.string_config_with(key, default, |entry| entry)
262    }
263
264    /// Adds a string configuration with customization callback.
265    pub fn string_config_with<F>(self, key: &str, default: impl Into<String>, f: F) -> Self
266    where
267        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
268    {
269        let default = default.into();
270        self.config_type_with(key, AgentValue::string(default), "string", f)
271    }
272
273    /// Adds a string configuration with empty default value.
274    pub fn string_config_default(self, key: &str) -> Self {
275        self.string_config(key, "")
276    }
277
278    /// Adds a multiline text configuration with a default value.
279    pub fn text_config(self, key: &str, default: impl Into<String>) -> Self {
280        self.text_config_with(key, default, |entry| entry)
281    }
282
283    /// Adds a text configuration with customization callback.
284    pub fn text_config_with<F>(self, key: &str, default: impl Into<String>, f: F) -> Self
285    where
286        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
287    {
288        let default = default.into();
289        self.config_type_with(key, AgentValue::string(default), "text", f)
290    }
291
292    /// Adds a text configuration with empty default value.
293    pub fn text_config_default(self, key: &str) -> Self {
294        self.text_config(key, "")
295    }
296
297    /// Adds an array configuration with a default value.
298    pub fn array_config(self, key: &str, default: impl Into<AgentValue>) -> Self {
299        self.array_config_with(key, default, |entry| entry)
300    }
301
302    /// Adds an array configuration with customization callback.
303    pub fn array_config_with<V: Into<AgentValue>, F>(self, key: &str, default: V, f: F) -> Self
304    where
305        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
306    {
307        self.config_type_with(key, default, "array", f)
308    }
309
310    /// Adds an array configuration with empty default value.
311    pub fn array_config_default(self, key: &str) -> Self {
312        self.array_config(key, AgentValue::array_default())
313    }
314
315    /// Adds an object configuration with a default value.
316    pub fn object_config<V: Into<AgentValue>>(self, key: &str, default: V) -> Self {
317        self.object_config_with(key, default, |entry| entry)
318    }
319
320    /// Adds an object configuration with customization callback.
321    pub fn object_config_with<V: Into<AgentValue>, F>(self, key: &str, default: V, f: F) -> Self
322    where
323        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
324    {
325        self.config_type_with(key, default, "object", f)
326    }
327
328    /// Adds an object configuration with empty default value.
329    pub fn object_config_default(self, key: &str) -> Self {
330        self.object_config(key, AgentValue::object_default())
331    }
332
333    /// Adds a custom-typed configuration with customization callback.
334    pub fn custom_config_with<V: Into<AgentValue>, F>(
335        self,
336        key: &str,
337        default: V,
338        type_: &str,
339        f: F,
340    ) -> Self
341    where
342        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
343    {
344        self.config_type_with(key, default, type_, f)
345    }
346
347    /// Internal: adds a configuration with specified type.
348    fn config_type_with<V: Into<AgentValue>, F>(
349        mut self,
350        key: &str,
351        default: V,
352        type_: &str,
353        f: F,
354    ) -> Self
355    where
356        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
357    {
358        let entry = AgentConfigSpec::new(default, type_);
359        self.insert_config_entry(key.into(), f(entry));
360        self
361    }
362
363    fn insert_config_entry(&mut self, key: String, entry: AgentConfigSpec) {
364        if let Some(configs) = self.configs.as_mut() {
365            configs.insert(key, entry);
366        } else {
367            let mut map = FnvIndexMap::default();
368            map.insert(key, entry);
369            self.configs = Some(map);
370        }
371    }
372
373    // Global Configs
374    //
375    // Global configurations are shared across all instances of this agent type.
376
377    /// Sets all global configuration specifications at once.
378    pub fn global_configs(mut self, configs: Vec<(&str, AgentConfigSpec)>) -> Self {
379        self.global_configs = Some(configs.into_iter().map(|(k, v)| (k.into(), v)).collect());
380        self
381    }
382
383    /// Adds a boolean global configuration.
384    pub fn boolean_global_config(self, key: &str, default: bool) -> Self {
385        self.boolean_global_config_with(key, default, |entry| entry)
386    }
387
388    /// Adds a boolean global configuration with customization callback.
389    pub fn boolean_global_config_with<F>(self, key: &str, default: bool, f: F) -> Self
390    where
391        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
392    {
393        self.global_config_type_with(key, default, "boolean", f)
394    }
395
396    /// Adds an integer global configuration.
397    pub fn integer_global_config(self, key: &str, default: i64) -> Self {
398        self.integer_global_config_with(key, default, |entry| entry)
399    }
400
401    /// Adds an integer global configuration with customization callback.
402    pub fn integer_global_config_with<F>(self, key: &str, default: i64, f: F) -> Self
403    where
404        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
405    {
406        self.global_config_type_with(key, default, "integer", f)
407    }
408
409    /// Adds a number (f64) global configuration.
410    pub fn number_global_config(self, key: &str, default: f64) -> Self {
411        self.number_global_config_with(key, default, |entry| entry)
412    }
413
414    /// Adds a number global configuration with customization callback.
415    pub fn number_global_config_with<F>(self, key: &str, default: f64, f: F) -> Self
416    where
417        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
418    {
419        self.global_config_type_with(key, default, "number", f)
420    }
421
422    /// Adds a string global configuration.
423    pub fn string_global_config(self, key: &str, default: impl Into<String>) -> Self {
424        self.string_global_config_with(key, default, |entry| entry)
425    }
426
427    /// Adds a string global configuration with customization callback.
428    pub fn string_global_config_with<F>(self, key: &str, default: impl Into<String>, f: F) -> Self
429    where
430        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
431    {
432        let default = default.into();
433        self.global_config_type_with(key, AgentValue::string(default), "string", f)
434    }
435
436    /// Adds a multiline text global configuration.
437    pub fn text_global_config(self, key: &str, default: impl Into<String>) -> Self {
438        self.text_global_config_with(key, default, |entry| entry)
439    }
440
441    /// Adds a text global configuration with customization callback.
442    pub fn text_global_config_with<F>(self, key: &str, default: impl Into<String>, f: F) -> Self
443    where
444        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
445    {
446        let default = default.into();
447        self.global_config_type_with(key, AgentValue::string(default), "text", f)
448    }
449
450    /// Adds an array global configuration.
451    pub fn array_global_config(self, key: &str, default: impl Into<AgentValue>) -> Self {
452        self.array_global_config_with(key, default, |entry| entry)
453    }
454
455    /// Adds an array global configuration with customization callback.
456    pub fn array_global_config_with<V: Into<AgentValue>, F>(
457        self,
458        key: &str,
459        default: V,
460        f: F,
461    ) -> Self
462    where
463        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
464    {
465        self.global_config_type_with(key, default, "array", f)
466    }
467
468    /// Adds an array global configuration with empty default value.
469    pub fn array_global_config_default(self, key: &str) -> Self {
470        self.array_global_config(key, AgentValue::array_default())
471    }
472
473    /// Adds an object global configuration.
474    pub fn object_global_config<V: Into<AgentValue>>(self, key: &str, default: V) -> Self {
475        self.object_global_config_with(key, default, |entry| entry)
476    }
477
478    /// Adds an object global configuration with customization callback.
479    pub fn object_global_config_with<V: Into<AgentValue>, F>(
480        self,
481        key: &str,
482        default: V,
483        f: F,
484    ) -> Self
485    where
486        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
487    {
488        self.global_config_type_with(key, default, "object", f)
489    }
490
491    /// Adds a custom-typed global configuration with customization callback.
492    pub fn custom_global_config_with<V: Into<AgentValue>, F>(
493        self,
494        key: &str,
495        default: V,
496        type_: &str,
497        f: F,
498    ) -> Self
499    where
500        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
501    {
502        self.global_config_type_with(key, default, type_, f)
503    }
504
505    fn global_config_type_with<V: Into<AgentValue>, F>(
506        mut self,
507        key: &str,
508        default: V,
509        type_: &str,
510        f: F,
511    ) -> Self
512    where
513        F: FnOnce(AgentConfigSpec) -> AgentConfigSpec,
514    {
515        let entry = AgentConfigSpec::new(default, type_);
516        self.insert_global_config_entry(key.into(), f(entry));
517        self
518    }
519
520    fn insert_global_config_entry(&mut self, key: String, entry: AgentConfigSpec) {
521        if let Some(configs) = self.global_configs.as_mut() {
522            configs.insert(key, entry);
523        } else {
524            let mut map = FnvIndexMap::default();
525            map.insert(key, entry);
526            self.global_configs = Some(map);
527        }
528    }
529
530    /// Configures this agent to run on a native OS thread.
531    ///
532    /// Use this for agents that perform blocking I/O or CPU-intensive operations
533    /// that would block the async runtime.
534    pub fn use_native_thread(mut self) -> Self {
535        self.native_thread = true;
536        self
537    }
538
539    /// Adds a UI hint. Returns self for method chaining.
540    pub fn hint(mut self, key: &str, value: impl Into<Value>) -> Self {
541        self.hints.insert(key.into(), value.into());
542        self
543    }
544
545    /// Creates a new agent specification from this definition.
546    ///
547    /// Generates a unique ID and copies the definition's ports and configs
548    /// to create a new instance specification.
549    pub fn to_spec(&self) -> AgentSpec {
550        AgentSpec {
551            id: new_id(),
552            def_name: self.name.clone(),
553            inputs: self.inputs.clone(),
554            outputs: self.outputs.clone(),
555            configs: self.configs.as_ref().map(|cfgs| {
556                cfgs.iter()
557                    .map(|(k, v)| (k.clone(), v.value.clone()))
558                    .collect()
559            }),
560            config_specs: self.configs.clone(),
561            disabled: false,
562            extensions: FnvIndexMap::default(),
563        }
564    }
565
566    /// Reconciles an existing `AgentSpec` with this definition for backward compatibility.
567    ///
568    /// When loading old JSON presets, the spec may not match the current definition.
569    /// This method:
570    /// - Fills missing config keys with definition defaults
571    /// - Renames stale keys (not in definition) with `_` prefix for lazy migration
572    /// - Overwrites `config_specs` with current definition metadata
573    /// - Overwrites ports with current definition ports
574    ///
575    /// Keys already starting with `_` are skipped during rename (idempotency).
576    /// `_`-prefixed keys are cleaned up by `AgentData::new()`.
577    ///
578    /// Config names must not start with `_` (reserved for stale key migration).
579    pub fn reconcile_spec(&self, spec: &mut AgentSpec) {
580        // Ports
581        if let Some(ref inputs) = self.inputs {
582            spec.inputs = Some(inputs.clone());
583        }
584        if let Some(ref outputs) = self.outputs {
585            spec.outputs = Some(outputs.clone());
586        }
587
588        // config_specs
589        spec.config_specs = self.configs.clone();
590
591        // Configs
592        let def_keys: Option<std::collections::HashSet<&str>> = self
593            .configs
594            .as_ref()
595            .map(|c| c.keys().map(|k| k.as_str()).collect());
596
597        if let Some(ref mut spec_configs) = spec.configs {
598            // Rename stale keys with `_` prefix (skip already-prefixed for idempotency)
599            let stale: Vec<String> = spec_configs
600                .keys()
601                .filter(|k| {
602                    !k.starts_with('_')
603                        && !def_keys.as_ref().is_some_and(|dk| dk.contains(k.as_str()))
604                })
605                .cloned()
606                .collect();
607            for key in stale {
608                if let Some(value) = spec_configs.remove(&key) {
609                    spec_configs.set(format!("_{key}"), value);
610                }
611            }
612
613            // Fill missing keys with definition defaults
614            if let Some(ref def_configs) = self.configs {
615                for (key, cs) in def_configs.iter() {
616                    if !spec_configs.contains_key(key) {
617                        spec_configs.set(key.clone(), cs.value.clone());
618                    }
619                }
620            }
621        } else if let Some(ref def_configs) = self.configs {
622            // spec.configs is None → create from definition defaults
623            spec.configs = Some(
624                def_configs
625                    .iter()
626                    .map(|(k, v)| (k.clone(), v.value.clone()))
627                    .collect(),
628            );
629        }
630
631        // Reorder configs to match definition key order
632        if let Some(ref mut spec_configs) = spec.configs {
633            if let Some(ref def_configs) = self.configs {
634                let mut reordered = AgentConfigs::new();
635                // First: definition keys in definition order
636                for (key, _) in def_configs.iter() {
637                    if let Ok(value) = spec_configs.get(key) {
638                        reordered.set(key.clone(), value.clone());
639                    }
640                }
641                // Then: remaining keys (stale `_`-prefixed, etc.)
642                for (key, value) in &*spec_configs {
643                    if !reordered.contains_key(key) {
644                        reordered.set(key.clone(), value.clone());
645                    }
646                }
647                *spec_configs = reordered;
648            }
649        }
650    }
651}
652
653impl AgentConfigSpec {
654    /// Creates a new configuration specification.
655    ///
656    /// # Arguments
657    ///
658    /// * `value` - Default value for this configuration
659    /// * `type_` - Type identifier (e.g., "string", "integer", "boolean")
660    pub fn new<V: Into<AgentValue>>(value: V, type_: &str) -> Self {
661        Self {
662            value: value.into(),
663            type_: Some(type_.into()),
664            ..Default::default()
665        }
666    }
667
668    /// Sets the display title. Returns self for method chaining.
669    pub fn title(mut self, title: &str) -> Self {
670        self.title = Some(title.into());
671        self
672    }
673
674    /// Hides the title in UI. Returns self for method chaining.
675    pub fn hide_title(mut self) -> Self {
676        self.hide_title = true;
677        self
678    }
679
680    /// Sets the description. Returns self for method chaining.
681    pub fn description(mut self, description: &str) -> Self {
682        self.description = Some(description.into());
683        self
684    }
685
686    /// Marks this config as hidden from UI. Returns self for method chaining.
687    pub fn hidden(mut self) -> Self {
688        self.hidden = true;
689        self
690    }
691
692    /// Marks this config as read-only. Returns self for method chaining.
693    pub fn readonly(mut self) -> Self {
694        self.readonly = true;
695        self
696    }
697
698    /// Marks this config as detail-only (shown only in detail view). Returns self for method chaining.
699    pub fn detail(mut self) -> Self {
700        self.detail = true;
701        self
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use im::{hashmap, vector};
708
709    use super::*;
710    use crate::config::AgentConfigs;
711
712    #[test]
713    fn test_agent_definition() {
714        let def = AgentDefinition::default();
715        assert_eq!(def.name, "");
716    }
717
718    #[test]
719    fn test_agent_definition_new_default() {
720        let def = AgentDefinition::new(
721            "test",
722            "echo",
723            Some(|_app, _id, _spec| Err(AgentError::NotImplemented("Echo agent".into()))),
724        );
725
726        assert_eq!(def.kind, "test");
727        assert_eq!(def.name, "echo");
728        assert!(def.title.is_none());
729        assert!(def.category.is_none());
730        assert!(def.inputs.is_none());
731        assert!(def.outputs.is_none());
732        assert!(def.configs.is_none());
733    }
734
735    #[test]
736    fn test_agent_definition_new() {
737        let def = echo_agent_definition();
738
739        assert_eq!(def.kind, "test");
740        assert_eq!(def.name, "echo");
741        assert_eq!(def.title.unwrap(), "Echo");
742        assert_eq!(def.category.unwrap(), "Test");
743        assert_eq!(def.inputs.unwrap(), vec!["in"]);
744        assert_eq!(def.outputs.unwrap(), vec!["out"]);
745        let default_configs = def.configs.unwrap();
746        assert_eq!(default_configs.len(), 2);
747        let entry = default_configs.get("value").unwrap();
748        assert_eq!(entry.value, AgentValue::string("abc"));
749        assert_eq!(entry.type_.as_ref().unwrap(), "string");
750        assert_eq!(entry.title.as_ref().unwrap(), "display_title");
751        assert_eq!(entry.description.as_ref().unwrap(), "display_description");
752        assert_eq!(entry.hide_title, false);
753        assert_eq!(entry.readonly, true);
754        assert_eq!(entry.detail, true);
755        let entry = default_configs.get("hide_title_value").unwrap();
756        assert_eq!(entry.value, AgentValue::integer(1));
757        assert_eq!(entry.type_.as_ref().unwrap(), "integer");
758        assert_eq!(entry.title, None);
759        assert_eq!(entry.description, None);
760        assert_eq!(entry.hide_title, true);
761        assert_eq!(entry.readonly, true);
762        assert_eq!(entry.detail, false);
763    }
764
765    #[test]
766    fn test_serialize_agent_definition() {
767        let def = AgentDefinition::new(
768            "test",
769            "echo",
770            Some(|_app, _id, _spec| Err(AgentError::NotImplemented("Echo agent".into()))),
771        );
772        let json = serde_json::to_string(&def).unwrap();
773        assert_eq!(json, r#"{"kind":"test","name":"echo"}"#);
774    }
775
776    #[test]
777    fn test_serialize_echo_agent_definition() {
778        let def = echo_agent_definition();
779        let json = serde_json::to_string(&def).unwrap();
780        print!("{}", json);
781        assert_eq!(
782            json,
783            r#"{"kind":"test","name":"echo","title":"Echo","category":"Test","inputs":["in"],"outputs":["out"],"configs":{"value":{"value":"abc","type":"string","title":"display_title","description":"display_description","readonly":true,"detail":true},"hide_title_value":{"value":1,"type":"integer","hide_title":true,"readonly":true}}}"#
784        );
785    }
786
787    #[test]
788    fn test_deserialize_echo_agent_definition() {
789        let json = r#"{"kind":"test","name":"echo","title":"Echo","category":"Test","inputs":["in"],"outputs":["out"],"configs":{"value":{"value":"abc","type":"string","title":"display_title","description":"display_description","readonly":true,"detail":true},"hide_title_value":{"value":1,"type":"integer","hide_title":true,"readonly":true}}}"#;
790        let def: AgentDefinition = serde_json::from_str(json).unwrap();
791        assert_eq!(def.kind, "test");
792        assert_eq!(def.name, "echo");
793        assert_eq!(def.title.unwrap(), "Echo");
794        assert_eq!(def.category.unwrap(), "Test");
795        assert_eq!(def.inputs.unwrap(), vec!["in"]);
796        assert_eq!(def.outputs.unwrap(), vec!["out"]);
797        let default_configs = def.configs.unwrap();
798        assert_eq!(default_configs.len(), 2);
799        let (key, entry) = default_configs.get_index(0).unwrap();
800        assert_eq!(key, "value");
801        assert_eq!(entry.type_.as_ref().unwrap(), "string");
802        assert_eq!(entry.title.as_ref().unwrap(), "display_title");
803        assert_eq!(entry.description.as_ref().unwrap(), "display_description");
804        assert_eq!(entry.hide_title, false);
805        assert_eq!(entry.detail, true);
806        let (key, entry) = default_configs.get_index(1).unwrap();
807        assert_eq!(key, "hide_title_value");
808        assert_eq!(entry.type_.as_ref().unwrap(), "integer");
809        assert_eq!(entry.title, None);
810        assert_eq!(entry.description, None);
811        assert_eq!(entry.hide_title, true);
812    }
813
814    #[test]
815    fn test_default_config_helpers() {
816        let custom_object_value =
817            AgentValue::object(hashmap! {"key".into() => AgentValue::string("value")});
818        let custom_array_value =
819            AgentValue::array(vector![AgentValue::integer(1), AgentValue::string("two")]);
820
821        let def = AgentDefinition::new("test", "helpers", None)
822            .unit_config("unit_value")
823            .boolean_config_default("boolean_value")
824            .boolean_config("boolean_custom", true)
825            .integer_config_default("integer_value")
826            .integer_config("integer_custom", 42)
827            .number_config_default("number_value")
828            .number_config("number_custom", 1.5)
829            .string_config_default("string_default")
830            .string_config("string_value", "value")
831            .text_config_default("text_value")
832            .text_config("text_custom", "custom")
833            .array_config_default("array_value")
834            .array_config("array_custom", custom_array_value.clone())
835            .object_config_default("object_value")
836            .object_config("object_custom", custom_object_value.clone());
837
838        let configs = def.configs.clone().expect("default configs should exist");
839        assert_eq!(configs.len(), 15);
840        let config_map: std::collections::HashMap<_, _> = configs.into_iter().collect();
841
842        let unit_entry = config_map.get("unit_value").unwrap();
843        assert_eq!(unit_entry.type_.as_deref(), Some("unit"));
844        assert_eq!(unit_entry.value, AgentValue::unit());
845
846        let boolean_entry = config_map.get("boolean_value").unwrap();
847        assert_eq!(boolean_entry.type_.as_deref(), Some("boolean"));
848        assert_eq!(boolean_entry.value, AgentValue::boolean(false));
849
850        let boolean_custom_entry = config_map.get("boolean_custom").unwrap();
851        assert_eq!(boolean_custom_entry.type_.as_deref(), Some("boolean"));
852        assert_eq!(boolean_custom_entry.value, AgentValue::boolean(true));
853
854        let integer_entry = config_map.get("integer_value").unwrap();
855        assert_eq!(integer_entry.type_.as_deref(), Some("integer"));
856        assert_eq!(integer_entry.value, AgentValue::integer(0));
857
858        let integer_custom_entry = config_map.get("integer_custom").unwrap();
859        assert_eq!(integer_custom_entry.type_.as_deref(), Some("integer"));
860        assert_eq!(integer_custom_entry.value, AgentValue::integer(42));
861
862        let number_entry = config_map.get("number_value").unwrap();
863        assert_eq!(number_entry.type_.as_deref(), Some("number"));
864        assert_eq!(number_entry.value, AgentValue::number(0.0));
865
866        let number_custom_entry = config_map.get("number_custom").unwrap();
867        assert_eq!(number_custom_entry.type_.as_deref(), Some("number"));
868        assert_eq!(number_custom_entry.value, AgentValue::number(1.5));
869
870        let string_default_entry = config_map.get("string_default").unwrap();
871        assert_eq!(string_default_entry.type_.as_deref(), Some("string"));
872        assert_eq!(string_default_entry.value, AgentValue::string(""));
873
874        let string_entry = config_map.get("string_value").unwrap();
875        assert_eq!(string_entry.type_.as_deref(), Some("string"));
876        assert_eq!(string_entry.value, AgentValue::string("value"));
877
878        let text_entry = config_map.get("text_value").unwrap();
879        assert_eq!(text_entry.type_.as_deref(), Some("text"));
880        assert_eq!(text_entry.value, AgentValue::string(""));
881
882        let text_custom_entry = config_map.get("text_custom").unwrap();
883        assert_eq!(text_custom_entry.type_.as_deref(), Some("text"));
884        assert_eq!(text_custom_entry.value, AgentValue::string("custom"));
885
886        let array_entry = config_map.get("array_value").unwrap();
887        assert_eq!(array_entry.type_.as_deref(), Some("array"));
888        assert_eq!(array_entry.value, AgentValue::array_default());
889
890        let array_custom_entry = config_map.get("array_custom").unwrap();
891        assert_eq!(array_custom_entry.type_.as_deref(), Some("array"));
892        assert_eq!(array_custom_entry.value, custom_array_value);
893
894        let object_entry = config_map.get("object_value").unwrap();
895        assert_eq!(object_entry.type_.as_deref(), Some("object"));
896        assert_eq!(object_entry.value, AgentValue::object_default());
897
898        let object_custom_entry = config_map.get("object_custom").unwrap();
899        assert_eq!(object_custom_entry.type_.as_deref(), Some("object"));
900        assert_eq!(object_custom_entry.value, custom_object_value);
901    }
902
903    #[test]
904    fn test_global_config_helpers() {
905        let custom_object_value =
906            AgentValue::object(hashmap! {"key".into() => AgentValue::string("value")});
907        let custom_array_value =
908            AgentValue::array(vector![AgentValue::integer(1), AgentValue::string("two")]);
909
910        let def = AgentDefinition::new("test", "helpers", None)
911            .boolean_global_config("global_boolean", true)
912            .integer_global_config("global_integer", 42)
913            .number_global_config("global_number", 1.5)
914            .string_global_config("global_string", "value")
915            .text_global_config("global_text", "global")
916            .array_global_config_default("global_array")
917            .array_global_config("global_array_custom", custom_array_value.clone())
918            .object_global_config("global_object", custom_object_value.clone());
919
920        let global_configs = def.global_configs.expect("global configs should exist");
921        assert_eq!(global_configs.len(), 8);
922        let config_map: std::collections::HashMap<_, _> = global_configs.into_iter().collect();
923
924        let entry = config_map.get("global_boolean").unwrap();
925        assert_eq!(entry.type_.as_deref(), Some("boolean"));
926        assert_eq!(entry.value, AgentValue::boolean(true));
927
928        let entry = config_map.get("global_integer").unwrap();
929        assert_eq!(entry.type_.as_deref(), Some("integer"));
930        assert_eq!(entry.value, AgentValue::integer(42));
931
932        let entry = config_map.get("global_number").unwrap();
933        assert_eq!(entry.type_.as_deref(), Some("number"));
934        assert_eq!(entry.value, AgentValue::number(1.5));
935
936        let entry = config_map.get("global_string").unwrap();
937        assert_eq!(entry.type_.as_deref(), Some("string"));
938        assert_eq!(entry.value, AgentValue::string("value"));
939
940        let entry = config_map.get("global_text").unwrap();
941        assert_eq!(entry.type_.as_deref(), Some("text"));
942        assert_eq!(entry.value, AgentValue::string("global"));
943
944        let entry = config_map.get("global_array").unwrap();
945        assert_eq!(entry.type_.as_deref(), Some("array"));
946        assert_eq!(entry.value, AgentValue::array_default());
947
948        let entry = config_map.get("global_array_custom").unwrap();
949        assert_eq!(entry.type_.as_deref(), Some("array"));
950        assert_eq!(entry.value, custom_array_value);
951
952        let entry = config_map.get("global_object").unwrap();
953        assert_eq!(entry.type_.as_deref(), Some("object"));
954        assert_eq!(entry.value, custom_object_value);
955    }
956
957    #[test]
958    fn test_config_helper_customization() {
959        let def = AgentDefinition::new("test", "custom", None)
960            .integer_config_with("custom_default", 1, |entry| entry.title("Custom"))
961            .text_global_config_with("custom_global", "value", |entry| {
962                entry.description("Global Desc")
963            });
964        // .text_display_config_with("custom_display", |entry| entry.title("Display"));
965
966        let default_entry = def.configs.as_ref().unwrap().get("custom_default").unwrap();
967        assert_eq!(default_entry.title.as_deref(), Some("Custom"));
968
969        let global_entry = def
970            .global_configs
971            .as_ref()
972            .unwrap()
973            .get("custom_global")
974            .unwrap();
975        assert_eq!(global_entry.description.as_deref(), Some("Global Desc"));
976    }
977
978    fn echo_agent_definition() -> AgentDefinition {
979        AgentDefinition::new(
980            "test",
981            "echo",
982            Some(|_app, _id, _spec| Err(AgentError::NotImplemented("Echo agent".into()))),
983        )
984        .title("Echo")
985        .category("Test")
986        .inputs(vec!["in"])
987        .outputs(vec!["out"])
988        .string_config_with("value", "abc", |entry| {
989            entry
990                .title("display_title")
991                .description("display_description")
992                .readonly()
993                .detail()
994        })
995        .integer_config_with("hide_title_value", 1, |entry| entry.hide_title().readonly())
996    }
997
998    // --- reconcile_spec tests ---
999
1000    fn reconcile_def() -> AgentDefinition {
1001        AgentDefinition::new("test", "reconcile", None)
1002            .inputs(vec!["in1", "in2"])
1003            .outputs(vec!["out"])
1004            .string_config("name", "default_name")
1005            .integer_config("count", 10)
1006            .boolean_config("enabled", true)
1007    }
1008
1009    #[test]
1010    fn test_reconcile_fills_missing_configs() {
1011        let def = reconcile_def();
1012        let mut configs = AgentConfigs::new();
1013        configs.set("name".into(), AgentValue::string("hello"));
1014        let mut spec = AgentSpec {
1015            configs: Some(configs),
1016            ..Default::default()
1017        };
1018
1019        def.reconcile_spec(&mut spec);
1020
1021        let c = spec.configs.as_ref().unwrap();
1022        assert_eq!(c.get_string_or_default("name"), "hello");
1023        assert_eq!(c.get_integer_or_default("count"), 10);
1024        assert_eq!(c.get_bool_or_default("enabled"), true);
1025    }
1026
1027    #[test]
1028    fn test_reconcile_renames_stale_keys() {
1029        let def = AgentDefinition::new("test", "r", None).string_config("name", "default");
1030        let mut configs = AgentConfigs::new();
1031        configs.set("name".into(), AgentValue::string("hello"));
1032        configs.set("old_key".into(), AgentValue::string("stale_val"));
1033        configs.set("removed".into(), AgentValue::integer(42));
1034        let mut spec = AgentSpec {
1035            configs: Some(configs),
1036            ..Default::default()
1037        };
1038
1039        def.reconcile_spec(&mut spec);
1040
1041        let c = spec.configs.as_ref().unwrap();
1042        assert_eq!(c.get_string_or_default("name"), "hello");
1043        assert!(c.get("old_key").is_err());
1044        assert_eq!(c.get("_old_key").unwrap(), &AgentValue::string("stale_val"));
1045        assert!(c.get("removed").is_err());
1046        assert_eq!(c.get("_removed").unwrap(), &AgentValue::integer(42));
1047    }
1048
1049    #[test]
1050    fn test_reconcile_skips_already_prefixed() {
1051        let def = AgentDefinition::new("test", "r", None).string_config("name", "default");
1052        let mut configs = AgentConfigs::new();
1053        configs.set("name".into(), AgentValue::string("hello"));
1054        configs.set("_old".into(), AgentValue::string("from_prev_reconcile"));
1055        let mut spec = AgentSpec {
1056            configs: Some(configs),
1057            ..Default::default()
1058        };
1059
1060        def.reconcile_spec(&mut spec);
1061
1062        let c = spec.configs.as_ref().unwrap();
1063        assert_eq!(
1064            c.get("_old").unwrap(),
1065            &AgentValue::string("from_prev_reconcile")
1066        );
1067        assert!(c.get("__old").is_err());
1068    }
1069
1070    #[test]
1071    fn test_reconcile_overwrites_config_specs() {
1072        let def = reconcile_def();
1073        let mut spec = AgentSpec {
1074            config_specs: Some(FnvIndexMap::default()),
1075            ..Default::default()
1076        };
1077
1078        def.reconcile_spec(&mut spec);
1079
1080        let specs = spec.config_specs.as_ref().unwrap();
1081        assert!(specs.contains_key("name"));
1082        assert!(specs.contains_key("count"));
1083        assert!(specs.contains_key("enabled"));
1084        assert_eq!(specs.len(), 3);
1085    }
1086
1087    #[test]
1088    fn test_reconcile_overwrites_ports() {
1089        let def = reconcile_def();
1090        let mut spec = AgentSpec {
1091            inputs: Some(vec!["old_in".into()]),
1092            outputs: Some(vec!["old_out".into()]),
1093            ..Default::default()
1094        };
1095
1096        def.reconcile_spec(&mut spec);
1097
1098        assert_eq!(
1099            spec.inputs.as_ref().unwrap(),
1100            &vec!["in1".to_string(), "in2".to_string()]
1101        );
1102        assert_eq!(spec.outputs.as_ref().unwrap(), &vec!["out".to_string()]);
1103    }
1104
1105    #[test]
1106    fn test_reconcile_preserves_ports_when_def_none() {
1107        let def = AgentDefinition::new("test", "r", None);
1108        let mut spec = AgentSpec {
1109            inputs: Some(vec!["custom_in".into()]),
1110            ..Default::default()
1111        };
1112
1113        def.reconcile_spec(&mut spec);
1114
1115        assert_eq!(
1116            spec.inputs.as_ref().unwrap(),
1117            &vec!["custom_in".to_string()]
1118        );
1119    }
1120
1121    #[test]
1122    fn test_reconcile_configs_none_creates_defaults() {
1123        let def = reconcile_def();
1124        let mut spec = AgentSpec::default();
1125        assert!(spec.configs.is_none());
1126
1127        def.reconcile_spec(&mut spec);
1128
1129        let c = spec.configs.as_ref().unwrap();
1130        assert_eq!(c.get_string_or_default("name"), "default_name");
1131        assert_eq!(c.get_integer_or_default("count"), 10);
1132        assert_eq!(c.get_bool_or_default("enabled"), true);
1133        // Key order matches definition order
1134        let keys: Vec<&String> = c.keys().collect();
1135        assert_eq!(keys, vec!["name", "count", "enabled"]);
1136    }
1137
1138    #[test]
1139    fn test_reconcile_def_configs_none_marks_all_stale() {
1140        let def = AgentDefinition::new("test", "r", None);
1141        let mut configs = AgentConfigs::new();
1142        configs.set("old_a".into(), AgentValue::string("a"));
1143        configs.set("old_b".into(), AgentValue::integer(1));
1144        let mut spec = AgentSpec {
1145            configs: Some(configs),
1146            ..Default::default()
1147        };
1148
1149        def.reconcile_spec(&mut spec);
1150
1151        let c = spec.configs.as_ref().unwrap();
1152        assert!(c.get("old_a").is_err());
1153        assert!(c.get("old_b").is_err());
1154        assert_eq!(c.get("_old_a").unwrap(), &AgentValue::string("a"));
1155        assert_eq!(c.get("_old_b").unwrap(), &AgentValue::integer(1));
1156    }
1157
1158    #[test]
1159    fn test_reconcile_preserves_user_values() {
1160        let def = reconcile_def();
1161        let mut configs = AgentConfigs::new();
1162        configs.set("name".into(), AgentValue::string("custom"));
1163        configs.set("count".into(), AgentValue::integer(42));
1164        configs.set("enabled".into(), AgentValue::boolean(false));
1165        let mut spec = AgentSpec {
1166            configs: Some(configs),
1167            ..Default::default()
1168        };
1169
1170        def.reconcile_spec(&mut spec);
1171
1172        let c = spec.configs.as_ref().unwrap();
1173        assert_eq!(c.get_string_or_default("name"), "custom");
1174        assert_eq!(c.get_integer_or_default("count"), 42);
1175        assert_eq!(c.get_bool_or_default("enabled"), false);
1176    }
1177
1178    #[test]
1179    fn test_reconcile_idempotent() {
1180        let def = reconcile_def();
1181        let mut configs = AgentConfigs::new();
1182        configs.set("name".into(), AgentValue::string("hello"));
1183        configs.set("old".into(), AgentValue::string("stale"));
1184        let mut spec = AgentSpec {
1185            configs: Some(configs),
1186            ..Default::default()
1187        };
1188
1189        def.reconcile_spec(&mut spec);
1190        let first = spec.clone();
1191
1192        def.reconcile_spec(&mut spec);
1193
1194        let c1 = first.configs.as_ref().unwrap();
1195        let c2 = spec.configs.as_ref().unwrap();
1196        assert_eq!(
1197            c1.get_string_or_default("name"),
1198            c2.get_string_or_default("name")
1199        );
1200        assert_eq!(
1201            c1.get_integer_or_default("count"),
1202            c2.get_integer_or_default("count")
1203        );
1204        assert_eq!(c1.get("_old").unwrap(), c2.get("_old").unwrap());
1205    }
1206
1207    #[test]
1208    fn test_reconcile_to_spec_is_noop() {
1209        let def = reconcile_def();
1210        let mut spec = def.to_spec();
1211        let original = spec.clone();
1212
1213        def.reconcile_spec(&mut spec);
1214
1215        let c1 = spec.configs.as_ref().unwrap();
1216        let c2 = original.configs.as_ref().unwrap();
1217        assert_eq!(
1218            c1.get_string_or_default("name"),
1219            c2.get_string_or_default("name")
1220        );
1221        assert_eq!(
1222            c1.get_integer_or_default("count"),
1223            c2.get_integer_or_default("count")
1224        );
1225        assert_eq!(
1226            c1.get_bool_or_default("enabled"),
1227            c2.get_bool_or_default("enabled")
1228        );
1229        assert_eq!(spec.inputs, original.inputs);
1230        assert_eq!(spec.outputs, original.outputs);
1231    }
1232
1233    #[test]
1234    fn test_reconcile_empty_configs() {
1235        let def = reconcile_def();
1236        let mut spec = AgentSpec {
1237            configs: Some(AgentConfigs::new()),
1238            ..Default::default()
1239        };
1240
1241        def.reconcile_spec(&mut spec);
1242
1243        let c = spec.configs.as_ref().unwrap();
1244        assert_eq!(c.get_string_or_default("name"), "default_name");
1245        assert_eq!(c.get_integer_or_default("count"), 10);
1246        assert_eq!(c.get_bool_or_default("enabled"), true);
1247    }
1248
1249    #[test]
1250    fn test_reconcile_mixed_stale_and_prefixed() {
1251        let def = AgentDefinition::new("test", "r", None).string_config("name", "default");
1252        let mut configs = AgentConfigs::new();
1253        configs.set("name".into(), AgentValue::string("hello"));
1254        configs.set("_prev_stale".into(), AgentValue::string("from_prev"));
1255        configs.set("removed".into(), AgentValue::integer(99));
1256        let mut spec = AgentSpec {
1257            configs: Some(configs),
1258            ..Default::default()
1259        };
1260
1261        def.reconcile_spec(&mut spec);
1262
1263        let c = spec.configs.as_ref().unwrap();
1264        assert_eq!(c.get_string_or_default("name"), "hello");
1265        // _prev_stale is kept as-is (already prefixed)
1266        assert_eq!(
1267            c.get("_prev_stale").unwrap(),
1268            &AgentValue::string("from_prev")
1269        );
1270        assert!(c.get("__prev_stale").is_err());
1271        // removed is newly prefixed
1272        assert!(c.get("removed").is_err());
1273        assert_eq!(c.get("_removed").unwrap(), &AgentValue::integer(99));
1274    }
1275
1276    #[test]
1277    fn test_reconcile_reorders_configs_to_definition_order() {
1278        let def = reconcile_def(); // defines: name, count, enabled
1279        let mut configs = AgentConfigs::new();
1280        // Insert in reverse order
1281        configs.set("enabled".into(), AgentValue::boolean(false));
1282        configs.set("count".into(), AgentValue::integer(42));
1283        configs.set("name".into(), AgentValue::string("custom"));
1284        let mut spec = AgentSpec {
1285            configs: Some(configs),
1286            ..Default::default()
1287        };
1288
1289        def.reconcile_spec(&mut spec);
1290
1291        let c = spec.configs.as_ref().unwrap();
1292        let keys: Vec<&String> = c.keys().collect();
1293        assert_eq!(keys, vec!["name", "count", "enabled"]);
1294        // Values are preserved
1295        assert_eq!(c.get_string_or_default("name"), "custom");
1296        assert_eq!(c.get_integer_or_default("count"), 42);
1297        assert_eq!(c.get_bool_or_default("enabled"), false);
1298    }
1299
1300    #[test]
1301    fn test_reconcile_reorder_stale_keys_at_end() {
1302        let def = reconcile_def(); // defines: name, count, enabled
1303        let mut configs = AgentConfigs::new();
1304        configs.set("old_key".into(), AgentValue::string("stale"));
1305        configs.set("enabled".into(), AgentValue::boolean(true));
1306        configs.set("name".into(), AgentValue::string("hello"));
1307        let mut spec = AgentSpec {
1308            configs: Some(configs),
1309            ..Default::default()
1310        };
1311
1312        def.reconcile_spec(&mut spec);
1313
1314        let c = spec.configs.as_ref().unwrap();
1315        let keys: Vec<&String> = c.keys().collect();
1316        // Definition keys first in definition order, then stale keys at end
1317        assert_eq!(keys, vec!["name", "count", "enabled", "_old_key"]);
1318    }
1319
1320    #[test]
1321    fn test_reconcile_reorder_is_idempotent() {
1322        let def = reconcile_def();
1323        let mut configs = AgentConfigs::new();
1324        configs.set("enabled".into(), AgentValue::boolean(false));
1325        configs.set("name".into(), AgentValue::string("hello"));
1326        configs.set("old".into(), AgentValue::string("stale"));
1327        let mut spec = AgentSpec {
1328            configs: Some(configs),
1329            ..Default::default()
1330        };
1331
1332        def.reconcile_spec(&mut spec);
1333        let order_first: Vec<String> = spec.configs.as_ref().unwrap().keys().cloned().collect();
1334
1335        def.reconcile_spec(&mut spec);
1336        let order_second: Vec<String> = spec.configs.as_ref().unwrap().keys().cloned().collect();
1337
1338        assert_eq!(order_first, order_second);
1339    }
1340
1341    // --- hints tests ---
1342
1343    #[test]
1344    fn test_hint_builder() {
1345        let def = AgentDefinition::new("test", "hinted", None)
1346            .hint("color", 3)
1347            .hint("width", 2)
1348            .hint("height", 1);
1349        assert_eq!(def.hints.len(), 3);
1350        assert_eq!(def.hints["color"], serde_json::json!(3));
1351        assert_eq!(def.hints["width"], serde_json::json!(2));
1352        assert_eq!(def.hints["height"], serde_json::json!(1));
1353    }
1354
1355    #[test]
1356    fn test_hint_string_value() {
1357        let def = AgentDefinition::new("test", "hinted", None).hint("label", "red");
1358        assert_eq!(def.hints["label"], serde_json::json!("red"));
1359    }
1360
1361    #[test]
1362    fn test_hint_boolean_value() {
1363        let def = AgentDefinition::new("test", "hinted", None).hint("resizable", true);
1364        assert_eq!(def.hints["resizable"], serde_json::json!(true));
1365    }
1366
1367    #[test]
1368    fn test_no_hints_serialization() {
1369        let def = AgentDefinition::new("test", "empty", None);
1370        let json = serde_json::to_string(&def).unwrap();
1371        assert!(!json.contains("hints"));
1372    }
1373
1374    #[test]
1375    fn test_hints_serialization_roundtrip() {
1376        let def = AgentDefinition::new("test", "hinted", None)
1377            .hint("color", 3)
1378            .hint("width", 2);
1379        let json = serde_json::to_string(&def).unwrap();
1380        assert!(json.contains(r#""hints""#));
1381        let parsed: AgentDefinition = serde_json::from_str(&json).unwrap();
1382        assert_eq!(parsed.hints.len(), 2);
1383        assert_eq!(parsed.hints["color"], serde_json::json!(3));
1384        assert_eq!(parsed.hints["width"], serde_json::json!(2));
1385    }
1386
1387    #[test]
1388    fn test_hints_deserialization_missing_field() {
1389        let json = r#"{"kind":"test","name":"no_hints"}"#;
1390        let def: AgentDefinition = serde_json::from_str(json).unwrap();
1391        assert!(def.hints.is_empty());
1392    }
1393}