fix: bug in next_single method and improved outcome handling for LLM node execution

This commit is contained in:
2026-05-20 16:27:25 -06:00
parent 26de81e84e
commit 98d16d9a56
5 changed files with 65 additions and 153 deletions
+7 -49
View File
@@ -1,4 +1,4 @@
use anyhow::{Result, bail};
use anyhow::Result;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -129,11 +129,11 @@ pub struct Node {
}
impl Node {
/// Returns the single next target as a string slice, or `None` if no next is
/// declared or if a multi-target fan-out is declared. Use this for read-only
/// inspection (e.g. tests). For execution paths that require single-target
/// semantics, use `next_single()` — it errors explicitly when a fan-out is
/// declared so the caller can surface a clear failure instead of skipping it.
/// Returns the single next target as a string slice for tests and other
/// read-only inspection. Returns `None` when no `next:` is declared at all,
/// OR when a real multi-target fan-out is declared (since a fan-out has no
/// "single" target). Execution paths use `static_next_targets` in the graph
/// executor instead.
#[allow(dead_code)]
pub fn next_target(&self) -> Option<&str> {
match &self.next {
@@ -143,16 +143,6 @@ impl Node {
Some(NextTargets::Many(_)) => None,
}
}
/// Returns the single next target as a string slice, or an explicit error if
/// the node declares a multi-target fan-out (which is not yet supported
/// pre-Phase-D). Returns `Ok(None)` when no next is declared at all.
pub fn next_single(&self) -> Result<Option<&str>> {
match &self.next {
None => Ok(None),
Some(targets) => Ok(Some(targets.single()?.as_str())),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -172,24 +162,9 @@ impl NextTargets {
}
/// True if this declares more than one parallel target (i.e., a real fan-out).
#[allow(dead_code)]
pub fn is_fan_out(&self) -> bool {
matches!(self, NextTargets::Many(v) if v.len() > 1)
}
/// Returns the single target if exactly one is declared, else errors with a
/// clear "not yet supported" message. Used by the v1 executor until parallel
/// branch execution lands in Phase D.
pub fn single(&self) -> Result<&String> {
match self {
NextTargets::One(s) => Ok(s),
NextTargets::Many(v) if v.len() == 1 => Ok(&v[0]),
NextTargets::Many(_) => bail!(
"Parallel fan-out (`next: [a, b, ...]`) is declared, but parallel \
branch execution is not yet implemented in this build."
),
}
}
}
impl From<String> for NextTargets {
@@ -971,23 +946,7 @@ next: retrieve
}
#[test]
fn next_single_errors_on_real_fan_out_with_clear_message() {
let yaml = r#"
id: triage
type: llm
prompt: Classify
next: [a, b]
"#;
let node: Node = serde_yaml::from_str(yaml).unwrap();
let err = node.next_single().unwrap_err().to_string();
assert!(err.contains("Parallel fan-out"), "got: {err}");
assert!(err.contains("not yet implemented"), "got: {err}");
}
#[test]
fn next_single_accepts_many_containing_exactly_one_target() {
fn next_target_treats_many_of_one_as_single() {
let yaml = r#"
id: triage
type: llm
@@ -997,7 +956,6 @@ next: [retrieve]
let node: Node = serde_yaml::from_str(yaml).unwrap();
assert_eq!(node.next_single().unwrap(), Some("retrieve"));
assert_eq!(node.next_target(), Some("retrieve"));
}