1use std::future::Future;
9use std::pin::Pin;
10
11pub trait EnumVariants {
14 fn variant_names() -> &'static [&'static str];
16}
17
18pub type EnumVariantsFn = fn() -> &'static [&'static str];
20
21pub type CapabilityExecutorFn =
26 fn(
27 serde_json::Value,
28 ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send>>;
29
30pub struct CapabilityExecutor {
32 pub module: &'static str,
34 pub capability_id: &'static str,
36 pub execute: CapabilityExecutorFn,
38}
39
40inventory::collect!(&'static CapabilityExecutor);
42
43pub async fn execute_capability(
46 module: &str,
47 capability_id: &str,
48 input: serde_json::Value,
49) -> Result<serde_json::Value, String> {
50 let module_lower = module.to_lowercase();
51
52 for executor in inventory::iter::<&'static CapabilityExecutor> {
53 if executor.module == module_lower && executor.capability_id == capability_id {
54 return (executor.execute)(input).await;
55 }
56 }
57
58 Err(format!("Unknown capability: {}:{}", module, capability_id))
59}
60
61#[derive(Debug, Clone)]
63pub struct CapabilityMeta {
64 pub module: Option<&'static str>,
66 pub capability_id: &'static str,
68 pub function_name: &'static str,
70 pub input_type: &'static str,
72 pub output_type: &'static str,
74 pub display_name: Option<&'static str>,
76 pub description: Option<&'static str>,
78 pub has_side_effects: bool,
80 pub is_idempotent: bool,
82 pub rate_limited: bool,
84}
85
86inventory::collect!(&'static CapabilityMeta);
88
89#[derive(Clone)]
91pub struct InputFieldMeta {
92 pub name: &'static str,
94 pub type_name: &'static str,
96 pub is_optional: bool,
98 pub display_name: Option<&'static str>,
100 pub description: Option<&'static str>,
102 pub example: Option<&'static str>,
104 pub default_value: Option<&'static str>,
106 pub enum_values_fn: Option<EnumVariantsFn>,
108}
109
110impl std::fmt::Debug for InputFieldMeta {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("InputFieldMeta")
113 .field("name", &self.name)
114 .field("type_name", &self.type_name)
115 .field("is_optional", &self.is_optional)
116 .field("display_name", &self.display_name)
117 .field("description", &self.description)
118 .field("example", &self.example)
119 .field("default_value", &self.default_value)
120 .field("enum_values_fn", &self.enum_values_fn.map(|_| "<fn>"))
121 .finish()
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct InputTypeMeta {
128 pub type_name: &'static str,
130 pub display_name: Option<&'static str>,
132 pub description: Option<&'static str>,
134 pub fields: &'static [InputFieldMeta],
136}
137
138inventory::collect!(&'static InputTypeMeta);
140
141#[derive(Debug, Clone)]
143pub struct OutputFieldMeta {
144 pub name: &'static str,
146 pub type_name: &'static str,
148 pub display_name: Option<&'static str>,
150 pub description: Option<&'static str>,
152 pub example: Option<&'static str>,
154 pub nullable: bool,
156 pub items_type_name: Option<&'static str>,
159 pub nested_type_name: Option<&'static str>,
162}
163
164#[derive(Debug, Clone)]
166pub struct OutputTypeMeta {
167 pub type_name: &'static str,
169 pub display_name: Option<&'static str>,
171 pub description: Option<&'static str>,
173 pub fields: &'static [OutputFieldMeta],
175}
176
177inventory::collect!(&'static OutputTypeMeta);
179
180pub fn get_all_capabilities() -> impl Iterator<Item = &'static CapabilityMeta> {
182 inventory::iter::<&'static CapabilityMeta>
183 .into_iter()
184 .copied()
185}
186
187pub fn get_all_input_types() -> impl Iterator<Item = &'static InputTypeMeta> {
189 inventory::iter::<&'static InputTypeMeta>
190 .into_iter()
191 .copied()
192}
193
194pub fn get_all_output_types() -> impl Iterator<Item = &'static OutputTypeMeta> {
196 inventory::iter::<&'static OutputTypeMeta>
197 .into_iter()
198 .copied()
199}
200
201pub fn find_input_type(type_name: &str) -> Option<&'static InputTypeMeta> {
203 get_all_input_types().find(|m| m.type_name == type_name)
204}
205
206pub fn find_output_type(type_name: &str) -> Option<&'static OutputTypeMeta> {
208 get_all_output_types().find(|m| m.type_name == type_name)
209}
210
211use serde::{Deserialize, Serialize};
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
220pub struct AgentInfo {
221 pub id: String,
222 pub name: String,
223 pub description: String,
224 #[serde(rename = "hasSideEffects")]
225 pub has_side_effects: bool,
226 #[serde(rename = "supportsConnections")]
227 pub supports_connections: bool,
228 #[serde(rename = "integrationIds")]
229 pub integration_ids: Vec<String>,
230 pub capabilities: Vec<CapabilityInfo>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
236pub struct CapabilityInfo {
237 pub id: String,
238 pub name: String,
239 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
240 pub display_name: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub description: Option<String>,
243 #[serde(rename = "inputType")]
244 pub input_type: String,
245 pub inputs: Vec<CapabilityField>,
246 pub output: FieldTypeInfo,
247 #[serde(rename = "hasSideEffects")]
248 pub has_side_effects: bool,
249 #[serde(rename = "isIdempotent")]
250 pub is_idempotent: bool,
251 #[serde(rename = "rateLimited")]
252 pub rate_limited: bool,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
258#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
259pub struct CapabilityField {
260 pub name: String,
261 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
262 pub display_name: Option<String>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub description: Option<String>,
265 #[serde(rename = "type")]
266 pub type_name: String,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub format: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub items: Option<FieldTypeInfo>,
271 pub required: bool,
272 #[serde(rename = "default", skip_serializing_if = "Option::is_none")]
273 pub default_value: Option<serde_json::Value>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub example: Option<serde_json::Value>,
276 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
277 pub enum_values: Option<Vec<String>>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
283#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
284pub struct FieldTypeInfo {
285 #[serde(rename = "type")]
286 pub type_name: String,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub format: Option<String>,
289 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
290 pub display_name: Option<String>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub description: Option<String>,
293 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
294 pub fields: Option<Box<Vec<OutputField>>>,
295 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
297 pub items: Option<Box<FieldTypeInfo>>,
298 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
300 pub nullable: bool,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
306#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
307pub struct OutputField {
308 pub name: String,
309 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
310 pub display_name: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub description: Option<String>,
313 #[serde(rename = "type")]
314 pub type_name: String,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub format: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub example: Option<serde_json::Value>,
319 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
321 pub nullable: bool,
322 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
324 pub items: Option<Box<FieldTypeInfo>>,
325 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
327 pub fields: Option<Box<Vec<OutputField>>>,
328}
329
330#[derive(Debug, Clone)]
336pub struct AgentModuleConfig {
337 pub id: &'static str,
338 pub name: &'static str,
339 pub description: &'static str,
340 pub has_side_effects: bool,
341 pub supports_connections: bool,
342 pub integration_ids: &'static [&'static str],
343 pub secure: bool,
347}
348
349inventory::collect!(&'static AgentModuleConfig);
351
352pub const BUILTIN_AGENT_MODULES: &[AgentModuleConfig] = &[
354 AgentModuleConfig {
355 id: "utils",
356 name: "Utils",
357 description: "Utility capabilities for random numbers, calculations, delays, timestamps, and country lookups",
358 has_side_effects: false,
359 supports_connections: false,
360 integration_ids: &[],
361 secure: false,
362 },
363 AgentModuleConfig {
364 id: "transform",
365 name: "Transform",
366 description: "Transform capabilities for data manipulation, filtering, sorting, and JSON operations",
367 has_side_effects: false,
368 supports_connections: false,
369 integration_ids: &[],
370 secure: false,
371 },
372 AgentModuleConfig {
373 id: "csv",
374 name: "Csv",
375 description: "CSV capabilities for parsing and working with CSV data",
376 has_side_effects: false,
377 supports_connections: false,
378 integration_ids: &[],
379 secure: false,
380 },
381 AgentModuleConfig {
382 id: "text",
383 name: "Text",
384 description: "Text capabilities for string manipulation, formatting, and text processing",
385 has_side_effects: false,
386 supports_connections: false,
387 integration_ids: &[],
388 secure: false,
389 },
390 AgentModuleConfig {
391 id: "xml",
392 name: "Xml",
393 description: "XML capabilities for parsing and working with XML data",
394 has_side_effects: false,
395 supports_connections: false,
396 integration_ids: &[],
397 secure: false,
398 },
399 AgentModuleConfig {
400 id: "datetime",
401 name: "DateTime",
402 description: "Date and time capabilities for parsing, formatting, calculating, and manipulating dates",
403 has_side_effects: false,
404 supports_connections: false,
405 integration_ids: &[],
406 secure: false,
407 },
408 AgentModuleConfig {
409 id: "http",
410 name: "HTTP",
411 description: "HTTP capabilities for making web requests with JSON/text/binary support (has side effects)",
412 has_side_effects: true,
413 supports_connections: true,
414 integration_ids: &["bearer", "api_key", "basic_auth"],
415 secure: true,
416 },
417 AgentModuleConfig {
418 id: "sftp",
419 name: "Sftp",
420 description: "SFTP capabilities for secure file transfer operations - list, download, upload, and delete files on remote servers (has side effects)",
421 has_side_effects: true,
422 supports_connections: true,
423 integration_ids: &["sftp"],
424 secure: true,
425 },
426 AgentModuleConfig {
427 id: "compression",
428 name: "Compression",
429 description: "Archive capabilities for creating and extracting ZIP archives, listing contents, and extracting individual files",
430 has_side_effects: false,
431 supports_connections: false,
432 integration_ids: &[],
433 secure: false,
434 },
435 AgentModuleConfig {
436 id: "file",
437 name: "File",
438 description: "File system capabilities for reading, writing, listing, copying, moving, and deleting files within the workflow workspace",
439 has_side_effects: true,
440 supports_connections: false,
441 integration_ids: &[],
442 secure: false,
443 },
444 AgentModuleConfig {
445 id: "object_model",
446 name: "Object Model",
447 description: "Object Model capabilities for database CRUD operations - create, query, and check instances in object model schemas (has side effects)",
448 has_side_effects: true,
449 supports_connections: false,
450 integration_ids: &[],
451 secure: false,
452 },
453];
454
455pub fn get_all_agent_modules() -> Vec<&'static AgentModuleConfig> {
459 use std::collections::HashSet;
460
461 let mut seen_ids = HashSet::new();
462 let mut modules = Vec::new();
463
464 for module in BUILTIN_AGENT_MODULES {
466 if seen_ids.insert(module.id) {
467 modules.push(module);
468 }
469 }
470
471 for module in inventory::iter::<&'static AgentModuleConfig> {
473 if seen_ids.insert(module.id) {
474 modules.push(*module);
475 }
476 }
477
478 modules
479}
480
481pub fn find_agent_module(id: &str) -> Option<&'static AgentModuleConfig> {
483 get_all_agent_modules().into_iter().find(|m| m.id == id)
484}
485
486pub type SchemaGeneratorFn = fn() -> schemars::schema::RootSchema;
492
493#[derive(Debug, Clone)]
495pub struct StepTypeMeta {
496 pub id: &'static str,
498 pub display_name: &'static str,
500 pub description: &'static str,
502 pub category: &'static str,
504 pub schema_fn: SchemaGeneratorFn,
506}
507
508inventory::collect!(&'static StepTypeMeta);
510
511pub fn get_all_step_types() -> impl Iterator<Item = &'static StepTypeMeta> {
513 inventory::iter::<&'static StepTypeMeta>
514 .into_iter()
515 .copied()
516}
517
518pub fn find_step_type(id: &str) -> Option<&'static StepTypeMeta> {
520 get_all_step_types().find(|m| m.id == id)
521}
522
523#[derive(Debug, Clone)]
529pub struct ConnectionFieldMeta {
530 pub name: &'static str,
532 pub type_name: &'static str,
534 pub is_optional: bool,
536 pub display_name: Option<&'static str>,
538 pub description: Option<&'static str>,
540 pub placeholder: Option<&'static str>,
542 pub default_value: Option<&'static str>,
544 pub is_secret: bool,
546}
547
548#[derive(Debug, Clone)]
550pub struct ConnectionTypeMeta {
551 pub integration_id: &'static str,
553 pub display_name: &'static str,
555 pub description: Option<&'static str>,
557 pub category: Option<&'static str>,
559 pub fields: &'static [ConnectionFieldMeta],
561}
562
563inventory::collect!(&'static ConnectionTypeMeta);
565
566pub fn get_all_connection_types() -> impl Iterator<Item = &'static ConnectionTypeMeta> {
568 inventory::iter::<&'static ConnectionTypeMeta>
569 .into_iter()
570 .copied()
571}
572
573pub fn find_connection_type(integration_id: &str) -> Option<&'static ConnectionTypeMeta> {
575 get_all_connection_types().find(|m| m.integration_id == integration_id)
576}
577
578fn rust_to_json_schema_type(rust_type: &str) -> (String, Option<String>, Option<String>) {
584 match rust_type {
585 "String" => ("string".to_string(), None, None),
586 "bool" => ("boolean".to_string(), None, None),
587 "i32" | "i64" | "u32" | "u64" | "usize" => ("integer".to_string(), None, None),
588 "f32" | "f64" => ("number".to_string(), Some("double".to_string()), None),
589 "Value" => ("any".to_string(), None, None), "()" => ("null".to_string(), None, None),
591 t if t.starts_with("Vec<") => {
592 let inner = t.trim_start_matches("Vec<").trim_end_matches('>');
593 let (inner_type, inner_format, _) = rust_to_json_schema_type(inner);
594 let items_json = if let Some(fmt) = inner_format {
595 format!(r#"{{"type": "{}", "format": "{}"}}"#, inner_type, fmt)
596 } else {
597 format!(r#"{{"type": "{}"}}"#, inner_type)
598 };
599 ("array".to_string(), None, Some(items_json))
600 }
601 t if t.starts_with("HashMap<") || t.starts_with("BTreeMap<") => {
602 ("object".to_string(), None, None)
603 }
604 _ => ("string".to_string(), None, None), }
606}
607
608fn input_field_to_api(field: &InputFieldMeta) -> CapabilityField {
610 let (json_type, format, items_json) = rust_to_json_schema_type(field.type_name);
611
612 let items = items_json.map(|items_str| {
613 let type_match = items_str
615 .split("\"type\": \"")
616 .nth(1)
617 .and_then(|s| s.split('"').next())
618 .unwrap_or("string");
619 let format_match = if items_str.contains("\"format\"") {
620 items_str
621 .split("\"format\": \"")
622 .nth(1)
623 .and_then(|s| s.split('"').next())
624 .map(|s| s.to_string())
625 } else {
626 None
627 };
628 FieldTypeInfo {
629 type_name: type_match.to_string(),
630 format: format_match,
631 display_name: None,
632 description: None,
633 fields: None,
634 items: None,
635 nullable: false,
636 }
637 });
638
639 let default_value = field
640 .default_value
641 .and_then(|s| serde_json::from_str(s).ok());
642
643 let example = field
644 .example
645 .map(|s| serde_json::Value::String(s.to_string()));
646
647 let enum_values = field
648 .enum_values_fn
649 .map(|f| f().iter().map(|s| s.to_string()).collect());
650
651 CapabilityField {
652 name: field.name.to_string(),
653 display_name: field.display_name.map(|s| s.to_string()),
654 description: field.description.map(|s| s.to_string()),
655 type_name: json_type,
656 format,
657 items,
658 required: !field.is_optional,
659 default_value,
660 example,
661 enum_values,
662 }
663}
664
665fn output_field_to_api(field: &OutputFieldMeta) -> OutputField {
667 let (type_name, format, _) = rust_to_json_schema_type(field.type_name);
668
669 OutputField {
670 name: field.name.to_string(),
671 display_name: field.display_name.map(|s| s.to_string()),
672 description: field.description.map(|s| s.to_string()),
673 type_name,
674 format,
675 example: field
676 .example
677 .map(|s| serde_json::Value::String(s.to_string())),
678 nullable: field.nullable,
679 items: None,
682 fields: None,
683 }
684}
685
686fn capability_to_api(
688 cap: &CapabilityMeta,
689 input_type_meta: Option<&InputTypeMeta>,
690 output_type_meta: Option<&OutputTypeMeta>,
691) -> CapabilityInfo {
692 let (output_type, output_format, _) = rust_to_json_schema_type(cap.output_type);
693
694 let inputs = input_type_meta
695 .map(|m| m.fields.iter().map(input_field_to_api).collect())
696 .unwrap_or_default();
697
698 let output_fields = output_type_meta
699 .map(|m| Box::new(m.fields.iter().map(output_field_to_api).collect::<Vec<_>>()));
700
701 CapabilityInfo {
702 id: cap.capability_id.to_string(),
703 name: cap.function_name.to_string(),
704 display_name: cap.display_name.map(|s| s.to_string()),
705 description: cap.description.map(|s| s.to_string()),
706 input_type: cap.input_type.to_string(),
707 inputs,
708 output: FieldTypeInfo {
709 type_name: output_type,
710 format: output_format,
711 display_name: output_type_meta.and_then(|m| m.display_name.map(|s| s.to_string())),
712 description: output_type_meta.and_then(|m| m.description.map(|s| s.to_string())),
713 fields: output_fields,
714 items: None,
715 nullable: false,
716 },
717 has_side_effects: cap.has_side_effects,
718 is_idempotent: cap.is_idempotent,
719 rate_limited: cap.rate_limited,
720 }
721}
722
723pub fn get_agents() -> Vec<AgentInfo> {
725 use std::collections::HashMap;
726
727 let input_types: HashMap<&str, &InputTypeMeta> =
729 get_all_input_types().map(|m| (m.type_name, m)).collect();
730
731 let output_types: HashMap<&str, &OutputTypeMeta> =
733 get_all_output_types().map(|m| (m.type_name, m)).collect();
734
735 let mut caps_by_module: HashMap<&str, Vec<&CapabilityMeta>> = HashMap::new();
737 for cap in get_all_capabilities() {
738 let module = cap.module.unwrap_or("unknown");
739 caps_by_module.entry(module).or_default().push(cap);
740 }
741
742 let mut agents = Vec::new();
744
745 for config in get_all_agent_modules() {
746 let caps = caps_by_module.get(config.id).cloned().unwrap_or_default();
747
748 if caps.is_empty() {
749 continue;
750 }
751
752 let capabilities: Vec<CapabilityInfo> = caps
753 .iter()
754 .map(|cap| {
755 let input_meta = input_types.get(cap.input_type).copied();
756 let output_meta = output_types.get(cap.output_type).copied();
757 capability_to_api(cap, input_meta, output_meta)
758 })
759 .collect();
760
761 agents.push(AgentInfo {
762 id: config.id.to_string(),
763 name: config.name.to_string(),
764 description: config.description.to_string(),
765 has_side_effects: config.has_side_effects,
766 supports_connections: config.supports_connections,
767 integration_ids: config
768 .integration_ids
769 .iter()
770 .map(|s| s.to_string())
771 .collect(),
772 capabilities,
773 });
774 }
775
776 agents
777}
778
779pub fn get_capability_inputs(agent_id: &str, capability_id: &str) -> Option<Vec<CapabilityField>> {
782 use std::collections::HashMap;
783
784 let input_types: HashMap<&str, &InputTypeMeta> =
786 get_all_input_types().map(|m| (m.type_name, m)).collect();
787
788 let agent_lower = agent_id.to_lowercase();
790 for cap in get_all_capabilities() {
791 let module = cap.module.unwrap_or("unknown");
792 if module == agent_lower && cap.capability_id == capability_id {
793 let input_meta = input_types.get(cap.input_type).copied();
795
796 let inputs: Vec<CapabilityField> = if let Some(meta) = input_meta {
798 meta.fields.iter().map(input_field_to_api).collect()
799 } else {
800 Vec::new()
801 };
802
803 return Some(inputs);
804 }
805 }
806
807 None
808}
809
810const PRIMITIVE_OUTPUT_TYPES: &[&str] = &[
816 "()", "bool", "i8",
819 "i16",
820 "i32",
821 "i64",
822 "i128",
823 "isize", "u8",
825 "u16",
826 "u32",
827 "u64",
828 "u128",
829 "usize", "f32",
831 "f64", "String", "Value", "serde_json::Value", ];
836
837fn is_primitive_output_type(type_name: &str) -> bool {
839 if PRIMITIVE_OUTPUT_TYPES.contains(&type_name) {
841 return true;
842 }
843
844 if let Some(inner) = type_name
846 .strip_prefix("Vec<")
847 .and_then(|s| s.strip_suffix('>'))
848 {
849 return is_primitive_output_type(inner);
850 }
851
852 if let Some(inner) = type_name
854 .strip_prefix("Option<")
855 .and_then(|s| s.strip_suffix('>'))
856 {
857 return is_primitive_output_type(inner);
858 }
859
860 if type_name.starts_with("HashMap<") || type_name.starts_with("BTreeMap<") {
862 return true; }
864
865 false
866}
867
868#[derive(Debug, Clone)]
870pub struct AgentValidationError {
871 pub module: String,
872 pub capability_id: String,
873 pub missing_input: bool,
874 pub missing_output: bool,
875 pub input_type: String,
876 pub output_type: String,
877}
878
879impl std::fmt::Display for AgentValidationError {
880 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
881 let mut issues = Vec::new();
882 if self.missing_input {
883 issues.push(format!("missing CapabilityInput for '{}'", self.input_type));
884 }
885 if self.missing_output {
886 issues.push(format!(
887 "missing CapabilityOutput for '{}'",
888 self.output_type
889 ));
890 }
891 write!(
892 f,
893 "{}:{} - {}",
894 self.module,
895 self.capability_id,
896 issues.join(", ")
897 )
898 }
899}
900
901pub fn validate_agent_metadata() -> Vec<AgentValidationError> {
916 use std::collections::HashMap;
917
918 let input_types: HashMap<&str, &InputTypeMeta> =
919 get_all_input_types().map(|m| (m.type_name, m)).collect();
920
921 let output_types: HashMap<&str, &OutputTypeMeta> =
922 get_all_output_types().map(|m| (m.type_name, m)).collect();
923
924 let is_valid_output = |type_name: &str| -> bool {
926 if is_primitive_output_type(type_name) {
927 return true;
928 }
929 if output_types.contains_key(type_name) {
930 return true;
931 }
932 if let Some(inner) = type_name
934 .strip_prefix("Vec<")
935 .and_then(|s| s.strip_suffix('>'))
936 {
937 return is_primitive_output_type(inner) || output_types.contains_key(inner);
938 }
939 if let Some(inner) = type_name
941 .strip_prefix("Option<")
942 .and_then(|s| s.strip_suffix('>'))
943 {
944 return is_primitive_output_type(inner) || output_types.contains_key(inner);
945 }
946 false
947 };
948
949 let mut errors = Vec::new();
950
951 for cap in get_all_capabilities() {
952 let module = cap.module.unwrap_or("unknown").to_string();
953 let missing_input = !input_types.contains_key(cap.input_type);
954 let missing_output = !is_valid_output(cap.output_type);
955
956 if missing_input || missing_output {
957 errors.push(AgentValidationError {
958 module,
959 capability_id: cap.capability_id.to_string(),
960 missing_input,
961 missing_output,
962 input_type: cap.input_type.to_string(),
963 output_type: cap.output_type.to_string(),
964 });
965 }
966 }
967
968 errors
969}
970
971pub fn validate_agent_metadata_or_panic() {
978 let errors = validate_agent_metadata();
979 if !errors.is_empty() {
980 let error_list: Vec<String> = errors.iter().map(|e| format!(" - {}", e)).collect();
981 panic!(
982 "Agent metadata validation failed!\n\
983 The following capabilities are missing CapabilityInput or CapabilityOutput definitions:\n\
984 {}\n\n\
985 To fix this:\n\
986 1. For input types: Add #[derive(CapabilityInput)] to the input struct\n\
987 2. For output types: Add #[derive(CapabilityOutput)] to the output struct\n\
988 \n\
989 Example:\n\
990 #[derive(Serialize, Deserialize, CapabilityOutput)]\n\
991 #[capability_output(display_name = \"My Output\")]\n\
992 pub struct MyCapabilityOutput {{\n\
993 #[field(display_name = \"Result\", description = \"The capability result\")]\n\
994 pub result: String,\n\
995 }}",
996 error_list.join("\n")
997 );
998 }
999}
1000
1001#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 #[test]
1010 fn test_builtin_agent_modules_count() {
1011 assert_eq!(
1013 BUILTIN_AGENT_MODULES.len(),
1014 11,
1015 "Expected 11 built-in agent modules"
1016 );
1017 }
1018
1019 #[test]
1020 fn test_builtin_agent_modules_ids() {
1021 let ids: Vec<&str> = BUILTIN_AGENT_MODULES.iter().map(|m| m.id).collect();
1022
1023 assert!(ids.contains(&"utils"), "Missing utils module");
1024 assert!(ids.contains(&"transform"), "Missing transform module");
1025 assert!(ids.contains(&"csv"), "Missing csv module");
1026 assert!(ids.contains(&"text"), "Missing text module");
1027 assert!(ids.contains(&"xml"), "Missing xml module");
1028 assert!(ids.contains(&"datetime"), "Missing datetime module");
1029 assert!(ids.contains(&"http"), "Missing http module");
1030 assert!(ids.contains(&"compression"), "Missing compression module");
1031 assert!(ids.contains(&"file"), "Missing file module");
1032 assert!(ids.contains(&"sftp"), "Missing sftp module");
1033 assert!(ids.contains(&"object_model"), "Missing object_model module");
1034 }
1035
1036 #[test]
1037 fn test_get_all_agent_modules_includes_builtins() {
1038 let modules = get_all_agent_modules();
1039
1040 assert!(
1042 modules.len() >= BUILTIN_AGENT_MODULES.len(),
1043 "get_all_agent_modules should include at least all built-in modules"
1044 );
1045
1046 let module_ids: Vec<&str> = modules.iter().map(|m| m.id).collect();
1048 for builtin in BUILTIN_AGENT_MODULES {
1049 assert!(
1050 module_ids.contains(&builtin.id),
1051 "Built-in module {} should be in get_all_agent_modules()",
1052 builtin.id
1053 );
1054 }
1055 }
1056
1057 #[test]
1058 fn test_get_all_agent_modules_deduplication() {
1059 let modules = get_all_agent_modules();
1060
1061 let mut seen_ids = std::collections::HashSet::new();
1063 for module in &modules {
1064 assert!(
1065 seen_ids.insert(module.id),
1066 "Duplicate module id found: {}",
1067 module.id
1068 );
1069 }
1070 }
1071
1072 #[test]
1073 fn test_find_agent_module_existing() {
1074 let http_module = find_agent_module("http");
1075 assert!(http_module.is_some(), "Should find http module");
1076
1077 let module = http_module.unwrap();
1078 assert_eq!(module.id, "http");
1079 assert_eq!(module.name, "HTTP");
1080 assert!(module.has_side_effects);
1081 assert!(module.supports_connections);
1082 assert!(module.secure);
1083 }
1084
1085 #[test]
1086 fn test_find_agent_module_non_existing() {
1087 let result = find_agent_module("non_existent_module");
1088 assert!(result.is_none(), "Should not find non-existent module");
1089 }
1090
1091 #[test]
1092 fn test_secure_modules() {
1093 for module in BUILTIN_AGENT_MODULES {
1095 match module.id {
1096 "http" | "sftp" => {
1097 assert!(module.secure, "{} module should be secure", module.id);
1098 }
1099 _ => {
1100 assert!(!module.secure, "{} module should not be secure", module.id);
1101 }
1102 }
1103 }
1104 }
1105
1106 #[test]
1107 fn test_side_effects_modules() {
1108 for module in BUILTIN_AGENT_MODULES {
1110 match module.id {
1111 "http" | "sftp" | "object_model" => {
1112 assert!(
1113 module.has_side_effects,
1114 "{} module should have side effects",
1115 module.id
1116 );
1117 }
1118 _ => {
1119 assert!(
1120 !module.has_side_effects,
1121 "{} module should not have side effects",
1122 module.id
1123 );
1124 }
1125 }
1126 }
1127 }
1128
1129 #[test]
1130 fn test_connection_supporting_modules() {
1131 for module in BUILTIN_AGENT_MODULES {
1133 match module.id {
1134 "http" | "sftp" => {
1135 assert!(
1136 module.supports_connections,
1137 "{} module should support connections",
1138 module.id
1139 );
1140 assert!(
1141 !module.integration_ids.is_empty(),
1142 "{} module should have integration IDs",
1143 module.id
1144 );
1145 }
1146 _ => {
1147 assert!(
1148 !module.supports_connections,
1149 "{} module should not support connections",
1150 module.id
1151 );
1152 }
1153 }
1154 }
1155 }
1156
1157 #[test]
1158 fn test_http_integration_ids() {
1159 let http_module = find_agent_module("http").unwrap();
1160 let integration_ids = http_module.integration_ids;
1161
1162 assert!(
1163 integration_ids.contains(&"bearer"),
1164 "http should support bearer"
1165 );
1166 assert!(
1167 integration_ids.contains(&"api_key"),
1168 "http should support api_key"
1169 );
1170 assert!(
1171 integration_ids.contains(&"basic_auth"),
1172 "http should support basic_auth"
1173 );
1174 }
1175
1176 #[test]
1177 fn test_sftp_integration_ids() {
1178 let sftp_module = find_agent_module("sftp").unwrap();
1179 let integration_ids = sftp_module.integration_ids;
1180
1181 assert!(
1182 integration_ids.contains(&"sftp"),
1183 "sftp should support sftp integration"
1184 );
1185 }
1186}