mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Ignore passthrough metadata when reconciling rollout items (#36221)
## What changed - Remove top-level `internal_chat_message_metadata_passthrough` from model items before rollout-trace normalization so replayed tool calls and outputs reuse their existing conversation items. - Keep nested metadata model-visible and continue rejecting call ID reuse when that content changes. ## Testing - Add reducer coverage for replayed tool search calls and tool outputs with top-level passthrough metadata, plus conflicting nested metadata. GitOrigin-RevId: db81b9957770a2226f77db1d9df87e75572fc6de
This commit is contained in:
@@ -40,7 +40,12 @@ pub(super) fn normalize_model_items(
|
||||
if item.get("type").and_then(Value::as_str) == Some("additional_tools") {
|
||||
continue;
|
||||
}
|
||||
normalized_items.push(normalize_model_item(item, raw_payload)?);
|
||||
|
||||
let mut model_visible_item = item.clone();
|
||||
if let Some(object) = model_visible_item.as_object_mut() {
|
||||
object.remove("internal_chat_message_metadata_passthrough");
|
||||
}
|
||||
normalized_items.push(normalize_model_item(&model_visible_item, raw_payload)?);
|
||||
}
|
||||
Ok(normalized_items)
|
||||
}
|
||||
|
||||
@@ -255,6 +255,188 @@ fn later_full_request_reuses_prior_json_tool_call_by_position() -> anyhow::Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_reuses_prior_tool_search_call_with_internal_metadata() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let writer = create_started_writer(&temp)?;
|
||||
start_turn(&writer, "turn-1")?;
|
||||
|
||||
let request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [message("user", "search")]
|
||||
}),
|
||||
)?;
|
||||
append_inference_start(&writer, "inference-1", "turn-1", request)?;
|
||||
|
||||
let response = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceResponse,
|
||||
&json!({
|
||||
"response_id": "resp-1",
|
||||
"output_items": [{
|
||||
"type": "tool_search_call",
|
||||
"status": "completed",
|
||||
"call_id": "call-search",
|
||||
"arguments": {
|
||||
"query": "spawn subagent launch manage agents report result",
|
||||
"limit": 10
|
||||
},
|
||||
"execution": "client"
|
||||
}]
|
||||
}),
|
||||
)?;
|
||||
append_inference_completion(&writer, "inference-1", "resp-1", response)?;
|
||||
start_turn(&writer, "turn-2")?;
|
||||
|
||||
let next_request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [
|
||||
message("user", "search"),
|
||||
{
|
||||
"type": "tool_search_call",
|
||||
"status": "completed",
|
||||
"call_id": "call-search",
|
||||
"arguments": {
|
||||
"query": "spawn subagent launch manage agents report result",
|
||||
"limit": 10
|
||||
},
|
||||
"execution": "client",
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn-1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
)?;
|
||||
append_inference_start(&writer, "inference-2", "turn-2", next_request)?;
|
||||
|
||||
let rollout = replay_bundle(temp.path())?;
|
||||
let first = &rollout.inference_calls["inference-1"];
|
||||
let second = &rollout.inference_calls["inference-2"];
|
||||
|
||||
assert_eq!(
|
||||
second.request_item_ids,
|
||||
vec![
|
||||
first.request_item_ids[0].clone(),
|
||||
first.response_item_ids[0].clone(),
|
||||
],
|
||||
);
|
||||
assert_eq!(rollout.conversation_items.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_reuses_prior_tool_outputs_with_internal_metadata() -> anyhow::Result<()> {
|
||||
for item_type in ["tool_search_output", "mcp_tool_call_output"] {
|
||||
let temp = TempDir::new()?;
|
||||
let writer = create_started_writer(&temp)?;
|
||||
start_turn(&writer, "turn-1")?;
|
||||
|
||||
let output = json!({
|
||||
"type": item_type,
|
||||
"status": "completed",
|
||||
"call_id": "call-search",
|
||||
"execution": "client",
|
||||
"tools": [{
|
||||
"name": "search",
|
||||
"internal_chat_message_metadata_passthrough": "model-visible"
|
||||
}]
|
||||
});
|
||||
let first_request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [message("user", "search"), output.clone()]
|
||||
}),
|
||||
)?;
|
||||
let first_request_payload_id = first_request.raw_payload_id.clone();
|
||||
append_inference_start(&writer, "inference-1", "turn-1", first_request)?;
|
||||
start_turn(&writer, "turn-2")?;
|
||||
|
||||
let mut replayed_output = output.clone();
|
||||
replayed_output["internal_chat_message_metadata_passthrough"] = json!({
|
||||
"turn_id": "turn-1"
|
||||
});
|
||||
let next_request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [message("user", "search"), replayed_output]
|
||||
}),
|
||||
)?;
|
||||
append_inference_start(&writer, "inference-2", "turn-2", next_request)?;
|
||||
|
||||
let rollout = replay_bundle(temp.path())?;
|
||||
let first = &rollout.inference_calls["inference-1"];
|
||||
let second = &rollout.inference_calls["inference-2"];
|
||||
|
||||
assert_eq!(second.request_item_ids, first.request_item_ids);
|
||||
assert_eq!(
|
||||
rollout.conversation_items[&first.request_item_ids[1]].body,
|
||||
ConversationBody {
|
||||
parts: vec![ConversationPart::Json {
|
||||
summary: serde_json::to_string(&output)?,
|
||||
raw_payload_id: first_request_payload_id,
|
||||
}],
|
||||
},
|
||||
);
|
||||
assert_eq!(rollout.conversation_items.len(), 2);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_output_call_id_reuse_with_different_nested_metadata_is_reducer_error() -> anyhow::Result<()>
|
||||
{
|
||||
let temp = TempDir::new()?;
|
||||
let writer = create_started_writer(&temp)?;
|
||||
start_turn(&writer, "turn-1")?;
|
||||
|
||||
let request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [{
|
||||
"type": "tool_search_output",
|
||||
"status": "completed",
|
||||
"call_id": "call-search",
|
||||
"execution": "client",
|
||||
"tools": [{
|
||||
"name": "search",
|
||||
"internal_chat_message_metadata_passthrough": "first"
|
||||
}]
|
||||
}]
|
||||
}),
|
||||
)?;
|
||||
append_inference_start(&writer, "inference-1", "turn-1", request)?;
|
||||
start_turn(&writer, "turn-2")?;
|
||||
|
||||
let conflicting_request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [{
|
||||
"type": "tool_search_output",
|
||||
"status": "completed",
|
||||
"call_id": "call-search",
|
||||
"execution": "client",
|
||||
"tools": [{
|
||||
"name": "search",
|
||||
"internal_chat_message_metadata_passthrough": "different"
|
||||
}],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn-1"
|
||||
}
|
||||
}]
|
||||
}),
|
||||
)?;
|
||||
append_inference_start(&writer, "inference-2", "turn-2", conflicting_request)?;
|
||||
|
||||
expect_replay_error(
|
||||
&temp,
|
||||
"model-visible call id call-search was reused with different content",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incremental_request_carries_prior_request_and_response_items_forward() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
|
||||
Reference in New Issue
Block a user