feat: LLM node failures propgate up

This commit is contained in:
2026-05-22 18:27:03 -06:00
parent 6221875f64
commit 0c2e4df647
+32 -16
View File
@@ -28,36 +28,46 @@ impl LlmNodeExecutor {
parent_ctx: &mut RequestContext, parent_ctx: &mut RequestContext,
) -> Result<LlmExecutionOutcome> { ) -> Result<LlmExecutionOutcome> {
let result = run(node, state_manager, parent_ctx).await; let result = run(node, state_manager, parent_ctx).await;
let (output, failed) = match result { let (output, failure_reason) = match result {
Ok(raw) => match &node.output_schema { Ok(raw) => match &node.output_schema {
Some(schema) => match structured::extract(&raw, schema, parent_ctx).await { Some(schema) => match structured::extract(&raw, schema, parent_ctx).await {
Ok(value) => (value, false), Ok(value) => (value, None),
Err(e) => { Err(e) => {
warn!("llm node structured extraction failed: {e}"); warn!("llm node structured extraction failed: {e}");
( (
Value::String(format!("LLM node structured-extraction failed: {e}")), Value::String(format!("LLM node structured-extraction failed: {e}")),
true, Some(format!("structured-extraction failed: {e}")),
) )
} }
}, },
None => (Value::String(raw), false), None => (Value::String(raw), None),
}, },
Err(e) => { Err(e) => {
warn!("llm node failed: {e}"); warn!("llm node failed: {e}");
(Value::String(format!("LLM node failed: {e}")), true) (
Value::String(format!("LLM node failed: {e}")),
Some(format!("LLM call failed: {e:#}")),
)
} }
}; };
apply_state_updates_with_output(node, state_manager, &output); apply_state_updates_with_output(node, state_manager, &output);
Ok(outcome_from(failed, node.fallback.as_deref())) outcome_from(failure_reason.as_deref(), node.fallback.as_deref())
} }
} }
fn outcome_from(failed: bool, fallback: Option<&str>) -> LlmExecutionOutcome { fn outcome_from(
if failed && let Some(fb) = fallback { failure_reason: Option<&str>,
LlmExecutionOutcome::FellBack(fb.to_string()) fallback: Option<&str>,
} else { ) -> Result<LlmExecutionOutcome> {
LlmExecutionOutcome::Continue match (failure_reason, fallback) {
(None, _) => Ok(LlmExecutionOutcome::Continue),
(Some(_), Some(fb)) => Ok(LlmExecutionOutcome::FellBack(fb.to_string())),
(Some(reason), None) => bail!(
"LLM node failed and no fallback declared: {reason}. \
Add a `fallback:` route on the node to route on failure, \
or fix the underlying error."
),
} }
} }
@@ -445,23 +455,29 @@ mod tests {
#[test] #[test]
fn outcome_from_success_is_continue() { fn outcome_from_success_is_continue() {
assert_eq!( assert_eq!(
outcome_from(false, Some("fb")), outcome_from(None, Some("fb")).unwrap(),
LlmExecutionOutcome::Continue
);
assert_eq!(
outcome_from(None, None).unwrap(),
LlmExecutionOutcome::Continue LlmExecutionOutcome::Continue
); );
assert_eq!(outcome_from(false, None), LlmExecutionOutcome::Continue);
} }
#[test] #[test]
fn outcome_from_failure_with_fallback_is_fell_back() { fn outcome_from_failure_with_fallback_is_fell_back() {
assert_eq!( assert_eq!(
outcome_from(true, Some("fb")), outcome_from(Some("HTTP 404"), Some("fb")).unwrap(),
LlmExecutionOutcome::FellBack("fb".to_string()) LlmExecutionOutcome::FellBack("fb".to_string())
); );
} }
#[test] #[test]
fn outcome_from_failure_without_fallback_is_continue() { fn outcome_from_failure_without_fallback_propagates_error() {
assert_eq!(outcome_from(true, None), LlmExecutionOutcome::Continue); let err = outcome_from(Some("HTTP 404"), None).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("no fallback declared"), "got: {msg}");
assert!(msg.contains("HTTP 404"), "got: {msg}");
} }
fn node_with_schema(updates: Option<HashMap<String, String>>, schema: Value) -> LlmNode { fn node_with_schema(updates: Option<HashMap<String, String>>, schema: Value) -> LlmNode {