mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
## Why Reducing code cell lifecycle events discarded their response payload references and lost the initial response status when the runtime status changed. ## What changed Store the initial response status and separate raw payload IDs for the initial response and final result on `CodeCell`. Carry payload references through queued lifecycle events so they survive replay when the source item arrives later. ## Testing Extend lifecycle tests to verify distinct initial and final payload IDs and the preserved initial status for yielded cells that complete and failed cells whose lifecycle events precede their source item. GitOrigin-RevId: 8f49a3174ea487a5d91ebc2a6c97133aacaabfd2
468 lines
16 KiB
Rust
468 lines
16 KiB
Rust
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
|
|
use crate::model::CodeCellRuntimeStatus;
|
|
use crate::model::ConversationItemKind;
|
|
use crate::model::ExecutionStatus;
|
|
use crate::model::ProducerRef;
|
|
use crate::model::ToolCallKind;
|
|
use crate::model::ToolCallSummary;
|
|
use crate::payload::RawPayloadKind;
|
|
use crate::raw_event::RawToolCallRequester;
|
|
use crate::raw_event::RawTraceEventPayload;
|
|
use crate::reducer::test_support::create_started_writer;
|
|
use crate::reducer::test_support::message;
|
|
use crate::reducer::test_support::start_turn;
|
|
use crate::reducer::test_support::start_turn_for_thread;
|
|
use crate::reducer::test_support::trace_context;
|
|
use crate::reducer::test_support::trace_context_for_thread;
|
|
use crate::replay_bundle;
|
|
|
|
#[test]
|
|
fn code_cell_lifecycle_links_nested_tools_waits_and_outputs() -> 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", "count files")]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceStarted {
|
|
inference_call_id: "inference-1".to_string(),
|
|
thread_id: "thread-root".to_string(),
|
|
codex_turn_id: "turn-1".to_string(),
|
|
model: "gpt-test".to_string(),
|
|
provider_name: "test-provider".to_string(),
|
|
request_payload: request,
|
|
})?;
|
|
let response = writer.write_json_payload(
|
|
RawPayloadKind::InferenceResponse,
|
|
&json!({
|
|
"response_id": "resp-1",
|
|
"output_items": [{
|
|
"type": "custom_tool_call",
|
|
"name": "exec",
|
|
"call_id": "call-code",
|
|
"input": "text('hi')"
|
|
}]
|
|
}),
|
|
)?;
|
|
// Runtime tool dispatch starts before the stream-completion hook has
|
|
// reduced the model response that requested `exec`.
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodeCellStarted {
|
|
runtime_cell_id: "1".to_string(),
|
|
model_visible_call_id: "call-code".to_string(),
|
|
source_js: "text('hi')".to_string(),
|
|
},
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceCompleted {
|
|
inference_call_id: "inference-1".to_string(),
|
|
response_id: Some("resp-1".to_string()),
|
|
upstream_request_id: None,
|
|
response_payload: response,
|
|
})?;
|
|
let initial_response = writer.write_json_payload(
|
|
RawPayloadKind::ToolResult,
|
|
&json!({"response": "initial caller result"}),
|
|
)?;
|
|
let final_response = writer.write_json_payload(
|
|
RawPayloadKind::ToolResult,
|
|
&json!({"response": "later runtime result"}),
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodeCellInitialResponse {
|
|
runtime_cell_id: "1".to_string(),
|
|
status: CodeCellRuntimeStatus::Yielded,
|
|
response_payload: Some(initial_response.clone()),
|
|
},
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::ToolCallStarted {
|
|
tool_call_id: "nested-tool-1".to_string(),
|
|
model_visible_call_id: None,
|
|
code_mode_runtime_tool_id: Some("tool-1".to_string()),
|
|
requester: RawToolCallRequester::CodeCell {
|
|
runtime_cell_id: "1".to_string(),
|
|
},
|
|
kind: ToolCallKind::ExecCommand,
|
|
summary: ToolCallSummary::Generic {
|
|
label: "exec_command".to_string(),
|
|
input_preview: Some("pwd".to_string()),
|
|
output_preview: None,
|
|
},
|
|
invocation_payload: None,
|
|
},
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::ToolCallEnded {
|
|
tool_call_id: "nested-tool-1".to_string(),
|
|
status: ExecutionStatus::Completed,
|
|
result_payload: None,
|
|
},
|
|
)?;
|
|
|
|
start_turn(&writer, "turn-2")?;
|
|
let followup = writer.write_json_payload(
|
|
RawPayloadKind::InferenceRequest,
|
|
&json!({
|
|
"previous_response_id": "resp-1",
|
|
"input": [{
|
|
"type": "custom_tool_call_output",
|
|
"call_id": "call-code",
|
|
"output": "Script running with cell ID 1"
|
|
}]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceStarted {
|
|
inference_call_id: "inference-2".to_string(),
|
|
thread_id: "thread-root".to_string(),
|
|
codex_turn_id: "turn-2".to_string(),
|
|
model: "gpt-test".to_string(),
|
|
provider_name: "test-provider".to_string(),
|
|
request_payload: followup,
|
|
})?;
|
|
let wait_request = writer.write_json_payload(
|
|
RawPayloadKind::ToolInvocation,
|
|
&json!({
|
|
"tool_name": "wait",
|
|
"tool_namespace": null,
|
|
"payload": {
|
|
"type": "function",
|
|
"arguments": "{\"cell_id\":\"1\"}"
|
|
}
|
|
}),
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-2"),
|
|
RawTraceEventPayload::ToolCallStarted {
|
|
tool_call_id: "wait-tool-1".to_string(),
|
|
model_visible_call_id: Some("wait-call".to_string()),
|
|
code_mode_runtime_tool_id: None,
|
|
requester: RawToolCallRequester::Model,
|
|
kind: ToolCallKind::Other {
|
|
name: "wait".to_string(),
|
|
},
|
|
summary: ToolCallSummary::Generic {
|
|
label: "wait".to_string(),
|
|
input_preview: Some("{\"cell_id\":\"1\"}".to_string()),
|
|
output_preview: None,
|
|
},
|
|
invocation_payload: Some(wait_request),
|
|
},
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-2"),
|
|
RawTraceEventPayload::CodeCellEnded {
|
|
runtime_cell_id: "1".to_string(),
|
|
status: CodeCellRuntimeStatus::Completed,
|
|
response_payload: Some(final_response.clone()),
|
|
},
|
|
)?;
|
|
|
|
let rollout = replay_bundle(temp.path())?;
|
|
let code_cell_id = test_reduced_code_cell_id("call-code");
|
|
let cell = &rollout.code_cells[&code_cell_id];
|
|
let output_item_id = rollout.inference_calls["inference-2"]
|
|
.request_item_ids
|
|
.last()
|
|
.expect("exec output item");
|
|
|
|
assert_eq!(
|
|
(
|
|
&cell.initial_response_status,
|
|
&cell.raw_initial_response_payload_id,
|
|
&cell.raw_result_payload_id,
|
|
),
|
|
(
|
|
&Some(CodeCellRuntimeStatus::Yielded),
|
|
&Some(initial_response.raw_payload_id),
|
|
&Some(final_response.raw_payload_id),
|
|
)
|
|
);
|
|
assert_eq!(cell.thread_id, "thread-root");
|
|
assert_eq!(cell.runtime_status, CodeCellRuntimeStatus::Completed);
|
|
assert_eq!(cell.execution.status, ExecutionStatus::Completed);
|
|
assert_eq!(cell.runtime_cell_id, Some("1".to_string()));
|
|
assert_eq!(cell.nested_tool_call_ids, vec!["nested-tool-1"]);
|
|
assert_eq!(cell.wait_tool_call_ids, vec!["wait-tool-1"]);
|
|
assert_eq!(cell.output_item_ids, vec![output_item_id.clone()]);
|
|
assert_eq!(
|
|
rollout.conversation_items[output_item_id].produced_by,
|
|
vec![ProducerRef::CodeCell {
|
|
code_cell_id: code_cell_id.clone(),
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
rollout.conversation_items[&cell.source_item_id].kind,
|
|
ConversationItemKind::CustomToolCall,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn fast_code_cell_lifecycle_waits_for_source_item() -> 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", "count files")]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceStarted {
|
|
inference_call_id: "inference-1".to_string(),
|
|
thread_id: "thread-root".to_string(),
|
|
codex_turn_id: "turn-1".to_string(),
|
|
model: "gpt-test".to_string(),
|
|
provider_name: "test-provider".to_string(),
|
|
request_payload: request,
|
|
})?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodeCellStarted {
|
|
runtime_cell_id: "1".to_string(),
|
|
model_visible_call_id: "call-code".to_string(),
|
|
source_js: "not valid js".to_string(),
|
|
},
|
|
)?;
|
|
let initial_response = writer.write_json_payload(
|
|
RawPayloadKind::ToolResult,
|
|
&json!({"response": "initial caller result"}),
|
|
)?;
|
|
let final_response = writer.write_json_payload(
|
|
RawPayloadKind::ToolResult,
|
|
&json!({"response": "later runtime result"}),
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodeCellInitialResponse {
|
|
runtime_cell_id: "1".to_string(),
|
|
status: CodeCellRuntimeStatus::Failed,
|
|
response_payload: Some(initial_response.clone()),
|
|
},
|
|
)?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodeCellEnded {
|
|
runtime_cell_id: "1".to_string(),
|
|
status: CodeCellRuntimeStatus::Failed,
|
|
response_payload: Some(final_response.clone()),
|
|
},
|
|
)?;
|
|
let response = writer.write_json_payload(
|
|
RawPayloadKind::InferenceResponse,
|
|
&json!({
|
|
"response_id": "resp-1",
|
|
"output_items": [{
|
|
"type": "custom_tool_call",
|
|
"name": "exec",
|
|
"call_id": "call-code",
|
|
"input": "not valid js"
|
|
}]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceCompleted {
|
|
inference_call_id: "inference-1".to_string(),
|
|
response_id: Some("resp-1".to_string()),
|
|
upstream_request_id: None,
|
|
response_payload: response,
|
|
})?;
|
|
|
|
let rollout = replay_bundle(temp.path())?;
|
|
let code_cell_id = test_reduced_code_cell_id("call-code");
|
|
let cell = &rollout.code_cells[&code_cell_id];
|
|
|
|
assert_eq!(
|
|
(
|
|
&cell.initial_response_status,
|
|
&cell.raw_initial_response_payload_id,
|
|
&cell.raw_result_payload_id,
|
|
),
|
|
(
|
|
&Some(CodeCellRuntimeStatus::Failed),
|
|
&Some(initial_response.raw_payload_id),
|
|
&Some(final_response.raw_payload_id),
|
|
)
|
|
);
|
|
assert_eq!(cell.thread_id, "thread-root");
|
|
assert_eq!(cell.runtime_status, CodeCellRuntimeStatus::Failed);
|
|
assert_eq!(cell.execution.status, ExecutionStatus::Failed);
|
|
assert_eq!(cell.runtime_cell_id, Some("1".to_string()));
|
|
assert_eq!(
|
|
rollout.conversation_items[&cell.source_item_id].kind,
|
|
ConversationItemKind::CustomToolCall,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn cancelled_turn_terminates_unfinished_code_cell() -> 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", "count files")]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceStarted {
|
|
inference_call_id: "inference-1".to_string(),
|
|
thread_id: "thread-root".to_string(),
|
|
codex_turn_id: "turn-1".to_string(),
|
|
model: "gpt-test".to_string(),
|
|
provider_name: "test-provider".to_string(),
|
|
request_payload: request,
|
|
})?;
|
|
let response = writer.write_json_payload(
|
|
RawPayloadKind::InferenceResponse,
|
|
&json!({
|
|
"response_id": "resp-1",
|
|
"output_items": [{
|
|
"type": "custom_tool_call",
|
|
"name": "exec",
|
|
"call_id": "call-code",
|
|
"input": "await tools.exec_command({cmd: 'slow'});"
|
|
}]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceCompleted {
|
|
inference_call_id: "inference-1".to_string(),
|
|
response_id: Some("resp-1".to_string()),
|
|
upstream_request_id: None,
|
|
response_payload: response,
|
|
})?;
|
|
writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodeCellStarted {
|
|
runtime_cell_id: "1".to_string(),
|
|
model_visible_call_id: "call-code".to_string(),
|
|
source_js: "await tools.exec_command({cmd: 'slow'});".to_string(),
|
|
},
|
|
)?;
|
|
let turn_end = writer.append_with_context(
|
|
trace_context("turn-1"),
|
|
RawTraceEventPayload::CodexTurnEnded {
|
|
codex_turn_id: "turn-1".to_string(),
|
|
status: ExecutionStatus::Cancelled,
|
|
},
|
|
)?;
|
|
|
|
let rollout = replay_bundle(temp.path())?;
|
|
let code_cell_id = test_reduced_code_cell_id("call-code");
|
|
let cell = &rollout.code_cells[&code_cell_id];
|
|
|
|
assert_eq!(cell.runtime_status, CodeCellRuntimeStatus::Terminated);
|
|
assert_eq!(cell.execution.status, ExecutionStatus::Cancelled);
|
|
assert_eq!(cell.execution.ended_seq, Some(turn_end.seq));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_code_cell_ids_can_repeat_across_threads() -> anyhow::Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let writer = create_started_writer(&temp)?;
|
|
writer.append(RawTraceEventPayload::ThreadStarted {
|
|
thread_id: "thread-child".to_string(),
|
|
agent_path: "/root/child".to_string(),
|
|
metadata_payload: None,
|
|
})?;
|
|
start_turn_for_thread(&writer, "thread-root", "turn-root")?;
|
|
start_turn_for_thread(&writer, "thread-child", "turn-child")?;
|
|
|
|
for (thread_id, turn_id, inference_call_id, call_id) in [
|
|
("thread-root", "turn-root", "inference-root", "call-root"),
|
|
(
|
|
"thread-child",
|
|
"turn-child",
|
|
"inference-child",
|
|
"call-child",
|
|
),
|
|
] {
|
|
let request = writer.write_json_payload(
|
|
RawPayloadKind::InferenceRequest,
|
|
&json!({
|
|
"input": [message("user", "run code")]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceStarted {
|
|
inference_call_id: inference_call_id.to_string(),
|
|
thread_id: thread_id.to_string(),
|
|
codex_turn_id: turn_id.to_string(),
|
|
model: "gpt-test".to_string(),
|
|
provider_name: "test-provider".to_string(),
|
|
request_payload: request,
|
|
})?;
|
|
writer.append_with_context(
|
|
trace_context_for_thread(thread_id, turn_id),
|
|
RawTraceEventPayload::CodeCellStarted {
|
|
runtime_cell_id: "1".to_string(),
|
|
model_visible_call_id: call_id.to_string(),
|
|
source_js: "text('hi')".to_string(),
|
|
},
|
|
)?;
|
|
let response = writer.write_json_payload(
|
|
RawPayloadKind::InferenceResponse,
|
|
&json!({
|
|
"response_id": format!("resp-{thread_id}"),
|
|
"output_items": [{
|
|
"type": "custom_tool_call",
|
|
"name": "exec",
|
|
"call_id": call_id,
|
|
"input": "text('hi')"
|
|
}]
|
|
}),
|
|
)?;
|
|
writer.append(RawTraceEventPayload::InferenceCompleted {
|
|
inference_call_id: inference_call_id.to_string(),
|
|
response_id: Some(format!("resp-{thread_id}")),
|
|
upstream_request_id: None,
|
|
response_payload: response,
|
|
})?;
|
|
writer.append_with_context(
|
|
trace_context_for_thread(thread_id, turn_id),
|
|
RawTraceEventPayload::CodeCellEnded {
|
|
runtime_cell_id: "1".to_string(),
|
|
status: CodeCellRuntimeStatus::Completed,
|
|
response_payload: None,
|
|
},
|
|
)?;
|
|
}
|
|
|
|
let rollout = replay_bundle(temp.path())?;
|
|
let root_cell_id = test_reduced_code_cell_id("call-root");
|
|
let child_cell_id = test_reduced_code_cell_id("call-child");
|
|
|
|
assert_eq!(rollout.code_cells[&root_cell_id].thread_id, "thread-root");
|
|
assert_eq!(rollout.code_cells[&child_cell_id].thread_id, "thread-child");
|
|
assert_eq!(
|
|
rollout.code_cells[&root_cell_id].runtime_cell_id,
|
|
Some("1".to_string())
|
|
);
|
|
assert_eq!(
|
|
rollout.code_cells[&child_cell_id].runtime_cell_id,
|
|
Some("1".to_string())
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn test_reduced_code_cell_id(model_visible_call_id: &str) -> String {
|
|
format!("code_cell:{model_visible_call_id}")
|
|
}
|