mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
## Why Asynchronous Guardian collection applied entry caps and compressed heartbeat messages before request preparation, while synchronous collection preserved originals. Preserve the captured evidence so request-time profiles and budgeting determine what is delivered. ## What changed - Replace `prepare_sync_context` with a shared `prepare_context` entry point for synchronous and asynchronous presentations. - Defer text caps, native-message omission, and heartbeat compression to preparation, preserving heartbeat reference numbering across transcript deltas. - Support elastic transcript preparation for both reviewer profiles. - Borrow collected asynchronous context instead of cloning the full transcript, and filter previous reviews against reviewer history during preparation. ## Testing Add coverage for collection preserving originals, fixed preparation truncating text and omitting oversized native entries, and elastic preparation deferring entry caps for both profiles. Update heartbeat tests to cover full and delta preparation without mutating collected evidence. GitOrigin-RevId: 4d4e448acf5dc8880a05c3c9a9754378135f61eb
200 lines
6.4 KiB
Rust
200 lines
6.4 KiB
Rust
use super::*;
|
|
use codex_protocol::models::ImageDetail;
|
|
use codex_protocol::models::ImageReference;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn delivery_preserves_arbitrary_message_boundaries_and_rejects_them_for_sync() {
|
|
let message = crate::PreviousReviews::try_from_fragments(vec![crate::PreviousReview {
|
|
id: codex_protocol_test_support::uuid_v7_item_id_generator_for_test()("review"),
|
|
fragment: "host-attested review".into(),
|
|
}])
|
|
.unwrap()
|
|
.into_annotated_message();
|
|
let sections = || {
|
|
vec![
|
|
SectionOutput {
|
|
id: "new_user_section",
|
|
delivery: text_content(vec!["first".into(), "second".into()]),
|
|
},
|
|
SectionOutput {
|
|
id: "new_message_section",
|
|
delivery: SectionDelivery::Message(crate::Budgeted::required(Box::new(
|
|
message.clone(),
|
|
))),
|
|
},
|
|
SectionOutput {
|
|
id: "another_user_section",
|
|
delivery: text_content(vec!["third".into()]),
|
|
},
|
|
]
|
|
};
|
|
assert_eq!(
|
|
ComposedContext {
|
|
sections: sections(),
|
|
truncations: Vec::new()
|
|
}
|
|
.into_messages(),
|
|
vec![
|
|
user_message(vec![
|
|
ContentItem::InputText {
|
|
text: "first".into()
|
|
},
|
|
ContentItem::InputText {
|
|
text: "second".into()
|
|
}
|
|
]),
|
|
message.item.clone(),
|
|
user_message(vec![ContentItem::InputText {
|
|
text: "third".into()
|
|
}]),
|
|
]
|
|
);
|
|
assert_eq!(
|
|
ComposedContext {
|
|
sections: sections(),
|
|
truncations: Vec::new()
|
|
}
|
|
.into_user_inputs(),
|
|
Err(SectionError::UnsupportedDelivery {
|
|
section: "new_message_section"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn long_text_and_file_image_delivery_is_lossless_bounded_and_fully_budgeted() {
|
|
let text = "é🙂\"\n".repeat(/*n*/ 20_000);
|
|
let file_image = ImageReference::File {
|
|
file_id: "file_123".to_owned(),
|
|
};
|
|
let context = ComposedContext {
|
|
sections: vec![SectionOutput {
|
|
id: "planned_action",
|
|
delivery: SectionDelivery::user_content(vec![
|
|
Budgeted::required(ContentItem::InputText { text: text.clone() }),
|
|
Budgeted::required(ContentItem::InputImage {
|
|
image: file_image.clone(),
|
|
detail: Some(ImageDetail::High),
|
|
}),
|
|
]),
|
|
}],
|
|
truncations: Vec::new(),
|
|
};
|
|
let estimated = context.estimated_tokens();
|
|
let messages = context.clone().into_messages();
|
|
let ResponseItem::Message { content, .. } = &messages[0] else {
|
|
panic!("user message")
|
|
};
|
|
let (image, text_content) = content.split_last().expect("message content");
|
|
assert_eq!(
|
|
image,
|
|
&ContentItem::InputImage {
|
|
image: file_image.clone(),
|
|
detail: Some(ImageDetail::High),
|
|
}
|
|
);
|
|
let parts = text_content
|
|
.iter()
|
|
.map(|item| match item {
|
|
ContentItem::InputText { text } => text.as_str(),
|
|
_ => panic!("text part"),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(parts.concat(), text);
|
|
assert!(
|
|
parts.iter().all(|part| part.len()
|
|
<= TruncationPolicy::Tokens(MAX_TEXT_PART_ESTIMATED_TOKENS).byte_budget())
|
|
);
|
|
assert!(estimated >= crate::estimate_input_tokens(&messages[0]));
|
|
let mut expected_inputs = parts
|
|
.into_iter()
|
|
.map(|part| UserInput::Text {
|
|
text: part.to_owned(),
|
|
text_elements: Vec::new(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
expected_inputs.push(UserInput::Image {
|
|
image: file_image,
|
|
detail: Some(ImageDetail::High),
|
|
});
|
|
assert_eq!(context.into_user_inputs().unwrap(), expected_inputs);
|
|
}
|
|
|
|
#[test]
|
|
fn sender_restrictions_survive_budget_trimming_for_both_reviewers() {
|
|
for presentation in [
|
|
ContextPresentation::SyncFull {
|
|
session_id: "receiver",
|
|
},
|
|
ContextPresentation::SyncDelta {
|
|
session_id: "receiver",
|
|
},
|
|
ContextPresentation::Async,
|
|
] {
|
|
let sender = vec!["user: Only use the staging pool.\n".to_owned()];
|
|
let context = CollectedContext {
|
|
sections: vec![
|
|
ContextSection::SenderUserMessages {
|
|
items: sender.clone(),
|
|
},
|
|
ContextSection::ConversationTranscript { items: Vec::new() },
|
|
],
|
|
}
|
|
.compose_prepared(
|
|
presentation,
|
|
PreparedTranscript {
|
|
items: vec![Budgeted::optional(
|
|
crate::TranscriptContent::Text("old tool output ".repeat(/*n*/ 2_000)),
|
|
BudgetPriority::Tool,
|
|
)],
|
|
omission_note: None,
|
|
truncations: Vec::new(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
let selected = context
|
|
.clone()
|
|
.enforce_budget(
|
|
crate::RequestBudget {
|
|
max_input_tokens: 1_000,
|
|
existing_context_tokens: 0,
|
|
},
|
|
"evidence omitted".to_owned(),
|
|
crate::HistoryTruncation::Allow,
|
|
)
|
|
.unwrap();
|
|
assert!(selected.estimated_tokens() <= 1_000);
|
|
assert_eq!(selected.truncations.len(), 1);
|
|
let sender_section = selected
|
|
.sections
|
|
.into_iter()
|
|
.find(|section| section.id == "sender_user_messages")
|
|
.unwrap();
|
|
let SectionDelivery::UserContent(items) = sender_section.delivery else {
|
|
panic!("sender context must remain user evidence");
|
|
};
|
|
assert_eq!(
|
|
items,
|
|
vec![Budgeted::required(SectionContent::Other(
|
|
ContentItem::InputText {
|
|
text: sender[0].clone()
|
|
}
|
|
))]
|
|
);
|
|
assert!(matches!(
|
|
context.enforce_budget(
|
|
crate::RequestBudget {
|
|
max_input_tokens: 1,
|
|
existing_context_tokens: 0
|
|
},
|
|
"evidence omitted".to_owned(),
|
|
crate::HistoryTruncation::Allow,
|
|
),
|
|
Err(SectionError::EvidenceLimitExceeded {
|
|
section: "request_budget"
|
|
})
|
|
));
|
|
}
|
|
}
|