Skip to main content

hydra_compiler/
executor.rs

1//! CompiledExecutor — runs compiled ASTs without LLM calls (zero tokens).
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Instant;
6
7use serde::{Deserialize, Serialize};
8
9use crate::ast::{ActionNode, CollectionExpr, ConditionExpr, ParamExpr};
10use crate::compiler::CompiledAction;
11
12/// Result of executing a compiled action
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ExecutionResult {
15    pub compiled_id: String,
16    pub signature: String,
17    pub success: bool,
18    pub tokens_used: u64,
19    pub duration_ms: u64,
20    pub steps_executed: u32,
21    pub results: Vec<StepResult>,
22    pub error: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct StepResult {
27    pub tool: String,
28    pub params: HashMap<String, serde_json::Value>,
29    pub result: serde_json::Value,
30    pub success: bool,
31}
32
33/// Callback type for dispatching tool execution through a real bridge.
34/// Arguments: (tool_name, resolved_params) -> Result<Value, error_message>
35pub type CompiledToolDispatcher = Arc<
36    dyn Fn(&str, &HashMap<String, serde_json::Value>) -> Result<serde_json::Value, String>
37        + Send
38        + Sync,
39>;
40
41/// Executes compiled action ASTs without any LLM calls
42pub struct CompiledExecutor {
43    /// Context: variable bindings from user input
44    variables: HashMap<String, serde_json::Value>,
45    /// Results from previous steps (for StoreResult / PreviousResult)
46    stored_results: HashMap<String, serde_json::Value>,
47    /// Optional dispatcher for real tool execution via sister bridges
48    tool_dispatcher: Option<CompiledToolDispatcher>,
49}
50
51impl CompiledExecutor {
52    pub fn new() -> Self {
53        Self {
54            variables: HashMap::new(),
55            stored_results: HashMap::new(),
56            tool_dispatcher: None,
57        }
58    }
59
60    pub fn with_variables(variables: HashMap<String, serde_json::Value>) -> Self {
61        Self {
62            variables,
63            stored_results: HashMap::new(),
64            tool_dispatcher: None,
65        }
66    }
67
68    /// Set a real tool dispatcher for bridging compiled action execution
69    /// through sister bridges instead of simulating results.
70    pub fn with_dispatcher(mut self, dispatcher: CompiledToolDispatcher) -> Self {
71        self.tool_dispatcher = Some(dispatcher);
72        self
73    }
74
75    /// Execute a compiled action. Returns zero tokens used.
76    pub fn execute(&mut self, compiled: &CompiledAction) -> ExecutionResult {
77        let start = Instant::now();
78        let mut results = Vec::new();
79
80        let success = self.execute_node(&compiled.ast, &mut results);
81
82        ExecutionResult {
83            compiled_id: compiled.id.clone(),
84            signature: compiled.signature.clone(),
85            success,
86            tokens_used: 0, // Zero tokens — that's the whole point
87            duration_ms: start.elapsed().as_millis() as u64,
88            steps_executed: results.len() as u32,
89            results,
90            error: if success {
91                None
92            } else {
93                Some("Step failed".into())
94            },
95        }
96    }
97
98    fn execute_node(&mut self, node: &ActionNode, results: &mut Vec<StepResult>) -> bool {
99        match node {
100            ActionNode::Action { tool, params } => {
101                let resolved = self.resolve_params(params);
102
103                let (result, success) = if let Some(ref dispatcher) = self.tool_dispatcher {
104                    // Dispatch through real sister bridge
105                    match dispatcher(tool, &resolved) {
106                        Ok(val) => (val, true),
107                        Err(err) => (serde_json::json!({ "error": err }), false),
108                    }
109                } else {
110                    // Fallback: simulate success
111                    (serde_json::json!({ "status": "ok", "tool": tool }), true)
112                };
113
114                results.push(StepResult {
115                    tool: tool.clone(),
116                    params: resolved,
117                    result: result.clone(),
118                    success,
119                });
120                success
121            }
122            ActionNode::Sequence(nodes) => {
123                for node in nodes {
124                    if !self.execute_node(node, results) {
125                        return false;
126                    }
127                }
128                true
129            }
130            ActionNode::If {
131                condition,
132                then,
133                else_,
134            } => {
135                if self.evaluate_condition(condition) {
136                    self.execute_node(then, results)
137                } else if let Some(else_node) = else_ {
138                    self.execute_node(else_node, results)
139                } else {
140                    true
141                }
142            }
143            ActionNode::ForEach {
144                variable,
145                collection,
146                body,
147            } => {
148                let items = self.resolve_collection(collection);
149                for item in items {
150                    self.variables.insert(variable.clone(), item);
151                    if !self.execute_node(body, results) {
152                        return false;
153                    }
154                }
155                true
156            }
157            ActionNode::StoreResult { key, action } => {
158                let prev_len = results.len();
159                let success = self.execute_node(action, results);
160                if success {
161                    if let Some(last) = results.get(prev_len) {
162                        self.stored_results.insert(key.clone(), last.result.clone());
163                    }
164                }
165                success
166            }
167        }
168    }
169
170    fn resolve_params(
171        &self,
172        params: &HashMap<String, ParamExpr>,
173    ) -> HashMap<String, serde_json::Value> {
174        params
175            .iter()
176            .map(|(k, v)| (k.clone(), self.resolve_param(v)))
177            .collect()
178    }
179
180    fn resolve_param(&self, expr: &ParamExpr) -> serde_json::Value {
181        match expr {
182            ParamExpr::Literal(v) => v.clone(),
183            ParamExpr::Variable(name) => self
184                .variables
185                .get(name)
186                .cloned()
187                .unwrap_or(serde_json::Value::Null),
188            ParamExpr::PreviousResult(key) => self
189                .stored_results
190                .get(key)
191                .cloned()
192                .unwrap_or(serde_json::Value::Null),
193            ParamExpr::Computed(_) => {
194                // In production: evaluate compute rules
195                serde_json::Value::Null
196            }
197        }
198    }
199
200    fn evaluate_condition(&self, condition: &ConditionExpr) -> bool {
201        match condition {
202            ConditionExpr::Exists(key) => {
203                self.stored_results.contains_key(key) || self.variables.contains_key(key)
204            }
205            ConditionExpr::Success(key) => self
206                .stored_results
207                .get(key)
208                .and_then(|v| v.get("status"))
209                .and_then(|s| s.as_str())
210                .map(|s| s == "ok")
211                .unwrap_or(false),
212            ConditionExpr::Equals { left, right } => {
213                let left_val = self
214                    .variables
215                    .get(left)
216                    .or_else(|| self.stored_results.get(left));
217                left_val.map(|v| v == right).unwrap_or(false)
218            }
219            ConditionExpr::And(conditions) => conditions.iter().all(|c| self.evaluate_condition(c)),
220            ConditionExpr::Or(conditions) => conditions.iter().any(|c| self.evaluate_condition(c)),
221            ConditionExpr::Not(inner) => !self.evaluate_condition(inner),
222        }
223    }
224
225    fn resolve_collection(&self, collection: &CollectionExpr) -> Vec<serde_json::Value> {
226        match collection {
227            CollectionExpr::Literal(items) => items.clone(),
228            CollectionExpr::FromResult(key) => self
229                .stored_results
230                .get(key)
231                .and_then(|v| v.as_array())
232                .cloned()
233                .unwrap_or_default(),
234            CollectionExpr::FromVariable(key) => self
235                .variables
236                .get(key)
237                .and_then(|v| v.as_array())
238                .cloned()
239                .unwrap_or_default(),
240        }
241    }
242}
243
244impl Default for CompiledExecutor {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250#[cfg(test)]
251#[path = "executor_tests.rs"]
252mod executor_tests;