1pub trait EnumVariants {
11 fn variant_names() -> &'static [&'static str];
13}
14
15pub type EnumVariantsFn = fn() -> &'static [&'static str];
17
18pub type CapabilityExecutorFn = fn(serde_json::Value) -> Result<serde_json::Value, String>;
20
21pub struct CapabilityExecutor {
23 pub module: &'static str,
25 pub capability_id: &'static str,
27 pub execute: CapabilityExecutorFn,
29}
30
31inventory::collect!(&'static CapabilityExecutor);
33
34pub fn execute_capability(
36 module: &str,
37 capability_id: &str,
38 input: serde_json::Value,
39) -> Result<serde_json::Value, String> {
40 let module_lower = module.to_lowercase();
41
42 for executor in inventory::iter::<&'static CapabilityExecutor> {
43 if executor.module == module_lower && executor.capability_id == capability_id {
44 return (executor.execute)(input);
45 }
46 }
47
48 Err(format!("Unknown capability: {}:{}", module, capability_id))
49}
50
51#[derive(Debug, Clone)]
53pub struct CapabilityMeta {
54 pub module: Option<&'static str>,
56 pub capability_id: &'static str,
58 pub function_name: &'static str,
60 pub input_type: &'static str,
62 pub output_type: &'static str,
64 pub display_name: Option<&'static str>,
66 pub description: Option<&'static str>,
68 pub has_side_effects: bool,
70 pub is_idempotent: bool,
72 pub rate_limited: bool,
74}
75
76inventory::collect!(&'static CapabilityMeta);
78
79#[derive(Clone)]
81pub struct InputFieldMeta {
82 pub name: &'static str,
84 pub type_name: &'static str,
86 pub is_optional: bool,
88 pub display_name: Option<&'static str>,
90 pub description: Option<&'static str>,
92 pub example: Option<&'static str>,
94 pub default_value: Option<&'static str>,
96 pub enum_values_fn: Option<EnumVariantsFn>,
98}
99
100impl std::fmt::Debug for InputFieldMeta {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("InputFieldMeta")
103 .field("name", &self.name)
104 .field("type_name", &self.type_name)
105 .field("is_optional", &self.is_optional)
106 .field("display_name", &self.display_name)
107 .field("description", &self.description)
108 .field("example", &self.example)
109 .field("default_value", &self.default_value)
110 .field("enum_values_fn", &self.enum_values_fn.map(|_| "<fn>"))
111 .finish()
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct InputTypeMeta {
118 pub type_name: &'static str,
120 pub display_name: Option<&'static str>,
122 pub description: Option<&'static str>,
124 pub fields: &'static [InputFieldMeta],
126}
127
128inventory::collect!(&'static InputTypeMeta);
130
131#[derive(Debug, Clone)]
133pub struct OutputFieldMeta {
134 pub name: &'static str,
136 pub type_name: &'static str,
138 pub display_name: Option<&'static str>,
140 pub description: Option<&'static str>,
142 pub example: Option<&'static str>,
144 pub nullable: bool,
146 pub items_type_name: Option<&'static str>,
149 pub nested_type_name: Option<&'static str>,
152}
153
154#[derive(Debug, Clone)]
156pub struct OutputTypeMeta {
157 pub type_name: &'static str,
159 pub display_name: Option<&'static str>,
161 pub description: Option<&'static str>,
163 pub fields: &'static [OutputFieldMeta],
165}
166
167inventory::collect!(&'static OutputTypeMeta);
169
170pub fn get_all_capabilities() -> impl Iterator<Item = &'static CapabilityMeta> {
172 inventory::iter::<&'static CapabilityMeta>
173 .into_iter()
174 .copied()
175}
176
177pub fn get_all_input_types() -> impl Iterator<Item = &'static InputTypeMeta> {
179 inventory::iter::<&'static InputTypeMeta>
180 .into_iter()
181 .copied()
182}
183
184pub fn get_all_output_types() -> impl Iterator<Item = &'static OutputTypeMeta> {
186 inventory::iter::<&'static OutputTypeMeta>
187 .into_iter()
188 .copied()
189}
190
191pub fn find_input_type(type_name: &str) -> Option<&'static InputTypeMeta> {
193 get_all_input_types().find(|m| m.type_name == type_name)
194}
195
196pub fn find_output_type(type_name: &str) -> Option<&'static OutputTypeMeta> {
198 get_all_output_types().find(|m| m.type_name == type_name)
199}
200
201use serde::{Deserialize, Serialize};
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
210pub struct AgentInfo {
211 pub id: String,
212 pub name: String,
213 pub description: String,
214 #[serde(rename = "hasSideEffects")]
215 pub has_side_effects: bool,
216 #[serde(rename = "supportsConnections")]
217 pub supports_connections: bool,
218 #[serde(rename = "integrationIds")]
219 pub integration_ids: Vec<String>,
220 pub capabilities: Vec<CapabilityInfo>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
226pub struct CapabilityInfo {
227 pub id: String,
228 pub name: String,
229 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
230 pub display_name: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub description: Option<String>,
233 #[serde(rename = "inputType")]
234 pub input_type: String,
235 pub inputs: Vec<CapabilityField>,
236 pub output: FieldTypeInfo,
237 #[serde(rename = "hasSideEffects")]
238 pub has_side_effects: bool,
239 #[serde(rename = "isIdempotent")]
240 pub is_idempotent: bool,
241 #[serde(rename = "rateLimited")]
242 pub rate_limited: bool,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
248#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
249pub struct CapabilityField {
250 pub name: String,
251 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
252 pub display_name: Option<String>,
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub description: Option<String>,
255 #[serde(rename = "type")]
256 pub type_name: String,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub format: Option<String>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub items: Option<FieldTypeInfo>,
261 pub required: bool,
262 #[serde(rename = "default", skip_serializing_if = "Option::is_none")]
263 pub default_value: Option<serde_json::Value>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub example: Option<serde_json::Value>,
266 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
267 pub enum_values: Option<Vec<String>>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
273#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
274pub struct FieldTypeInfo {
275 #[serde(rename = "type")]
276 pub type_name: String,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub format: Option<String>,
279 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
280 pub display_name: Option<String>,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub description: Option<String>,
283 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
284 pub fields: Option<Box<Vec<OutputField>>>,
285 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
287 pub items: Option<Box<FieldTypeInfo>>,
288 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
290 pub nullable: bool,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
296#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
297pub struct OutputField {
298 pub name: String,
299 #[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
300 pub display_name: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub description: Option<String>,
303 #[serde(rename = "type")]
304 pub type_name: String,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub format: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub example: Option<serde_json::Value>,
309 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311 pub nullable: bool,
312 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
314 pub items: Option<Box<FieldTypeInfo>>,
315 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
317 pub fields: Option<Box<Vec<OutputField>>>,
318}
319
320#[derive(Debug, Clone)]
326pub struct AgentModuleConfig {
327 pub id: &'static str,
328 pub name: &'static str,
329 pub description: &'static str,
330 pub has_side_effects: bool,
331 pub supports_connections: bool,
332 pub integration_ids: &'static [&'static str],
333 pub secure: bool,
337}
338
339inventory::collect!(&'static AgentModuleConfig);
341
342pub const BUILTIN_AGENT_MODULES: &[AgentModuleConfig] = &[
344 AgentModuleConfig {
345 id: "utils",
346 name: "Utils",
347 description: "Utility capabilities for random numbers, calculations, delays, timestamps, and country lookups",
348 has_side_effects: false,
349 supports_connections: false,
350 integration_ids: &[],
351 secure: false,
352 },
353 AgentModuleConfig {
354 id: "transform",
355 name: "Transform",
356 description: "Transform capabilities for data manipulation, filtering, sorting, and JSON operations",
357 has_side_effects: false,
358 supports_connections: false,
359 integration_ids: &[],
360 secure: false,
361 },
362 AgentModuleConfig {
363 id: "csv",
364 name: "Csv",
365 description: "CSV capabilities for parsing and working with CSV data",
366 has_side_effects: false,
367 supports_connections: false,
368 integration_ids: &[],
369 secure: false,
370 },
371 AgentModuleConfig {
372 id: "text",
373 name: "Text",
374 description: "Text capabilities for string manipulation, formatting, and text processing",
375 has_side_effects: false,
376 supports_connections: false,
377 integration_ids: &[],
378 secure: false,
379 },
380 AgentModuleConfig {
381 id: "xml",
382 name: "Xml",
383 description: "XML capabilities for parsing and working with XML data",
384 has_side_effects: false,
385 supports_connections: false,
386 integration_ids: &[],
387 secure: false,
388 },
389 AgentModuleConfig {
390 id: "http",
391 name: "HTTP",
392 description: "HTTP capabilities for making web requests with JSON/text/binary support (has side effects)",
393 has_side_effects: true,
394 supports_connections: true,
395 integration_ids: &["bearer", "api_key", "basic_auth"],
396 secure: true,
397 },
398 AgentModuleConfig {
399 id: "sftp",
400 name: "Sftp",
401 description: "SFTP capabilities for secure file transfer operations - list, download, upload, and delete files on remote servers (has side effects)",
402 has_side_effects: true,
403 supports_connections: true,
404 integration_ids: &["sftp"],
405 secure: true,
406 },
407 AgentModuleConfig {
408 id: "object_model",
409 name: "Object Model",
410 description: "Object Model capabilities for database CRUD operations - create, query, and check instances in object model schemas (has side effects)",
411 has_side_effects: true,
412 supports_connections: false,
413 integration_ids: &[],
414 secure: false,
415 },
416];
417
418pub fn get_all_agent_modules() -> Vec<&'static AgentModuleConfig> {
422 use std::collections::HashSet;
423
424 let mut seen_ids = HashSet::new();
425 let mut modules = Vec::new();
426
427 for module in BUILTIN_AGENT_MODULES {
429 if seen_ids.insert(module.id) {
430 modules.push(module);
431 }
432 }
433
434 for module in inventory::iter::<&'static AgentModuleConfig> {
436 if seen_ids.insert(module.id) {
437 modules.push(*module);
438 }
439 }
440
441 modules
442}
443
444pub fn find_agent_module(id: &str) -> Option<&'static AgentModuleConfig> {
446 get_all_agent_modules().into_iter().find(|m| m.id == id)
447}
448
449pub type SchemaGeneratorFn = fn() -> schemars::schema::RootSchema;
455
456#[derive(Debug, Clone)]
458pub struct StepTypeMeta {
459 pub id: &'static str,
461 pub display_name: &'static str,
463 pub description: &'static str,
465 pub category: &'static str,
467 pub schema_fn: SchemaGeneratorFn,
469}
470
471inventory::collect!(&'static StepTypeMeta);
473
474pub fn get_all_step_types() -> impl Iterator<Item = &'static StepTypeMeta> {
476 inventory::iter::<&'static StepTypeMeta>
477 .into_iter()
478 .copied()
479}
480
481pub fn find_step_type(id: &str) -> Option<&'static StepTypeMeta> {
483 get_all_step_types().find(|m| m.id == id)
484}
485
486#[derive(Debug, Clone)]
492pub struct ConnectionFieldMeta {
493 pub name: &'static str,
495 pub type_name: &'static str,
497 pub is_optional: bool,
499 pub display_name: Option<&'static str>,
501 pub description: Option<&'static str>,
503 pub placeholder: Option<&'static str>,
505 pub default_value: Option<&'static str>,
507 pub is_secret: bool,
509}
510
511#[derive(Debug, Clone)]
513pub struct ConnectionTypeMeta {
514 pub integration_id: &'static str,
516 pub display_name: &'static str,
518 pub description: Option<&'static str>,
520 pub category: Option<&'static str>,
522 pub fields: &'static [ConnectionFieldMeta],
524}
525
526inventory::collect!(&'static ConnectionTypeMeta);
528
529pub fn get_all_connection_types() -> impl Iterator<Item = &'static ConnectionTypeMeta> {
531 inventory::iter::<&'static ConnectionTypeMeta>
532 .into_iter()
533 .copied()
534}
535
536pub fn find_connection_type(integration_id: &str) -> Option<&'static ConnectionTypeMeta> {
538 get_all_connection_types().find(|m| m.integration_id == integration_id)
539}
540
541fn rust_to_json_schema_type(rust_type: &str) -> (String, Option<String>, Option<String>) {
547 match rust_type {
548 "String" => ("string".to_string(), None, None),
549 "bool" => ("boolean".to_string(), None, None),
550 "i32" | "i64" | "u32" | "u64" | "usize" => ("integer".to_string(), None, None),
551 "f32" | "f64" => ("number".to_string(), Some("double".to_string()), None),
552 "Value" => ("object".to_string(), None, None),
553 "()" => ("null".to_string(), None, None),
554 t if t.starts_with("Vec<") => {
555 let inner = t.trim_start_matches("Vec<").trim_end_matches('>');
556 let (inner_type, inner_format, _) = rust_to_json_schema_type(inner);
557 let items_json = if let Some(fmt) = inner_format {
558 format!(r#"{{"type": "{}", "format": "{}"}}"#, inner_type, fmt)
559 } else {
560 format!(r#"{{"type": "{}"}}"#, inner_type)
561 };
562 ("array".to_string(), None, Some(items_json))
563 }
564 t if t.starts_with("HashMap<") || t.starts_with("BTreeMap<") => {
565 ("object".to_string(), None, None)
566 }
567 _ => ("string".to_string(), None, None), }
569}
570
571fn input_field_to_api(field: &InputFieldMeta) -> CapabilityField {
573 let (json_type, format, items_json) = rust_to_json_schema_type(field.type_name);
574
575 let items = items_json.map(|items_str| {
576 let type_match = items_str
578 .split("\"type\": \"")
579 .nth(1)
580 .and_then(|s| s.split('"').next())
581 .unwrap_or("string");
582 let format_match = if items_str.contains("\"format\"") {
583 items_str
584 .split("\"format\": \"")
585 .nth(1)
586 .and_then(|s| s.split('"').next())
587 .map(|s| s.to_string())
588 } else {
589 None
590 };
591 FieldTypeInfo {
592 type_name: type_match.to_string(),
593 format: format_match,
594 display_name: None,
595 description: None,
596 fields: None,
597 items: None,
598 nullable: false,
599 }
600 });
601
602 let default_value = field
603 .default_value
604 .and_then(|s| serde_json::from_str(s).ok());
605
606 let example = field
607 .example
608 .map(|s| serde_json::Value::String(s.to_string()));
609
610 let enum_values = field
611 .enum_values_fn
612 .map(|f| f().iter().map(|s| s.to_string()).collect());
613
614 CapabilityField {
615 name: field.name.to_string(),
616 display_name: field.display_name.map(|s| s.to_string()),
617 description: field.description.map(|s| s.to_string()),
618 type_name: json_type,
619 format,
620 items,
621 required: !field.is_optional,
622 default_value,
623 example,
624 enum_values,
625 }
626}
627
628fn output_field_to_api(field: &OutputFieldMeta) -> OutputField {
630 let (type_name, format, _) = rust_to_json_schema_type(field.type_name);
631
632 OutputField {
633 name: field.name.to_string(),
634 display_name: field.display_name.map(|s| s.to_string()),
635 description: field.description.map(|s| s.to_string()),
636 type_name,
637 format,
638 example: field
639 .example
640 .map(|s| serde_json::Value::String(s.to_string())),
641 nullable: field.nullable,
642 items: None,
645 fields: None,
646 }
647}
648
649fn capability_to_api(
651 cap: &CapabilityMeta,
652 input_type_meta: Option<&InputTypeMeta>,
653 output_type_meta: Option<&OutputTypeMeta>,
654) -> CapabilityInfo {
655 let (output_type, output_format, _) = rust_to_json_schema_type(cap.output_type);
656
657 let inputs = input_type_meta
658 .map(|m| m.fields.iter().map(input_field_to_api).collect())
659 .unwrap_or_default();
660
661 let output_fields = output_type_meta
662 .map(|m| Box::new(m.fields.iter().map(output_field_to_api).collect::<Vec<_>>()));
663
664 CapabilityInfo {
665 id: cap.capability_id.to_string(),
666 name: cap.function_name.to_string(),
667 display_name: cap.display_name.map(|s| s.to_string()),
668 description: cap.description.map(|s| s.to_string()),
669 input_type: cap.input_type.to_string(),
670 inputs,
671 output: FieldTypeInfo {
672 type_name: output_type,
673 format: output_format,
674 display_name: output_type_meta.and_then(|m| m.display_name.map(|s| s.to_string())),
675 description: output_type_meta.and_then(|m| m.description.map(|s| s.to_string())),
676 fields: output_fields,
677 items: None,
678 nullable: false,
679 },
680 has_side_effects: cap.has_side_effects,
681 is_idempotent: cap.is_idempotent,
682 rate_limited: cap.rate_limited,
683 }
684}
685
686pub fn get_agents() -> Vec<AgentInfo> {
688 use std::collections::HashMap;
689
690 let input_types: HashMap<&str, &InputTypeMeta> =
692 get_all_input_types().map(|m| (m.type_name, m)).collect();
693
694 let output_types: HashMap<&str, &OutputTypeMeta> =
696 get_all_output_types().map(|m| (m.type_name, m)).collect();
697
698 let mut caps_by_module: HashMap<&str, Vec<&CapabilityMeta>> = HashMap::new();
700 for cap in get_all_capabilities() {
701 let module = cap.module.unwrap_or("unknown");
702 caps_by_module.entry(module).or_default().push(cap);
703 }
704
705 let mut agents = Vec::new();
707
708 for config in get_all_agent_modules() {
709 let caps = caps_by_module.get(config.id).cloned().unwrap_or_default();
710
711 if caps.is_empty() {
712 continue;
713 }
714
715 let capabilities: Vec<CapabilityInfo> = caps
716 .iter()
717 .map(|cap| {
718 let input_meta = input_types.get(cap.input_type).copied();
719 let output_meta = output_types.get(cap.output_type).copied();
720 capability_to_api(cap, input_meta, output_meta)
721 })
722 .collect();
723
724 agents.push(AgentInfo {
725 id: config.id.to_string(),
726 name: config.name.to_string(),
727 description: config.description.to_string(),
728 has_side_effects: config.has_side_effects,
729 supports_connections: config.supports_connections,
730 integration_ids: config
731 .integration_ids
732 .iter()
733 .map(|s| s.to_string())
734 .collect(),
735 capabilities,
736 });
737 }
738
739 agents
740}
741
742pub fn get_capability_inputs(agent_id: &str, capability_id: &str) -> Option<Vec<CapabilityField>> {
745 use std::collections::HashMap;
746
747 let input_types: HashMap<&str, &InputTypeMeta> =
749 get_all_input_types().map(|m| (m.type_name, m)).collect();
750
751 let agent_lower = agent_id.to_lowercase();
753 for cap in get_all_capabilities() {
754 let module = cap.module.unwrap_or("unknown");
755 if module == agent_lower && cap.capability_id == capability_id {
756 let input_meta = input_types.get(cap.input_type).copied();
758
759 let inputs: Vec<CapabilityField> = if let Some(meta) = input_meta {
761 meta.fields.iter().map(input_field_to_api).collect()
762 } else {
763 Vec::new()
764 };
765
766 return Some(inputs);
767 }
768 }
769
770 None
771}
772
773const PRIMITIVE_OUTPUT_TYPES: &[&str] = &[
779 "()", "bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", "f32", "f64", "String", "Value", ];
787
788fn is_primitive_output_type(type_name: &str) -> bool {
790 if PRIMITIVE_OUTPUT_TYPES.contains(&type_name) {
792 return true;
793 }
794
795 if let Some(inner) = type_name
797 .strip_prefix("Vec<")
798 .and_then(|s| s.strip_suffix('>'))
799 {
800 return is_primitive_output_type(inner);
801 }
802
803 if let Some(inner) = type_name
805 .strip_prefix("Option<")
806 .and_then(|s| s.strip_suffix('>'))
807 {
808 return is_primitive_output_type(inner);
809 }
810
811 if type_name.starts_with("HashMap<") || type_name.starts_with("BTreeMap<") {
813 return true; }
815
816 false
817}
818
819#[derive(Debug, Clone)]
821pub struct AgentValidationError {
822 pub module: String,
823 pub capability_id: String,
824 pub missing_input: bool,
825 pub missing_output: bool,
826 pub input_type: String,
827 pub output_type: String,
828}
829
830impl std::fmt::Display for AgentValidationError {
831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832 let mut issues = Vec::new();
833 if self.missing_input {
834 issues.push(format!("missing CapabilityInput for '{}'", self.input_type));
835 }
836 if self.missing_output {
837 issues.push(format!(
838 "missing CapabilityOutput for '{}'",
839 self.output_type
840 ));
841 }
842 write!(
843 f,
844 "{}:{} - {}",
845 self.module,
846 self.capability_id,
847 issues.join(", ")
848 )
849 }
850}
851
852pub fn validate_agent_metadata() -> Vec<AgentValidationError> {
867 use std::collections::HashMap;
868
869 let input_types: HashMap<&str, &InputTypeMeta> =
870 get_all_input_types().map(|m| (m.type_name, m)).collect();
871
872 let output_types: HashMap<&str, &OutputTypeMeta> =
873 get_all_output_types().map(|m| (m.type_name, m)).collect();
874
875 let is_valid_output = |type_name: &str| -> bool {
877 if is_primitive_output_type(type_name) {
878 return true;
879 }
880 if output_types.contains_key(type_name) {
881 return true;
882 }
883 if let Some(inner) = type_name
885 .strip_prefix("Vec<")
886 .and_then(|s| s.strip_suffix('>'))
887 {
888 return is_primitive_output_type(inner) || output_types.contains_key(inner);
889 }
890 if let Some(inner) = type_name
892 .strip_prefix("Option<")
893 .and_then(|s| s.strip_suffix('>'))
894 {
895 return is_primitive_output_type(inner) || output_types.contains_key(inner);
896 }
897 false
898 };
899
900 let mut errors = Vec::new();
901
902 for cap in get_all_capabilities() {
903 let module = cap.module.unwrap_or("unknown").to_string();
904 let missing_input = !input_types.contains_key(cap.input_type);
905 let missing_output = !is_valid_output(cap.output_type);
906
907 if missing_input || missing_output {
908 errors.push(AgentValidationError {
909 module,
910 capability_id: cap.capability_id.to_string(),
911 missing_input,
912 missing_output,
913 input_type: cap.input_type.to_string(),
914 output_type: cap.output_type.to_string(),
915 });
916 }
917 }
918
919 errors
920}
921
922pub fn validate_agent_metadata_or_panic() {
929 let errors = validate_agent_metadata();
930 if !errors.is_empty() {
931 let error_list: Vec<String> = errors.iter().map(|e| format!(" - {}", e)).collect();
932 panic!(
933 "Agent metadata validation failed!\n\
934 The following capabilities are missing CapabilityInput or CapabilityOutput definitions:\n\
935 {}\n\n\
936 To fix this:\n\
937 1. For input types: Add #[derive(CapabilityInput)] to the input struct\n\
938 2. For output types: Add #[derive(CapabilityOutput)] to the output struct\n\
939 \n\
940 Example:\n\
941 #[derive(Serialize, Deserialize, CapabilityOutput)]\n\
942 #[capability_output(display_name = \"My Output\")]\n\
943 pub struct MyCapabilityOutput {{\n\
944 #[field(display_name = \"Result\", description = \"The capability result\")]\n\
945 pub result: String,\n\
946 }}",
947 error_list.join("\n")
948 );
949 }
950}
951
952use std::cell::RefCell;
957
958thread_local! {
959 static CURRENT_INPUT: RefCell<Option<serde_json::Value>> = const { RefCell::new(None) };
963}
964
965pub fn set_current_input(input: &serde_json::Value) {
968 CURRENT_INPUT.with(|c| {
969 *c.borrow_mut() = Some(input.clone());
970 });
971}
972
973pub fn clear_current_input() {
976 CURRENT_INPUT.with(|c| {
977 *c.borrow_mut() = None;
978 });
979}
980
981pub fn get_current_input() -> Option<serde_json::Value> {
984 CURRENT_INPUT.with(|c| c.borrow().clone())
985}
986
987#[cfg(test)]
992mod tests {
993 use super::*;
994
995 #[test]
996 fn test_builtin_agent_modules_count() {
997 assert_eq!(
999 BUILTIN_AGENT_MODULES.len(),
1000 8,
1001 "Expected 8 built-in agent modules"
1002 );
1003 }
1004
1005 #[test]
1006 fn test_builtin_agent_modules_ids() {
1007 let ids: Vec<&str> = BUILTIN_AGENT_MODULES.iter().map(|m| m.id).collect();
1008
1009 assert!(ids.contains(&"utils"), "Missing utils module");
1010 assert!(ids.contains(&"transform"), "Missing transform module");
1011 assert!(ids.contains(&"csv"), "Missing csv module");
1012 assert!(ids.contains(&"text"), "Missing text module");
1013 assert!(ids.contains(&"xml"), "Missing xml module");
1014 assert!(ids.contains(&"http"), "Missing http module");
1015 assert!(ids.contains(&"sftp"), "Missing sftp module");
1016 assert!(ids.contains(&"object_model"), "Missing object_model module");
1017 }
1018
1019 #[test]
1020 fn test_get_all_agent_modules_includes_builtins() {
1021 let modules = get_all_agent_modules();
1022
1023 assert!(
1025 modules.len() >= BUILTIN_AGENT_MODULES.len(),
1026 "get_all_agent_modules should include at least all built-in modules"
1027 );
1028
1029 let module_ids: Vec<&str> = modules.iter().map(|m| m.id).collect();
1031 for builtin in BUILTIN_AGENT_MODULES {
1032 assert!(
1033 module_ids.contains(&builtin.id),
1034 "Built-in module {} should be in get_all_agent_modules()",
1035 builtin.id
1036 );
1037 }
1038 }
1039
1040 #[test]
1041 fn test_get_all_agent_modules_deduplication() {
1042 let modules = get_all_agent_modules();
1043
1044 let mut seen_ids = std::collections::HashSet::new();
1046 for module in &modules {
1047 assert!(
1048 seen_ids.insert(module.id),
1049 "Duplicate module id found: {}",
1050 module.id
1051 );
1052 }
1053 }
1054
1055 #[test]
1056 fn test_find_agent_module_existing() {
1057 let http_module = find_agent_module("http");
1058 assert!(http_module.is_some(), "Should find http module");
1059
1060 let module = http_module.unwrap();
1061 assert_eq!(module.id, "http");
1062 assert_eq!(module.name, "HTTP");
1063 assert!(module.has_side_effects);
1064 assert!(module.supports_connections);
1065 assert!(module.secure);
1066 }
1067
1068 #[test]
1069 fn test_find_agent_module_non_existing() {
1070 let result = find_agent_module("non_existent_module");
1071 assert!(result.is_none(), "Should not find non-existent module");
1072 }
1073
1074 #[test]
1075 fn test_secure_modules() {
1076 for module in BUILTIN_AGENT_MODULES {
1078 match module.id {
1079 "http" | "sftp" => {
1080 assert!(module.secure, "{} module should be secure", module.id);
1081 }
1082 _ => {
1083 assert!(!module.secure, "{} module should not be secure", module.id);
1084 }
1085 }
1086 }
1087 }
1088
1089 #[test]
1090 fn test_side_effects_modules() {
1091 for module in BUILTIN_AGENT_MODULES {
1093 match module.id {
1094 "http" | "sftp" | "object_model" => {
1095 assert!(
1096 module.has_side_effects,
1097 "{} module should have side effects",
1098 module.id
1099 );
1100 }
1101 _ => {
1102 assert!(
1103 !module.has_side_effects,
1104 "{} module should not have side effects",
1105 module.id
1106 );
1107 }
1108 }
1109 }
1110 }
1111
1112 #[test]
1113 fn test_connection_supporting_modules() {
1114 for module in BUILTIN_AGENT_MODULES {
1116 match module.id {
1117 "http" | "sftp" => {
1118 assert!(
1119 module.supports_connections,
1120 "{} module should support connections",
1121 module.id
1122 );
1123 assert!(
1124 !module.integration_ids.is_empty(),
1125 "{} module should have integration IDs",
1126 module.id
1127 );
1128 }
1129 _ => {
1130 assert!(
1131 !module.supports_connections,
1132 "{} module should not support connections",
1133 module.id
1134 );
1135 }
1136 }
1137 }
1138 }
1139
1140 #[test]
1141 fn test_http_integration_ids() {
1142 let http_module = find_agent_module("http").unwrap();
1143 let integration_ids = http_module.integration_ids;
1144
1145 assert!(
1146 integration_ids.contains(&"bearer"),
1147 "http should support bearer"
1148 );
1149 assert!(
1150 integration_ids.contains(&"api_key"),
1151 "http should support api_key"
1152 );
1153 assert!(
1154 integration_ids.contains(&"basic_auth"),
1155 "http should support basic_auth"
1156 );
1157 }
1158
1159 #[test]
1160 fn test_sftp_integration_ids() {
1161 let sftp_module = find_agent_module("sftp").unwrap();
1162 let integration_ids = sftp_module.integration_ids;
1163
1164 assert!(
1165 integration_ids.contains(&"sftp"),
1166 "sftp should support sftp integration"
1167 );
1168 }
1169}