Internal workflow execution with mode-specific optimizations and tool call handling. When `guardrail_enforcer` is `Some`, the core encodes before LLM and decodes after LLM; we decode before tool usage only (no encode after tool).
(
llm_config: graphbit_core::llm::LlmConfig,
workflow: graphbit_core::workflow::Workflow,
config: ExecutionConfig,
guardrail_enforcer: Option<Arc<Enforcer>>,
)
| 1660 | /// When `guardrail_enforcer` is `Some`, the core encodes before LLM and decodes after LLM; |
| 1661 | /// we decode before tool usage only (no encode after tool). |
| 1662 | async fn execute_workflow_internal( |
| 1663 | llm_config: graphbit_core::llm::LlmConfig, |
| 1664 | workflow: graphbit_core::workflow::Workflow, |
| 1665 | config: ExecutionConfig, |
| 1666 | guardrail_enforcer: Option<Arc<Enforcer>>, |
| 1667 | ) -> Result<graphbit_core::types::WorkflowContext, graphbit_core::errors::GraphBitError> { |
| 1668 | let conditional_handlers = |
| 1669 | crate::workflow::node::build_core_conditional_handlers(&workflow)?; |
| 1670 | let executor = match config.mode { |
| 1671 | ExecutionMode::Balanced => CoreWorkflowExecutor::new() |
| 1672 | .with_default_llm_config(llm_config.clone()) |
| 1673 | .with_conditional_handlers(conditional_handlers), |
| 1674 | }; |
| 1675 | |
| 1676 | // Execute the workflow (core applies encode before LLM, decode after LLM when enforcer is Some) |
| 1677 | let mut context = executor |
| 1678 | .execute(workflow.clone(), guardrail_enforcer.clone()) |
| 1679 | .await?; |
| 1680 | |
| 1681 | // Store LLM config in context metadata for tool call handling |
| 1682 | if let Ok(llm_config_json) = serde_json::to_value(&llm_config) { |
| 1683 | context |
| 1684 | .metadata |
| 1685 | .insert("llm_config".to_string(), llm_config_json); |
| 1686 | } |
| 1687 | |
| 1688 | // Store workflow name in context metadata for result schema |
| 1689 | context.metadata.insert( |
| 1690 | "workflow_name".to_string(), |
| 1691 | serde_json::Value::String(workflow.name.clone()), |
| 1692 | ); |
| 1693 | |
| 1694 | // Check if any node outputs contain tool_calls_required responses and handle them |
| 1695 | let mut context = context; |
| 1696 | let mut rerun_attempts = 0; |
| 1697 | loop { |
| 1698 | let (ctx, nodes_with_tool_calls) = Self::handle_tool_calls_in_context( |
| 1699 | context, |
| 1700 | &workflow, |
| 1701 | guardrail_enforcer.as_ref().map(|arc| arc.as_ref()), |
| 1702 | ) |
| 1703 | .await?; |
| 1704 | context = ctx; |
| 1705 | |
| 1706 | if nodes_with_tool_calls.is_empty() { |
| 1707 | break; |
| 1708 | } |
| 1709 | |
| 1710 | // Identify downstream nodes that depend on tool-resolved outputs and need rerun. |
| 1711 | // This includes nodes that themselves may have tool calls, as they can still depend on |
| 1712 | // upstream nodes whose outputs were just resolved. |
| 1713 | let mut downstream_nodes: HashSet<String> = HashSet::new(); |
| 1714 | if let Some(deps_obj) = context.metadata.get("node_dependencies").and_then(|v| v.as_object()) { |
| 1715 | let mut queue: Vec<String> = nodes_with_tool_calls.clone(); |
| 1716 | while let Some(parent_id) = queue.pop() { |
| 1717 | for (node_id, parents) in deps_obj.iter() { |
| 1718 | if downstream_nodes.contains(node_id) { |
| 1719 | continue; |
nothing calls this directly
no test coverage detected