mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Extract guardian transcript selection into guardian-context (#45417)
## What changed Move full/delta transcript selection into the shared `TranscriptMode::select` API and use it when building guardian prompts. Export `TranscriptCursor`, `TranscriptMode`, and `TranscriptSelection` from `codex-guardian-context`. Preserve full-transcript fallback when the history version changes or the saved cursor exceeds the collected entry count. Select entries before profile retention, preserving their numbering and returning a proposed cursor that counts all collected entries. Hosts remain responsible for committing and invalidating cursors. ## Testing Add a regression test verifying that sliding-window retention preserves the collected-entry cursor and that an appended entry is selected and numbered correctly in the next delta. GitOrigin-RevId: a2192c08e23c18302b0105ba47aeb2780eb4a015
This commit is contained in:
@@ -13,6 +13,9 @@ use codex_guardian_context::PlannedActionKind;
|
||||
use codex_guardian_context::SectionError;
|
||||
use codex_guardian_context::SectionHistory;
|
||||
use codex_guardian_context::SectionInput;
|
||||
pub(crate) use codex_guardian_context::TranscriptCursor as GuardianTranscriptCursor;
|
||||
pub(crate) use codex_guardian_context::TranscriptMode as GuardianPromptMode;
|
||||
use codex_guardian_context::TranscriptSelection;
|
||||
use codex_guardian_context::default_registry;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
@@ -44,19 +47,6 @@ pub(crate) struct GuardianPromptItems {
|
||||
pub(crate) node_repl_evidence_sequence: u64,
|
||||
}
|
||||
|
||||
/// Points to the end of the transcript that the guardian has already reviewed.
|
||||
/// The saved count is only reusable when `parent_history_version` still matches.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct GuardianTranscriptCursor {
|
||||
pub(crate) parent_history_version: u64,
|
||||
pub(crate) transcript_entry_count: usize,
|
||||
}
|
||||
|
||||
pub(crate) enum GuardianPromptMode {
|
||||
Full,
|
||||
Delta { cursor: GuardianTranscriptCursor },
|
||||
}
|
||||
|
||||
/// Builds the guardian user content items from:
|
||||
/// - a compact transcript for authorization and local context
|
||||
/// - the exact action JSON being proposed for approval
|
||||
@@ -180,41 +170,23 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn(
|
||||
permissions.as_ref(),
|
||||
node_repl_context.as_ref(),
|
||||
)?;
|
||||
let transcript_entries = sections.transcript_entries();
|
||||
let transcript_cursor = GuardianTranscriptCursor {
|
||||
parent_history_version: history.review_history_version(),
|
||||
transcript_entry_count: transcript_entries.len(),
|
||||
};
|
||||
|
||||
let prompt_shape = match mode {
|
||||
GuardianPromptMode::Full => GuardianPromptShape::Full,
|
||||
GuardianPromptMode::Delta { cursor } => {
|
||||
if cursor.parent_history_version == transcript_cursor.parent_history_version
|
||||
&& cursor.transcript_entry_count <= transcript_cursor.transcript_entry_count
|
||||
{
|
||||
GuardianPromptShape::Delta {
|
||||
already_seen_entry_count: cursor.transcript_entry_count,
|
||||
}
|
||||
} else {
|
||||
GuardianPromptShape::Full
|
||||
}
|
||||
}
|
||||
};
|
||||
let (selection, transcript_cursor) = mode.select(
|
||||
sections.transcript_entries(),
|
||||
history.review_history_version(),
|
||||
);
|
||||
let session_id = session.thread_id.to_string();
|
||||
let (transcript_entries, offset, placeholder, presentation) = match prompt_shape {
|
||||
GuardianPromptShape::Full => (
|
||||
transcript_entries,
|
||||
let (transcript_entries, offset, placeholder, presentation) = match selection {
|
||||
TranscriptSelection::Full(entries) => (
|
||||
entries,
|
||||
0,
|
||||
"<no retained transcript entries>",
|
||||
ContextPresentation::SyncFull {
|
||||
session_id: &session_id,
|
||||
},
|
||||
),
|
||||
GuardianPromptShape::Delta {
|
||||
already_seen_entry_count,
|
||||
} => (
|
||||
&transcript_entries[already_seen_entry_count..],
|
||||
already_seen_entry_count,
|
||||
TranscriptSelection::Delta { entries, offset } => (
|
||||
entries,
|
||||
offset,
|
||||
"<no retained transcript delta entries>",
|
||||
ContextPresentation::SyncDelta {
|
||||
session_id: &session_id,
|
||||
@@ -257,11 +229,6 @@ fn parent_turn_permissions(context: &GuardianReviewContext) -> PermissionContext
|
||||
}
|
||||
}
|
||||
|
||||
enum GuardianPromptShape {
|
||||
Full,
|
||||
Delta { already_seen_entry_count: usize },
|
||||
}
|
||||
|
||||
/// Exercises the sync profile through the host's existing transcript tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn render_guardian_transcript_entries(
|
||||
|
||||
60
codex-rs/guardian-context/src/cursor.rs
Normal file
60
codex-rs/guardian-context/src/cursor.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Selects full or incremental review evidence before profile retention.
|
||||
//! Selection only proposes the next cursor; callers commit it with reviewed history.
|
||||
//! Hosts must invalidate cursors when collection settings or history offsets change.
|
||||
|
||||
use crate::ConversationTranscriptEntry;
|
||||
|
||||
/// End of the collected transcript in one host-owned review-history generation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TranscriptCursor {
|
||||
pub parent_history_version: u64,
|
||||
pub transcript_entry_count: usize,
|
||||
}
|
||||
|
||||
/// Whether to start a transcript or continue from previously reviewed evidence.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TranscriptMode {
|
||||
Full,
|
||||
Delta { cursor: TranscriptCursor },
|
||||
}
|
||||
|
||||
/// Selected evidence and its original numbering, before rendering or admission.
|
||||
pub enum TranscriptSelection<'a> {
|
||||
Full(&'a [ConversationTranscriptEntry]),
|
||||
Delta {
|
||||
entries: &'a [ConversationTranscriptEntry],
|
||||
offset: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl TranscriptMode {
|
||||
/// Falls back to full evidence if the saved cursor no longer addresses this history.
|
||||
/// The returned cursor counts collected entries, including any later omitted by a profile.
|
||||
pub fn select(
|
||||
self,
|
||||
entries: &[ConversationTranscriptEntry],
|
||||
parent_history_version: u64,
|
||||
) -> (TranscriptSelection<'_>, TranscriptCursor) {
|
||||
let next_cursor = TranscriptCursor {
|
||||
parent_history_version,
|
||||
transcript_entry_count: entries.len(),
|
||||
};
|
||||
let selection = match self {
|
||||
Self::Delta { cursor }
|
||||
if cursor.parent_history_version == parent_history_version
|
||||
&& cursor.transcript_entry_count <= entries.len() =>
|
||||
{
|
||||
TranscriptSelection::Delta {
|
||||
entries: &entries[cursor.transcript_entry_count..],
|
||||
offset: cursor.transcript_entry_count,
|
||||
}
|
||||
}
|
||||
Self::Full | Self::Delta { .. } => TranscriptSelection::Full(entries),
|
||||
};
|
||||
(selection, next_cursor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cursor_tests.rs"]
|
||||
mod tests;
|
||||
63
codex-rs/guardian-context/src/cursor_tests.rs
Normal file
63
codex-rs/guardian-context/src/cursor_tests.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! Checks that profile retention cannot move the collected-transcript cursor backwards.
|
||||
|
||||
use super::*;
|
||||
use crate::ContextProfile;
|
||||
use crate::ConversationTranscriptEntryKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn delta_cursor_tracks_collected_entries_across_sliding_window_retention() {
|
||||
let entries = ["first", "second", "third", "fourth"]
|
||||
.into_iter()
|
||||
.map(|text| ConversationTranscriptEntry {
|
||||
kind: ConversationTranscriptEntryKind::Assistant,
|
||||
text: text.to_owned(),
|
||||
original_bytes: text.len(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut profile = ContextProfile::asynchronous();
|
||||
profile.retention.max_recent_non_user_entries = 1;
|
||||
|
||||
let (selection, cursor) =
|
||||
TranscriptMode::Full.select(&entries[..3], /*parent_history_version*/ 7);
|
||||
let TranscriptSelection::Full(initial) = selection else {
|
||||
panic!("first request must select a full transcript");
|
||||
};
|
||||
let initial = profile.render_transcript(initial, /*entry_number_offset*/ 0);
|
||||
assert_eq!(
|
||||
initial
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.content)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["[3] assistant: third\n"],
|
||||
);
|
||||
|
||||
let (selection, next_cursor) =
|
||||
TranscriptMode::Delta { cursor }.select(&entries, /*parent_history_version*/ 7);
|
||||
let TranscriptSelection::Delta {
|
||||
entries: delta,
|
||||
offset,
|
||||
} = selection
|
||||
else {
|
||||
panic!("an append must select only the new entry");
|
||||
};
|
||||
let delta = profile.render_transcript(delta, offset);
|
||||
assert_eq!(
|
||||
(
|
||||
delta
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.content)
|
||||
.collect::<Vec<_>>(),
|
||||
next_cursor,
|
||||
),
|
||||
(
|
||||
vec!["[4] assistant: fourth\n".to_owned()],
|
||||
TranscriptCursor {
|
||||
parent_history_version: 7,
|
||||
transcript_entry_count: 4,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
//! without section composition.
|
||||
//! Contributor failures abort collection without returning partial context.
|
||||
//! Sections preserve source-specific evidence and share prompt framing, while
|
||||
//! profiles retain the consumer-specific transcript policy. Hosts own full/delta
|
||||
//! cursors, compaction and request lifecycles.
|
||||
//! profiles retain the consumer-specific transcript policy. Shared full/delta selection
|
||||
//! proposes cursors; hosts own their admission, compaction and request lifecycles.
|
||||
//! Registered contributors declare their scope once and are collected only for
|
||||
//! matching context consumers. History and collection settings are borrowed for
|
||||
//! each request so the default registry can be reused without retaining state.
|
||||
@@ -52,6 +52,7 @@ pub use enforcement::HistoryTruncation;
|
||||
pub(crate) use enforcement::Retention;
|
||||
mod budget;
|
||||
mod composition;
|
||||
mod cursor;
|
||||
pub use budget::DEFAULT_MAX_INPUT_TOKENS;
|
||||
pub use budget::REQUEST_TOKENS_BOUNDARIES;
|
||||
pub use budget::REQUEST_TOKENS_METRIC;
|
||||
@@ -61,6 +62,9 @@ pub use budget::SECTION_COST_METRIC;
|
||||
pub use budget::SectionCost;
|
||||
pub use budget::effective_input_token_limit;
|
||||
pub use budget::estimate_input_tokens;
|
||||
pub use cursor::TranscriptCursor;
|
||||
pub use cursor::TranscriptMode;
|
||||
pub use cursor::TranscriptSelection;
|
||||
mod profile;
|
||||
pub use composition::CollectedContext;
|
||||
pub use composition::ComposedContext;
|
||||
|
||||
Reference in New Issue
Block a user