fix(core/compact): prune orphan tool outputs before compacting

This commit is contained in:
James Peter
2025-11-28 10:50:08 +10:00
parent ca8f0efaad
commit 8ba5f744da
2 changed files with 51 additions and 0 deletions
+48
View File
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::sync::Arc;
use super::AgentTask;
@@ -233,6 +234,8 @@ pub(super) async fn perform_compaction(
// background maintenance task and should not affect rollout reconstruction.
loop {
prune_orphan_tool_outputs(&mut turn_input);
let mut prompt = Prompt::default();
prompt.input = turn_input.clone();
prompt.store = !sess.disable_response_storage;
@@ -663,6 +666,51 @@ pub fn sanitize_items_for_compact(items: Vec<ResponseItem>) -> Vec<ResponseItem>
.collect()
}
/// Remove tool outputs that no longer have a matching tool call in the
/// conversation slice. This can happen if earlier items are trimmed for
/// context overflow, leaving orphaned outputs that will be rejected by the
/// compact endpoint.
pub fn prune_orphan_tool_outputs(items: &mut Vec<ResponseItem>) -> usize {
let mut seen_calls: HashSet<String> = HashSet::new();
for item in items.iter() {
match item {
ResponseItem::FunctionCall { call_id, .. }
| ResponseItem::CustomToolCall { call_id, .. } => {
seen_calls.insert(call_id.clone());
}
ResponseItem::LocalShellCall {
id,
call_id,
..
} => {
if let Some(call_id) = call_id {
seen_calls.insert(call_id.clone());
}
if let Some(id) = id {
// Chat Completions flow sets only `id`; outputs use that value as call_id.
seen_calls.insert(id.clone());
}
}
_ => {}
}
}
let before = items.len();
items.retain(|item| match item {
ResponseItem::FunctionCallOutput { call_id, .. }
| ResponseItem::CustomToolCallOutput { call_id, .. } => seen_calls.contains(call_id),
_ => true,
});
let removed = before.saturating_sub(items.len());
if removed > 0 {
tracing::warn!("Dropping {removed} orphaned tool output(s) during compaction");
}
removed
}
fn compaction_checkpoint_warning_event() -> EventMsg {
EventMsg::CompactionCheckpointWarning(CompactionCheckpointWarningEvent {
message: COMPACTION_CHECKPOINT_MESSAGE.to_string(),
+3
View File
@@ -2,6 +2,7 @@ use std::sync::Arc;
use super::compact::{
is_context_overflow_error,
prune_orphan_tool_outputs,
response_input_from_core_items,
sanitize_items_for_compact,
send_compaction_checkpoint_warning,
@@ -87,6 +88,8 @@ async fn run_remote_compact_task_inner(
let max_retries = turn_context.client.get_provider().stream_max_retries();
let mut retries = 0;
let new_history = loop {
prune_orphan_tool_outputs(&mut turn_items);
let mut prompt = Prompt::default();
prompt.input = turn_items.clone();
prompt.base_instructions_override = turn_context.base_instructions.clone();