Skip to content

Commit 69a8947

Browse files
committed
[codex-analytics] guardian review truncation
1 parent 4e7399c commit 69a8947

4 files changed

Lines changed: 149 additions & 37 deletions

File tree

codex-rs/core/src/guardian/approval_request.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -184,32 +184,49 @@ fn guardian_command_source_tool_name(source: GuardianCommandSource) -> &'static
184184
}
185185
}
186186

187-
fn truncate_guardian_action_value(value: Value) -> Value {
187+
fn truncate_guardian_action_value(value: Value) -> (Value, bool) {
188188
match value {
189-
Value::String(text) => Value::String(guardian_truncate_text(
190-
&text,
191-
GUARDIAN_MAX_ACTION_STRING_TOKENS,
192-
)),
193-
Value::Array(values) => Value::Array(
194-
values
189+
Value::String(text) => {
190+
let (text, truncated) =
191+
guardian_truncate_text(&text, GUARDIAN_MAX_ACTION_STRING_TOKENS);
192+
(Value::String(text), truncated)
193+
}
194+
Value::Array(values) => {
195+
let mut truncated = false;
196+
let values = values
195197
.into_iter()
196-
.map(truncate_guardian_action_value)
197-
.collect::<Vec<_>>(),
198-
),
198+
.map(|value| {
199+
let (value, value_truncated) = truncate_guardian_action_value(value);
200+
truncated |= value_truncated;
201+
value
202+
})
203+
.collect::<Vec<_>>();
204+
(Value::Array(values), truncated)
205+
}
199206
Value::Object(values) => {
200207
let mut entries = values.into_iter().collect::<Vec<_>>();
201208
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
202-
Value::Object(
203-
entries
204-
.into_iter()
205-
.map(|(key, value)| (key, truncate_guardian_action_value(value)))
206-
.collect(),
207-
)
209+
let mut truncated = false;
210+
let values = entries
211+
.into_iter()
212+
.map(|(key, value)| {
213+
let (value, value_truncated) = truncate_guardian_action_value(value);
214+
truncated |= value_truncated;
215+
(key, value)
216+
})
217+
.collect();
218+
(Value::Object(values), truncated)
208219
}
209-
other => other,
220+
other => (other, false),
210221
}
211222
}
212223

224+
#[derive(Debug, Clone, PartialEq, Eq)]
225+
pub(crate) struct FormattedGuardianAction {
226+
pub(crate) text: String,
227+
pub(crate) truncated: bool,
228+
}
229+
213230
pub(crate) fn guardian_approval_request_to_json(
214231
action: &GuardianApprovalRequest,
215232
) -> serde_json::Result<Value> {
@@ -482,8 +499,11 @@ pub(crate) fn guardian_request_turn_id<'a>(
482499

483500
pub(crate) fn format_guardian_action_pretty(
484501
action: &GuardianApprovalRequest,
485-
) -> serde_json::Result<String> {
486-
let mut value = guardian_approval_request_to_json(action)?;
487-
value = truncate_guardian_action_value(value);
488-
serde_json::to_string_pretty(&value)
502+
) -> serde_json::Result<FormattedGuardianAction> {
503+
let value = guardian_approval_request_to_json(action)?;
504+
let (value, truncated) = truncate_guardian_action_value(value);
505+
Ok(FormattedGuardianAction {
506+
text: serde_json::to_string_pretty(&value)?,
507+
truncated,
508+
})
489509
}

codex-rs/core/src/guardian/prompt.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ impl GuardianTranscriptEntryKind {
5959
pub(crate) struct GuardianPromptItems {
6060
pub(crate) items: Vec<UserInput>,
6161
pub(crate) transcript_cursor: GuardianTranscriptCursor,
62+
pub(crate) reviewed_action_truncated: bool,
6263
}
6364

6465
/// Points to the end of the transcript that the guardian has already reviewed.
@@ -179,11 +180,12 @@ pub(crate) async fn build_guardian_prompt_items(
179180
.to_string(),
180181
);
181182
push_text("Planned action JSON:\n".to_string());
182-
push_text(format!("{planned_action_json}\n"));
183+
push_text(format!("{}\n", planned_action_json.text));
183184
push_text(">>> APPROVAL REQUEST END\n".to_string());
184185
Ok(GuardianPromptItems {
185186
items,
186187
transcript_cursor,
188+
reviewed_action_truncated: planned_action_json.truncated,
187189
})
188190
}
189191

@@ -243,7 +245,7 @@ fn render_guardian_transcript_entries_with_offset(
243245
} else {
244246
GUARDIAN_MAX_MESSAGE_ENTRY_TOKENS
245247
};
246-
let text = guardian_truncate_text(&entry.text, token_cap);
248+
let (text, _) = guardian_truncate_text(&entry.text, token_cap);
247249
let rendered = format!(
248250
"[{}] {}: {}",
249251
index + entry_number_offset + 1,
@@ -423,28 +425,28 @@ pub(crate) fn collect_guardian_transcript_entries(
423425
entries
424426
}
425427

426-
pub(crate) fn guardian_truncate_text(content: &str, token_cap: usize) -> String {
428+
pub(crate) fn guardian_truncate_text(content: &str, token_cap: usize) -> (String, bool) {
427429
if content.is_empty() {
428-
return String::new();
430+
return (String::new(), false);
429431
}
430432

431433
let max_bytes = approx_bytes_for_tokens(token_cap);
432434
if content.len() <= max_bytes {
433-
return content.to_string();
435+
return (content.to_string(), false);
434436
}
435437

436438
let omitted_tokens = approx_tokens_from_byte_count(content.len().saturating_sub(max_bytes));
437439
let marker = format!("<{TRUNCATION_TAG} omitted_approx_tokens=\"{omitted_tokens}\" />");
438440
if max_bytes <= marker.len() {
439-
return marker;
441+
return (marker, true);
440442
}
441443

442444
let available_bytes = max_bytes.saturating_sub(marker.len());
443445
let prefix_budget = available_bytes / 2;
444446
let suffix_budget = available_bytes.saturating_sub(prefix_budget);
445447
let (prefix, suffix) = split_guardian_truncation_bounds(content, prefix_budget, suffix_budget);
446448

447-
format!("{prefix}{marker}{suffix}")
449+
(format!("{prefix}{marker}{suffix}"), true)
448450
}
449451

450452
fn split_guardian_truncation_bounds(

codex-rs/core/src/guardian/review_session.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ async fn run_review_on_session(
672672
);
673673
}
674674
};
675+
let reviewed_action_truncated = prompt_items.reviewed_action_truncated;
675676
let transcript_cursor = prompt_items.transcript_cursor;
676677
let token_usage_at_review_start = review_session
677678
.codex
@@ -711,6 +712,7 @@ async fn run_review_on_session(
711712
}
712713
Err(outcome) => return (outcome, false, analytics_result),
713714
}
715+
analytics_result.reviewed_action_truncated = reviewed_action_truncated;
714716

715717
let outcome =
716718
wait_for_guardian_review(review_session, deadline, params.external_cancel.as_ref()).await;

codex-rs/core/src/guardian/tests.rs

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -574,11 +574,12 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
574574
fn guardian_truncate_text_keeps_prefix_suffix_and_xml_marker() {
575575
let content = "prefix ".repeat(200) + &" suffix".repeat(200);
576576

577-
let truncated = guardian_truncate_text(&content, /*token_cap*/ 20);
577+
let (truncated, was_truncated) = guardian_truncate_text(&content, /*token_cap*/ 20);
578578

579579
assert!(truncated.starts_with("prefix"));
580580
assert!(truncated.contains("<truncated omitted_approx_tokens=\""));
581581
assert!(truncated.ends_with("suffix"));
582+
assert!(was_truncated);
582583
}
583584

584585
#[test]
@@ -593,9 +594,27 @@ fn format_guardian_action_pretty_truncates_large_string_fields() -> serde_json::
593594

594595
let rendered = format_guardian_action_pretty(&action)?;
595596

596-
assert!(rendered.contains("\"tool\": \"apply_patch\""));
597-
assert!(rendered.contains("<truncated omitted_approx_tokens="));
598-
assert!(rendered.len() < patch.len());
597+
assert!(rendered.text.contains("\"tool\": \"apply_patch\""));
598+
assert!(rendered.text.contains("<truncated omitted_approx_tokens="));
599+
assert!(rendered.text.len() < patch.len());
600+
assert!(rendered.truncated);
601+
Ok(())
602+
}
603+
604+
#[test]
605+
fn format_guardian_action_pretty_reports_no_truncation_for_small_payload() -> serde_json::Result<()>
606+
{
607+
let action = GuardianApprovalRequest::ApplyPatch {
608+
id: "patch-1".to_string(),
609+
cwd: test_path_buf("/tmp").abs(),
610+
files: Vec::new(),
611+
patch: "line\n".to_string(),
612+
};
613+
614+
let rendered = format_guardian_action_pretty(&action)?;
615+
616+
assert!(rendered.text.contains("\"tool\": \"apply_patch\""));
617+
assert!(!rendered.truncated);
599618
Ok(())
600619
}
601620

@@ -996,11 +1015,20 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
9961015
/*external_cancel*/ None,
9971016
)
9981017
.await;
999-
let (GuardianReviewOutcome::Completed(assessment), _) = outcome else {
1018+
let (GuardianReviewOutcome::Completed(assessment), metadata) = outcome else {
10001019
panic!("expected guardian assessment");
10011020
};
1021+
let guardian_thread_id = metadata
1022+
.guardian_thread_id
1023+
.as_deref()
1024+
.expect("guardian thread id");
10021025
assert_eq!(assessment.outcome, GuardianAssessmentOutcome::Allow);
1003-
1026+
assert_ne!(guardian_thread_id, session.conversation_id.to_string());
1027+
ThreadId::from_string(guardian_thread_id).expect("guardian thread id should be a valid UUID");
1028+
assert!(matches!(
1029+
metadata.guardian_session_kind,
1030+
Some(codex_analytics::GuardianReviewSessionKind::TrunkNew)
1031+
));
10041032
let request = request_log.single_request();
10051033
let request_body = request.body_json();
10061034
assert_eq!(
@@ -1032,6 +1060,21 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
10321060
"required": ["outcome"]
10331061
}))
10341062
);
1063+
let request_model = request_body
1064+
.get("model")
1065+
.and_then(|value| value.as_str())
1066+
.expect("guardian request should include a model");
1067+
let request_reasoning_effort = request_body
1068+
.get("reasoning")
1069+
.and_then(|reasoning| reasoning.get("effort"))
1070+
.and_then(|value| value.as_str());
1071+
assert_eq!(metadata.guardian_model.as_deref(), Some(request_model));
1072+
assert_eq!(
1073+
metadata.guardian_reasoning_effort.as_deref(),
1074+
request_reasoning_effort
1075+
);
1076+
assert_eq!(metadata.had_prior_review_context, Some(false));
1077+
10351078
let mut settings = Settings::clone_current();
10361079
settings.set_snapshot_path("snapshots");
10371080
settings.set_prepend_module_to_snapshot(false);
@@ -1235,18 +1278,63 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
12351278
)
12361279
.await;
12371280

