runtara_dsl/
agent_meta.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Agent capability metadata types for runtime introspection
4//!
5//! These types are used by the `runtara-agent-macro` crate to generate
6//! metadata that can be collected at runtime using the `inventory` crate.
7
8use std::future::Future;
9use std::pin::Pin;
10
11/// Trait for types that can provide their enum variant names.
12/// Used by the CapabilityInput macro to extract enum values for API metadata.
13pub trait EnumVariants {
14    /// Returns the variant names as they appear in JSON serialization
15    fn variant_names() -> &'static [&'static str];
16}
17
18/// Function pointer type for getting enum variant names
19pub type EnumVariantsFn = fn() -> &'static [&'static str];
20
21/// Async executor function type - returns a boxed future.
22/// This allows both sync and async capabilities to be executed uniformly:
23/// - Async capabilities return futures directly
24/// - Sync capabilities are wrapped with tokio::task::spawn_blocking
25pub type CapabilityExecutorFn =
26    fn(
27        serde_json::Value,
28    ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send>>;
29
30/// Executor for an agent capability - registered via inventory
31pub struct CapabilityExecutor {
32    /// The agent module name (e.g., "utils", "transform")
33    pub module: &'static str,
34    /// Capability ID in kebab-case (e.g., "random-double")
35    pub capability_id: &'static str,
36    /// The executor function
37    pub execute: CapabilityExecutorFn,
38}
39
40// Register CapabilityExecutor with inventory
41inventory::collect!(&'static CapabilityExecutor);
42
43/// Execute a capability by module and capability_id using inventory-registered executors.
44/// This is an async function that awaits the capability's future.
45pub 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/// Metadata for an agent capability
62#[derive(Debug, Clone)]
63pub struct CapabilityMeta {
64    /// The agent module name (e.g., "utils", "transform")
65    pub module: Option<&'static str>,
66    /// Capability ID in kebab-case (e.g., "random-double")
67    pub capability_id: &'static str,
68    /// The Rust function name (e.g., "random_double")
69    pub function_name: &'static str,
70    /// Input type name (e.g., "RandomDoubleInput")
71    pub input_type: &'static str,
72    /// Output type name (e.g., "f64", "Value")
73    pub output_type: &'static str,
74    /// Display name for UI
75    pub display_name: Option<&'static str>,
76    /// Description of the capability
77    pub description: Option<&'static str>,
78    /// Whether this capability has side effects
79    pub has_side_effects: bool,
80    /// Whether this capability is idempotent
81    pub is_idempotent: bool,
82    /// Whether this capability requires rate limiting (external API calls)
83    pub rate_limited: bool,
84}
85
86// Register CapabilityMeta with inventory
87inventory::collect!(&'static CapabilityMeta);
88
89/// Metadata for an input field
90#[derive(Clone)]
91pub struct InputFieldMeta {
92    /// Field name
93    pub name: &'static str,
94    /// Type name (without Option wrapper)
95    pub type_name: &'static str,
96    /// Whether this field is optional
97    pub is_optional: bool,
98    /// Display name for UI
99    pub display_name: Option<&'static str>,
100    /// Description of the field
101    pub description: Option<&'static str>,
102    /// Example value
103    pub example: Option<&'static str>,
104    /// Default value as JSON string
105    pub default_value: Option<&'static str>,
106    /// Function to get enum values (for types implementing EnumVariants)
107    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/// Metadata for an input type (struct)
126#[derive(Debug, Clone)]
127pub struct InputTypeMeta {
128    /// Type name (e.g., "RandomDoubleInput")
129    pub type_name: &'static str,
130    /// Display name for UI
131    pub display_name: Option<&'static str>,
132    /// Description of the type
133    pub description: Option<&'static str>,
134    /// Fields in this type
135    pub fields: &'static [InputFieldMeta],
136}
137
138// Register InputTypeMeta with inventory
139inventory::collect!(&'static InputTypeMeta);
140
141/// Metadata for an output field
142#[derive(Debug, Clone)]
143pub struct OutputFieldMeta {
144    /// Field name
145    pub name: &'static str,
146    /// Type name (e.g., "String", "Vec<Product>", "Option<i32>")
147    pub type_name: &'static str,
148    /// Display name for UI
149    pub display_name: Option<&'static str>,
150    /// Description of the field
151    pub description: Option<&'static str>,
152    /// Example value
153    pub example: Option<&'static str>,
154    /// Whether this field can be null (true for Option<T> types)
155    pub nullable: bool,
156    /// For array types (Vec<T>), describes the item type name
157    /// This is the type name that can be looked up in the output types registry
158    pub items_type_name: Option<&'static str>,
159    /// For nested object types, the type name that can be looked up in the output types registry
160    /// This enables recursive type resolution for complex nested structures
161    pub nested_type_name: Option<&'static str>,
162}
163
164/// Metadata for an output type (struct)
165#[derive(Debug, Clone)]
166pub struct OutputTypeMeta {
167    /// Type name (e.g., "RandomDoubleOutput")
168    pub type_name: &'static str,
169    /// Display name for UI
170    pub display_name: Option<&'static str>,
171    /// Description of the type
172    pub description: Option<&'static str>,
173    /// Fields in this type
174    pub fields: &'static [OutputFieldMeta],
175}
176
177// Register OutputTypeMeta with inventory
178inventory::collect!(&'static OutputTypeMeta);
179
180/// Get all registered capability metadata
181pub fn get_all_capabilities() -> impl Iterator<Item = &'static CapabilityMeta> {
182    inventory::iter::<&'static CapabilityMeta>
183        .into_iter()
184        .copied()
185}
186
187/// Get all registered input type metadata
188pub fn get_all_input_types() -> impl Iterator<Item = &'static InputTypeMeta> {
189    inventory::iter::<&'static InputTypeMeta>
190        .into_iter()
191        .copied()
192}
193
194/// Get all registered output type metadata
195pub fn get_all_output_types() -> impl Iterator<Item = &'static OutputTypeMeta> {
196    inventory::iter::<&'static OutputTypeMeta>
197        .into_iter()
198        .copied()
199}
200
201/// Find input type metadata by type name
202pub fn find_input_type(type_name: &str) -> Option<&'static InputTypeMeta> {
203    get_all_input_types().find(|m| m.type_name == type_name)
204}
205
206/// Find output type metadata by type name
207pub fn find_output_type(type_name: &str) -> Option<&'static OutputTypeMeta> {
208    get_all_output_types().find(|m| m.type_name == type_name)
209}
210
211// ============================================================================
212// API-Compatible Types (for REST API serialization)
213// ============================================================================
214
215use serde::{Deserialize, Serialize};
216
217/// API-compatible agent info
218#[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/// API-compatible capability info
234#[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/// API-compatible capability field info.
256/// Used for agent inputs and scenario input/output schemas.
257#[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/// API-compatible field type info.
281/// Describes the type of a field, including nested structures.
282#[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    /// For array types, describes the item type
296    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
297    pub items: Option<Box<FieldTypeInfo>>,
298    /// Whether this field can be null
299    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
300    pub nullable: bool,
301}
302
303/// API-compatible output field info.
304/// Describes an output field with type information.
305#[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    /// Whether this field can be null
320    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
321    pub nullable: bool,
322    /// For array types, describes the item type
323    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
324    pub items: Option<Box<FieldTypeInfo>>,
325    /// For nested object types, the fields of the nested object
326    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
327    pub fields: Option<Box<Vec<OutputField>>>,
328}
329
330// ============================================================================
331// Agent Module Configuration (static metadata not derivable from macros)
332// ============================================================================
333
334/// Static configuration for agent modules
335#[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    /// Whether this agent can receive sensitive connection data from Connection steps.
344    /// Only secure agents (http, sftp) should have this set to true.
345    /// This prevents connection credentials from leaking through non-secure agents.
346    pub secure: bool,
347}
348
349// Register AgentModuleConfig with inventory
350inventory::collect!(&'static AgentModuleConfig);
351
352/// Built-in agent module configurations
353pub 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
455/// Get all agent modules (built-in + inventory-registered).
456/// Built-in modules take precedence over inventory-registered ones with the same id.
457/// Modules are deduplicated by id.
458pub 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    // Add built-in modules first (they take precedence)
465    for module in BUILTIN_AGENT_MODULES {
466        if seen_ids.insert(module.id) {
467            modules.push(module);
468        }
469    }
470
471    // Add inventory-registered modules (skip if id already exists)
472    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
481/// Find agent module config by id
482pub fn find_agent_module(id: &str) -> Option<&'static AgentModuleConfig> {
483    get_all_agent_modules().into_iter().find(|m| m.id == id)
484}
485
486// ============================================================================
487// Step Type Metadata (for automatic DSL generation)
488// ============================================================================
489
490/// Function pointer type for generating JSON schema
491pub type SchemaGeneratorFn = fn() -> schemars::schema::RootSchema;
492
493/// Metadata for a step type - registered via inventory
494#[derive(Debug, Clone)]
495pub struct StepTypeMeta {
496    /// Step type ID in PascalCase (e.g., "Conditional", "Agent")
497    pub id: &'static str,
498    /// Display name for UI
499    pub display_name: &'static str,
500    /// Description of the step type
501    pub description: &'static str,
502    /// Category: "control" or "execution"
503    pub category: &'static str,
504    /// Function to generate JSON Schema for this step type
505    pub schema_fn: SchemaGeneratorFn,
506}
507
508// Register StepTypeMeta with inventory
509inventory::collect!(&'static StepTypeMeta);
510
511/// Get all registered step type metadata
512pub fn get_all_step_types() -> impl Iterator<Item = &'static StepTypeMeta> {
513    inventory::iter::<&'static StepTypeMeta>
514        .into_iter()
515        .copied()
516}
517
518/// Find step type metadata by id
519pub fn find_step_type(id: &str) -> Option<&'static StepTypeMeta> {
520    get_all_step_types().find(|m| m.id == id)
521}
522
523// ============================================================================
524// Connection Type Metadata (for connection form generation)
525// ============================================================================
526
527/// Metadata for a connection field parameter
528#[derive(Debug, Clone)]
529pub struct ConnectionFieldMeta {
530    /// Field name (used in JSON)
531    pub name: &'static str,
532    /// Type name (String, u16, bool, etc.)
533    pub type_name: &'static str,
534    /// Whether this field is optional
535    pub is_optional: bool,
536    /// Display name for UI
537    pub display_name: Option<&'static str>,
538    /// Description of the field
539    pub description: Option<&'static str>,
540    /// Placeholder text for the input
541    pub placeholder: Option<&'static str>,
542    /// Default value as JSON string
543    pub default_value: Option<&'static str>,
544    /// Whether this is a secret field (password, API key, etc.)
545    pub is_secret: bool,
546}
547
548/// Metadata for a connection type - registered via inventory
549#[derive(Debug, Clone)]
550pub struct ConnectionTypeMeta {
551    /// Unique identifier for this connection type (e.g., "bearer", "sftp")
552    pub integration_id: &'static str,
553    /// Display name for UI (e.g., "Bearer Token", "SFTP")
554    pub display_name: &'static str,
555    /// Description of this connection type
556    pub description: Option<&'static str>,
557    /// Category for grouping (e.g., "ecommerce", "file_storage", "llm")
558    pub category: Option<&'static str>,
559    /// Fields required for this connection type
560    pub fields: &'static [ConnectionFieldMeta],
561}
562
563// Register ConnectionTypeMeta with inventory
564inventory::collect!(&'static ConnectionTypeMeta);
565
566/// Get all registered connection type metadata
567pub fn get_all_connection_types() -> impl Iterator<Item = &'static ConnectionTypeMeta> {
568    inventory::iter::<&'static ConnectionTypeMeta>
569        .into_iter()
570        .copied()
571}
572
573/// Find connection type metadata by integration_id
574pub fn find_connection_type(integration_id: &str) -> Option<&'static ConnectionTypeMeta> {
575    get_all_connection_types().find(|m| m.integration_id == integration_id)
576}
577
578// ============================================================================
579// Conversion Functions (inventory metadata -> API types)
580// ============================================================================
581
582/// Convert Rust type to JSON Schema type
583fn 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), // Value can be any JSON type
590        "()" => ("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), // Default fallback
605    }
606}
607
608/// Convert InputFieldMeta to CapabilityField
609fn 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        // Parse items JSON to extract type and format
614        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
665/// Convert OutputFieldMeta to OutputField
666fn 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        // Note: items and fields are populated by frontend lookup using items_type_name/nested_type_name
680        // We don't recursively resolve here to avoid serde Deserialize stack overflow issues
681        items: None,
682        fields: None,
683    }
684}
685
686/// Convert CapabilityMeta to CapabilityInfo
687fn 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
723/// Build API-compatible agent list from inventory-registered metadata
724pub fn get_agents() -> Vec<AgentInfo> {
725    use std::collections::HashMap;
726
727    // Collect all input types into a map for lookup
728    let input_types: HashMap<&str, &InputTypeMeta> =
729        get_all_input_types().map(|m| (m.type_name, m)).collect();
730
731    // Collect all output types into a map for lookup
732    let output_types: HashMap<&str, &OutputTypeMeta> =
733        get_all_output_types().map(|m| (m.type_name, m)).collect();
734
735    // Group capabilities by module
736    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    // Build agent info for each module
743    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
779/// Get input field definitions for a specific capability.
780/// Returns None if the agent or capability is not found.
781pub fn get_capability_inputs(agent_id: &str, capability_id: &str) -> Option<Vec<CapabilityField>> {
782    use std::collections::HashMap;
783
784    // Collect all input types into a map for lookup
785    let input_types: HashMap<&str, &InputTypeMeta> =
786        get_all_input_types().map(|m| (m.type_name, m)).collect();
787
788    // Find the capability (case-insensitive module match)
789    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            // Get input type metadata
794            let input_meta = input_types.get(cap.input_type).copied();
795
796            // Build the inputs list using the existing conversion function
797            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
810// ============================================================================
811// Validation Functions
812// ============================================================================
813
814/// Primitive types that don't require CapabilityOutput registration
815const PRIMITIVE_OUTPUT_TYPES: &[&str] = &[
816    "()",   // Unit type
817    "bool", // Boolean
818    "i8",
819    "i16",
820    "i32",
821    "i64",
822    "i128",
823    "isize", // Signed integers
824    "u8",
825    "u16",
826    "u32",
827    "u64",
828    "u128",
829    "usize", // Unsigned integers
830    "f32",
831    "f64",               // Floats
832    "String",            // String
833    "Value",             // serde_json::Value - dynamic JSON (short form)
834    "serde_json::Value", // serde_json::Value - dynamic JSON (fully qualified)
835];
836
837/// Check if a type is a primitive that doesn't need CapabilityOutput
838fn is_primitive_output_type(type_name: &str) -> bool {
839    // Check exact matches
840    if PRIMITIVE_OUTPUT_TYPES.contains(&type_name) {
841        return true;
842    }
843
844    // Check Vec<T> where T is primitive
845    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    // Check Option<T> where T is primitive
853    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    // Check HashMap<K, V> where V is primitive (common for headers, etc.)
861    if type_name.starts_with("HashMap<") || type_name.starts_with("BTreeMap<") {
862        return true; // Allow map types as they're typically dynamic
863    }
864
865    false
866}
867
868/// Validation error for missing agent metadata
869#[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
901/// Validate that all registered capabilities have corresponding input and output metadata.
902///
903/// Returns a list of validation errors. If the list is empty, all capabilities are valid.
904///
905/// # Example
906/// ```ignore
907/// let errors = validate_agent_metadata();
908/// if !errors.is_empty() {
909///     for error in &errors {
910///         eprintln!("Agent validation error: {}", error);
911///     }
912///     panic!("Agent metadata validation failed");
913/// }
914/// ```
915pub 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    // Helper to check if output type is valid (primitive, registered, or Vec/Option of registered)
925    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        // Check Vec<T> where T is a registered output type
933        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        // Check Option<T> where T is a registered output type
940        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
971/// Validate agent metadata and panic if any capabilities are missing input/output definitions.
972///
973/// Call this at application startup to ensure all agents are properly defined.
974///
975/// # Panics
976/// Panics with a detailed error message listing all capabilities with missing metadata.
977pub 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// ============================================================================
1002// Tests
1003// ============================================================================
1004
1005#[cfg(test)]
1006mod tests {
1007    use super::*;
1008
1009    #[test]
1010    fn test_builtin_agent_modules_count() {
1011        // Verify we have the expected number of built-in modules
1012        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        // Should include all built-in modules
1041        assert!(
1042            modules.len() >= BUILTIN_AGENT_MODULES.len(),
1043            "get_all_agent_modules should include at least all built-in modules"
1044        );
1045
1046        // Verify built-in module IDs are present
1047        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        // Check for duplicates
1062        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        // Only http and sftp should be secure
1094        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        // http, sftp, and object_model have side effects
1109        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        // Only http and sftp support connections
1132        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}