mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
Preserve tool output truncation budgets across resume and fork (#44248)
## Why Replaying tool outputs under a different model can expand or shrink the history shown to the model if truncation uses the new model's budget. ## What changed Save the originating history truncation budget on function and custom tool outputs and reuse it during replay, preserving existing tool-specific overrides. Include the existing 20% serialization allowance once, before converting byte budgets to tokens. Rename the metadata field to `history_truncation_token_limit` while retaining `fallback_token_limit_override` as its serialized name for compatibility. ## Testing Add regression coverage for resume and fork with different model budgets, including custom tool outputs. Extend unit coverage for existing overrides and byte-budget conversion. GitOrigin-RevId: a62c8fd3e43198e2252b150fba72f022939c1070
This commit is contained in:
1 parent
b64de2f3ad
commit
aa88a0333c
15 files changed
+287
-47
No files matched your search
@@ -172,6 +172,7 @@ use codex_thread_store::ThreadPersistenceMetadata;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_utils_audio::prepare_response_items as prepare_audio_response_items;
|
||||
use codex_utils_git_discovery::GitRootDiscovery;
|
||||
use codex_utils_output_truncation::with_serialization_allowance;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::future::Shared;
|
||||
@@ -3429,9 +3430,25 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
model_info: &ModelInfo,
|
||||
items: Vec<ResponseItemEnvelope>,
|
||||
mut items: Vec<ResponseItemEnvelope>,
|
||||
image_preparations: Vec<ImagePreparationMetadata>,
|
||||
) {
|
||||
// Save the originating history budget for replay.
|
||||
// Preserve any existing tool-specific override.
|
||||
let policy: codex_utils_output_truncation::TruncationPolicy =
|
||||
model_info.truncation_policy.into();
|
||||
for envelope in &mut items {
|
||||
if matches!(
|
||||
envelope.item,
|
||||
ResponseItem::FunctionCallOutput { .. } | ResponseItem::CustomToolCallOutput { .. }
|
||||
) {
|
||||
envelope
|
||||
.metadata
|
||||
.get_or_insert_default()
|
||||
.history_truncation_token_limit
|
||||
.get_or_insert_with(|| with_serialization_allowance(policy).token_budget());
|
||||
}
|
||||
}
|
||||
let response_items = items
|
||||
.iter()
|
||||
.map(|envelope| envelope.item.clone())
|
||||
|
||||
@@ -2532,10 +2532,17 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(false; "plain items")]
|
||||
#[test_case::test_case(true; "annotated items")]
|
||||
#[test_case::test_case(false, None, false, 5; "plain items")]
|
||||
#[test_case::test_case(true, None, false, 5; "annotated items")]
|
||||
#[test_case::test_case(true, Some(100), false, 100; "existing tool budget")]
|
||||
#[test_case::test_case(false, None, true, 31; "byte policy includes allowance before conversion")]
|
||||
#[tokio::test]
|
||||
async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: bool) {
|
||||
async fn annotated_history_uses_explicit_model_without_a_step(
|
||||
retain_metadata: bool,
|
||||
saved_budget: Option<usize>,
|
||||
byte_policy: bool,
|
||||
expected_budget: usize,
|
||||
) {
|
||||
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
CodexAuth::from_api_key("Test API Key"),
|
||||
Vec::new(),
|
||||
@@ -2548,8 +2555,11 @@ async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: b
|
||||
)
|
||||
.await;
|
||||
let mut model_info = turn_context.model_info().as_ref().clone();
|
||||
model_info.truncation_policy =
|
||||
codex_protocol::openai_models::TruncationPolicyConfig::tokens(/*limit*/ 4);
|
||||
model_info.truncation_policy = if byte_policy {
|
||||
codex_protocol::openai_models::TruncationPolicyConfig::bytes(/*limit*/ 101)
|
||||
} else {
|
||||
codex_protocol::openai_models::TruncationPolicyConfig::tokens(/*limit*/ 4)
|
||||
};
|
||||
let text = "diagnostic line\n".repeat(50);
|
||||
let mut item = ResponseItem::FunctionCallOutput {
|
||||
id: Some(ResponseItemId::with_suffix("fco", "existing")),
|
||||
@@ -2564,24 +2574,52 @@ async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: b
|
||||
};
|
||||
item.set_turn_id_if_missing("existing-turn");
|
||||
item.set_create_time_if_missing(123.into());
|
||||
let mut expected = ResponseItemEnvelope {
|
||||
let expected = ResponseItemEnvelope {
|
||||
item,
|
||||
metadata: retain_metadata.then_some(CodexHarnessMetadata {
|
||||
client_authored: true,
|
||||
history_truncation_token_limit: saved_budget,
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
let mut expected = vec![
|
||||
expected,
|
||||
ResponseItemEnvelope {
|
||||
item: ResponseItem::CustomToolCallOutput {
|
||||
id: Some(ResponseItemId::with_suffix("ctco", "existing")),
|
||||
call_id: "call-custom".to_string(),
|
||||
name: None,
|
||||
output: FunctionCallOutputPayload::from_text(text.clone()),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
metadata: retain_metadata.then_some(CodexHarnessMetadata {
|
||||
client_authored: true,
|
||||
history_truncation_token_limit: saved_budget,
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
];
|
||||
expected[1].item.set_turn_id_if_missing("existing-turn");
|
||||
expected[1].item.set_create_time_if_missing(123.into());
|
||||
session
|
||||
.record_annotated_conversation_items(&turn_context, &model_info, vec![expected.clone()])
|
||||
.record_annotated_conversation_items(&turn_context, &model_info, expected.clone())
|
||||
.await;
|
||||
let ResponseItem::FunctionCallOutput { output, .. } = &mut expected.item else {
|
||||
unreachable!("fixture is a tool output");
|
||||
};
|
||||
output.body = FunctionCallOutputBody::Text(codex_utils_output_truncation::truncate_text(
|
||||
&text,
|
||||
codex_utils_output_truncation::TruncationPolicy::Tokens(4) * 1.2,
|
||||
));
|
||||
assert_eq!(session.clone_history().await.annotated_items(), &[expected]);
|
||||
for envelope in &mut expected {
|
||||
envelope
|
||||
.metadata
|
||||
.get_or_insert_default()
|
||||
.history_truncation_token_limit = Some(expected_budget);
|
||||
let (ResponseItem::FunctionCallOutput { output, .. }
|
||||
| ResponseItem::CustomToolCallOutput { output, .. }) = &mut envelope.item
|
||||
else {
|
||||
unreachable!("fixture is a tool output");
|
||||
};
|
||||
output.body = FunctionCallOutputBody::Text(codex_utils_output_truncation::truncate_text(
|
||||
&text,
|
||||
codex_utils_output_truncation::TruncationPolicy::Tokens(expected_budget),
|
||||
));
|
||||
}
|
||||
assert_eq!(session.clone_history().await.annotated_items(), &expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user