fix: ACP session/prompt now drives the full tool-execution loop
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled

run_prompt_turn previously performed a single completion and returned,
leaving tool calls unexecuted. It now mirrors start_directive's loop:
call -> execute tools -> merge results -> continue until no tool results,
then check the pending-agents guardrail before returning.

The pending-agents guardrail injects a reminder prompt when sub-agents
are pending, matching the same loop termination semantics as --headless.
The session is NOT exited between turns (unlike start_directive) so
multi-turn ACP conversations retain history across session/prompt calls.

Manual gate (tool-probe agent, fs_write + fs_read tools):
  id 3 result: {"output":"DONE:probe.txt","stopReason":"end_turn"}
  probe.txt exists: YES, content: hello
This commit is contained in:
2026-07-28 15:42:51 -06:00
parent 06b2c384e3
commit 7f7ea758a7
+28 -10
View File
@@ -1,6 +1,7 @@
use super::types::{METHOD_NOT_FOUND, PARSE_ERROR, Request, Response}; use super::types::{METHOD_NOT_FOUND, PARSE_ERROR, Request, Response};
use crate::client::call_chat_completions_streaming; use crate::client::call_chat_completions_streaming;
use crate::config::{Input, RenderMode, RequestContext}; use crate::config::{Input, RenderMode, RequestContext};
use crate::function::supervisor::{GuardrailAction, check_pending_agents_guardrail};
use crate::utils; use crate::utils;
use crate::utils::AbortSignal; use crate::utils::AbortSignal;
use anyhow::Result; use anyhow::Result;
@@ -198,16 +199,33 @@ async fn run_prompt_turn(
abort: AbortSignal, abort: AbortSignal,
) -> Result<String> { ) -> Result<String> {
ctx.render_mode = RenderMode::Silent; ctx.render_mode = RenderMode::Silent;
let input = Input::from_str(ctx, text, None)?; let mut input = Input::from_str(ctx, text, None)?;
ctx.before_chat_completion(&input)?; loop {
let client = input.create_client()?; ctx.before_chat_completion(&input)?;
let (output, tool_results) = let client = input.create_client()?;
call_chat_completions_streaming(&input, client.as_ref(), ctx, abort).await?; let (output, tool_results) =
let app = Arc::clone(&ctx.app.config); call_chat_completions_streaming(&input, client.as_ref(), ctx, abort.clone()).await?;
let app = Arc::clone(&ctx.app.config);
ctx.after_chat_completion(app.as_ref(), &input, &output, &tool_results)?; ctx.after_chat_completion(app.as_ref(), &input, &output, &tool_results)?;
if !tool_results.is_empty() {
Ok(output) input = input.merge_tool_results(output, tool_results);
continue;
}
match check_pending_agents_guardrail(ctx) {
GuardrailAction::Inject(prompt) => {
input = Input::from_str(ctx, &prompt, None)?;
}
GuardrailAction::ForceTerminate(ids) => {
warn!(
"Pending-agent guardrail force-cancelled {} agent(s): {:?}",
ids.len(),
ids
);
return Ok(output);
}
GuardrailAction::NoAction => return Ok(output),
}
}
} }
fn handle_session_cancel(state: &mut AcpServerState) { fn handle_session_cancel(state: &mut AcpServerState) {