orchestral_runtime/action/
factory.rs1use 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#[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
24pub trait ActionFactory: Send + Sync {
26 fn build(&self, spec: &ActionSpec) -> Result<Arc<dyn Action>, ActionBuildError>;
27}
28
29pub 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}