refactor: migrated the next_node and apply_state_updates logic for LLM nodes into the LlmExecutor

This commit is contained in:
2026-05-14 12:08:55 -06:00
parent 5f044cab2b
commit 70cde455ab
2 changed files with 64 additions and 64 deletions
+3 -32
View File
@@ -7,11 +7,11 @@
//! template as the graph's return value. //! template as the graph's return value.
use super::agent::AgentNodeExecutor; use super::agent::AgentNodeExecutor;
use super::llm::{self, LlmNodeExecutor}; use super::llm::LlmNodeExecutor;
use super::parser::GraphParser; use super::parser::GraphParser;
use super::script::ScriptExecutor; use super::script::ScriptExecutor;
use super::state::StateManager; 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::user_interaction::{ApprovalNodeExecutor, InputNodeExecutor};
use super::validator::GraphValidator; use super::validator::GraphValidator;
use crate::config::RequestContext; use crate::config::RequestContext;
@@ -220,42 +220,13 @@ async fn step(
Ok(StepResult::Continue(next)) Ok(StepResult::Continue(next))
} }
NodeType::Llm(llm_node) => { NodeType::Llm(llm_node) => {
let result = LlmNodeExecutor::execute(llm_node, state, ctx).await; let next = LlmNodeExecutor::execute(llm_node, node.next.as_deref(), 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())?;
Ok(StepResult::Continue(next)) Ok(StepResult::Continue(next))
} }
NodeType::End(end_node) => Ok(StepResult::End(resolve_end_output(end_node, state))), 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<String> {
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` /// Apply the end node's `state_updates`, then interpolate its `output`
/// template against the resulting state. Both use lenient interpolation /// template against the resulting state. Both use lenient interpolation
/// so the graph still produces a result even when some keys are absent. /// so the graph still produces a result even when some keys are absent.
+61 -32
View File
@@ -13,7 +13,7 @@ use super::state::StateManager;
use super::types::LlmNode; use super::types::LlmNode;
use crate::config::RequestContext; use crate::config::RequestContext;
use crate::utils::dimmed_text; use crate::utils::dimmed_text;
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use serde_json::Value; use serde_json::Value;
const OUTPUT_KEY: &str = "output"; const OUTPUT_KEY: &str = "output";
@@ -21,48 +21,77 @@ const OUTPUT_KEY: &str = "output";
pub struct LlmNodeExecutor; pub struct LlmNodeExecutor;
impl LlmNodeExecutor { impl LlmNodeExecutor {
/// Interpolate the node's templates, run the LLM call, then return /// Run the LLM call and resolve routing. Returns the next node ID
/// the model's final response. State updates are applied by the /// to visit. Handles the tolerant-fail contract internally:
/// graph executor (which knows whether to use the success path or /// success → `node_next`; failure with `fallback` → `fallback`;
/// the failure path). /// 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( pub async fn execute(
node: &LlmNode, node: &LlmNode,
node_next: Option<&str>,
state_manager: &mut StateManager, state_manager: &mut StateManager,
_parent_ctx: &mut RequestContext, parent_ctx: &mut RequestContext,
) -> Result<String> { ) -> Result<String> {
let _instructions = state_manager let result = run(node, state_manager, parent_ctx).await;
.interpolate(&node.instructions) let (output, failed) = match result {
.context("Failed to interpolate llm node instructions")?; Ok(out) => (out, false),
let _prompt = state_manager Err(e) => {
.interpolate(&node.prompt) warn!("llm node failed: {e}");
.context("Failed to interpolate llm node prompt")?; (format!("LLM node failed: {e}"), true)
}
eprintln!( };
"{}", apply_state_updates_with_output(node, state_manager, &output);
dimmed_text(&format!( next_for_llm_node(node_next, failed, node.fallback.as_deref())
"▸ llm call: model={} tools={}",
node.model.as_deref().unwrap_or("<active>"),
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)"
);
} }
} }
async fn run(
node: &LlmNode,
state_manager: &mut StateManager,
_parent_ctx: &mut RequestContext,
) -> Result<String> {
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("<active>"),
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<String> {
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 /// Expose the LLM call's final output as `{{output}}` for the duration
/// of `state_updates` evaluation, then restore the prior value (or set /// of `state_updates` evaluation, then restore the prior value (or set
/// it to `Null` if there wasn't one). Same pattern as /// it to `Null` if there wasn't one). Same pattern as
/// `AgentNodeExecutor`'s `{{output}}` scoping. /// `AgentNodeExecutor`'s `{{output}}` scoping.
pub fn apply_state_updates_with_output( fn apply_state_updates_with_output(node: &LlmNode, state_manager: &mut StateManager, output: &str) {
node: &LlmNode,
state_manager: &mut StateManager,
output: &str,
) {
let Some(updates) = &node.state_updates else { let Some(updates) = &node.state_updates else {
return; return;
}; };