Skip to main content

orchestral_runtime/action/
factory.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use thiserror::Error;
5
6use orchestral_core::action::{Action, ActionContext, ActionInput, ActionMeta, ActionResult};
7use orchestral_core::config::ActionSpec;
8
9use super::builtin::build_builtin_action;
10use super::document::build_document_action;
11
12use super::mcp::build_mcp_action;
13use super::structured::build_structured_action;
14
15/// Action factory errors
16#[derive(Debug, Error)]
17pub enum ActionBuildError {
18    #[error("unknown action kind: {0}")]
19    UnknownKind(String),
20    #[error("invalid action config: {0}")]
21    InvalidConfig(String),
22}
23
24/// Action factory trait
25pub trait ActionFactory: Send + Sync {
26    fn build(&self, spec: &ActionSpec) -> Result<Arc<dyn Action>, ActionBuildError>;
27}
28
29/// Default factory for built-in actions
30pub struct DefaultActionFactory;
31
32impl DefaultActionFactory {
33    pub fn new() -> Self {
34        Self
35    }
36}
37
38impl Default for DefaultActionFactory {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl ActionFactory for DefaultActionFactory {
45    fn build(&self, spec: &ActionSpec) -> Result<Arc<dyn Action>, ActionBuildError> {
46        let wrap = |action: Arc<dyn Action>| -> Result<Arc<dyn Action>, ActionBuildError> {
47            Ok(Arc::new(ConfiguredAction::new(action, spec)))
48        };
49
50        if let Some(a) = build_builtin_action(spec) {
51            return wrap(Arc::from(a));
52        }
53        if let Some(a) = build_document_action(spec)? {
54            return wrap(Arc::from(a));
55        }
56        if let Some(a) = build_structured_action(spec)? {
57            return wrap(Arc::from(a));
58        }
59        if let Some(a) = build_mcp_action(spec)? {
60            return wrap(Arc::from(a));
61        }
62        Err(ActionBuildError::UnknownKind(spec.kind.clone()))
63    }
64}
65
66struct ConfiguredAction {
67    inner: Arc<dyn Action>,
68    metadata: ActionMeta,
69}
70
71impl ConfiguredAction {
72    fn new(inner: Arc<dyn Action>, spec: &ActionSpec) -> Self {
73        let metadata = merge_action_metadata(inner.metadata(), spec);
74        Self { inner, metadata }
75    }
76}
77
78#[async_trait]
79impl Action for ConfiguredAction {
80    fn name(&self) -> &str {
81        self.inner.name()
82    }
83
84    fn description(&self) -> &str {
85        self.inner.description()
86    }
87
88    fn metadata(&self) -> ActionMeta {
89        self.metadata.clone()
90    }
91
92    async fn run(&self, input: ActionInput, ctx: ActionContext) -> ActionResult {
93        self.inner.run(input, ctx).await
94    }
95}
96
97fn merge_action_metadata(base: ActionMeta, spec: &ActionSpec) -> ActionMeta {
98    let mut merged = base;
99    if let Some(category) = &spec.category {
100        if !category.trim().is_empty() {
101            merged.category = Some(category.clone());
102        }
103    }
104    if let Some(interface) = spec.interface.as_ref() {
105        if !interface.input_schema.is_null() {
106            merged.input_schema = interface.input_schema.clone();
107        }
108        if !interface.output_schema.is_null() {
109            merged.output_schema = interface.output_schema.clone();
110        }
111    }
112    merged
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use orchestral_core::config::ActionInterfaceSpec;
119    use serde_json::json;
120
121    #[test]
122    fn test_merge_action_metadata_overrides_declared_interface() {
123        let base = ActionMeta::new("echo", "echo")
124            .with_input_schema(json!({"type":"object","properties":{"old":{"type":"string"}}}))
125            .with_output_schema(json!({"type":"object","properties":{"old":{"type":"string"}}}));
126        let interface = ActionInterfaceSpec {
127            input_schema: json!({"type":"object","properties":{"message":{"type":"string"}}}),
128            output_schema: json!({"type":"object","properties":{"result":{"type":"string"}}}),
129        };
130        let spec = ActionSpec {
131            name: "echo".to_string(),
132            kind: "echo".to_string(),
133            description: None,
134            category: Some("direct".to_string()),
135            config: json!({}),
136            interface: Some(interface.clone()),
137        };
138
139        let merged = merge_action_metadata(base, &spec);
140        assert_eq!(merged.input_schema, interface.input_schema);
141        assert_eq!(merged.output_schema, interface.output_schema);
142        assert_eq!(merged.category.as_deref(), Some("direct"));
143    }
144}