feat: enhance responsiveness by implementing abort checks in streaming API calls

This commit is contained in:
asepharyana
2026-07-13 14:39:39 +07:00
parent 8388a83af0
commit 0d6f558b2b
3 changed files with 73 additions and 38 deletions
+37 -8
View File
@@ -278,10 +278,10 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
///
/// Flow: inject system prompt (with workspace tree if available) → for each
/// step: resolve provider config, build an LLM client, call
/// `chat_with_tools_non_streaming`, process tool calls (gated against both
/// the allowlist and Harness-style content safety checks) or collect text
/// output → send `SubagentEvent`s on `tx` → break on first text-only
/// (non-empty) response.
/// `chat_with_tools_streaming` (with abort check per SSE event), process
/// tool calls (gated against both the allowlist and Harness-style content
/// safety checks) or collect text output → send `SubagentEvent`s on `tx` →
/// break on first text-only (non-empty) response.
///
/// Why: runs synchronously on a dedicated thread so the main async event
/// loop is not blocked. Tool gating prevents restricted, risky, or
@@ -333,15 +333,44 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
anyhow::bail!("subagent aborted by parent at step {step}");
}
// Use the structured tool-calling API so the LLM can request tools with
// proper arguments, exactly like the main agent does.
let (response, _usage) = match client.chat_with_tools_non_streaming(&messages, tdefs_opt.clone()) {
// Use streaming API so the abort flag is checked per SSE event,
// making the subagent responsive to cancellation even during an
// LLM call (non-streaming would block for 10-30s unchecked).
let stream_result = client.chat_with_tools_streaming(
&messages,
tdefs_opt.clone(),
Some(0.7),
Some(4096),
|_event| -> bool {
// Check abort on every SSE event for responsive cancellation.
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
return false; // signals provider to abort
}
// We don't stream tokens to the UI for subagents — just
// need the assembled message at the end.
true
},
);
let (response, _usage) = match stream_result {
Ok(result) => result,
Err(e) => {
let is_abort = ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst))
|| e.to_string().contains("aborted");
let _ = tx.blocking_send(SubagentEvent::StepFailed {
step,
error: e.to_string(),
error: if is_abort {
"subagent aborted by user".to_string()
} else {
e.to_string()
},
});
if is_abort {
anyhow::bail!("subagent aborted by parent at step {step}");
}
// No non-streaming fallback — API must support streaming.
// Non-streaming calls block for up to 1 min without checking
// abort_flag, making cancellation unresponsive.
anyhow::bail!("subagent call failed at step {step}: {e}");
}
};