Compare commits

...

1 Commits

Author SHA1 Message Date
Roy Han
38f370ad61 Sanitize legacy context compaction history 2026-05-20 17:40:57 -07:00
2 changed files with 70 additions and 3 deletions

View File

@@ -359,6 +359,8 @@ impl ContextManager {
/// 2. every output has a corresponding call entry
/// 3. when images are unsupported, image content is stripped from messages and tool outputs
fn normalize_history(&mut self, input_modalities: &[InputModality]) {
normalize_legacy_context_compaction_items(&mut self.items);
// all function/tool calls must have a corresponding output
normalize::ensure_call_outputs_present(&mut self.items);
@@ -390,6 +392,11 @@ impl ContextManager {
name: name.clone(),
output: truncate_function_output_payload(output, policy_with_serialization_budget),
},
ResponseItem::ContextCompaction {
encrypted_content: Some(encrypted_content),
} => ResponseItem::Compaction {
encrypted_content: encrypted_content.clone(),
},
ResponseItem::Message { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::LocalShellCall { .. }
@@ -454,6 +461,21 @@ impl ContextManager {
}
}
fn normalize_legacy_context_compaction_items(items: &mut Vec<ResponseItem>) {
*items = std::mem::take(items)
.into_iter()
.filter_map(|item| match item {
ResponseItem::ContextCompaction {
encrypted_content: Some(encrypted_content),
} => Some(ResponseItem::Compaction { encrypted_content }),
ResponseItem::ContextCompaction {
encrypted_content: None,
} => None,
item => Some(item),
})
.collect();
}
pub(crate) fn truncate_function_output_payload(
output: &FunctionCallOutputPayload,
policy: TruncationPolicy,
@@ -490,9 +512,14 @@ fn is_api_message(message: &ResponseItem) -> bool {
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::Compaction { .. }
| ResponseItem::ContextCompaction { .. } => true,
ResponseItem::CompactionTrigger => false,
ResponseItem::Other => false,
| ResponseItem::ContextCompaction {
encrypted_content: Some(_),
} => true,
ResponseItem::ContextCompaction {
encrypted_content: None,
}
| ResponseItem::CompactionTrigger
| ResponseItem::Other => false,
}
}

View File

@@ -306,6 +306,46 @@ fn for_prompt_preserves_inter_agent_assistant_messages() {
assert_eq!(history.for_prompt(&default_input_modalities()), vec![item]);
}
#[test]
fn legacy_context_compaction_items_are_not_sent_to_prompt() {
let legacy_compaction = ResponseItem::ContextCompaction {
encrypted_content: Some("encrypted-legacy-context".to_string()),
};
let lifecycle_marker = ResponseItem::ContextCompaction {
encrypted_content: None,
};
let history = create_history_with_items(vec![legacy_compaction, lifecycle_marker]);
assert_eq!(
history.raw_items(),
vec![ResponseItem::Compaction {
encrypted_content: "encrypted-legacy-context".to_string(),
}]
);
let user = user_msg("after compaction");
let mut history = ContextManager::new();
history.replace(vec![
ResponseItem::ContextCompaction {
encrypted_content: Some("encrypted-legacy-context".to_string()),
},
ResponseItem::ContextCompaction {
encrypted_content: None,
},
user.clone(),
]);
assert_eq!(
history.for_prompt(&default_input_modalities()),
vec![
ResponseItem::Compaction {
encrypted_content: "encrypted-legacy-context".to_string(),
},
user,
]
);
}
#[test]
fn drop_last_n_user_turns_treats_inter_agent_assistant_messages_as_instruction_turns() {
let first_turn = user_input_text_msg("first");