1238-
let (GuardianReviewOutcome::Completed(first_assessment), _) = first_outcome else {
1281+
let (GuardianReviewOutcome::Completed(first_assessment), first_metadata) = first_outcome else {
12391282
panic!("expected first guardian assessment");
12401283
};
1241-
let (GuardianReviewOutcome::Completed(second_assessment), _) = second_outcome else {
1284+
let (GuardianReviewOutcome::Completed(second_assessment), second_metadata) = second_outcome
1285+
else {
12421286
panic!("expected second guardian assessment");
12431287
};
1244-
let (GuardianReviewOutcome::Completed(third_assessment), _) = third_outcome else {
1288+
let (GuardianReviewOutcome::Completed(third_assessment), third_metadata) = third_outcome else {
12451289
panic!("expected third guardian assessment");
12461290
};
12471291
assert_eq!(first_assessment.outcome, GuardianAssessmentOutcome::Allow);
12481292
assert_eq!(second_assessment.outcome, GuardianAssessmentOutcome::Allow);
12491293
assert_eq!(third_assessment.outcome, GuardianAssessmentOutcome::Allow);
1294+
assert!(matches!(
1295+
first_metadata.guardian_session_kind,
1296+
Some(codex_analytics::GuardianReviewSessionKind::TrunkNew)
1297+
));
1298+
assert!(matches!(
1299+
second_metadata.guardian_session_kind,
1300+
Some(codex_analytics::GuardianReviewSessionKind::TrunkReused)
1301+
));
1302+
assert!(matches!(
1303+
third_metadata.guardian_session_kind,
1304+
Some(codex_analytics::GuardianReviewSessionKind::TrunkReused)
1305+
));
1306+
ThreadId::from_string(
1307+
first_metadata
1308+
.guardian_thread_id
1309+
.as_deref()
1310+
.expect("first guardian thread id"),
1311+
)
1312+
.expect("first guardian thread id should be a valid UUID");
1313+
ThreadId::from_string(
1314+
second_metadata
1315+
.guardian_thread_id
1316+
.as_deref()
1317+
.expect("second guardian thread id"),
1318+
)
1319+
.expect("second guardian thread id should be a valid UUID");
1320+
ThreadId::from_string(
1321+
third_metadata
1322+
.guardian_thread_id
1323+
.as_deref()
1324+
.expect("third guardian thread id"),
1325+
)
1326+
.expect("third guardian thread id should be a valid UUID");
1327+
assert_eq!(first_metadata.had_prior_review_context, Some(false));
1328+
assert_eq!(second_metadata.had_prior_review_context, Some(true));
1329+
assert_eq!(third_metadata.had_prior_review_context, Some(true));
1330+
assert_eq!(
1331+
first_metadata.guardian_thread_id,
1332+
second_metadata.guardian_thread_id
1333+
);
1334+
assert_eq!(
1335+
second_metadata.guardian_thread_id,
1336+
third_metadata.guardian_thread_id
1337+
);
12501338

12511339
let requests = request_log.requests();
12521340
assert_eq!(requests.len(), 3);

0 commit comments

Comments
 (0)