mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Preserve Guardian transcript boundaries in sampling input (#38555)
## What changed - Represent Guardian sampling input as an ordered list of text entries. - Send each transcript entry and approval-request delimiter as a separate input content item while preserving the existing transcript byte limit. ## Testing - Update transcript and sampler tests to verify entry boundaries, multi-item request serialization, and bounded transcript behavior. GitOrigin-RevId: 433e2e6ca18ce9288025715f5956295bfe852e98
This commit is contained in:
@@ -127,14 +127,16 @@ impl ToolLifecycleContributor for GuardianV2Extension {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let classification_input = format!(
|
||||
">>> TRANSCRIPT START\n{transcript}>>> TRANSCRIPT END\n\n\
|
||||
The Codex agent has requested the following action:\n\
|
||||
>>> APPROVAL REQUEST START\n\
|
||||
Planned action JSON:\n\
|
||||
{planned_action}\n\
|
||||
>>> APPROVAL REQUEST END\n"
|
||||
);
|
||||
let mut classification_input = vec![">>> TRANSCRIPT START\n".to_owned()];
|
||||
classification_input.extend(transcript);
|
||||
classification_input.extend([
|
||||
">>> TRANSCRIPT END\n\n".to_owned(),
|
||||
"The Codex agent has requested the following action:\n".to_owned(),
|
||||
">>> APPROVAL REQUEST START\n".to_owned(),
|
||||
"Planned action JSON:\n".to_owned(),
|
||||
format!("{planned_action}\n"),
|
||||
">>> APPROVAL REQUEST END\n".to_owned(),
|
||||
]);
|
||||
let result: Result<(), String> = async {
|
||||
let output = sampler
|
||||
.sample(LunaSamplingRequest {
|
||||
|
||||
@@ -162,24 +162,27 @@ async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<
|
||||
json!({"type": "number", "minimum": 0.0, "maximum": 1.0})
|
||||
);
|
||||
assert_eq!(
|
||||
request["input"][2]["content"][0]["text"],
|
||||
concat!(
|
||||
">>> TRANSCRIPT START\n",
|
||||
"[1] user: Inspect the repository guidelines.\n",
|
||||
"[2] reasoning: Find the repository documentation.\n",
|
||||
"[3] tool list_dir call: {\"path\":\".\"}\n",
|
||||
"[4] tool list_dir result: README.md\n",
|
||||
"[5] tool read_file call: {\"path\":\"README.md\"}\n",
|
||||
">>> TRANSCRIPT END\n\n",
|
||||
"The Codex agent has requested the following action:\n",
|
||||
">>> APPROVAL REQUEST START\n",
|
||||
"Planned action JSON:\n",
|
||||
"{\n",
|
||||
" \"path\": \"README.md\",\n",
|
||||
" \"tool\": \"read_file\"\n",
|
||||
"}\n",
|
||||
">>> APPROVAL REQUEST END\n",
|
||||
)
|
||||
request["input"][2]["content"],
|
||||
json!([
|
||||
{"type": "input_text", "text": ">>> TRANSCRIPT START\n"},
|
||||
{"type": "input_text", "text": "[1] user: Inspect the repository guidelines.\n"},
|
||||
{"type": "input_text", "text": "[2] reasoning: Find the repository documentation.\n"},
|
||||
{"type": "input_text", "text": "[3] tool list_dir call: {\"path\":\".\"}\n"},
|
||||
{"type": "input_text", "text": "[4] tool list_dir result: README.md\n"},
|
||||
{"type": "input_text", "text": "[5] tool read_file call: {\"path\":\"README.md\"}\n"},
|
||||
{"type": "input_text", "text": ">>> TRANSCRIPT END\n\n"},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "The Codex agent has requested the following action:\n"
|
||||
},
|
||||
{"type": "input_text", "text": ">>> APPROVAL REQUEST START\n"},
|
||||
{"type": "input_text", "text": "Planned action JSON:\n"},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "{\n \"path\": \"README.md\",\n \"tool\": \"read_file\"\n}\n"
|
||||
},
|
||||
{"type": "input_text", "text": ">>> APPROVAL REQUEST END\n"},
|
||||
])
|
||||
);
|
||||
let score = tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
|
||||
@@ -66,8 +66,8 @@ pub struct LunaSamplerConfig {
|
||||
pub struct LunaSamplingRequest {
|
||||
/// Trusted instructions describing the requested classification.
|
||||
pub instructions: String,
|
||||
/// Untrusted input that the model should classify.
|
||||
pub input: String,
|
||||
/// Ordered untrusted input entries that the model should classify.
|
||||
pub input: Vec<String>,
|
||||
/// Strict JSON schema constraining the model response.
|
||||
pub output_schema: Value,
|
||||
/// Reasoning budget explicitly selected for this request.
|
||||
@@ -284,9 +284,11 @@ impl LunaSampler {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_owned(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: request.input,
|
||||
}],
|
||||
content: request
|
||||
.input
|
||||
.into_iter()
|
||||
.map(|text| ContentItem::InputText { text })
|
||||
.collect(),
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ fn sampler_config(base_url: String) -> LunaSamplerConfig {
|
||||
fn sample_request(turn_id: &str) -> LunaSamplingRequest {
|
||||
LunaSamplingRequest {
|
||||
instructions: "Return a risk score.".to_owned(),
|
||||
input: "The user requested a README summary.".to_owned(),
|
||||
input: vec!["The user requested a README summary.".to_owned()],
|
||||
output_schema: json!({
|
||||
"type": "object",
|
||||
"properties": { "score": { "type": "number" } },
|
||||
@@ -157,7 +157,10 @@ async fn preconnected_sampler_reuses_authenticated_websocket_for_structured_requ
|
||||
let first = sampler
|
||||
.sample(LunaSamplingRequest {
|
||||
instructions: "Return a risk score.".to_owned(),
|
||||
input: "The user requested a README summary.".to_owned(),
|
||||
input: vec![
|
||||
"The user requested a README summary.".to_owned(),
|
||||
"The assistant inspected README.md.".to_owned(),
|
||||
],
|
||||
output_schema: schema.clone(),
|
||||
reasoning_effort: ReasoningEffort::None,
|
||||
turn_id: "turn-1".to_owned(),
|
||||
@@ -178,7 +181,7 @@ async fn preconnected_sampler_reuses_authenticated_websocket_for_structured_requ
|
||||
let second = sampler
|
||||
.sample(LunaSamplingRequest {
|
||||
instructions: "Return a risk score.".to_owned(),
|
||||
input: "The user requested a source review.".to_owned(),
|
||||
input: vec!["The user requested a source review.".to_owned()],
|
||||
output_schema: schema,
|
||||
reasoning_effort: ReasoningEffort::Medium,
|
||||
turn_id: "turn-2".to_owned(),
|
||||
@@ -189,6 +192,13 @@ async fn preconnected_sampler_reuses_authenticated_websocket_for_structured_requ
|
||||
assert_eq!(second, r#"{"score":0.75}"#);
|
||||
let requests = server.single_connection();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(
|
||||
requests[0].body_json()["input"][2]["content"],
|
||||
json!([
|
||||
{"type": "input_text", "text": "The user requested a README summary."},
|
||||
{"type": "input_text", "text": "The assistant inspected README.md."},
|
||||
])
|
||||
);
|
||||
for (index, request) in requests.iter().enumerate() {
|
||||
let request = request.body_json();
|
||||
assert_eq!(request["type"], "response.create");
|
||||
@@ -254,7 +264,7 @@ async fn sampler_returns_complete_json_before_terminal_response_events() -> Resu
|
||||
Duration::from_secs(2),
|
||||
sampler.sample(LunaSamplingRequest {
|
||||
instructions: "Return a risk score.".to_owned(),
|
||||
input: "The user requested a README summary.".to_owned(),
|
||||
input: vec!["The user requested a README summary.".to_owned()],
|
||||
output_schema: json!({
|
||||
"type": "object",
|
||||
"properties": { "score": { "type": "number" } },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use codex_extension_api::ResponseItem;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -36,8 +37,12 @@ impl Default for TranscriptConfig {
|
||||
}
|
||||
|
||||
impl TranscriptConfig {
|
||||
pub(crate) fn build<'a>(&self, items: impl IntoIterator<Item = &'a ResponseItem>) -> String {
|
||||
let mut transcript = String::new();
|
||||
pub(crate) fn build<'a>(
|
||||
&self,
|
||||
items: impl IntoIterator<Item = &'a ResponseItem>,
|
||||
) -> Vec<String> {
|
||||
let mut transcript = VecDeque::new();
|
||||
let mut transcript_bytes = 0;
|
||||
let mut tool_names_by_call_id = HashMap::new();
|
||||
let mut entry_number = 0;
|
||||
|
||||
@@ -173,18 +178,30 @@ impl TranscriptConfig {
|
||||
continue;
|
||||
}
|
||||
entry_number += 1;
|
||||
transcript.push_str(&format!("[{entry_number}] {role}: {text}\n"));
|
||||
let entry = format!("[{entry_number}] {role}: {text}\n");
|
||||
transcript_bytes += entry.len();
|
||||
transcript.push_back(entry);
|
||||
|
||||
if transcript.len() > MAX_TRANSCRIPT_BYTES {
|
||||
let mut first_retained_byte = transcript.len() - MAX_TRANSCRIPT_BYTES;
|
||||
while !transcript.is_char_boundary(first_retained_byte) {
|
||||
first_retained_byte += 1;
|
||||
while transcript_bytes > MAX_TRANSCRIPT_BYTES {
|
||||
let Some(first_entry) = transcript.front_mut() else {
|
||||
break;
|
||||
};
|
||||
let bytes_to_remove = transcript_bytes - MAX_TRANSCRIPT_BYTES;
|
||||
if first_entry.len() <= bytes_to_remove {
|
||||
transcript_bytes -= first_entry.len();
|
||||
transcript.pop_front();
|
||||
} else {
|
||||
let mut first_retained_byte = bytes_to_remove;
|
||||
while !first_entry.is_char_boundary(first_retained_byte) {
|
||||
first_retained_byte += 1;
|
||||
}
|
||||
first_entry.drain(..first_retained_byte);
|
||||
transcript_bytes -= first_retained_byte;
|
||||
}
|
||||
transcript.drain(..first_retained_byte);
|
||||
}
|
||||
}
|
||||
|
||||
transcript
|
||||
transcript.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,13 +62,12 @@ fn transcript_keeps_conversation_and_configured_sources() {
|
||||
let transcript = TranscriptConfig::default().build(&items);
|
||||
assert_eq!(
|
||||
transcript,
|
||||
concat!(
|
||||
vec![
|
||||
"[1] user: Inspect the workspace.\n",
|
||||
"[2] tool exec_command call: {}\n",
|
||||
"[3] tool exec_command result: Workspace inspected.\n",
|
||||
"[4] reasoning: Review the current files.\n",
|
||||
"Plaintext reasoning.\n",
|
||||
)
|
||||
"[4] reasoning: Review the current files.\nPlaintext reasoning.\n",
|
||||
]
|
||||
);
|
||||
|
||||
let output_and_reasoning = TranscriptConfig {
|
||||
@@ -78,12 +77,11 @@ fn transcript_keeps_conversation_and_configured_sources() {
|
||||
let transcript = output_and_reasoning.build(&items);
|
||||
assert_eq!(
|
||||
transcript,
|
||||
concat!(
|
||||
vec![
|
||||
"[1] user: Inspect the workspace.\n",
|
||||
"[2] tool exec_command result: Workspace inspected.\n",
|
||||
"[3] reasoning: Review the current files.\n",
|
||||
"Plaintext reasoning.\n",
|
||||
)
|
||||
"[3] reasoning: Review the current files.\nPlaintext reasoning.\n",
|
||||
]
|
||||
);
|
||||
|
||||
let calls_only = TranscriptConfig {
|
||||
@@ -93,10 +91,10 @@ fn transcript_keeps_conversation_and_configured_sources() {
|
||||
let transcript = calls_only.build(&items);
|
||||
assert_eq!(
|
||||
transcript,
|
||||
concat!(
|
||||
vec![
|
||||
"[1] user: Inspect the workspace.\n",
|
||||
"[2] tool exec_command call: {}\n",
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,8 +123,12 @@ fn transcript_retains_the_most_recent_bounded_content() {
|
||||
|
||||
let transcript = TranscriptConfig::default().build(&items);
|
||||
|
||||
assert!(transcript.len() <= MAX_TRANSCRIPT_BYTES);
|
||||
assert!(transcript.contains("latest response"));
|
||||
assert!(transcript.iter().map(String::len).sum::<usize>() <= MAX_TRANSCRIPT_BYTES);
|
||||
assert!(
|
||||
transcript
|
||||
.iter()
|
||||
.any(|entry| entry.contains("latest response"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -154,7 +156,10 @@ fn transcript_keeps_only_manual_approval_developer_messages() {
|
||||
];
|
||||
|
||||
let transcript = TranscriptConfig::default().build(&items);
|
||||
assert_eq!(transcript, format!("[1] developer: {approval_text}\n"));
|
||||
assert_eq!(
|
||||
transcript,
|
||||
vec![format!("[1] developer: {approval_text}\n")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -224,10 +229,10 @@ fn transcript_omits_media_payloads_and_keeps_readable_content() {
|
||||
let transcript = TranscriptConfig::default().build(&items);
|
||||
assert_eq!(
|
||||
transcript,
|
||||
concat!(
|
||||
vec![
|
||||
"[1] user: Review this screenshot.\n",
|
||||
"[2] tool result: Screenshot captured.\n",
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -280,11 +285,10 @@ fn transcript_omits_encrypted_messages_arguments_and_tool_outputs() {
|
||||
let transcript = TranscriptConfig::default().build(&items);
|
||||
assert_eq!(
|
||||
transcript,
|
||||
concat!(
|
||||
"[1] assistant: Agent message from worker:\n",
|
||||
"The workspace is ready.\n",
|
||||
vec![
|
||||
"[1] assistant: Agent message from worker:\nThe workspace is ready.\n",
|
||||
"[2] tool exec_command call: {}\n",
|
||||
"[3] tool exec_command result: Command completed.\n",
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user