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
15pub type AgentDefinitions = FnvIndexMap<String, AgentDefinition>;
17
18#[derive(Debug, Default, Serialize, Deserialize, Clone)]
24pub struct AgentDefinition {
25 pub kind: String,
27
28 pub name: String,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub title: Option<String>,
34
35 #[serde(default, skip_serializing_if = "<&bool>::not")]
37 pub hide_title: bool,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub description: Option<String>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub category: Option<String>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub inputs: Option<Vec<String>>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub outputs: Option<Vec<String>>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub configs: Option<AgentConfigSpecs>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub global_configs: Option<AgentGlobalConfigSpecs>,
62
63 #[serde(default, skip_serializing_if = "FnvIndexMap::is_empty")]
65 pub hints: FnvIndexMap<String, Value>,
66
67 #[serde(default, skip_serializing_if = "<&bool>::not")]
69 pub native_thread: bool,
70
71 #[serde(skip)]
73 pub new_boxed: Option<AgentNewBoxedFn>,
74}
75
76pub type AgentConfigSpecs = FnvIndexMap<String, AgentConfigSpec>;
78
79pub type AgentGlobalConfigSpecs = FnvIndexMap<String, AgentConfigSpec>;
81
82#[derive(Debug, Default, Serialize, Deserialize, Clone)]
87pub struct AgentConfigSpec {
88 pub value: AgentValue,
90
91 #[serde(rename = "type")]
93 pub type_: Option<String>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub title: Option<String>,
98
99 #[serde(default, skip_serializing_if = "<&bool>::not")]
101 pub hide_title: bool,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub description: Option<String>,
106
107 #[serde(default, skip_serializing_if = "<&bool>::not")]
109 pub hidden: bool,
110
111 #[serde(default, skip_serializing_if = "<&bool>::not")]
113 pub readonly: bool,
114
115 #[serde(default, skip_serializing_if = "<&bool>::not")]
117 pub detail: bool,
118}
119
120pub type AgentNewBoxedFn =
125 fn(ma: ModularAgent, id: String, spec: AgentSpec) -> Result<Box<dyn Agent>, AgentError>;
126
127impl AgentDefinition {
128 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 pub fn title(mut self, title: &str) -> Self {
150 self.title = Some(title.into());
151 self
152 }
153
154 pub fn hide_title(mut self) -> Self {
156 self.hide_title = true;
157 self
158 }
159
160 pub fn description(mut self, description: &str) -> Self {
162 self.description = Some(description.into());
163 self
164 }
165
166 pub fn category(mut self, category: &str) -> Self {
168 self.category = Some(category.into());
169 self
170 }
171
172 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 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 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 pub fn unit_config(self, key: &str) -> Self {
194 self.unit_config_with(key, |entry| entry)
195 }
196
197 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 pub fn boolean_config(self, key: &str, default: bool) -> Self {
207 self.boolean_config_with(key, default, |entry| entry)
208 }
209
210 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 pub fn boolean_config_default(self, key: &str) -> Self {
220 self.boolean_config(key, false)
221 }
222
223 pub fn integer_config(self, key: &str, default: i64) -> Self {
225 self.integer_config_with(key, default, |entry| entry)
226 }
227
228 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 pub fn integer_config_default(self, key: &str) -> Self {
238 self.integer_config(key, 0)
239 }
240
241 pub fn number_config(self, key: &str, default: f64) -> Self {
243 self.number_config_with(key, default, |entry| entry)
244 }
245
246 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 pub fn number_config_default(self, key: &str) -> Self {
256 self.number_config(key, 0.0)
257 }
258
259 pub fn string_config(self, key: &str, default: impl Into<String>) -> Self {
261 self.string_config_with(key, default, |entry| entry)
262 }
263
264 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 pub fn string_config_default(self, key: &str) -> Self {
275 self.string_config(key, "")
276 }
277
278 pub fn text_config(self, key: &str, default: impl Into<String>) -> Self {
280 self.text_config_with(key, default, |entry| entry)
281 }
282
283 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 pub fn text_config_default(self, key: &str) -> Self {
294 self.text_config(key, "")
295 }
296
297 pub fn array_config(self, key: &str, default: impl Into<AgentValue>) -> Self {
299 self.array_config_with(key, default, |entry| entry)
300 }
301
302 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 pub fn array_config_default(self, key: &str) -> Self {
312 self.array_config(key, AgentValue::array_default())
313 }
314
315 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 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 pub fn object_config_default(self, key: &str) -> Self {
330 self.object_config(key, AgentValue::object_default())
331 }
332
333 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 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 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 pub fn boolean_global_config(self, key: &str, default: bool) -> Self {
385 self.boolean_global_config_with(key, default, |entry| entry)
386 }
387
388 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 pub fn integer_global_config(self, key: &str, default: i64) -> Self {
398 self.integer_global_config_with(key, default, |entry| entry)
399 }
400
401 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 pub fn number_global_config(self, key: &str, default: f64) -> Self {
411 self.number_global_config_with(key, default, |entry| entry)
412 }
413
414 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 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 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 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 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 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 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 pub fn array_global_config_default(self, key: &str) -> Self {
470 self.array_global_config(key, AgentValue::array_default())
471 }
472
473 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 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 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 pub fn use_native_thread(mut self) -> Self {
535 self.native_thread = true;
536 self
537 }
538
539 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 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 pub fn reconcile_spec(&self, spec: &mut AgentSpec) {
580 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 spec.config_specs = self.configs.clone();
590
591 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 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 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 = Some(
624 def_configs
625 .iter()
626 .map(|(k, v)| (k.clone(), v.value.clone()))
627 .collect(),
628 );
629 }
630
631 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 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 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 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 pub fn title(mut self, title: &str) -> Self {
670 self.title = Some(title.into());
671 self
672 }
673
674 pub fn hide_title(mut self) -> Self {
676 self.hide_title = true;
677 self
678 }
679
680 pub fn description(mut self, description: &str) -> Self {
682 self.description = Some(description.into());
683 self
684 }
685
686 pub fn hidden(mut self) -> Self {
688 self.hidden = true;
689 self
690 }
691
692 pub fn readonly(mut self) -> Self {
694 self.readonly = true;
695 self
696 }
697
698 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 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 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 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 assert_eq!(
1267 c.get("_prev_stale").unwrap(),
1268 &AgentValue::string("from_prev")
1269 );
1270 assert!(c.get("__prev_stale").is_err());
1271 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(); let mut configs = AgentConfigs::new();
1280 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 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(); 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 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 #[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}