1use std::collections::HashMap;
29use std::sync::Arc;
30
31use oneai_core::error::{OneAIError, Result};
32use oneai_core::traits::{InteractionGate, LlmProvider, Tool};
33use oneai_core::{InferenceRequest, InferenceResponse, Message, Role};
34
35use crate::state_graph::{
36 StateGraph, GraphState, GraphEdge, NodeAction, EdgeCondition, GraphExecutionResult,
37};
38
39#[async_trait::async_trait]
50pub trait DelegateFactory: Send + Sync {
51 async fn delegate(&self, agent_kind: &str, task: &str) -> Result<String>;
53}
54
55pub struct NoopDelegateFactory;
58
59#[async_trait::async_trait]
60impl DelegateFactory for NoopDelegateFactory {
61 async fn delegate(&self, agent_kind: &str, _task: &str) -> Result<String> {
62 Err(OneAIError::Workflow(format!(
63 "Delegate action '{}' not supported — no DelegateFactory configured", agent_kind
64 )))
65 }
66}
67
68#[derive(Debug, Clone)]
72pub struct ActionResult {
73 pub output: String,
75 pub error: Option<String>,
77}
78
79#[async_trait::async_trait]
100pub trait GraphActionExecutor: Send + Sync {
101 async fn execute_llm_infer(
110 &self,
111 action: &NodeAction,
112 state: &mut GraphState,
113 ) -> Result<ActionResult>;
114
115 async fn execute_tool_call(
122 &self,
123 tool_name: &str,
124 args: &serde_json::Value,
125 state: &mut GraphState,
126 ) -> Result<ActionResult>;
127
128 async fn execute_paradigm_switch(
134 &self,
135 paradigm: &str,
136 state: &mut GraphState,
137 ) -> Result<ActionResult>;
138
139 async fn parse_decision(
144 &self,
145 response: &InferenceResponse,
146 state: &mut GraphState,
147 ) -> Result<oneai_core::GraphDecision>;
148}
149
150pub struct DirectProviderActionExecutor {
164 provider: Arc<dyn LlmProvider>,
165 tools: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>>,
166}
167
168impl DirectProviderActionExecutor {
169 pub fn new(
171 provider: Arc<dyn LlmProvider>,
172 tools: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>>,
173 ) -> Self {
174 Self { provider, tools }
175 }
176}
177
178#[async_trait::async_trait]
179impl GraphActionExecutor for DirectProviderActionExecutor {
180 async fn execute_llm_infer(
181 &self,
182 action: &NodeAction,
183 state: &mut GraphState,
184 ) -> Result<ActionResult> {
185 let (system_prompt_override, _use_streaming, include_tool_definitions,
187 tool_filter_override, thinking_budget, temperature, max_tokens) = match action {
188 NodeAction::LlmInfer {
189 system_prompt_override, use_streaming, include_tool_definitions,
190 tool_filter_override, thinking_budget, temperature, max_tokens,
191 } => (
192 system_prompt_override.clone(),
193 *use_streaming,
194 *include_tool_definitions,
195 tool_filter_override.clone(),
196 *thinking_budget,
197 *temperature,
198 *max_tokens,
199 ),
200 _ => return Err(OneAIError::Workflow("Expected LlmInfer action".to_string())),
201 };
202
203 let system_prompt = system_prompt_override
205 .unwrap_or_else(|| "You are an intelligent agent. Respond to the task.".to_string());
206
207 let mut conversation = state.conversation.clone();
208 if !conversation.messages.iter().any(|m| m.role == Role::System) {
209 conversation.add_message(Message::system(&system_prompt));
210 }
211
212 let tool_defs = if include_tool_definitions {
214 let tools_map = self.tools.read().await;
215 if let Some(filter) = &tool_filter_override {
216 tools_map.values()
218 .filter(|t| filter.contains(&t.name().to_string()))
219 .map(|t| oneai_core::ToolDefinition {
220 name: t.name().to_string(),
221 description: t.description().to_string(),
222 parameters_schema: t.parameters_schema(),
223 })
224 .collect()
225 } else {
226 tools_map.values().map(|t| oneai_core::ToolDefinition {
228 name: t.name().to_string(),
229 description: t.description().to_string(),
230 parameters_schema: t.parameters_schema(),
231 }).collect()
232 }
233 } else {
234 vec![] };
236
237 let request = InferenceRequest {
238 conversation,
239 tools: tool_defs,
240 max_tokens: max_tokens.or(Some(4096)),
241 temperature: temperature.or(Some(0.3)),
242 top_p: None,
243 stop_sequences: vec![],
244 constrained_output: None,
245 thinking_budget,
246 metadata: HashMap::new(),
247 };
248
249 let response = self.provider.infer(request).await?;
250 let output = response.message.text_content();
251
252 state.conversation.add_message(response.message.clone());
254
255 let _decision = self.parse_decision(&response, state).await?;
257
258 Ok(ActionResult {
259 output,
260 error: None,
261 })
262 }
263
264 async fn execute_tool_call(
265 &self,
266 tool_name: &str,
267 args: &serde_json::Value,
268 state: &mut GraphState,
269 ) -> Result<ActionResult> {
270 let tools_map = self.tools.read().await;
271 let tool = tools_map.get(tool_name)
272 .ok_or_else(|| OneAIError::Workflow(
273 format!("Tool '{}' not found for ToolCall node", tool_name)
274 ))?;
275
276 let output = tool.execute(args.clone()).await?;
277
278 state.conversation.add_message(Message::tool_result(
279 format!("graph_tool_{}", tool_name),
280 output.content.clone(),
281 ));
282
283 Ok(ActionResult {
284 output: output.content,
285 error: output.error,
286 })
287 }
288
289 async fn execute_paradigm_switch(
290 &self,
291 paradigm: &str,
292 state: &mut GraphState,
293 ) -> Result<ActionResult> {
294 state.active_paradigm = Some(paradigm.to_string());
295 state.parsed_decision = None; Ok(ActionResult {
298 output: format!("Paradigm switched to: {}", paradigm),
299 error: None,
300 })
301 }
302
303 async fn parse_decision(
304 &self,
305 response: &InferenceResponse,
306 state: &mut GraphState,
307 ) -> Result<oneai_core::GraphDecision> {
308 let mut tool_calls = Vec::new();
311 let mut text_parts = Vec::new();
312
313 for block in &response.message.content {
314 match block {
315 oneai_core::ContentBlock::ToolCall { id: _, name, args } => {
316 if name == "delegate" {
318 let args_value: serde_json::Value = serde_json::from_str(args)
320 .unwrap_or_else(|_| serde_json::json!({}));
321 let agent_kind = args_value.get("agent_type")
322 .and_then(|v| v.as_str()).unwrap_or("Explore").to_string();
323 let task = args_value.get("task")
324 .and_then(|v| v.as_str()).unwrap_or("").to_string();
325 let decision = oneai_core::GraphDecision::Delegate {
326 agent_kind,
327 task,
328 };
329 state.parsed_decision = Some(decision.clone());
330 return Ok(decision);
331 }
332 if name == "switch_paradigm" {
333 let args_value: serde_json::Value = serde_json::from_str(args)
334 .unwrap_or_else(|_| serde_json::json!({}));
335 let paradigm = args_value.get("paradigm")
336 .and_then(|v| v.as_str()).unwrap_or("react").to_string();
337 let decision = oneai_core::GraphDecision::SwitchParadigm { paradigm };
338 state.parsed_decision = Some(decision.clone());
339 return Ok(decision);
340 }
341 tool_calls.push(name.clone());
342 }
343 oneai_core::ContentBlock::Text { text } => {
344 text_parts.push(text.clone());
345 }
346 _ => {}
347 }
348 }
349
350 let decision = if !tool_calls.is_empty() {
351 oneai_core::GraphDecision::ToolCalls {
352 count: tool_calls.len(),
353 }
354 } else {
355 oneai_core::GraphDecision::DirectAnswer {
356 text: text_parts.join("\n"),
357 }
358 };
359
360 state.parsed_decision = Some(decision.clone());
361 Ok(decision)
362 }
363}
364
365pub struct StateGraphExecutor {
384 action_executor: Arc<dyn GraphActionExecutor>,
388 delegate_factory: Arc<dyn DelegateFactory>,
390 interaction_gate: Arc<dyn InteractionGate>,
392 max_iterations: usize,
395}
396
397impl StateGraphExecutor {
398 pub fn new(
403 action_executor: Arc<dyn GraphActionExecutor>,
404 delegate_factory: Arc<dyn DelegateFactory>,
405 interaction_gate: Arc<dyn InteractionGate>,
406 max_iterations: usize,
407 ) -> Self {
408 Self {
409 action_executor,
410 delegate_factory,
411 interaction_gate,
412 max_iterations,
413 }
414 }
415
416 pub fn with_defaults(
418 action_executor: Arc<dyn GraphActionExecutor>,
419 delegate_factory: Arc<dyn DelegateFactory>,
420 interaction_gate: Arc<dyn InteractionGate>,
421 ) -> Self {
422 Self::new(action_executor, delegate_factory, interaction_gate, 50)
423 }
424
425 pub fn with_direct_provider(
432 provider: Arc<dyn LlmProvider>,
433 tools: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>>,
434 delegate_factory: Arc<dyn DelegateFactory>,
435 interaction_gate: Arc<dyn InteractionGate>,
436 max_iterations: usize,
437 ) -> Self {
438 let action_executor = Arc::new(DirectProviderActionExecutor::new(provider, tools));
439 Self::new(action_executor, delegate_factory, interaction_gate, max_iterations)
440 }
441
442 pub fn with_direct_provider_defaults(
445 provider: Arc<dyn LlmProvider>,
446 tools: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn Tool>>>>,
447 delegate_factory: Arc<dyn DelegateFactory>,
448 interaction_gate: Arc<dyn InteractionGate>,
449 ) -> Self {
450 Self::with_direct_provider(provider, tools, delegate_factory, interaction_gate, 50)
451 }
452
453 pub async fn execute(
459 &self,
460 graph: &StateGraph,
461 initial_state: GraphState,
462 ) -> Result<GraphExecutionResult> {
463 let mut state = initial_state;
464 let mut current_node_id = graph.entry_point.clone();
465 let mut iterations = 0;
466 let mut interrupt_checkpoints = Vec::new();
467
468 while iterations < self.max_iterations {
469 iterations += 1;
470 state.iteration_count = iterations;
471
472 let node = graph.get_node(¤t_node_id)
474 .ok_or_else(|| OneAIError::Workflow(
475 format!("Node '{}' not found in graph '{}'", current_node_id, graph.name)
476 ))?;
477
478 if graph.terminal_nodes.contains(¤t_node_id) {
480 let action_result = self.execute_node_action(&node.action, &mut state).await?;
481 state.last_result = Some(action_result.output.clone());
482 state.last_error = action_result.error.clone();
483
484 return Ok(GraphExecutionResult {
485 name: graph.name.clone(),
486 final_state: state,
487 completed: true,
488 terminal_node: Some(current_node_id),
489 iterations,
490 interrupt_checkpoints,
491 });
492 }
493
494 if node.interrupt {
496 let checkpoint_id = format!("interrupt_{}_{}", graph.name, current_node_id);
497 interrupt_checkpoints.push(checkpoint_id.clone());
498
499 let approval_request = oneai_core::ApprovalRequest {
501 tool_name: "state_graph_interrupt".into(),
502 args: serde_json::json!({
503 "node": current_node_id,
504 "description": match &node.action {
505 NodeAction::HumanApproval { description } => description.clone(),
506 _ => "Interrupt point reached".to_string(),
507 },
508 "state": state.variables,
509 }),
510 risk_level: oneai_core::RiskLevel::Medium,
511 permission_level: Some(oneai_core::PermissionLevel::Standard),
512 justification: format!("StateGraph interrupt at node '{}' in graph '{}'",
513 current_node_id, graph.name),
514 };
515
516 let approval = self
517 .interaction_gate
518 .request(oneai_core::InteractionRequest::ToolApproval {
519 approval: approval_request,
520 })
521 .await?;
522 match approval {
523 oneai_core::InteractionResponse::Abort { reason } => {
524 state.should_terminate = true;
525 state.last_error = Some(format!("Interrupt denied: {}", reason));
526 return Ok(GraphExecutionResult {
527 name: graph.name.clone(),
528 final_state: state,
529 completed: false,
530 terminal_node: None,
531 iterations,
532 interrupt_checkpoints,
533 });
534 }
535 oneai_core::InteractionResponse::Revise { feedback } => {
536 state.should_terminate = true;
538 state.last_error = Some(format!("Interrupt denied: {}", feedback));
539 return Ok(GraphExecutionResult {
540 name: graph.name.clone(),
541 final_state: state,
542 completed: false,
543 terminal_node: None,
544 iterations,
545 interrupt_checkpoints,
546 });
547 }
548 _ => { }
549 }
550 }
551
552 let action_result = self.execute_node_action(&node.action, &mut state).await?;
554
555 state.last_result = Some(action_result.output.clone());
557 state.last_error = action_result.error.clone();
558 if state.should_terminate {
559 break;
560 }
561
562 let edges = graph.get_edges_from(¤t_node_id);
564 let next_node = self.route_next_node(&edges, &state)?;
565
566 if next_node.is_none() {
567 tracing::warn!(
569 "No matching edge condition from node '{}' in graph '{}'. Terminating.",
570 current_node_id, graph.name
571 );
572 break;
573 }
574
575 current_node_id = next_node.unwrap();
576 }
577
578 let should_terminate = state.should_terminate;
580 if iterations >= self.max_iterations {
581 tracing::warn!(
582 "StateGraph '{}' exceeded max iterations ({}). Terminating.",
583 graph.name, self.max_iterations
584 );
585 }
586
587 Ok(GraphExecutionResult {
588 name: graph.name.clone(),
589 final_state: state,
590 completed: !should_terminate,
591 terminal_node: None,
592 iterations,
593 interrupt_checkpoints,
594 })
595 }
596
597 async fn execute_node_action(
603 &self,
604 action: &NodeAction,
605 state: &mut GraphState,
606 ) -> Result<ActionResult> {
607 match action {
608 NodeAction::LlmInfer { .. } => {
609 self.action_executor.execute_llm_infer(action, state).await
612 }
613
614 NodeAction::ToolCall { tool_name, args_template } => {
615 let resolved_name = interpolate_graph_template(tool_name, &state.variables);
617 let resolved_args = if let Some(template) = args_template {
618 let json_str = interpolate_graph_template(template, &state.variables);
619 serde_json::from_str(&json_str)
620 .unwrap_or(serde_json::json!({}))
621 } else {
622 serde_json::json!({})
623 };
624
625 self.action_executor.execute_tool_call(&resolved_name, &resolved_args, state).await
628 }
629
630 NodeAction::Delegate { agent_kind, task_template } => {
631 let task = interpolate_graph_template(task_template, &state.variables);
632
633 let result = self.delegate_factory.delegate(agent_kind, &task).await?;
634
635 state.conversation.add_message(Message::assistant(
637 format!("[Delegate {}]: {}", agent_kind, result)
638 ));
639
640 state.parsed_decision = Some(oneai_core::GraphDecision::Delegate {
642 agent_kind: agent_kind.clone(),
643 task,
644 });
645
646 Ok(ActionResult {
647 output: result,
648 error: None,
649 })
650 }
651
652 NodeAction::HumanApproval { description } => {
653 Ok(ActionResult {
656 output: description.clone(),
657 error: None,
658 })
659 }
660
661 NodeAction::ConditionCheck { condition } => {
662 let result = self.evaluate_condition_expression(condition, state)?;
663 state.variables.insert("_condition_result".to_string(), result.to_string());
664 Ok(ActionResult {
665 output: result.to_string(),
666 error: None,
667 })
668 }
669
670 NodeAction::SwitchParadigm { paradigm } => {
671 self.action_executor.execute_paradigm_switch(paradigm, state).await
673 }
674 }
675 }
676
677 fn route_next_node(
683 &self,
684 edges: &[&GraphEdge],
685 state: &GraphState,
686 ) -> Result<Option<String>> {
687 for edge in edges {
688 if let Some(condition) = &edge.condition {
689 if self.evaluate_edge_condition(condition, state)? {
690 return Ok(Some(edge.to.clone()));
691 }
692 } else {
693 return Ok(Some(edge.to.clone()));
695 }
696 }
697 Ok(None)
698 }
699
700 fn evaluate_edge_condition(
707 &self,
708 condition: &EdgeCondition,
709 state: &GraphState,
710 ) -> Result<bool> {
711 match condition {
712 EdgeCondition::HasToolCalls => {
713 Ok(state.parsed_decision.as_ref()
715 .map(|d| d.has_tool_calls())
716 .unwrap_or(false))
717 }
718
719 EdgeCondition::IsFinalAnswer => {
720 Ok(state.parsed_decision.as_ref()
722 .map(|d| d.is_final())
723 .unwrap_or(false))
724 }
725
726 EdgeCondition::RequestsDelegation => {
727 Ok(state.parsed_decision.as_ref()
729 .map(|d| d.is_delegation())
730 .unwrap_or(false))
731 }
732
733 EdgeCondition::ErrorOccurred => {
734 Ok(state.last_error.is_some())
735 }
736
737 EdgeCondition::StateEquals { variable, value } => {
738 Ok(state.variables.get(variable) == Some(value))
739 }
740
741 EdgeCondition::Always => Ok(true),
742
743 EdgeCondition::Custom { name, description } => {
744 tracing::warn!(
745 "Custom condition '{}' ('{}') not registered. Defaulting to false.",
746 name, description
747 );
748 Ok(false)
749 }
750
751 EdgeCondition::ParadigmEquals { paradigm } => {
752 Ok(state.active_paradigm.as_ref() == Some(paradigm))
753 }
754
755 EdgeCondition::IterationExceeds { count } => {
756 Ok(state.iteration_count > *count)
757 }
758 }
759 }
760
761 fn evaluate_condition_expression(&self, condition: &str, state: &GraphState) -> Result<bool> {
769 if condition == "has_tool_calls" {
770 return self.evaluate_edge_condition(&EdgeCondition::HasToolCalls, state);
771 }
772 if condition == "is_final_answer" {
773 return self.evaluate_edge_condition(&EdgeCondition::IsFinalAnswer, state);
774 }
775 if condition == "error_occurred" {
776 return self.evaluate_edge_condition(&EdgeCondition::ErrorOccurred, state);
777 }
778 if let Some((var, val)) = condition.split_once("==") {
780 return Ok(state.variables.get(var.trim()) == Some(&val.trim().to_string()));
781 }
782 Ok(state.variables.get(condition).map(|v| v == "true").unwrap_or(false))
784 }
785}
786
787pub fn interpolate_graph_template(template: &str, variables: &HashMap<String, String>) -> String {
794 let mut result = template.to_string();
795 for (key, value) in variables {
796 result = result.replace(&format!("{{{{{}}}}}", key), value);
797 }
798 result
799}
800
801#[cfg(test)]
804mod tests {
805 use super::*;
806 use crate::state_graph::{StateGraph, GraphNode};
807
808 #[allow(dead_code)] fn make_simple_graph() -> StateGraph {
810 let mut graph = StateGraph::new("test", "start");
812
813 graph.add_node(GraphNode {
814 id: "start".to_string(),
815 action: NodeAction::ConditionCheck {
816 condition: "ready==true".to_string(),
817 },
818 interrupt: false,
819 metadata: HashMap::new(),
820 });
821
822 graph.add_node(GraphNode {
823 id: "end".to_string(),
824 action: NodeAction::LlmInfer {
825 system_prompt_override: Some("Final answer".to_string()),
826 use_streaming: false,
827 include_tool_definitions: false,
828 tool_filter_override: None,
829 thinking_budget: None,
830 temperature: None,
831 max_tokens: None,
832 },
833 interrupt: false,
834 metadata: HashMap::new(),
835 });
836
837 graph.add_edge(GraphEdge {
838 from: "start".to_string(),
839 to: "end".to_string(),
840 condition: Some(EdgeCondition::Always),
841 metadata: HashMap::new(),
842 });
843
844 graph.add_terminal("end".to_string());
845
846 graph
847 }
848
849 #[test]
850 fn test_evaluate_edge_condition_always() {
851 let state = GraphState::new();
852 let executor = make_test_executor();
853
854 let result = executor.evaluate_edge_condition(&EdgeCondition::Always, &state).unwrap();
855 assert!(result);
856 }
857
858 #[test]
859 fn test_evaluate_edge_condition_error_occurred() {
860 let mut state = GraphState::new();
861 let executor = make_test_executor();
862
863 assert!(!executor.evaluate_edge_condition(&EdgeCondition::ErrorOccurred, &state).unwrap());
865
866 state.last_error = Some("test error".to_string());
868 assert!(executor.evaluate_edge_condition(&EdgeCondition::ErrorOccurred, &state).unwrap());
869 }
870
871 #[test]
872 fn test_evaluate_edge_condition_state_equals() {
873 let mut state = GraphState::new();
874 let executor = make_test_executor();
875
876 let cond = EdgeCondition::StateEquals { variable: "mode".to_string(), value: "react".to_string() };
878 assert!(!executor.evaluate_edge_condition(&cond, &state).unwrap());
879
880 state.variables.insert("mode".to_string(), "plan".to_string());
882 assert!(!executor.evaluate_edge_condition(&cond, &state).unwrap());
883
884 state.variables.insert("mode".to_string(), "react".to_string());
886 assert!(executor.evaluate_edge_condition(&cond, &state).unwrap());
887 }
888
889 #[test]
890 fn test_evaluate_edge_condition_has_tool_calls() {
891 let mut state = GraphState::new();
892 let executor = make_test_executor();
893
894 assert!(!executor.evaluate_edge_condition(&EdgeCondition::HasToolCalls, &state).unwrap());
896
897 state.parsed_decision = Some(oneai_core::GraphDecision::DirectAnswer {
899 text: "Just a text answer".to_string(),
900 });
901 assert!(!executor.evaluate_edge_condition(&EdgeCondition::HasToolCalls, &state).unwrap());
902
903 state.parsed_decision = Some(oneai_core::GraphDecision::ToolCalls { count: 1 });
905 assert!(executor.evaluate_edge_condition(&EdgeCondition::HasToolCalls, &state).unwrap());
906 }
907
908 #[test]
909 fn test_evaluate_edge_condition_is_final_answer() {
910 let mut state = GraphState::new();
911 let executor = make_test_executor();
912
913 assert!(!executor.evaluate_edge_condition(&EdgeCondition::IsFinalAnswer, &state).unwrap());
915
916 state.parsed_decision = Some(oneai_core::GraphDecision::DirectAnswer {
918 text: "The answer is 42".to_string(),
919 });
920 assert!(executor.evaluate_edge_condition(&EdgeCondition::IsFinalAnswer, &state).unwrap());
921
922 state.parsed_decision = Some(oneai_core::GraphDecision::ToolCalls { count: 2 });
924 assert!(!executor.evaluate_edge_condition(&EdgeCondition::IsFinalAnswer, &state).unwrap());
925 }
926
927 #[test]
928 fn test_evaluate_edge_condition_requests_delegation() {
929 let mut state = GraphState::new();
930 let executor = make_test_executor();
931
932 state.parsed_decision = Some(oneai_core::GraphDecision::Delegate {
934 agent_kind: "Explore".to_string(),
935 task: "Search the codebase".to_string(),
936 });
937 assert!(executor.evaluate_edge_condition(&EdgeCondition::RequestsDelegation, &state).unwrap());
938
939 state.parsed_decision = Some(oneai_core::GraphDecision::ToolCalls { count: 1 });
941 assert!(!executor.evaluate_edge_condition(&EdgeCondition::RequestsDelegation, &state).unwrap());
942 }
943
944 #[test]
945 fn test_evaluate_edge_condition_paradigm_equals() {
946 let mut state = GraphState::new();
947 let executor = make_test_executor();
948
949 let cond = EdgeCondition::ParadigmEquals { paradigm: "react".to_string() };
951 assert!(!executor.evaluate_edge_condition(&cond, &state).unwrap());
952
953 state.active_paradigm = Some("plan".to_string());
955 assert!(!executor.evaluate_edge_condition(&cond, &state).unwrap());
956
957 state.active_paradigm = Some("react".to_string());
959 assert!(executor.evaluate_edge_condition(&cond, &state).unwrap());
960 }
961
962 #[test]
963 fn test_evaluate_edge_condition_iteration_exceeds() {
964 let mut state = GraphState::new();
965 let executor = make_test_executor();
966
967 let cond = EdgeCondition::IterationExceeds { count: 5 };
969 assert!(!executor.evaluate_edge_condition(&cond, &state).unwrap());
970
971 state.iteration_count = 10;
973 assert!(executor.evaluate_edge_condition(&cond, &state).unwrap());
974 }
975
976 #[test]
977 fn test_interpolate_graph_template() {
978 let vars = HashMap::from([
979 ("selected_tool".to_string(), "shell".to_string()),
980 ("command".to_string(), "ls -la".to_string()),
981 ]);
982
983 let result = interpolate_graph_template("{{selected_tool}} with {{command}}", &vars);
984 assert_eq!(result, "shell with ls -la");
985 }
986
987 #[test]
988 fn test_evaluate_condition_expression() {
989 let mut state = GraphState::new();
990 let executor = make_test_executor();
991
992 state.variables.insert("mode".to_string(), "react".to_string());
994 assert!(executor.evaluate_condition_expression("mode==react", &state).unwrap());
995 assert!(!executor.evaluate_condition_expression("mode==plan", &state).unwrap());
996
997 state.last_error = Some("error".to_string());
999 assert!(executor.evaluate_condition_expression("error_occurred", &state).unwrap());
1000 }
1001
1002 #[allow(dead_code)] struct TestStateGraphExecutor {
1007 #[allow(dead_code)]
1008 max_iterations: usize,
1009 }
1010
1011 impl TestStateGraphExecutor {
1012 fn evaluate_edge_condition(&self, condition: &EdgeCondition, state: &GraphState) -> Result<bool> {
1013 match condition {
1014 EdgeCondition::HasToolCalls => {
1015 Ok(state.parsed_decision.as_ref()
1016 .map(|d| d.has_tool_calls())
1017 .unwrap_or(false))
1018 }
1019 EdgeCondition::IsFinalAnswer => {
1020 Ok(state.parsed_decision.as_ref()
1021 .map(|d| d.is_final())
1022 .unwrap_or(false))
1023 }
1024 EdgeCondition::RequestsDelegation => {
1025 Ok(state.parsed_decision.as_ref()
1026 .map(|d| d.is_delegation())
1027 .unwrap_or(false))
1028 }
1029 EdgeCondition::ErrorOccurred => Ok(state.last_error.is_some()),
1030 EdgeCondition::StateEquals { variable, value } => {
1031 Ok(state.variables.get(variable) == Some(value))
1032 }
1033 EdgeCondition::Always => Ok(true),
1034 EdgeCondition::Custom { name, .. } => {
1035 tracing::warn!("Custom condition '{}' not registered, defaulting to false", name);
1036 Ok(false)
1037 }
1038 EdgeCondition::ParadigmEquals { paradigm } => {
1039 Ok(state.active_paradigm.as_ref() == Some(paradigm))
1040 }
1041 EdgeCondition::IterationExceeds { count } => {
1042 Ok(state.iteration_count > *count)
1043 }
1044 }
1045 }
1046
1047 fn evaluate_condition_expression(&self, condition: &str, state: &GraphState) -> Result<bool> {
1048 if condition == "has_tool_calls" {
1049 return self.evaluate_edge_condition(&EdgeCondition::HasToolCalls, state);
1050 }
1051 if condition == "is_final_answer" {
1052 return self.evaluate_edge_condition(&EdgeCondition::IsFinalAnswer, state);
1053 }
1054 if condition == "error_occurred" {
1055 return self.evaluate_edge_condition(&EdgeCondition::ErrorOccurred, state);
1056 }
1057 if let Some((var, val)) = condition.split_once("==") {
1058 return Ok(state.variables.get(var.trim()) == Some(&val.trim().to_string()));
1059 }
1060 Ok(state.variables.get(condition).map(|v| v == "true").unwrap_or(false))
1061 }
1062 }
1063
1064 fn make_test_executor() -> TestStateGraphExecutor {
1065 TestStateGraphExecutor { max_iterations: 50 }
1066 }
1067
1068 #[test]
1069 fn test_graph_decision_enum() {
1070 let decision = oneai_core::GraphDecision::DirectAnswer {
1071 text: "42".to_string(),
1072 };
1073 assert!(decision.is_final());
1074 assert!(!decision.has_tool_calls());
1075
1076 let tool_calls = oneai_core::GraphDecision::ToolCalls { count: 2 };
1077 assert!(tool_calls.has_tool_calls());
1078 assert!(!tool_calls.is_final());
1079
1080 let delegate = oneai_core::GraphDecision::Delegate {
1081 agent_kind: "Explore".to_string(),
1082 task: "Search".to_string(),
1083 };
1084 assert!(delegate.is_delegation());
1085 assert!(!delegate.has_tool_calls());
1086
1087 let switch = oneai_core::GraphDecision::SwitchParadigm {
1088 paradigm: "plan".to_string(),
1089 };
1090 assert!(!switch.is_final());
1091 assert!(!switch.has_tool_calls());
1092 }
1093}