mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Keep verified question/answer records whole under elastic budgets (#59902)
## Why Truncating verified answers can obscure the association between questions and responses. Elastic entry caps must preserve that association. ## What changed Mark verified answer records in root conversation and trusted user answer sections so elastic preparation keeps them intact when they fit. Replace oversized records with the protected incomplete-answer notice and record zero retained evidence bytes. ## Testing Add a regression test with two questions and opposite answers, checking that both evidence sections omit oversized records whole and preserve complete records when the entry cap allows them. GitOrigin-RevId: 0fe9e8c0955cb09845a929274e3d52b816119624
This commit is contained in:
@@ -121,8 +121,11 @@ pub(crate) fn root_conversation_items(
|
||||
| GuardianRootMessage::RetainedContextScope
|
||||
| GuardianRootMessage::LegacyContextScope => true,
|
||||
};
|
||||
let verified_answer = matches!(message, GuardianRootMessage::UserInput(_));
|
||||
let text = message.render();
|
||||
let item = if protected {
|
||||
let item = if verified_answer {
|
||||
crate::Budgeted::verified_answer(text)
|
||||
} else if protected {
|
||||
crate::Budgeted::protected(text)
|
||||
} else {
|
||||
crate::Budgeted::required(text)
|
||||
|
||||
@@ -336,6 +336,7 @@ impl CollectedContext {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| Budgeted {
|
||||
verified_answer: false,
|
||||
protected: matches!(
|
||||
item.kind,
|
||||
RetainedInstructionKind::Framing
|
||||
@@ -364,10 +365,12 @@ impl CollectedContext {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let mut output =
|
||||
Budgeted::required(ContentItem::InputText { text: item.text });
|
||||
output.protected = item.protected;
|
||||
output
|
||||
let content = ContentItem::InputText { text: item.text };
|
||||
if item.protected {
|
||||
Budgeted::protected(content)
|
||||
} else {
|
||||
Budgeted::verified_answer(content)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
@@ -396,6 +399,7 @@ impl CollectedContext {
|
||||
id: "conversation_transcript",
|
||||
delivery: SectionDelivery::Message(Budgeted {
|
||||
protected: entry.protected,
|
||||
verified_answer: entry.verified_answer,
|
||||
content: Box::new(ResponseItemEnvelope::new(*message)),
|
||||
retention: entry.retention,
|
||||
source: entry.source,
|
||||
@@ -426,6 +430,7 @@ impl CollectedContext {
|
||||
};
|
||||
items.push(Budgeted {
|
||||
protected: entry.protected,
|
||||
verified_answer: entry.verified_answer,
|
||||
content,
|
||||
retention: entry.retention,
|
||||
source: entry.source,
|
||||
@@ -499,6 +504,7 @@ impl CollectedContext {
|
||||
let Budgeted {
|
||||
content,
|
||||
protected,
|
||||
verified_answer,
|
||||
source,
|
||||
retention,
|
||||
} = item;
|
||||
@@ -524,6 +530,7 @@ impl CollectedContext {
|
||||
Some(Ok(Budgeted {
|
||||
content,
|
||||
protected,
|
||||
verified_answer,
|
||||
source,
|
||||
retention,
|
||||
}))
|
||||
|
||||
@@ -45,6 +45,8 @@ pub struct Budgeted<T> {
|
||||
pub content: T,
|
||||
/// Host framing and exact actions use global space, outside section quotas.
|
||||
pub(crate) protected: bool,
|
||||
/// Host-verified question/answer records must be kept or omitted whole.
|
||||
pub(crate) verified_answer: bool,
|
||||
pub(crate) source: Option<codex_history::RetainedSource>,
|
||||
pub(crate) retention: Retention,
|
||||
}
|
||||
@@ -54,6 +56,7 @@ impl<T> Budgeted<T> {
|
||||
Self {
|
||||
content,
|
||||
protected: false,
|
||||
verified_answer: false,
|
||||
source: None,
|
||||
retention: Retention::Required,
|
||||
}
|
||||
@@ -66,10 +69,18 @@ impl<T> Budgeted<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verified_answer(content: T) -> Self {
|
||||
Self {
|
||||
verified_answer: true,
|
||||
..Self::required(content)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map<U>(self, transform: impl FnOnce(T) -> U) -> Budgeted<U> {
|
||||
Budgeted {
|
||||
content: transform(self.content),
|
||||
protected: self.protected,
|
||||
verified_answer: self.verified_answer,
|
||||
source: self.source,
|
||||
retention: self.retention,
|
||||
}
|
||||
@@ -79,6 +90,7 @@ impl<T> Budgeted<T> {
|
||||
Self {
|
||||
content,
|
||||
protected: false,
|
||||
verified_answer: false,
|
||||
source: None,
|
||||
retention: Retention::Historical,
|
||||
}
|
||||
@@ -88,6 +100,7 @@ impl<T> Budgeted<T> {
|
||||
Self {
|
||||
content,
|
||||
protected: false,
|
||||
verified_answer: false,
|
||||
source: None,
|
||||
retention: Retention::Optional(priority),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Applies configured section text caps exactly once before allocation.
|
||||
//! Supporting records remain candidates after truncation. Framing, exact actions,
|
||||
//! images and opaque native payloads stay whole. Selection cannot change prepared bytes.
|
||||
//! images and opaque native payloads stay whole. Verified answers that exceed their
|
||||
//! entry cap are omitted whole. Selection cannot change prepared bytes.
|
||||
|
||||
use crate::ComposedContext;
|
||||
use crate::ElasticBudgetConfig;
|
||||
@@ -22,7 +23,7 @@ impl ComposedContext {
|
||||
/// can only select whole entries, so later grants cannot change their text.
|
||||
/// Omitted groups inherit the same defaults used by allocation.
|
||||
/// The existing minimum marker/framing is retained when the cap cannot hold it.
|
||||
/// Entry preparation never selects items; allocation selects whole supporting records.
|
||||
/// Oversized verified answers leave an omission notice; allocation selects the remaining records.
|
||||
pub fn prepare_elastic_entries(
|
||||
mut self,
|
||||
config: &ElasticBudgetConfig,
|
||||
@@ -67,6 +68,18 @@ impl ComposedContext {
|
||||
continue;
|
||||
}
|
||||
let original_bytes = item.content.evidence_bytes();
|
||||
if item.verified_answer {
|
||||
self.truncations.push(TruncationObservation {
|
||||
source: item.source.clone(),
|
||||
component: section.id,
|
||||
original_bytes,
|
||||
retained_bytes: 0,
|
||||
});
|
||||
*item = crate::Budgeted::protected(SectionContent::from(
|
||||
crate::GuardianRootMessage::IncompleteVerifiedAnswers.render(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
truncate_content_to_fit(
|
||||
&mut item.content,
|
||||
maximum.saturating_sub(content_framing_tokens(/*item_count*/ 1)),
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
//! Exercises cache-prefix stability through the public collection and composition API.
|
||||
//! Exercises cache-prefix and evidence integrity through public collection and composition.
|
||||
|
||||
use codex_context_fragments::ContextualUserFragment;
|
||||
use codex_guardian_context::ActionPresentation;
|
||||
use codex_guardian_context::ContextBudgeting;
|
||||
use codex_guardian_context::ContextPresentation;
|
||||
use codex_guardian_context::ContextProfile;
|
||||
use codex_guardian_context::ContextTarget;
|
||||
use codex_guardian_context::ElasticBudgetConfig;
|
||||
use codex_guardian_context::ElasticSection;
|
||||
use codex_guardian_context::ElasticSectionLimits;
|
||||
use codex_guardian_context::GuardianRootMessage;
|
||||
use codex_guardian_context::PlannedAction;
|
||||
use codex_guardian_context::PlannedActionKind;
|
||||
use codex_guardian_context::PreviousReviews;
|
||||
use codex_guardian_context::RequestBudget;
|
||||
use codex_guardian_context::SectionHistory;
|
||||
use codex_guardian_context::SectionInput;
|
||||
use codex_guardian_context::TranscriptFormat;
|
||||
use codex_guardian_context::TrustedSkills;
|
||||
use codex_guardian_context::TrustedTool;
|
||||
use codex_guardian_context::TrustedToolSource;
|
||||
use codex_guardian_context::collect_verified_answer_candidates;
|
||||
use codex_guardian_context::default_registry;
|
||||
use codex_guardian_context::prepare_sync_context;
|
||||
use codex_guardian_context::render_original_verified_answer;
|
||||
use codex_history::RetainedContext;
|
||||
use codex_history::RetainedContextEvent;
|
||||
use codex_history::RetainedInputSource;
|
||||
use codex_history::RetainedUserMessage;
|
||||
use codex_history::VerifiedAnswer;
|
||||
use codex_history::VerifiedQuestionAnswer;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
@@ -202,3 +214,127 @@ fn changing_retained_context_and_attestations_preserves_history_before_the_curre
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elastic_verified_answers_preserve_question_answer_identity() {
|
||||
let answer = VerifiedAnswer {
|
||||
turn_id: "turn".to_owned(),
|
||||
call_id: "confirm".to_owned(),
|
||||
questions: vec![
|
||||
VerifiedQuestionAnswer {
|
||||
question: format!(
|
||||
"May I delete the production database? {}",
|
||||
"A".repeat(2_500)
|
||||
),
|
||||
answer: "No.".to_owned(),
|
||||
},
|
||||
VerifiedQuestionAnswer {
|
||||
question: format!("May I list local files instead? {}", "B".repeat(2_500)),
|
||||
answer: "Yes.".to_owned(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let original = render_original_verified_answer(&answer);
|
||||
let mut retained = RetainedContext::default();
|
||||
retained.record(
|
||||
&RetainedContextEvent::VerifiedAnswer {
|
||||
answer,
|
||||
acceptance_order: Some(7),
|
||||
},
|
||||
&uuid_v7_item_id_generator_for_test(),
|
||||
);
|
||||
let trusted_answers = collect_verified_answer_candidates(&retained);
|
||||
let root = [GuardianRootMessage::UserInput(original.clone())];
|
||||
// The retained record must stand on its own after the raw transcript is compacted.
|
||||
let history = History {
|
||||
items: Vec::new(),
|
||||
retained: Some(retained),
|
||||
};
|
||||
let profile = ContextProfile::synchronous();
|
||||
for entry_tokens in [900, 1_800] {
|
||||
let collected = default_registry()
|
||||
.prepare(&SectionInput {
|
||||
target: ContextTarget::Sync,
|
||||
history: &history,
|
||||
transcript: &profile.transcript,
|
||||
root_conversation: &root,
|
||||
root_conversation_candidates: None,
|
||||
trusted_user_answers: &trusted_answers,
|
||||
planned_action: None,
|
||||
permissions: None,
|
||||
previous_reviews: None,
|
||||
trusted_tool: None,
|
||||
trusted_skill_paths: &[],
|
||||
images: None,
|
||||
node_repl: None,
|
||||
})
|
||||
.unwrap();
|
||||
let limits = ElasticSectionLimits::new(
|
||||
/*min_total_section_tokens*/ entry_tokens,
|
||||
/*max_section_entry_tokens*/ entry_tokens,
|
||||
)
|
||||
.unwrap();
|
||||
let config = ElasticBudgetConfig {
|
||||
sections: [
|
||||
(ElasticSection::RootConversationMessages, limits),
|
||||
(ElasticSection::TrustedUserAnswers, limits),
|
||||
(
|
||||
ElasticSection::TranscriptMessages,
|
||||
// The other section defaults total 9,100 tokens; minimums must sum to 128K.
|
||||
ElasticSectionLimits::new(
|
||||
/*min_total_section_tokens*/ 118_900 - 2 * entry_tokens,
|
||||
/*max_section_entry_tokens*/ 5_000,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
};
|
||||
let messages = prepare_sync_context(
|
||||
collected,
|
||||
&profile,
|
||||
/*entry_number_offset*/ 0,
|
||||
ContextPresentation::SyncFull {
|
||||
session_id: "worker",
|
||||
},
|
||||
ContextBudgeting::Elastic,
|
||||
)
|
||||
.unwrap()
|
||||
.prepare_elastic_entries(&config)
|
||||
.unwrap()
|
||||
.enforce_budget(
|
||||
RequestBudget {
|
||||
max_input_tokens: 128_000,
|
||||
existing_context_tokens: 0,
|
||||
},
|
||||
"Evidence omitted.".to_owned(),
|
||||
)
|
||||
.unwrap()
|
||||
.into_messages();
|
||||
let omission = GuardianRootMessage::IncompleteVerifiedAnswers.render();
|
||||
let evidence = messages
|
||||
.into_iter()
|
||||
.flat_map(|message| match message {
|
||||
ResponseItem::Message { content, .. } => content,
|
||||
_ => panic!("text evidence"),
|
||||
})
|
||||
.filter_map(|item| match item {
|
||||
ContentItem::InputText { text }
|
||||
if text.contains("assistant:") || text == omission =>
|
||||
{
|
||||
Some(text)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected = if entry_tokens == 900 {
|
||||
vec![omission.clone(), omission]
|
||||
} else {
|
||||
vec![
|
||||
original.clone(),
|
||||
format!("Retained source order: 7\n{original}"),
|
||||
]
|
||||
};
|
||||
assert_eq!(evidence, expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user