From 70cde455abdb6e763489610204cf596c776513b2 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 14 May 2026 12:08:55 -0600 Subject: [PATCH] refactor: migrated the next_node and apply_state_updates logic for LLM nodes into the LlmExecutor --- src/graph/executor.rs | 35 ++-------------- src/graph/llm.rs | 93 ++++++++++++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/src/graph/executor.rs b/src/graph/executor.rs index 525d12f..a72364f 100644 --- a/src/graph/executor.rs +++ b/src/graph/executor.rs @@ -7,11 +7,11 @@ //! template as the graph's return value. use super::agent::AgentNodeExecutor; -use super::llm::{self, LlmNodeExecutor}; +use super::llm::LlmNodeExecutor; use super::parser::GraphParser; use super::script::ScriptExecutor; use super::state::StateManager; -use super::types::{EndNode, Graph, LlmNode, Node, NodeType}; +use super::types::{EndNode, Graph, Node, NodeType}; use super::user_interaction::{ApprovalNodeExecutor, InputNodeExecutor}; use super::validator::GraphValidator; use crate::config::RequestContext; @@ -220,42 +220,13 @@ async fn step( Ok(StepResult::Continue(next)) } NodeType::Llm(llm_node) => { - let result = LlmNodeExecutor::execute(llm_node, state, ctx).await; - let (output, failed) = match result { - Ok(out) => (out, false), - Err(e) => { - warn!("[graph:{}] llm node '{}' failed: {e}", graph_name, current); - (format!("LLM node failed: {e}"), true) - } - }; - apply_state_updates_with_llm_output(llm_node, state, &output); - let next = next_for_llm_node(node, failed, llm_node.fallback.as_deref())?; + let next = LlmNodeExecutor::execute(llm_node, node.next.as_deref(), state, ctx).await?; Ok(StepResult::Continue(next)) } NodeType::End(end_node) => Ok(StepResult::End(resolve_end_output(end_node, state))), } } -fn next_for_llm_node(node: &Node, failed: bool, fallback: Option<&str>) -> Result { - if failed && let Some(fb) = fallback { - return Ok(fb.to_string()); - } - node.next.clone().ok_or_else(|| { - anyhow!( - "llm node '{}' has no `next` set; llm nodes need static routing", - node.id - ) - }) -} - -fn apply_state_updates_with_llm_output( - node: &super::types::LlmNode, - state: &mut StateManager, - output: &str, -) { - crate::graph::llm::apply_state_updates_with_output(node, state, output); -} - /// Apply the end node's `state_updates`, then interpolate its `output` /// template against the resulting state. Both use lenient interpolation /// so the graph still produces a result even when some keys are absent. diff --git a/src/graph/llm.rs b/src/graph/llm.rs index ca31353..179ec71 100644 --- a/src/graph/llm.rs +++ b/src/graph/llm.rs @@ -13,7 +13,7 @@ use super::state::StateManager; use super::types::LlmNode; use crate::config::RequestContext; use crate::utils::dimmed_text; -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use serde_json::Value; const OUTPUT_KEY: &str = "output"; @@ -21,48 +21,77 @@ const OUTPUT_KEY: &str = "output"; pub struct LlmNodeExecutor; impl LlmNodeExecutor { - /// Interpolate the node's templates, run the LLM call, then return - /// the model's final response. State updates are applied by the - /// graph executor (which knows whether to use the success path or - /// the failure path). + /// Run the LLM call and resolve routing. Returns the next node ID + /// to visit. Handles the tolerant-fail contract internally: + /// success → `node_next`; failure with `fallback` → `fallback`; + /// failure without `fallback` → `node_next`. State updates are + /// applied in both success and failure paths, with `{{output}}` + /// resolving to the LLM's response or an error description. pub async fn execute( node: &LlmNode, + node_next: Option<&str>, state_manager: &mut StateManager, - _parent_ctx: &mut RequestContext, + parent_ctx: &mut RequestContext, ) -> Result { - let _instructions = state_manager - .interpolate(&node.instructions) - .context("Failed to interpolate llm node instructions")?; - let _prompt = state_manager - .interpolate(&node.prompt) - .context("Failed to interpolate llm node prompt")?; - - eprintln!( - "{}", - dimmed_text(&format!( - "▸ llm call: model={} tools={}", - node.model.as_deref().unwrap_or(""), - describe_tools_filter(node.tools.as_deref()) - )) - ); - - bail!( - "llm node execution body not yet implemented — see \ - docs/implementation/graph-agents/10.5-llm-nodes.md \ - (steps 3 & 5 of the implementation order)" - ); + let result = run(node, state_manager, parent_ctx).await; + let (output, failed) = match result { + Ok(out) => (out, false), + Err(e) => { + warn!("llm node failed: {e}"); + (format!("LLM node failed: {e}"), true) + } + }; + apply_state_updates_with_output(node, state_manager, &output); + next_for_llm_node(node_next, failed, node.fallback.as_deref()) } } +async fn run( + node: &LlmNode, + state_manager: &mut StateManager, + _parent_ctx: &mut RequestContext, +) -> Result { + let _instructions = state_manager + .interpolate(&node.instructions) + .context("Failed to interpolate llm node instructions")?; + let _prompt = state_manager + .interpolate(&node.prompt) + .context("Failed to interpolate llm node prompt")?; + + eprintln!( + "{}", + dimmed_text(&format!( + "▸ llm call: model={} tools={}", + node.model.as_deref().unwrap_or(""), + describe_tools_filter(node.tools.as_deref()) + )) + ); + + bail!( + "llm node execution body not yet implemented — see \ + docs/implementation/graph-agents/10.5-llm-nodes.md \ + (steps 3 & 5 of the implementation order)" + ); +} + +fn next_for_llm_node( + node_next: Option<&str>, + failed: bool, + fallback: Option<&str>, +) -> Result { + if failed && let Some(fb) = fallback { + return Ok(fb.to_string()); + } + node_next + .map(String::from) + .ok_or_else(|| anyhow::anyhow!("llm node has no `next` set; llm nodes need static routing")) +} + /// Expose the LLM call's final output as `{{output}}` for the duration /// of `state_updates` evaluation, then restore the prior value (or set /// it to `Null` if there wasn't one). Same pattern as /// `AgentNodeExecutor`'s `{{output}}` scoping. -pub fn apply_state_updates_with_output( - node: &LlmNode, - state_manager: &mut StateManager, - output: &str, -) { +fn apply_state_updates_with_output(node: &LlmNode, state_manager: &mut StateManager, output: &str) { let Some(updates) = &node.state_updates else { return; };