mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
Centralize Guardian transcript policy in context profiles (#43806)
## What changed Add `ContextProfile` to `codex-guardian-context` and route synchronous and asynchronous Guardian transcript rendering through it. Move default limits, retention, formatting, and async chunked eviction into the shared crate while preserving the distinct retention policies and host-managed full/delta cursors. Carry transcript truncation observations through `RenderedTranscript` into context composition, and derive async image collection flags from the resolved profile. ## Testing Add a profile regression test covering distinct sync/async retention priorities, original entry numbering, omission notes, and async truncation observations. Adapt existing transcript tests to the updated input API. GitOrigin-RevId: 7f7dc249629d8017e9547085f6617f815ae43a1b
This commit is contained in:
1 parent
0034ef93a7
commit
0337192dfd
11 files changed
+412
-358
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//! Composes collected evidence into ordered sections with explicit delivery.
|
||||
//! Hosts select the transcript and retain cursor/budget policy; composition owns
|
||||
//! Profiles retain the host-selected transcript slice; composition owns
|
||||
//! framing, message boundaries and section placement, without retaining history.
|
||||
|
||||
use codex_context_fragments::ContextualUserFragment;
|
||||
@@ -24,6 +24,7 @@ pub enum ContextPresentation<'a> {
|
||||
pub struct RenderedTranscript {
|
||||
pub items: Vec<String>,
|
||||
pub omission_note: Option<String>,
|
||||
pub truncations: Vec<TruncationObservation>,
|
||||
}
|
||||
|
||||
/// Evidence collected successfully before host transcript selection.
|
||||
@@ -67,7 +68,7 @@ impl CollectedContext {
|
||||
pub fn compose(
|
||||
self,
|
||||
presentation: ContextPresentation<'_>,
|
||||
transcript: RenderedTranscript,
|
||||
mut transcript: RenderedTranscript,
|
||||
) -> Result<ComposedContext, SectionError> {
|
||||
let (action, intro, start, end, session_id) = match presentation {
|
||||
ContextPresentation::SyncFull { session_id } => (
|
||||
@@ -97,7 +98,7 @@ impl CollectedContext {
|
||||
),
|
||||
};
|
||||
let mut sections = Vec::new();
|
||||
let mut truncations = Vec::new();
|
||||
let mut truncations = std::mem::take(&mut transcript.truncations);
|
||||
if let Some(intro) = intro {
|
||||
sections.push((
|
||||
0,
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! without section composition.
|
||||
//! Contributor failures abort collection without returning partial context.
|
||||
//! Sections preserve source-specific evidence and share prompt framing, while
|
||||
//! hosts retain transcript selection, compaction and request lifecycles.
|
||||
//! profiles retain the consumer-specific transcript policy. Hosts own full/delta
|
||||
//! cursors, 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.
|
||||
@@ -48,10 +49,12 @@ mod retained_instructions;
|
||||
|
||||
mod action;
|
||||
mod composition;
|
||||
mod profile;
|
||||
pub use composition::CollectedContext;
|
||||
pub use composition::ComposedContext;
|
||||
pub use composition::ContextPresentation;
|
||||
pub use composition::RenderedTranscript;
|
||||
pub use profile::ContextProfile;
|
||||
mod authorization;
|
||||
mod entry;
|
||||
mod history;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Resolved Guardian evidence profiles and pure transcript retention.
|
||||
//! Sync keeps recent entries; async protects approvals/final answers and evicts
|
||||
//! in cacheable chunks. Hosts still own history snapshots and delivery cursors.
|
||||
|
||||
use codex_protocol::protocol::TruncationPolicy;
|
||||
|
||||
use crate::ContextTarget;
|
||||
use crate::ConversationTranscriptConfig;
|
||||
use crate::ConversationTranscriptEntry;
|
||||
use crate::ConversationTranscriptEntryKind;
|
||||
use crate::ConversationTranscriptOptions;
|
||||
use crate::RenderedTranscript;
|
||||
use crate::TranscriptEntryLimits;
|
||||
use crate::TranscriptRetentionConfig;
|
||||
use crate::TruncationObservation;
|
||||
use crate::UserMessageCost;
|
||||
use crate::select_user_messages;
|
||||
|
||||
use self::window::TranscriptWindow;
|
||||
mod window;
|
||||
|
||||
/// Request-local policy resolved from the consumer's model and configuration.
|
||||
/// Registry scope follows `target`; source flags and caps apply before retention.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ContextProfile {
|
||||
pub target: ContextTarget,
|
||||
pub transcript: ConversationTranscriptConfig,
|
||||
pub retention: TranscriptRetentionConfig,
|
||||
pub include_images: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum TranscriptEntryKind {
|
||||
User,
|
||||
ProtectedMessage,
|
||||
Message,
|
||||
Tool,
|
||||
}
|
||||
|
||||
struct TranscriptEntry {
|
||||
kind: TranscriptEntryKind,
|
||||
text: String,
|
||||
tokens: usize,
|
||||
original_bytes: usize,
|
||||
retained_bytes: usize,
|
||||
}
|
||||
|
||||
impl ContextProfile {
|
||||
/// Existing synchronous defaults before the host resolves the REPL output cap.
|
||||
pub const fn synchronous() -> Self {
|
||||
Self {
|
||||
target: ContextTarget::Sync,
|
||||
transcript: ConversationTranscriptConfig {
|
||||
options: ConversationTranscriptOptions {
|
||||
include_tool_calls: true,
|
||||
include_tool_outputs: true,
|
||||
include_reasoning: false,
|
||||
},
|
||||
entry_limits: TranscriptEntryLimits {
|
||||
message_tokens: 5_000,
|
||||
tool_tokens: 1_000,
|
||||
node_repl_output_tokens: 1_000,
|
||||
},
|
||||
},
|
||||
retention: TranscriptRetentionConfig {
|
||||
max_message_transcript_tokens: 20_000,
|
||||
max_tool_transcript_tokens: 10_000,
|
||||
max_recent_non_user_entries: 40,
|
||||
},
|
||||
include_images: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Existing asynchronous defaults before model and local overrides.
|
||||
pub const fn asynchronous() -> Self {
|
||||
Self {
|
||||
target: ContextTarget::Async,
|
||||
transcript: ConversationTranscriptConfig {
|
||||
options: ConversationTranscriptOptions {
|
||||
include_tool_calls: true,
|
||||
include_tool_outputs: true,
|
||||
include_reasoning: false,
|
||||
},
|
||||
entry_limits: TranscriptEntryLimits {
|
||||
message_tokens: 2_000,
|
||||
tool_tokens: 1_000,
|
||||
node_repl_output_tokens: 1_000,
|
||||
},
|
||||
},
|
||||
retention: TranscriptRetentionConfig {
|
||||
max_message_transcript_tokens: 10_000,
|
||||
max_tool_transcript_tokens: 10_000,
|
||||
max_recent_non_user_entries: 40,
|
||||
},
|
||||
include_images: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects bounded entries without advancing the host's full/delta cursor.
|
||||
/// The host supplies the slice and original offset; empty placeholders depend
|
||||
/// on its full/delta presentation and are supplied after selection.
|
||||
pub fn render_transcript(
|
||||
&self,
|
||||
transcript_entries: &[ConversationTranscriptEntry],
|
||||
entry_number_offset: usize,
|
||||
) -> RenderedTranscript {
|
||||
let entries = transcript_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| {
|
||||
let kind = match &entry.kind {
|
||||
ConversationTranscriptEntryKind::User => TranscriptEntryKind::User,
|
||||
ConversationTranscriptEntryKind::Developer
|
||||
| ConversationTranscriptEntryKind::ProtectedAssistant => {
|
||||
TranscriptEntryKind::ProtectedMessage
|
||||
}
|
||||
ConversationTranscriptEntryKind::Assistant
|
||||
| ConversationTranscriptEntryKind::Reasoning => TranscriptEntryKind::Message,
|
||||
ConversationTranscriptEntryKind::ToolCall(_)
|
||||
| ConversationTranscriptEntryKind::ToolOutput(_)
|
||||
| ConversationTranscriptEntryKind::NodeReplToolOutput(_) => {
|
||||
TranscriptEntryKind::Tool
|
||||
}
|
||||
};
|
||||
let number = index + entry_number_offset + 1;
|
||||
let role = entry.kind.role();
|
||||
let suffix = match self.target {
|
||||
ContextTarget::Sync => "",
|
||||
ContextTarget::Async => "\n",
|
||||
};
|
||||
let text = format!("[{number}] {role}: {}{suffix}", entry.text);
|
||||
TranscriptEntry {
|
||||
kind,
|
||||
tokens: TruncationPolicy::Bytes(text.len()).token_budget(),
|
||||
text,
|
||||
original_bytes: entry.original_bytes,
|
||||
retained_bytes: entry.text.len(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let user_messages = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, entry)| {
|
||||
(entry.kind == TranscriptEntryKind::User).then_some(UserMessageCost {
|
||||
index,
|
||||
tokens: entry.tokens,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let selection =
|
||||
select_user_messages(&user_messages, self.retention.max_message_transcript_tokens);
|
||||
let mut included = vec![false; entries.len()];
|
||||
for index in selection.indices {
|
||||
included[index] = true;
|
||||
}
|
||||
match self.target {
|
||||
ContextTarget::Sync => {
|
||||
let mut message_tokens = selection.tokens;
|
||||
let mut tool_tokens = 0usize;
|
||||
let mut retained_non_user_entries = 0;
|
||||
for (index, entry) in entries.iter().enumerate().rev() {
|
||||
if entry.kind == TranscriptEntryKind::User
|
||||
|| retained_non_user_entries >= self.retention.max_recent_non_user_entries
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let (tokens, limit) = if entry.kind == TranscriptEntryKind::Tool {
|
||||
(&mut tool_tokens, self.retention.max_tool_transcript_tokens)
|
||||
} else {
|
||||
(
|
||||
&mut message_tokens,
|
||||
self.retention.max_message_transcript_tokens,
|
||||
)
|
||||
};
|
||||
if tokens.saturating_add(entry.tokens) <= limit {
|
||||
included[index] = true;
|
||||
retained_non_user_entries += 1;
|
||||
*tokens += entry.tokens;
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextTarget::Async => {
|
||||
let available = self
|
||||
.retention
|
||||
.max_message_transcript_tokens
|
||||
.saturating_sub(selection.tokens);
|
||||
let mut window = TranscriptWindow::new(&entries, &self.retention, available);
|
||||
for index in 0..entries.len() {
|
||||
window.insert(index);
|
||||
}
|
||||
for index in window.into_indices() {
|
||||
included[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let omission_note = (self.target == ContextTarget::Sync
|
||||
&& included.iter().any(|included| !included))
|
||||
.then(|| "Some conversation entries were omitted.".to_owned());
|
||||
let mut truncations = Vec::new();
|
||||
let items = entries
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, entry)| {
|
||||
let component = match entry.kind {
|
||||
TranscriptEntryKind::User => "transcript_user",
|
||||
TranscriptEntryKind::ProtectedMessage | TranscriptEntryKind::Message => {
|
||||
"transcript_message"
|
||||
}
|
||||
TranscriptEntryKind::Tool => "transcript_tool",
|
||||
};
|
||||
let retained_bytes = if included[index] {
|
||||
entry.retained_bytes
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if self.target == ContextTarget::Async && entry.original_bytes > retained_bytes {
|
||||
truncations.push(TruncationObservation {
|
||||
component,
|
||||
original_bytes: entry.original_bytes,
|
||||
retained_bytes,
|
||||
});
|
||||
}
|
||||
included[index].then_some(entry.text)
|
||||
})
|
||||
.collect();
|
||||
RenderedTranscript {
|
||||
items,
|
||||
omission_note,
|
||||
truncations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "profile_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,227 @@
|
||||
//! Retains protected async messages and recent tools with chunked eviction.
|
||||
//! Entry pools preserve the existing async cache behavior and tool reservation.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::TranscriptRetentionConfig;
|
||||
|
||||
use super::TranscriptEntry;
|
||||
use super::TranscriptEntryKind;
|
||||
|
||||
const MIN_RECENT_TOOL_ENTRIES: usize = 5;
|
||||
|
||||
#[derive(Default)]
|
||||
struct EntryPool {
|
||||
indices: VecDeque<usize>,
|
||||
tokens: usize,
|
||||
}
|
||||
|
||||
impl EntryPool {
|
||||
fn push(&mut self, index: usize, tokens: usize) {
|
||||
self.indices.push_back(index);
|
||||
self.tokens += tokens;
|
||||
}
|
||||
|
||||
fn pop_oldest(&mut self, entries: &[TranscriptEntry]) {
|
||||
if let Some(index) = self.indices.pop_front() {
|
||||
self.tokens -= entries[index].tokens;
|
||||
}
|
||||
}
|
||||
|
||||
fn evict_oldest(&mut self, count: usize, entries: &[TranscriptEntry]) {
|
||||
for _ in 0..count.min(self.indices.len()) {
|
||||
self.pop_oldest(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct TranscriptWindow<'a> {
|
||||
entries: &'a [TranscriptEntry],
|
||||
message_token_budget: usize,
|
||||
tool_token_budget: usize,
|
||||
max_entries: usize,
|
||||
protected_messages: EntryPool,
|
||||
ordinary_messages: EntryPool,
|
||||
tools: EntryPool,
|
||||
}
|
||||
|
||||
impl<'a> TranscriptWindow<'a> {
|
||||
pub(super) fn new(
|
||||
entries: &'a [TranscriptEntry],
|
||||
config: &TranscriptRetentionConfig,
|
||||
message_token_budget: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
entries,
|
||||
message_token_budget,
|
||||
tool_token_budget: config.max_tool_transcript_tokens,
|
||||
max_entries: config.max_recent_non_user_entries,
|
||||
protected_messages: EntryPool::default(),
|
||||
ordinary_messages: EntryPool::default(),
|
||||
tools: EntryPool::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn insert(&mut self, index: usize) {
|
||||
let kind = self.entries[index].kind;
|
||||
let tokens = self.entries[index].tokens;
|
||||
|
||||
if kind == TranscriptEntryKind::User
|
||||
|| self.max_entries == 0
|
||||
|| !self.make_room_for_token_budget(kind, tokens)
|
||||
|| !self.make_room_for_entry_limit(kind)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.pool_mut(kind).push(index, tokens);
|
||||
}
|
||||
|
||||
pub(super) fn into_indices(self) -> impl Iterator<Item = usize> {
|
||||
self.protected_messages
|
||||
.indices
|
||||
.into_iter()
|
||||
.chain(self.ordinary_messages.indices)
|
||||
.chain(self.tools.indices)
|
||||
}
|
||||
|
||||
fn make_room_for_token_budget(&mut self, kind: TranscriptEntryKind, tokens: usize) -> bool {
|
||||
let token_budget = match kind {
|
||||
TranscriptEntryKind::ProtectedMessage | TranscriptEntryKind::Message => {
|
||||
self.message_token_budget
|
||||
}
|
||||
TranscriptEntryKind::Tool => self.tool_token_budget,
|
||||
TranscriptEntryKind::User => unreachable!("user entries were selected separately"),
|
||||
};
|
||||
let protected_tokens = if kind == TranscriptEntryKind::Message {
|
||||
self.protected_messages.tokens
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Rejected commentary must not evict the protected context it cannot fit beside.
|
||||
if protected_tokens.saturating_add(tokens) > token_budget {
|
||||
return false;
|
||||
}
|
||||
|
||||
let eviction_order: &[TranscriptEntryKind] = match kind {
|
||||
TranscriptEntryKind::ProtectedMessage => &[
|
||||
TranscriptEntryKind::Message,
|
||||
TranscriptEntryKind::ProtectedMessage,
|
||||
],
|
||||
TranscriptEntryKind::Message => &[TranscriptEntryKind::Message],
|
||||
TranscriptEntryKind::Tool => &[TranscriptEntryKind::Tool],
|
||||
TranscriptEntryKind::User => unreachable!("user entries were selected separately"),
|
||||
};
|
||||
let entries = self.entries;
|
||||
|
||||
for &eviction_kind in eviction_order {
|
||||
if self.has_token_capacity(kind, tokens, token_budget) {
|
||||
break;
|
||||
}
|
||||
|
||||
let pool_len = self.pool(eviction_kind).indices.len();
|
||||
if pool_len == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.pool_mut(eviction_kind)
|
||||
.evict_oldest(pool_len.div_ceil(2), entries);
|
||||
while !self.has_token_capacity(kind, tokens, token_budget)
|
||||
&& !self.pool(eviction_kind).indices.is_empty()
|
||||
{
|
||||
self.pool_mut(eviction_kind).pop_oldest(entries);
|
||||
}
|
||||
}
|
||||
|
||||
self.has_token_capacity(kind, tokens, token_budget)
|
||||
}
|
||||
|
||||
fn make_room_for_entry_limit(&mut self, kind: TranscriptEntryKind) -> bool {
|
||||
let retained_count = self.protected_messages.indices.len()
|
||||
+ self.ordinary_messages.indices.len()
|
||||
+ self.tools.indices.len();
|
||||
if retained_count < self.max_entries {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Smaller windows still leave one slot available for protected context.
|
||||
let minimum_existing_tools = MIN_RECENT_TOOL_ENTRIES
|
||||
.min(self.max_entries.saturating_sub(1))
|
||||
.saturating_sub(usize::from(kind == TranscriptEntryKind::Tool));
|
||||
let removable_tools = self
|
||||
.tools
|
||||
.indices
|
||||
.len()
|
||||
.saturating_sub(minimum_existing_tools);
|
||||
let removable_count = self.ordinary_messages.indices.len() + removable_tools;
|
||||
|
||||
if removable_count > 0 {
|
||||
let evidence_count = self.ordinary_messages.indices.len() + self.tools.indices.len();
|
||||
let eviction_count = evidence_count.div_ceil(2).min(removable_count);
|
||||
|
||||
for _ in 0..eviction_count {
|
||||
let oldest_tool = (self.tools.indices.len() > minimum_existing_tools)
|
||||
.then(|| self.tools.indices.front())
|
||||
.flatten();
|
||||
let pool = match (self.ordinary_messages.indices.front(), oldest_tool) {
|
||||
(Some(message), Some(tool)) if message < tool => &mut self.ordinary_messages,
|
||||
(Some(_), Some(_)) | (None, Some(_)) => &mut self.tools,
|
||||
(Some(_), None) => &mut self.ordinary_messages,
|
||||
(None, None) => unreachable!("removable evidence was counted before eviction"),
|
||||
};
|
||||
pool.pop_oldest(self.entries);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
kind,
|
||||
TranscriptEntryKind::ProtectedMessage | TranscriptEntryKind::Tool
|
||||
) {
|
||||
let protected_count = self.protected_messages.indices.len();
|
||||
self.protected_messages
|
||||
.evict_oldest(protected_count.div_ceil(2), self.entries);
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn has_token_capacity(
|
||||
&self,
|
||||
kind: TranscriptEntryKind,
|
||||
tokens: usize,
|
||||
token_budget: usize,
|
||||
) -> bool {
|
||||
let retained_tokens = match kind {
|
||||
TranscriptEntryKind::ProtectedMessage | TranscriptEntryKind::Message => self
|
||||
.protected_messages
|
||||
.tokens
|
||||
.saturating_add(self.ordinary_messages.tokens),
|
||||
TranscriptEntryKind::Tool => self.tools.tokens,
|
||||
TranscriptEntryKind::User => unreachable!("user entries were selected separately"),
|
||||
};
|
||||
|
||||
retained_tokens.saturating_add(tokens) <= token_budget
|
||||
}
|
||||
|
||||
fn pool(&self, kind: TranscriptEntryKind) -> &EntryPool {
|
||||
match kind {
|
||||
TranscriptEntryKind::ProtectedMessage => &self.protected_messages,
|
||||
TranscriptEntryKind::Message => &self.ordinary_messages,
|
||||
TranscriptEntryKind::Tool => &self.tools,
|
||||
TranscriptEntryKind::User => unreachable!("user entries were selected separately"),
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_mut(&mut self, kind: TranscriptEntryKind) -> &mut EntryPool {
|
||||
match kind {
|
||||
TranscriptEntryKind::ProtectedMessage => &mut self.protected_messages,
|
||||
TranscriptEntryKind::Message => &mut self.ordinary_messages,
|
||||
TranscriptEntryKind::Tool => &mut self.tools,
|
||||
TranscriptEntryKind::User => unreachable!("user entries were selected separately"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Profile retention keeps the existing sync and async evidence priorities.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn profiles_preserve_distinct_retention_and_original_numbering() {
|
||||
let entries = [
|
||||
(ConversationTranscriptEntryKind::User, "inspect only"),
|
||||
(
|
||||
ConversationTranscriptEntryKind::Developer,
|
||||
"approved action",
|
||||
),
|
||||
(ConversationTranscriptEntryKind::Assistant, "working"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(kind, text)| ConversationTranscriptEntry {
|
||||
kind,
|
||||
text: text.to_owned(),
|
||||
original_bytes: text.len(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut sync = ContextProfile::synchronous();
|
||||
sync.retention.max_recent_non_user_entries = 1;
|
||||
let mut asynchronous = ContextProfile::asynchronous();
|
||||
asynchronous.retention.max_recent_non_user_entries = 1;
|
||||
let sync = sync.render_transcript(&entries, /*entry_number_offset*/ 7);
|
||||
let asynchronous = asynchronous.render_transcript(&entries, /*entry_number_offset*/ 0);
|
||||
assert_eq!(
|
||||
(sync.items, sync.omission_note),
|
||||
(
|
||||
vec![
|
||||
"[8] user: inspect only".to_owned(),
|
||||
"[10] assistant: working".to_owned()
|
||||
],
|
||||
Some("Some conversation entries were omitted.".to_owned()),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
(asynchronous.items, asynchronous.omission_note),
|
||||
(
|
||||
vec![
|
||||
"[1] user: inspect only\n".to_owned(),
|
||||
"[2] developer: approved action\n".to_owned()
|
||||
],
|
||||
None,
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
asynchronous
|
||||
.truncations
|
||||
.into_iter()
|
||||
.map(|observation| (
|
||||
observation.component,
|
||||
observation.original_bytes,
|
||||
observation.retained_bytes,
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("transcript_message", "working".len(), 0)],
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Collects bounded conversation evidence before consumer-specific rendering.
|
||||
//!
|
||||
//! Both Guardian consumers receive the same role and tool-source attribution,
|
||||
//! with per-entry caps applied before accumulation. Consumers retain their own
|
||||
//! transcript selection, aggregate budgets, and formatting. Tool outputs with a
|
||||
//! with per-entry caps applied before accumulation. Resolved context profiles
|
||||
//! apply aggregate retention after the host selects its full/delta slice. Tool outputs with a
|
||||
//! call ID retain their generic label when the call is unavailable. Outputs
|
||||
//! without a call ID require an explicit name.
|
||||
|
||||
@@ -65,9 +65,8 @@ pub struct TranscriptEntryLimits {
|
||||
|
||||
/// Aggregate limits for retaining rendered transcript entries.
|
||||
///
|
||||
/// Sync and async consumers keep their existing selection rules. These limits
|
||||
/// configure those rules without introducing another sync/async policy selector.
|
||||
/// Collection applies per-entry caps; aggregate retention remains with the host.
|
||||
/// Context profiles apply the sync or async selection rules using these limits.
|
||||
/// Collection applies per-entry caps before profile retention.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TranscriptRetentionConfig {
|
||||
/// Budget for rendered user, developer, assistant, and reasoning entries.
|
||||
@@ -106,7 +105,7 @@ impl SectionContributor for ConversationTranscriptSection {
|
||||
/// Extracts bounded transcript entries without composing other context sections.
|
||||
///
|
||||
/// Entries preserve conversation order and role/tool attribution. Per-entry
|
||||
/// limits apply during collection; consumers own aggregate retention and rendering.
|
||||
/// limits apply during collection; context profiles own aggregate retention.
|
||||
pub fn collect_transcript(
|
||||
history: &dyn SectionHistory,
|
||||
config: &ConversationTranscriptConfig,
|
||||
|
||||
Reference in new issue
Block a user