Defer synchronous Guardian tool-call truncation to preparation (#56505)

## What changed

Preserve complete tool-call text when collecting synchronous Guardian context, then apply the profile's tool token limit when preparing the transcript. Calculate retained bytes and token usage from the truncated text.

## Testing

Extend the oversized-action regression test to verify that a subsequent review includes the previous call's arguments truncated to the 1,000-token history limit.

GitOrigin-RevId: 2b52505d9ccfa3651b3535e67cc12597be819459
This commit is contained in:
ioanbudea-OAI
2026-10-23 15:37:39 +00:00
committed by copyberry
parent 2839ed763b
commit 1c0c45bfc3
5 changed files with 53 additions and 18 deletions
+2 -2
View File
@@ -267,8 +267,8 @@ pub(crate) fn render_guardian_transcript_entries(
/// Keep both tool calls and tool results here. The reviewer often needs the
/// agent's exact queried path / arguments as well as the returned evidence to
/// decide whether the pending approval is justified.
/// Per-entry truncation happens during collection, using the current review's
/// Node REPL cap; the cursor still counts every non-empty evidence entry.
/// Tool-call truncation happens in sync preparation; other caps still apply
/// during collection. The cursor counts every non-empty evidence entry.
pub(super) fn collect_guardian_context(
history: &dyn SectionHistory,
node_repl_result_token_limit: usize,
@@ -808,6 +808,9 @@ async fn oversized_action_preserves_review_policy_and_next_review(
.message_input_texts("user")
.concat();
assert!(context.contains("echo complete-action-review"));
let historical_arguments =
codex_guardian_context::truncate_text(&oversized, /*max_tokens*/ 1_000);
assert!(context.contains(&format!("tool exec_command call: {historical_arguments}\n")));
assert!(
requests
.last()
@@ -294,17 +294,15 @@ fn planned_action_budget_omits_descriptions_without_changing_arguments() {
)
});
let required = bounded.render(action_presentation);
let context = (crate::CollectedContext {
sections: vec![crate::ContextSection::PlannedAction(action.clone())],
})
.compose(
presentation,
crate::PreparedTranscript {
items: Vec::new(),
omission_note: None,
truncations: Vec::new(),
},
)
let context = {
let context = crate::CollectedContext {
sections: vec![crate::ContextSection::PlannedAction(action.clone())],
};
let profile = &crate::ContextProfile::synchronous();
let transcript = profile
.prepare_transcript(context.transcript_entries(), /*entry_number_offset*/ 0);
context.compose(presentation, transcript)
}
.unwrap();
let mut required_items = required.iter().map(|item| text(item)).collect::<Vec<_>>();
required_items.push(text("evidence omitted"));
+24 -3
View File
@@ -1,4 +1,4 @@
//! Resolved Guardian evidence profiles and pure transcript retention.
//! Resolved Guardian evidence profiles, transcript admission and retention.
//! Sync keeps recent entries; async protects approvals/final answers and evicts
//! in cacheable chunks. Hosts still own history snapshots and delivery cursors.
//! User messages and manual approvals stay complete until whole-request admission.
@@ -148,17 +148,38 @@ impl ContextProfile {
.as_ref()
.filter(|_| entry.kind == ConversationTranscriptEntryKind::User);
let prepare_text = |text: &str| {
let text = match self.target {
ContextTarget::Sync => match &entry.kind {
ConversationTranscriptEntryKind::ToolCall(_) => {
std::borrow::Cow::Owned(crate::truncate_text(
text,
self.transcript.entry_limits.tool_tokens,
))
}
ConversationTranscriptEntryKind::User
| ConversationTranscriptEntryKind::Developer
| ConversationTranscriptEntryKind::Assistant
| ConversationTranscriptEntryKind::ProtectedAssistant
| ConversationTranscriptEntryKind::Reasoning
| ConversationTranscriptEntryKind::ToolOutput(_)
| ConversationTranscriptEntryKind::NodeReplToolOutput(_) => {
std::borrow::Cow::Borrowed(text)
}
},
ContextTarget::Async => std::borrow::Cow::Borrowed(text),
};
let retained_bytes = text.len();
let record = TranscriptRecord::new(
&entry.kind,
number,
text.to_owned(),
text.into_owned(),
retained_source.map(|retained| retained.order.clone()),
self.transcript_format,
suffix,
);
let tokens =
TruncationPolicy::Bytes(record.rendered().text_bytes).token_budget();
(TranscriptContent::Record(record), tokens, text.len())
(TranscriptContent::Record(record), tokens, retained_bytes)
};
let (content, tokens, retained_bytes) = match &entry.content {
TranscriptContent::Text(text) => prepare_text(text),
+15 -2
View File
@@ -2,7 +2,7 @@
//!
//! Both Guardian consumers receive the same role and tool-source attribution,
//! with generated compaction summaries attributed to the assistant, complete user
//! messages and capped non-user entries. Resolved context profiles
//! messages and capped non-user entries, except sync tool calls prepared later. 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.
@@ -118,7 +118,7 @@ impl SectionContributor for ConversationTranscriptSection {
fn contribute(&self, input: &SectionInput<'_>) -> Result<Option<ContextSection>, SectionError> {
Ok(Some(ContextSection::ConversationTranscript {
items: collect_transcript(input.history, input.transcript),
items: collect_transcript_for_target(input.history, input.transcript, input.target),
}))
}
}
@@ -130,6 +130,14 @@ impl SectionContributor for ConversationTranscriptSection {
pub fn collect_transcript(
history: &dyn SectionHistory,
config: &ConversationTranscriptConfig,
) -> Vec<ConversationTranscriptEntry> {
collect_transcript_for_target(history, config, crate::ContextTarget::Async)
}
fn collect_transcript_for_target(
history: &dyn SectionHistory,
config: &ConversationTranscriptConfig,
target: crate::ContextTarget,
) -> Vec<ConversationTranscriptEntry> {
let mut entries = Vec::new();
let mut tool_names_by_call_id = HashMap::new();
@@ -402,6 +410,11 @@ pub fn collect_transcript(
| ConversationTranscriptEntryKind::Reasoning => {
truncate_text(&text, config.entry_limits.message_tokens)
}
ConversationTranscriptEntryKind::ToolCall(_)
if target == crate::ContextTarget::Sync =>
{
text
}
ConversationTranscriptEntryKind::ToolCall(_)
| ConversationTranscriptEntryKind::ToolOutput(_) => {
truncate_text(&text, config.entry_limits.tool_tokens)