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
8/// Trait for types that can provide their enum variant names.
9/// Used by the CapabilityInput macro to extract enum values for API metadata.
10pub trait EnumVariants {
11    /// Returns the variant names as they appear in JSON serialization
12    fn variant_names() -> &'static [&'static str];
13}
14
15/// Function pointer type for getting enum variant names
16pub type EnumVariantsFn = fn() -> &'static [&'static str];
17
18/// Function pointer type for capability executors
19pub type CapabilityExecutorFn = fn(serde_json::Value) -> Result<serde_json::Value, String>;
20
21/// Executor for an agent capability - registered via inventory
22pub struct CapabilityExecutor {
23    /// The agent module name (e.g., "utils", "transform")
24    pub module: &'static str,
25    /// Capability ID in kebab-case (e.g., "random-double")
26    pub capability_id: &'static str,
27    /// The executor function
28    pub execute: CapabilityExecutorFn,
29}
30
31// Register CapabilityExecutor with inventory
32inventory::collect!(&'static CapabilityExecutor);
33
34/// Execute a capability by module and capability_id using inventory-registered executors
35pub 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/// Metadata for an agent capability
52#[derive(Debug, Clone)]
53pub struct CapabilityMeta {
54    /// The agent module name (e.g., "utils", "transform")
55    pub module: Option<&'static str>,
56    /// Capability ID in kebab-case (e.g., "random-double")
57    pub capability_id: &'static str,
58    /// The Rust function name (e.g., "random_double")
59    pub function_name: &'static str,
60    /// Input type name (e.g., "RandomDoubleInput")
61    pub input_type: &'static str,
62    /// Output type name (e.g., "f64", "Value")
63    pub output_type: &'static str,
64    /// Display name for UI
65    pub display_name: Option<&'static str>,
66    /// Description of the capability
67    pub description: Option<&'static str>,
68    /// Whether this capability has side effects
69    pub has_side_effects: bool,
70    /// Whether this capability is idempotent
71    pub is_idempotent: bool,
72    /// Whether this capability requires rate limiting (external API calls)
73    pub rate_limited: bool,
74}
75
76// Register CapabilityMeta with inventory
77inventory::collect!(&'static CapabilityMeta);
78
79/// Metadata for an input field
80#[derive(Clone)]
81pub struct InputFieldMeta {
82    /// Field name
83    pub name: &'static str,
84    /// Type name (without Option wrapper)
85    pub type_name: &'static str,
86    /// Whether this field is optional
87    pub is_optional: bool,
88    /// Display name for UI
89    pub display_name: Option<&'static str>,
90    /// Description of the field
91    pub description: Option<&'static str>,
92    /// Example value
93    pub example: Option<&'static str>,
94    /// Default value as JSON string
95    pub default_value: Option<&'static str>,
96    /// Function to get enum values (for types implementing EnumVariants)
97    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/// Metadata for an input type (struct)
116#[derive(Debug, Clone)]
117pub struct InputTypeMeta {
118    /// Type name (e.g., "RandomDoubleInput")
119    pub type_name: &'static str,
120    /// Display name for UI
121    pub display_name: Option<&'static str>,
122    /// Description of the type
123    pub description: Option<&'static str>,
124    /// Fields in this type
125    pub fields: &'static [InputFieldMeta],
126}
127
128// Register InputTypeMeta with inventory
129inventory::collect!(&'static InputTypeMeta);
130
131/// Metadata for an output field
132#[derive(Debug, Clone)]
133pub struct OutputFieldMeta {
134    /// Field name
135    pub name: &'static str,
136    /// Type name (e.g., "String", "Vec<Product>", "Option<i32>")
137    pub type_name: &'static str,
138    /// Display name for UI
139    pub display_name: Option<&'static str>,
140    /// Description of the field
141    pub description: Option<&'static str>,
142    /// Example value
143    pub example: Option<&'static str>,
144    /// Whether this field can be null (true for Option<T> types)
145    pub nullable: bool,
146    /// For array types (Vec<T>), describes the item type name
147    /// This is the type name that can be looked up in the output types registry
148    pub items_type_name: Option<&'static str>,
149    /// For nested object types, the type name that can be looked up in the output types registry
150    /// This enables recursive type resolution for complex nested structures
151    pub nested_type_name: Option<&'static str>,
152}
153
154/// Metadata for an output type (struct)
155#[derive(Debug, Clone)]
156pub struct OutputTypeMeta {
157    /// Type name (e.g., "RandomDoubleOutput")
158    pub type_name: &'static str,
159    /// Display name for UI
160    pub display_name: Option<&'static str>,
161    /// Description of the type
162    pub description: Option<&'static str>,
163    /// Fields in this type
164    pub fields: &'static [OutputFieldMeta],
165}
166
167// Register OutputTypeMeta with inventory
168inventory::collect!(&'static OutputTypeMeta);
169
170/// Get all registered capability metadata
171pub fn get_all_capabilities() -> impl Iterator<Item = &'static CapabilityMeta> {
172    inventory::iter::<&'static CapabilityMeta>
173        .into_iter()
174        .copied()
175}
176
177/// Get all registered input type metadata
178pub fn get_all_input_types() -> impl Iterator<Item = &'static InputTypeMeta> {
179    inventory::iter::<&'static InputTypeMeta>
180        .into_iter()
181        .copied()
182}
183
184/// Get all registered output type metadata
185pub fn get_all_output_types() -> impl Iterator<Item = &'static OutputTypeMeta> {
186    inventory::iter::<&'static OutputTypeMeta>
187        .into_iter()
188        .copied()
189}
190
191/// Find input type metadata by type name
192pub fn find_input_type(type_name: &str) -> Option<&'static InputTypeMeta> {
193    get_all_input_types().find(|m| m.type_name == type_name)
194}
195
196/// Find output type metadata by type name
197pub fn find_output_type(type_name: &str) -> Option<&'static OutputTypeMeta> {
198    get_all_output_types().find(|m| m.type_name == type_name)
199}
200
201// ============================================================================
202// API-Compatible Types (for REST API serialization)
203// ============================================================================
204
205use serde::{Deserialize, Serialize};
206
207/// API-compatible agent info
208#[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/// API-compatible capability info
224#[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/// API-compatible capability field info.
246/// Used for agent inputs and scenario input/output schemas.
247#[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/// API-compatible field type info.
271/// Describes the type of a field, including nested structures.
272#[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    /// For array types, describes the item type
286    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
287    pub items: Option<Box<FieldTypeInfo>>,
288    /// Whether this field can be null
289    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
290    pub nullable: bool,
291}
292
293/// API-compatible output field info.
294/// Describes an output field with type information.
295#[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    /// Whether this field can be null
310    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
311    pub nullable: bool,
312    /// For array types, describes the item type
313    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
314    pub items: Option<Box<FieldTypeInfo>>,
315    /// For nested object types, the fields of the nested object
316    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
317    pub fields: Option<Box<Vec<OutputField>>>,
318}
319
320// ============================================================================
321// Agent Module Configuration (static metadata not derivable from macros)
322// ============================================================================
323
324/// Static configuration for agent modules
325#[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    /// Whether this agent can receive sensitive connection data from Connection steps.
334    /// Only secure agents (http, sftp) should have this set to true.
335    /// This prevents connection credentials from leaking through non-secure agents.
336    pub secure: bool,
337}
338
339// Register AgentModuleConfig with inventory
340inventory::collect!(&'static AgentModuleConfig);
341
342/// Built-in agent module configurations
343pub 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
418/// Get all agent modules (built-in + inventory-registered).
419/// Built-in modules take precedence over inventory-registered ones with the same id.
420/// Modules are deduplicated by id.
421pub 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    // Add built-in modules first (they take precedence)
428    for module in BUILTIN_AGENT_MODULES {
429        if seen_ids.insert(module.id) {
430            modules.push(module);
431        }
432    }
433
434    // Add inventory-registered modules (skip if id already exists)
435    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
444/// Find agent module config by id
445pub fn find_agent_module(id: &str) -> Option<&'static AgentModuleConfig> {
446    get_all_agent_modules().into_iter().find(|m| m.id == id)
447}
448
449// ============================================================================
450// Step Type Metadata (for automatic DSL generation)
451// ============================================================================
452
453/// Function pointer type for generating JSON schema
454pub type SchemaGeneratorFn = fn() -> schemars::schema::RootSchema;
455
456/// Metadata for a step type - registered via inventory
457#[derive(Debug, Clone)]
458pub struct StepTypeMeta {
459    /// Step type ID in PascalCase (e.g., "Conditional", "Agent")
460    pub id: &'static str,
461    /// Display name for UI
462    pub display_name: &'static str,
463    /// Description of the step type
464    pub description: &'static str,
465    /// Category: "control" or "execution"
466    pub category: &'static str,
467    /// Function to generate JSON Schema for this step type
468    pub schema_fn: SchemaGeneratorFn,
469}
470
471// Register StepTypeMeta with inventory
472inventory::collect!(&'static StepTypeMeta);
473
474/// Get all registered step type metadata
475pub fn get_all_step_types() -> impl Iterator<Item = &'static StepTypeMeta> {
476    inventory::iter::<&'static StepTypeMeta>
477        .into_iter()
478        .copied()
479}
480
481/// Find step type metadata by id
482pub fn find_step_type(id: &str) -> Option<&'static StepTypeMeta> {
483    get_all_step_types().find(|m| m.id == id)
484}
485
486// ============================================================================
487// Connection Type Metadata (for connection form generation)
488// ============================================================================
489
490/// Metadata for a connection field parameter
491#[derive(Debug, Clone)]
492pub struct ConnectionFieldMeta {
493    /// Field name (used in JSON)
494    pub name: &'static str,
495    /// Type name (String, u16, bool, etc.)
496    pub type_name: &'static str,
497    /// Whether this field is optional
498    pub is_optional: bool,
499    /// Display name for UI
500    pub display_name: Option<&'static str>,
501    /// Description of the field
502    pub description: Option<&'static str>,
503    /// Placeholder text for the input
504    pub placeholder: Option<&'static str>,
505    /// Default value as JSON string
506    pub default_value: Option<&'static str>,
507    /// Whether this is a secret field (password, API key, etc.)
508    pub is_secret: bool,
509}
510
511/// Metadata for a connection type - registered via inventory
512#[derive(Debug, Clone)]
513pub struct ConnectionTypeMeta {
514    /// Unique identifier for this connection type (e.g., "bearer", "sftp")
515    pub integration_id: &'static str,
516    /// Display name for UI (e.g., "Bearer Token", "SFTP")
517    pub display_name: &'static str,
518    /// Description of this connection type
519    pub description: Option<&'static str>,
520    /// Category for grouping (e.g., "ecommerce", "file_storage", "llm")
521    pub category: Option<&'static str>,
522    /// Fields required for this connection type
523    pub fields: &'static [ConnectionFieldMeta],
524}
525
526// Register ConnectionTypeMeta with inventory
527inventory::collect!(&'static ConnectionTypeMeta);
528
529/// Get all registered connection type metadata
530pub fn get_all_connection_types() -> impl Iterator<Item = &'static ConnectionTypeMeta> {
531    inventory::iter::<&'static ConnectionTypeMeta>
532        .into_iter()
533        .copied()
534}
535
536/// Find connection type metadata by integration_id
537pub fn find_connection_type(integration_id: &str) -> Option<&'static ConnectionTypeMeta> {
538    get_all_connection_types().find(|m| m.integration_id == integration_id)
539}
540
541// ============================================================================
542// Conversion Functions (inventory metadata -> API types)
543// ============================================================================
544
545/// Convert Rust type to JSON Schema type
546fn 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), // Default fallback
568    }
569}
570
571/// Convert InputFieldMeta to CapabilityField
572fn 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        // Parse items JSON to extract type and format
577        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
628/// Convert OutputFieldMeta to OutputField
629fn 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        // Note: items and fields are populated by frontend lookup using items_type_name/nested_type_name
643        // We don't recursively resolve here to avoid serde Deserialize stack overflow issues
644        items: None,
645        fields: None,
646    }
647}
648
649/// Convert CapabilityMeta to CapabilityInfo
650fn 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
686/// Build API-compatible agent list from inventory-registered metadata
687pub fn get_agents() -> Vec<AgentInfo> {
688    use std::collections::HashMap;
689
690    // Collect all input types into a map for lookup
691    let input_types: HashMap<&str, &InputTypeMeta> =
692        get_all_input_types().map(|m| (m.type_name, m)).collect();
693
694    // Collect all output types into a map for lookup
695    let output_types: HashMap<&str, &OutputTypeMeta> =
696        get_all_output_types().map(|m| (m.type_name, m)).collect();
697
698    // Group capabilities by module
699    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    // Build agent info for each module
706    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
742/// Get input field definitions for a specific capability.
743/// Returns None if the agent or capability is not found.
744pub fn get_capability_inputs(agent_id: &str, capability_id: &str) -> Option<Vec<CapabilityField>> {
745    use std::collections::HashMap;
746
747    // Collect all input types into a map for lookup
748    let input_types: HashMap<&str, &InputTypeMeta> =
749        get_all_input_types().map(|m| (m.type_name, m)).collect();
750
751    // Find the capability (case-insensitive module match)
752    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            // Get input type metadata
757            let input_meta = input_types.get(cap.input_type).copied();
758
759            // Build the inputs list using the existing conversion function
760            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
773// ============================================================================
774// Validation Functions
775// ============================================================================
776
777/// Primitive types that don't require CapabilityOutput registration
778const PRIMITIVE_OUTPUT_TYPES: &[&str] = &[
779    "()",   // Unit type
780    "bool", // Boolean
781    "i8", "i16", "i32", "i64", "i128", "isize", // Signed integers
782    "u8", "u16", "u32", "u64", "u128", "usize", // Unsigned integers
783    "f32", "f64",    // Floats
784    "String", // String
785    "Value",  // serde_json::Value - dynamic JSON
786];
787
788/// Check if a type is a primitive that doesn't need CapabilityOutput
789fn is_primitive_output_type(type_name: &str) -> bool {
790    // Check exact matches
791    if PRIMITIVE_OUTPUT_TYPES.contains(&type_name) {
792        return true;
793    }
794
795    // Check Vec<T> where T is primitive
796    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    // Check Option<T> where T is primitive
804    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    // Check HashMap<K, V> where V is primitive (common for headers, etc.)
812    if type_name.starts_with("HashMap<") || type_name.starts_with("BTreeMap<") {
813        return true; // Allow map types as they're typically dynamic
814    }
815
816    false
817}
818
819/// Validation error for missing agent metadata
820#[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
852/// Validate that all registered capabilities have corresponding input and output metadata.
853///
854/// Returns a list of validation errors. If the list is empty, all capabilities are valid.
855///
856/// # Example
857/// ```ignore
858/// let errors = validate_agent_metadata();
859/// if !errors.is_empty() {
860///     for error in &errors {
861///         eprintln!("Agent validation error: {}", error);
862///     }
863///     panic!("Agent metadata validation failed");
864/// }
865/// ```
866pub 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    // Helper to check if output type is valid (primitive, registered, or Vec/Option of registered)
876    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        // Check Vec<T> where T is a registered output type
884        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        // Check Option<T> where T is a registered output type
891        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
922/// Validate agent metadata and panic if any capabilities are missing input/output definitions.
923///
924/// Call this at application startup to ensure all agents are properly defined.
925///
926/// # Panics
927/// Panics with a detailed error message listing all capabilities with missing metadata.
928pub 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
952// ============================================================================
953// Current Input Context (for connection resolution)
954// ============================================================================
955
956use std::cell::RefCell;
957
958thread_local! {
959    /// Thread-local storage for the current capability input JSON.
960    /// This allows connection resolution to access the `_connection` field
961    /// that was injected by the generated workflow code.
962    static CURRENT_INPUT: RefCell<Option<serde_json::Value>> = const { RefCell::new(None) };
963}
964
965/// Store the current input JSON in thread-local storage.
966/// Called by the generated capability executor before invoking the capability function.
967pub fn set_current_input(input: &serde_json::Value) {
968    CURRENT_INPUT.with(|c| {
969        *c.borrow_mut() = Some(input.clone());
970    });
971}
972
973/// Clear the current input from thread-local storage.
974/// Called by the generated capability executor after the capability function returns.
975pub fn clear_current_input() {
976    CURRENT_INPUT.with(|c| {
977        *c.borrow_mut() = None;
978    });
979}
980
981/// Get the current input JSON from thread-local storage.
982/// Returns None if no input is currently stored.
983pub fn get_current_input() -> Option<serde_json::Value> {
984    CURRENT_INPUT.with(|c| c.borrow().clone())
985}
986
987// ============================================================================
988// Tests
989// ============================================================================
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    #[test]
996    fn test_builtin_agent_modules_count() {
997        // Verify we have the expected number of built-in modules
998        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        // Should include all built-in modules
1024        assert!(
1025            modules.len() >= BUILTIN_AGENT_MODULES.len(),
1026            "get_all_agent_modules should include at least all built-in modules"
1027        );
1028
1029        // Verify built-in module IDs are present
1030        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        // Check for duplicates
1045        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        // Only http and sftp should be secure
1077        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        // http, sftp, and object_model have side effects
1092        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        // Only http and sftp support connections
1115        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}