From 36601d8668c7a5d83127d2695506d00174613088 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 10 Sep 2026 16:24:31 -0600 Subject: [PATCH] feat(graph): treat llm max_iterations: 0 as no turn cap --- src/graph/llm.rs | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/graph/llm.rs b/src/graph/llm.rs index b473db0..bf00441 100644 --- a/src/graph/llm.rs +++ b/src/graph/llm.rs @@ -257,6 +257,11 @@ async fn run_with_retries( Err(last_err.unwrap_or_else(|| anyhow!("llm node exhausted retries"))) } +/// Whether `turn` (0-based) is the final turn the node's cap allows. A cap of 0 means no cap. +pub(crate) fn is_last_turn(turn: u32, max_iterations: u32) -> bool { + max_iterations > 0 && turn + 1 == max_iterations +} + async fn run_chat_loop(node: &LlmNode, prompt: &str, ctx: &mut RequestContext) -> Result { let abort = create_abort_signal(); let app_cfg = Arc::clone(&ctx.app.config); @@ -264,7 +269,8 @@ async fn run_chat_loop(node: &LlmNode, prompt: &str, ctx: &mut RequestContext) - let mut input = Input::from_str(ctx, prompt, role_for_input)?; let mut accumulated = String::new(); - for turn in 0..node.max_iterations { + let mut turn: u32 = 0; + loop { let client = input.create_client()?; ctx.before_chat_completion(&input)?; let (output, tool_results) = @@ -291,7 +297,7 @@ async fn run_chat_loop(node: &LlmNode, prompt: &str, ctx: &mut RequestContext) - return Ok(accumulated); } GuardrailAction::Inject(prompt) => { - if turn + 1 == node.max_iterations { + if is_last_turn(turn, node.max_iterations) { bail!( "llm node hit max_iterations ({}) before LLM concluded", node.max_iterations @@ -299,22 +305,19 @@ async fn run_chat_loop(node: &LlmNode, prompt: &str, ctx: &mut RequestContext) - } let role = ctx.role.clone(); input = Input::from_str(ctx, &prompt, role)?; - continue; } } + } else { + if is_last_turn(turn, node.max_iterations) { + bail!( + "llm node hit max_iterations ({}) before LLM concluded", + node.max_iterations + ); + } + input = input.merge_tool_results(output, tool_results); } - - if turn + 1 == node.max_iterations { - bail!( - "llm node hit max_iterations ({}) before LLM concluded", - node.max_iterations - ); - } - - input = input.merge_tool_results(output, tool_results); + turn = turn.saturating_add(1); } - - bail!("llm node ended without producing output") } fn build_inline_role( @@ -736,4 +739,19 @@ mod tests { Some(Duration::from_secs(5)) ); } + + #[test] + fn is_last_turn_bounded() { + assert!(is_last_turn(0, 1)); + assert!(!is_last_turn(0, 10)); + assert!(is_last_turn(9, 10)); + assert!(!is_last_turn(10, 10)); + } + + #[test] + fn is_last_turn_zero_cap_never_fires() { + assert!(!is_last_turn(0, 0)); + assert!(!is_last_turn(9, 0)); + assert!(!is_last_turn(u32::MAX, 0)); + } }