fix: drive tool rounds iteratively instead of recursively to prevent stack overflow on long agentic turns to fix latent stack overflow recursion bug

This commit is contained in:
Dark-Alex-17 committed 2026-09-03 16:09:41 -06:00
1 parent cb5b46800f
commit 206f36d826
2 files changed
+183 -184

No files matched your search

+28 -28
View File
@@ -556,7 +556,6 @@ async fn run(
} }
} }
#[async_recursion::async_recursion]
async fn start_directive( async fn start_directive(
ctx: &mut RequestContext, ctx: &mut RequestContext,
input: Input, input: Input,
@@ -576,37 +575,36 @@ async fn start_directive(
return Ok(()); return Ok(());
} }
let client = input.create_client()?;
let extract_code = !*IS_STDOUT_TERMINAL && code_mode; let extract_code = !*IS_STDOUT_TERMINAL && code_mode;
ctx.before_chat_completion(&input)?; let mut input = input;
let (output, tool_results) = if !input.stream() || extract_code { loop {
call_chat_completions( let client = input.create_client()?;
&input, ctx.before_chat_completion(&input)?;
true, let (output, tool_results) = if !input.stream() || extract_code {
extract_code, call_chat_completions(
client.as_ref(), &input,
ctx, true,
abort_signal.clone(), extract_code,
) client.as_ref(),
.await? ctx,
} else { abort_signal.clone(),
call_chat_completions_streaming(&input, client.as_ref(), ctx, abort_signal.clone()).await? )
}; .await?
ctx.after_chat_completion(app.as_ref(), &input, &output, &tool_results)?; } else {
call_chat_completions_streaming(&input, client.as_ref(), ctx, abort_signal.clone())
.await?
};
ctx.after_chat_completion(app.as_ref(), &input, &output, &tool_results)?;
if !tool_results.is_empty() {
input = input.merge_tool_results(output, tool_results);
continue;
}
if !tool_results.is_empty() {
start_directive(
ctx,
input.merge_tool_results(output, tool_results),
code_mode,
abort_signal,
)
.await?;
} else {
match check_pending_tasks_guardrail(ctx) { match check_pending_tasks_guardrail(ctx) {
GuardrailAction::Inject(prompt) => { GuardrailAction::Inject(prompt) => {
let guardrail_input = Input::from_str(ctx, &prompt, None)?; input = Input::from_str(ctx, &prompt, None)?;
return start_directive(ctx, guardrail_input, code_mode, abort_signal).await; continue;
} }
GuardrailAction::ForceTerminate(ids) => { GuardrailAction::ForceTerminate(ids) => {
warn!( warn!(
@@ -617,6 +615,8 @@ async fn start_directive(
} }
GuardrailAction::NoAction => {} GuardrailAction::NoAction => {}
} }
break;
} }
ctx.exit_session()?; ctx.exit_session()?;
+155 -156
View File
@@ -1424,74 +1424,73 @@ pub async fn run_repl_command(
Ok(false) Ok(false)
} }
#[async_recursion::async_recursion]
async fn ask( async fn ask(
ctx: &mut RequestContext, ctx: &mut RequestContext,
abort_signal: AbortSignal, abort_signal: AbortSignal,
mut input: Input, input: Input,
with_embeddings: bool, with_embeddings: bool,
) -> Result<()> { ) -> Result<()> {
if input.is_empty() { let mut input = input;
return Ok(()); let mut with_embeddings = with_embeddings;
} loop {
ctx.session_abort = Some(abort_signal.clone()); if input.is_empty() {
if with_embeddings { return Ok(());
input.use_embeddings(abort_signal.clone()).await?;
}
while ctx.is_compressing_session() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let app = Arc::clone(&ctx.app.config);
if graph::active_agent_graph_name(ctx).is_some() {
ctx.before_chat_completion(&input)?;
let output =
graph::run_active_agent_graph(ctx, &input.text(), abort_signal.clone()).await?;
app.print_markdown(&output)?;
ctx.after_chat_completion(app.as_ref(), &input, &output, &[])?;
return Ok(());
}
let client = input.create_client()?;
ctx.before_chat_completion(&input)?;
let (output, tool_results) = {
let result = if input.stream() {
call_chat_completions_streaming(&input, client.as_ref(), ctx, abort_signal.clone())
.await
} else {
call_chat_completions(
&input,
true,
false,
client.as_ref(),
ctx,
abort_signal.clone(),
)
.await
};
match result {
Ok(v) => v,
Err(err) => {
ctx.on_chat_completion_error(app.as_ref(), &input);
return Err(err);
}
} }
}; ctx.session_abort = Some(abort_signal.clone());
ctx.after_chat_completion(app.as_ref(), &input, &output, &tool_results)?; if with_embeddings {
if !tool_results.is_empty() { input.use_embeddings(abort_signal.clone()).await?;
ask( with_embeddings = false;
ctx, }
abort_signal, while ctx.is_compressing_session() {
input.merge_tool_results(output, tool_results), tokio::time::sleep(std::time::Duration::from_millis(100)).await;
false, }
)
.await let app = Arc::clone(&ctx.app.config);
} else {
if graph::active_agent_graph_name(ctx).is_some() {
ctx.before_chat_completion(&input)?;
let output =
graph::run_active_agent_graph(ctx, &input.text(), abort_signal.clone()).await?;
app.print_markdown(&output)?;
ctx.after_chat_completion(app.as_ref(), &input, &output, &[])?;
return Ok(());
}
let client = input.create_client()?;
ctx.before_chat_completion(&input)?;
let (output, tool_results) = {
let result = if input.stream() {
call_chat_completions_streaming(&input, client.as_ref(), ctx, abort_signal.clone())
.await
} else {
call_chat_completions(
&input,
true,
false,
client.as_ref(),
ctx,
abort_signal.clone(),
)
.await
};
match result {
Ok(v) => v,
Err(err) => {
ctx.on_chat_completion_error(app.as_ref(), &input);
return Err(err);
}
}
};
ctx.after_chat_completion(app.as_ref(), &input, &output, &tool_results)?;
if !tool_results.is_empty() {
input = input.merge_tool_results(output, tool_results);
continue;
}
match check_pending_tasks_guardrail(ctx) { match check_pending_tasks_guardrail(ctx) {
GuardrailAction::Inject(prompt) => { GuardrailAction::Inject(prompt) => {
let guardrail_input = Input::from_str(ctx, &prompt, None)?; input = Input::from_str(ctx, &prompt, None)?;
return ask(ctx, abort_signal, guardrail_input, false).await; continue;
} }
GuardrailAction::ForceTerminate(ids) => { GuardrailAction::ForceTerminate(ids) => {
warn!( warn!(
@@ -1533,103 +1532,103 @@ async fn ask(
format!("{prompt}\n\n{todo_state}") format!("{prompt}\n\n{todo_state}")
}; };
let continuation_input = Input::from_str(ctx, &full_prompt, None)?; input = Input::from_str(ctx, &full_prompt, None)?;
ask(ctx, abort_signal, continuation_input, false).await continue;
} else {
reset_continuation(ctx);
if let Some(reason) = ctx.auto_continue_paused.clone()
&& ctx.todo_list.has_incomplete()
{
let remaining = ctx.todo_list.incomplete_count();
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!(
"\n⏸ {}",
color.italic().paint(format!(
"Auto-continue paused, awaiting user ({remaining} incomplete todo(s) preserved): {reason}"
))
);
}
if ctx.maybe_autoname_session() {
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!("\n📢 {}", color.italic().paint("Autonaming the session."),);
if let Err(err) = ctx.autoname_session(app.as_ref()).await {
warn!("Failed to autonaming the session: {err}");
}
if let Some(session) = ctx.session.as_mut() {
session.set_autonaming(false);
}
}
let needs_compression = ctx
.session
.as_ref()
.is_some_and(|s| s.needs_compression(app.compression_threshold));
if needs_compression {
let agent_can_continue_after_compress =
should_continue(ctx) && !abort_signal.aborted_ctrlc();
if let Some(session) = ctx.session.as_mut() {
session.set_compressing(true);
}
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!("\n📢 {}", color.italic().paint("Compressing the session."),);
if let Err(err) = ctx.compress_session().await {
warn!("Failed to compress the session: {err}");
}
if let Some(session) = ctx.session.as_mut() {
session.set_compressing(false);
}
if agent_can_continue_after_compress {
let full_prompt = {
let config = ctx.auto_continue_config();
let todo_state = ctx.todo_list.render_for_model();
let remaining = ctx.todo_list.incomplete_count();
ctx.increment_auto_continue_count();
let count = ctx.auto_continue_count;
let max = config.max_continues;
let prompt = config
.continuation_prompt
.as_deref()
.unwrap_or(DEFAULT_CONTINUATION_PROMPT);
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!(
"\n📋 {}",
color.italic().paint(format!(
"Auto-continuing after compression ({count}/{max}): {remaining} incomplete todo(s) remain"
))
);
format!("{prompt}\n\n{todo_state}")
};
let continuation_input = Input::from_str(ctx, &full_prompt, None)?;
return ask(ctx, abort_signal, continuation_input, false).await;
}
}
Ok(())
} }
reset_continuation(ctx);
if let Some(reason) = ctx.auto_continue_paused.clone()
&& ctx.todo_list.has_incomplete()
{
let remaining = ctx.todo_list.incomplete_count();
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!(
"\n⏸ {}",
color.italic().paint(format!(
"Auto-continue paused, awaiting user ({remaining} incomplete todo(s) preserved): {reason}"
))
);
}
if ctx.maybe_autoname_session() {
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!("\n📢 {}", color.italic().paint("Autonaming the session."),);
if let Err(err) = ctx.autoname_session(app.as_ref()).await {
warn!("Failed to autonaming the session: {err}");
}
if let Some(session) = ctx.session.as_mut() {
session.set_autonaming(false);
}
}
let needs_compression = ctx
.session
.as_ref()
.is_some_and(|s| s.needs_compression(app.compression_threshold));
if needs_compression {
let agent_can_continue_after_compress =
should_continue(ctx) && !abort_signal.aborted_ctrlc();
if let Some(session) = ctx.session.as_mut() {
session.set_compressing(true);
}
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!("\n📢 {}", color.italic().paint("Compressing the session."),);
if let Err(err) = ctx.compress_session().await {
warn!("Failed to compress the session: {err}");
}
if let Some(session) = ctx.session.as_mut() {
session.set_compressing(false);
}
if agent_can_continue_after_compress {
let full_prompt = {
let config = ctx.auto_continue_config();
let todo_state = ctx.todo_list.render_for_model();
let remaining = ctx.todo_list.incomplete_count();
ctx.increment_auto_continue_count();
let count = ctx.auto_continue_count;
let max = config.max_continues;
let prompt = config
.continuation_prompt
.as_deref()
.unwrap_or(DEFAULT_CONTINUATION_PROMPT);
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!(
"\n📋 {}",
color.italic().paint(format!(
"Auto-continuing after compression ({count}/{max}): {remaining} incomplete todo(s) remain"
))
);
format!("{prompt}\n\n{todo_state}")
};
input = Input::from_str(ctx, &full_prompt, None)?;
continue;
}
}
return Ok(());
} }
} }