mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Rewrite oversized tool outputs during remote compaction (#26251)
## Why When trying to fit history under compaction limit rewrite output items instead of removing them entirely. Otherwise we're breaking incrementality in relation to the previous response.
This commit is contained in:
committed by
GitHub
Unverified
parent
11bceb8f8b
commit
4231472c03
@@ -9,7 +9,6 @@ use crate::compact::insert_initial_context_before_last_real_user_or_summary;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::context_manager::TotalTokenUsageBreakdown;
|
||||
use crate::context_manager::estimate_response_item_model_visible_bytes;
|
||||
use crate::context_manager::is_codex_generated_item;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
@@ -28,6 +27,8 @@ use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::ContextCompactionItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -38,6 +39,9 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
|
||||
const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str =
|
||||
"Output exceeded the available model context and was truncated";
|
||||
|
||||
pub(crate) async fn run_inline_remote_auto_compact_task(
|
||||
sess: Arc<Session>,
|
||||
turn_context: Arc<TurnContext>,
|
||||
@@ -168,21 +172,21 @@ async fn run_remote_compact_task_inner_impl(
|
||||
.await;
|
||||
let mut history = sess.clone_history().await;
|
||||
let base_instructions = sess.get_base_instructions().await;
|
||||
let deleted_items = trim_function_call_history_to_fit_context_window(
|
||||
let rewritten_outputs = trim_function_call_history_to_fit_context_window(
|
||||
&mut history,
|
||||
turn_context.as_ref(),
|
||||
&base_instructions,
|
||||
);
|
||||
if deleted_items > 0 {
|
||||
if rewritten_outputs > 0 {
|
||||
info!(
|
||||
turn_id = %turn_context.sub_id,
|
||||
deleted_items,
|
||||
"trimmed history items before remote compaction"
|
||||
rewritten_outputs,
|
||||
"rewrote history outputs before remote compaction"
|
||||
);
|
||||
}
|
||||
// This is the history selected for remote compaction, after any trimming required to fit the
|
||||
// compact endpoint. The checkpoint below records it separately from the next sampling request,
|
||||
// whose prompt will repeat current developer/context prefix items.
|
||||
// This is the history selected for remote compaction, after any output rewriting required to
|
||||
// fit the compact endpoint. The checkpoint below records it separately from the next sampling
|
||||
// request, whose prompt will repeat current developer/context prefix items.
|
||||
let trace_input_history = history.raw_items().to_vec();
|
||||
let prompt_input = history.for_prompt(&turn_context.model_info.input_modalities);
|
||||
let tool_router = built_tools(
|
||||
@@ -379,26 +383,68 @@ pub(crate) fn trim_function_call_history_to_fit_context_window(
|
||||
turn_context: &TurnContext,
|
||||
base_instructions: &BaseInstructions,
|
||||
) -> usize {
|
||||
let mut deleted_items = 0usize;
|
||||
let Some(context_window) = turn_context.model_context_window() else {
|
||||
return deleted_items;
|
||||
return 0;
|
||||
};
|
||||
let mut rewritten_outputs = 0usize;
|
||||
let item_count = history.raw_items().len();
|
||||
|
||||
while history
|
||||
.estimate_token_count_with_base_instructions(base_instructions)
|
||||
.is_some_and(|estimated_tokens| estimated_tokens > context_window)
|
||||
{
|
||||
let Some(last_item) = history.raw_items().last() else {
|
||||
for index in (0..item_count).rev() {
|
||||
if history
|
||||
.estimate_token_count_with_base_instructions(base_instructions)
|
||||
.is_none_or(|estimated_tokens| estimated_tokens <= context_window)
|
||||
{
|
||||
break;
|
||||
}
|
||||
let Some(rewritten_item) = history
|
||||
.raw_items()
|
||||
.get(index)
|
||||
.and_then(rewritten_output_for_context_window)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
if !is_codex_generated_item(last_item) {
|
||||
break;
|
||||
}
|
||||
if !history.remove_last_item() {
|
||||
break;
|
||||
}
|
||||
deleted_items += 1;
|
||||
let mut items = history.raw_items().to_vec();
|
||||
items[index] = rewritten_item;
|
||||
history.replace(items);
|
||||
rewritten_outputs += 1;
|
||||
}
|
||||
|
||||
deleted_items
|
||||
rewritten_outputs
|
||||
}
|
||||
|
||||
fn rewritten_output_for_context_window(item: &ResponseItem) -> Option<ResponseItem> {
|
||||
Some(match item {
|
||||
ResponseItem::FunctionCallOutput { call_id, output } => ResponseItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output: truncated_output_payload(output),
|
||||
},
|
||||
ResponseItem::CustomToolCallOutput {
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
} => ResponseItem::CustomToolCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
name: name.clone(),
|
||||
output: truncated_output_payload(output),
|
||||
},
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id,
|
||||
status,
|
||||
execution,
|
||||
..
|
||||
} => ResponseItem::ToolSearchOutput {
|
||||
call_id: call_id.clone(),
|
||||
status: status.clone(),
|
||||
execution: execution.clone(),
|
||||
tools: Vec::new(),
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn truncated_output_payload(output: &FunctionCallOutputPayload) -> FunctionCallOutputPayload {
|
||||
FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()),
|
||||
success: output.success,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,16 +185,16 @@ async fn run_remote_compact_task_inner_impl(
|
||||
|
||||
let mut history = sess.clone_history().await;
|
||||
let base_instructions = sess.get_base_instructions().await;
|
||||
let deleted_items = trim_function_call_history_to_fit_context_window(
|
||||
let rewritten_outputs = trim_function_call_history_to_fit_context_window(
|
||||
&mut history,
|
||||
turn_context.as_ref(),
|
||||
&base_instructions,
|
||||
);
|
||||
if deleted_items > 0 {
|
||||
if rewritten_outputs > 0 {
|
||||
info!(
|
||||
turn_id = %turn_context.sub_id,
|
||||
deleted_items,
|
||||
"trimmed history items before remote compaction v2"
|
||||
rewritten_outputs,
|
||||
"rewrote history outputs before remote compaction v2"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,16 +174,6 @@ impl ContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_last_item(&mut self) -> bool {
|
||||
if let Some(removed) = self.items.pop() {
|
||||
normalize::remove_corresponding_for(&mut self.items, &removed);
|
||||
self.history_version = self.history_version.saturating_add(1);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace(&mut self, items: Vec<ResponseItem>) {
|
||||
self.items = items;
|
||||
self.history_version = self.history_version.saturating_add(1);
|
||||
@@ -734,15 +724,6 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_codex_generated_item(item: &ResponseItem) -> bool {
|
||||
matches!(
|
||||
item,
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
) || matches!(item, ResponseItem::Message { role, .. } if role == "developer")
|
||||
}
|
||||
|
||||
pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool {
|
||||
let ResponseItem::Message { role, content, .. } = item else {
|
||||
return false;
|
||||
|
||||
@@ -651,28 +651,6 @@ fn remove_first_item_removes_matching_call_for_output() {
|
||||
assert_eq!(h.raw_items(), vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_last_item_removes_matching_call_for_output() {
|
||||
let items = vec![
|
||||
user_msg("before tool call"),
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-delete-last".to_string(),
|
||||
},
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: "call-delete-last".to_string(),
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
},
|
||||
];
|
||||
let mut h = create_history_with_items(items);
|
||||
|
||||
assert!(h.remove_last_item());
|
||||
assert_eq!(h.raw_items(), vec![user_msg("before tool call")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_last_turn_images_replaces_tool_output_images() {
|
||||
let items = vec![
|
||||
|
||||
@@ -5,6 +5,5 @@ pub(crate) mod updates;
|
||||
pub(crate) use history::ContextManager;
|
||||
pub(crate) use history::TotalTokenUsageBreakdown;
|
||||
pub(crate) use history::estimate_response_item_model_visible_bytes;
|
||||
pub(crate) use history::is_codex_generated_item;
|
||||
pub(crate) use history::is_user_turn_boundary;
|
||||
pub(crate) use history::truncate_function_output_payload;
|
||||
|
||||
Reference in New Issue
Block a user