mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
denormalize sandbox and support input/output uuid collapse
This commit is contained in:
+27
-27
@@ -1071,6 +1071,17 @@ fn session_source_metadata_patch(session_source: &SessionSource) -> ResponseItem
|
||||
}
|
||||
}
|
||||
|
||||
fn base_item_metadata_patch(turn_context: &TurnContext) -> ResponseItemMetadata {
|
||||
let mut patch = ResponseItemMetadata {
|
||||
sandbox_policy: Some(sandbox_policy_to_metadata(
|
||||
turn_context.sandbox_policy.get(),
|
||||
)),
|
||||
..ResponseItemMetadata::default()
|
||||
};
|
||||
patch.merge_from(session_source_metadata_patch(&turn_context.session_source));
|
||||
patch
|
||||
}
|
||||
|
||||
fn response_item_tool_call_id(item: &ResponseItem) -> Option<&str> {
|
||||
match item {
|
||||
ResponseItem::LocalShellCall {
|
||||
@@ -1146,7 +1157,6 @@ struct ToolApprovalMetadataSnapshot {
|
||||
}
|
||||
|
||||
fn stamp_tool_approval_metadata_with_snapshot(
|
||||
turn_context: &TurnContext,
|
||||
response_item: ResponseItem,
|
||||
snapshot: Option<&ToolApprovalMetadataSnapshot>,
|
||||
) -> ResponseItem {
|
||||
@@ -1164,9 +1174,6 @@ fn stamp_tool_approval_metadata_with_snapshot(
|
||||
Some(metadata) => metadata,
|
||||
None => return response_item,
|
||||
};
|
||||
metadata.sandbox_policy = Some(sandbox_policy_to_metadata(
|
||||
turn_context.sandbox_policy.get(),
|
||||
));
|
||||
|
||||
match outcome {
|
||||
Some(outcome) => {
|
||||
@@ -3458,7 +3465,7 @@ impl Session {
|
||||
|
||||
pub(crate) async fn stamp_tool_approval_metadata(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
_turn_context: &TurnContext,
|
||||
response_item: ResponseItem,
|
||||
) -> ResponseItem {
|
||||
if !self.enabled(Feature::ItemMetadata) {
|
||||
@@ -3477,12 +3484,12 @@ impl Session {
|
||||
pending_approval_call_ids,
|
||||
}
|
||||
};
|
||||
stamp_tool_approval_metadata_with_snapshot(turn_context, response_item, Some(&snapshot))
|
||||
stamp_tool_approval_metadata_with_snapshot(response_item, Some(&snapshot))
|
||||
}
|
||||
|
||||
pub(crate) async fn stamp_tool_approval_metadata_on_items(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
_turn_context: &TurnContext,
|
||||
response_items: Vec<ResponseItem>,
|
||||
) -> Vec<ResponseItem> {
|
||||
if !self.enabled(Feature::ItemMetadata) {
|
||||
@@ -3504,9 +3511,7 @@ impl Session {
|
||||
|
||||
response_items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
stamp_tool_approval_metadata_with_snapshot(turn_context, item, Some(&snapshot))
|
||||
})
|
||||
.map(|item| stamp_tool_approval_metadata_with_snapshot(item, Some(&snapshot)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -3548,7 +3553,7 @@ impl Session {
|
||||
turn_context: &TurnContext,
|
||||
items: &[ResponseItem],
|
||||
) {
|
||||
let items = self.prepare_history_items(items);
|
||||
let items = self.prepare_history_items(items, turn_context);
|
||||
self.record_into_history_prepared(&items, turn_context)
|
||||
.await;
|
||||
self.persist_rollout_response_items(&items).await;
|
||||
@@ -3561,17 +3566,25 @@ impl Session {
|
||||
items: &[ResponseItem],
|
||||
turn_context: &TurnContext,
|
||||
) {
|
||||
let items = self.prepare_history_items(items);
|
||||
let items = self.prepare_history_items(items, turn_context);
|
||||
self.record_into_history_prepared(&items, turn_context)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn prepare_history_items(&self, items: &[ResponseItem]) -> Vec<ResponseItem> {
|
||||
fn prepare_history_items(
|
||||
&self,
|
||||
items: &[ResponseItem],
|
||||
turn_context: &TurnContext,
|
||||
) -> Vec<ResponseItem> {
|
||||
if self.enabled(Feature::ItemMetadata) {
|
||||
let metadata_patch = base_item_metadata_patch(turn_context);
|
||||
items
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(ResponseItem::with_generated_metadata_uuid)
|
||||
.map(|item| {
|
||||
stamp_response_item_metadata_on_response_item(item, metadata_patch.clone())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
items.to_vec()
|
||||
@@ -4043,14 +4056,6 @@ impl Session {
|
||||
turn_context: &TurnContext,
|
||||
response_item: ResponseItem,
|
||||
) {
|
||||
let response_item = if self.enabled(Feature::ItemMetadata) {
|
||||
stamp_response_item_metadata_on_response_item(
|
||||
response_item,
|
||||
session_source_metadata_patch(&turn_context.session_source),
|
||||
)
|
||||
} else {
|
||||
response_item
|
||||
};
|
||||
let response_item = self
|
||||
.stamp_tool_approval_metadata(turn_context, response_item)
|
||||
.await;
|
||||
@@ -4074,12 +4079,7 @@ impl Session {
|
||||
message_metadata: Option<ResponseItemMetadata>,
|
||||
) {
|
||||
let metadata_patch = if self.enabled(Feature::ItemMetadata) {
|
||||
let mut patch = message_metadata.unwrap_or_default();
|
||||
patch.sandbox_policy = Some(sandbox_policy_to_metadata(
|
||||
turn_context.sandbox_policy.get(),
|
||||
));
|
||||
patch.merge_from(session_source_metadata_patch(&turn_context.session_source));
|
||||
Some(patch)
|
||||
Some(message_metadata.unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -4402,6 +4402,7 @@ async fn task_finish_emits_prompt_queued_metadata_for_injected_user_input_when_f
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn record_response_item_stamps_session_source_on_all_supported_variants() {
|
||||
let (mut sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get());
|
||||
Arc::get_mut(&mut sess)
|
||||
.expect("session should be uniquely owned in this test")
|
||||
.features
|
||||
@@ -4589,7 +4590,10 @@ async fn record_response_item_stamps_session_source_on_all_supported_variants()
|
||||
| ResponseItem::Compaction {
|
||||
metadata: Some(metadata),
|
||||
..
|
||||
} => metadata.session_source == Some(expected_session_source.clone()),
|
||||
} => {
|
||||
metadata.session_source == Some(expected_session_source.clone())
|
||||
&& metadata.sandbox_policy == Some(expected_sandbox_policy.clone())
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.count();
|
||||
@@ -4815,6 +4819,7 @@ async fn tool_call_metadata_stamps_non_escalated_false_when_feature_enabled() {
|
||||
async fn handle_output_item_done_stamps_tool_call_metadata_when_feature_enabled() {
|
||||
let (mut sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get());
|
||||
let expected_session_source = session_source_to_metadata(&tc.session_source);
|
||||
Arc::get_mut(&mut sess)
|
||||
.expect("session should be uniquely owned in this test")
|
||||
.features
|
||||
@@ -4865,6 +4870,7 @@ async fn handle_output_item_done_stamps_tool_call_metadata_when_feature_enabled(
|
||||
&& metadata.is_tool_call_escalated == Some(false)
|
||||
&& metadata.review_decision.is_none()
|
||||
&& metadata.sandbox_policy == Some(expected_sandbox_policy.clone())
|
||||
&& metadata.session_source == Some(expected_session_source.clone())
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ pub(crate) async fn handle_output_item_done(
|
||||
body: FunctionCallOutputBody::Text(msg.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
metadata: None,
|
||||
};
|
||||
record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item)
|
||||
.await;
|
||||
@@ -273,6 +274,7 @@ pub(crate) async fn handle_output_item_done(
|
||||
body: FunctionCallOutputBody::Text(message),
|
||||
..Default::default()
|
||||
},
|
||||
metadata: None,
|
||||
};
|
||||
record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item)
|
||||
.await;
|
||||
@@ -382,29 +384,36 @@ pub(crate) fn last_assistant_message_from_item(
|
||||
|
||||
pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Option<ResponseItem> {
|
||||
match input {
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
Some(ResponseItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output: output.clone(),
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output,
|
||||
metadata,
|
||||
} => Some(ResponseItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output: output.clone(),
|
||||
metadata: metadata.clone(),
|
||||
}),
|
||||
ResponseInputItem::CustomToolCallOutput {
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
metadata,
|
||||
} => Some(ResponseItem::CustomToolCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
name: name.clone(),
|
||||
output: output.clone(),
|
||||
metadata: None,
|
||||
metadata: metadata.clone(),
|
||||
}),
|
||||
ResponseInputItem::McpToolCallOutput { call_id, output } => {
|
||||
ResponseInputItem::McpToolCallOutput {
|
||||
call_id,
|
||||
output,
|
||||
metadata,
|
||||
} => {
|
||||
let output = output.as_function_call_output_payload();
|
||||
Some(ResponseItem::FunctionCallOutput {
|
||||
call_id: call_id.clone(),
|
||||
output,
|
||||
metadata: None,
|
||||
metadata: metadata.clone(),
|
||||
})
|
||||
}
|
||||
ResponseInputItem::ToolSearchOutput {
|
||||
@@ -412,12 +421,13 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
metadata,
|
||||
} => Some(ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(call_id.clone()),
|
||||
status: status.clone(),
|
||||
execution: execution.clone(),
|
||||
tools: tools.clone(),
|
||||
metadata: None,
|
||||
metadata: metadata.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ impl CodeModeProcess {
|
||||
call_id: notify.call_id.clone(),
|
||||
name: Some(PUBLIC_TOOL_NAME.to_string()),
|
||||
output: FunctionCallOutputPayload::from_text(notify.text),
|
||||
metadata: None,
|
||||
}])
|
||||
.await
|
||||
.is_err()
|
||||
|
||||
@@ -104,6 +104,7 @@ impl ToolOutput for CallToolResult {
|
||||
ResponseInputItem::McpToolCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
output: self.clone(),
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +152,7 @@ impl ToolOutput for ToolSearchOutput {
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,10 +256,12 @@ impl ToolOutput for AbortedToolOutput {
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: Vec::new(),
|
||||
metadata: None,
|
||||
},
|
||||
ToolPayload::Mcp { .. } => ResponseInputItem::McpToolCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
output: CallToolResult::from_error_text(self.message.clone()),
|
||||
metadata: None,
|
||||
},
|
||||
_ => function_tool_response(
|
||||
call_id,
|
||||
@@ -454,12 +458,14 @@ fn function_tool_response(
|
||||
call_id: call_id.to_string(),
|
||||
name: None,
|
||||
output: FunctionCallOutputPayload { body, success },
|
||||
metadata: None,
|
||||
};
|
||||
}
|
||||
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
output: FunctionCallOutputPayload { body, success },
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ fn function_payloads_remain_function_outputs() {
|
||||
.to_response_item("fn-1", &payload);
|
||||
|
||||
match response {
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id, output, ..
|
||||
} => {
|
||||
assert_eq!(call_id, "fn-1");
|
||||
assert_eq!(output.content_items(), None);
|
||||
assert_eq!(output.body.to_text().as_deref(), Some("ok"));
|
||||
@@ -164,6 +166,7 @@ fn tool_search_payloads_roundtrip_as_tool_search_outputs() {
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(call_id, "search-1");
|
||||
assert_eq!(status, "completed");
|
||||
@@ -258,7 +261,9 @@ fn exec_command_tool_output_formats_truncated_response() {
|
||||
.to_response_item("call-42", &payload);
|
||||
|
||||
match response {
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id, output, ..
|
||||
} => {
|
||||
assert_eq!(call_id, "call-42");
|
||||
assert_eq!(output.success, Some(true));
|
||||
let text = output
|
||||
|
||||
@@ -41,6 +41,7 @@ impl ToolOutput for PlanToolOutput {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
output,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ impl ToolOutput for ViewImageOutput {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
output,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ fn summarize_tool_call_response_for_multimodal_function_output() {
|
||||
detail: None,
|
||||
},
|
||||
]),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let actual = JsReplManager::summarize_tool_call_response(&response);
|
||||
@@ -383,6 +384,7 @@ fn summarize_tool_call_response_for_multimodal_custom_output() {
|
||||
detail: None,
|
||||
},
|
||||
]),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let actual = JsReplManager::summarize_tool_call_response(&response);
|
||||
|
||||
@@ -141,6 +141,7 @@ impl ToolCallRuntime {
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: Vec::new(),
|
||||
metadata: None,
|
||||
},
|
||||
ToolPayload::Custom { .. } => ResponseInputItem::CustomToolCallOutput {
|
||||
call_id: call.call_id,
|
||||
@@ -149,6 +150,7 @@ impl ToolCallRuntime {
|
||||
body: codex_protocol::models::FunctionCallOutputBody::Text(message),
|
||||
success: Some(false),
|
||||
},
|
||||
metadata: None,
|
||||
},
|
||||
_ => ResponseInputItem::FunctionCallOutput {
|
||||
call_id: call.call_id,
|
||||
@@ -156,6 +158,7 @@ impl ToolCallRuntime {
|
||||
body: codex_protocol::models::FunctionCallOutputBody::Text(message),
|
||||
success: Some(false),
|
||||
},
|
||||
metadata: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RawResponseItemEvent;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
@@ -58,6 +59,7 @@ use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
use futures::StreamExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -90,6 +92,33 @@ fn message_input_texts(item: &serde_json::Value) -> Vec<&str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn assistant_message_item_by_text<'a>(
|
||||
input: &'a [serde_json::Value],
|
||||
text: &str,
|
||||
) -> &'a serde_json::Value {
|
||||
input
|
||||
.iter()
|
||||
.find(|item| {
|
||||
if item.get("type").and_then(serde_json::Value::as_str) != Some("message")
|
||||
|| item.get("role").and_then(serde_json::Value::as_str) != Some("assistant")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
item.get("content")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|content| {
|
||||
content.iter().any(|entry| {
|
||||
entry.get("type").and_then(serde_json::Value::as_str) == Some("output_text")
|
||||
&& entry.get("text").and_then(serde_json::Value::as_str) == Some(text)
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!("submitted assistant message input item not found for text: {text}")
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes an `auth.json` into the provided `codex_home` with the specified parameters.
|
||||
/// Returns the fake JWT string written to `tokens.id_token`.
|
||||
#[expect(clippy::unwrap_used)]
|
||||
@@ -634,7 +663,14 @@ async fn includes_conversation_id_and_model_headers_in_request() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ItemMetadata)
|
||||
.expect("feature flag should be enabled for this test");
|
||||
});
|
||||
let test = builder
|
||||
.build(&server)
|
||||
.await
|
||||
@@ -2572,7 +2608,14 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
|
||||
let request_log = mount_sse_sequence(&server, vec![sse1.clone(), sse1.clone(), sse1]).await;
|
||||
|
||||
let mut builder = test_codex().with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ItemMetadata)
|
||||
.expect("feature flag should be enabled for this test");
|
||||
});
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
.await
|
||||
@@ -2669,3 +2712,102 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
"request 3 tail mismatch",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn replayed_output_item_keeps_uuid_across_turns() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = MockServer::start().await;
|
||||
let sse_raw = r##"[
|
||||
{"type":"response.output_item.added", "item":{
|
||||
"type":"message", "role":"assistant",
|
||||
"content":[{"type":"output_text","text":""}]
|
||||
}},
|
||||
{"type":"response.output_text.delta", "delta":"Hey "},
|
||||
{"type":"response.output_text.delta", "delta":"there"},
|
||||
{"type":"response.output_text.delta", "delta":"!\n"},
|
||||
{"type":"response.output_item.done", "item":{
|
||||
"type":"message", "role":"assistant",
|
||||
"content":[{"type":"output_text","text":"Hey there!\n"}]
|
||||
}},
|
||||
{"type":"response.completed", "response": {"id": "__ID__"}}
|
||||
]"##;
|
||||
let sse1 = core_test_support::load_sse_fixture_with_id_from_str(sse_raw, "resp1");
|
||||
let request_log = mount_sse_sequence(&server, vec![sse1.clone(), sse1]).await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ItemMetadata)
|
||||
.expect("feature flag should be enabled for this test");
|
||||
});
|
||||
let codex = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create new conversation")
|
||||
.codex;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "U1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let original_uuid = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::RawResponseItem(RawResponseItemEvent {
|
||||
item:
|
||||
ResponseItem::Message {
|
||||
role,
|
||||
content,
|
||||
metadata: Some(metadata),
|
||||
..
|
||||
},
|
||||
}) if role == "assistant"
|
||||
&& *content
|
||||
== vec![ContentItem::OutputText {
|
||||
text: "Hey there!\n".to_string(),
|
||||
}] =>
|
||||
{
|
||||
metadata.uuid.clone()
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "U2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let requests = request_log.requests();
|
||||
assert_eq!(requests.len(), 2, "expected 2 requests (one per turn)");
|
||||
let request_2_input = requests[1]
|
||||
.body_json()
|
||||
.get("input")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.cloned()
|
||||
.expect("request 2 missing input array");
|
||||
let replayed_uuid = assistant_message_item_by_text(&request_2_input, "Hey there!\n")
|
||||
.get("metadata")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|metadata| metadata.get("uuid"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("replayed assistant message should carry a uuid");
|
||||
|
||||
assert_eq!(replayed_uuid, original_uuid);
|
||||
}
|
||||
|
||||
@@ -237,10 +237,16 @@ pub enum ResponseInputItem {
|
||||
#[ts(as = "FunctionCallOutputBody")]
|
||||
#[schemars(with = "FunctionCallOutputBody")]
|
||||
output: FunctionCallOutputPayload,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
McpToolCallOutput {
|
||||
call_id: String,
|
||||
output: CallToolResult,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
CustomToolCallOutput {
|
||||
call_id: String,
|
||||
@@ -250,6 +256,9 @@ pub enum ResponseInputItem {
|
||||
#[ts(as = "FunctionCallOutputBody")]
|
||||
#[schemars(with = "FunctionCallOutputBody")]
|
||||
output: FunctionCallOutputPayload,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
ToolSearchOutput {
|
||||
call_id: String,
|
||||
@@ -257,6 +266,9 @@ pub enum ResponseInputItem {
|
||||
execution: String,
|
||||
#[ts(type = "unknown[]")]
|
||||
tools: Vec<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1328,40 +1340,50 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput {
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id,
|
||||
output,
|
||||
metadata: None,
|
||||
metadata,
|
||||
} => Self::FunctionCallOutput {
|
||||
call_id,
|
||||
output,
|
||||
metadata,
|
||||
},
|
||||
ResponseInputItem::McpToolCallOutput { call_id, output } => {
|
||||
ResponseInputItem::McpToolCallOutput {
|
||||
call_id,
|
||||
output,
|
||||
metadata,
|
||||
} => {
|
||||
let output = output.into_function_call_output_payload();
|
||||
Self::FunctionCallOutput {
|
||||
call_id,
|
||||
output,
|
||||
metadata: None,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
ResponseInputItem::CustomToolCallOutput {
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
metadata,
|
||||
} => Self::CustomToolCallOutput {
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
metadata: None,
|
||||
metadata,
|
||||
},
|
||||
ResponseInputItem::ToolSearchOutput {
|
||||
call_id,
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
metadata,
|
||||
} => Self::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
metadata: None,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2661,6 +2683,7 @@ mod tests {
|
||||
let item = ResponseInputItem::FunctionCallOutput {
|
||||
call_id: "call1".into(),
|
||||
output: FunctionCallOutputPayload::from_text("ok".into()),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&item)?;
|
||||
@@ -2679,6 +2702,7 @@ mod tests {
|
||||
body: FunctionCallOutputBody::Text("bad".into()),
|
||||
success: Some(false),
|
||||
},
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&item)?;
|
||||
@@ -2722,6 +2746,7 @@ mod tests {
|
||||
let item = ResponseInputItem::FunctionCallOutput {
|
||||
call_id: "call1".into(),
|
||||
output: payload,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&item)?;
|
||||
@@ -2744,6 +2769,7 @@ mod tests {
|
||||
detail: None,
|
||||
},
|
||||
]),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&item)?;
|
||||
@@ -2755,6 +2781,117 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_call_output_roundtrips_metadata() -> Result<()> {
|
||||
let input = ResponseInputItem::FunctionCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
session_source: Some(SessionSourceMetadata::Exec),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
output: FunctionCallOutputPayload::from_text("ok".to_string()),
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
session_source: Some(SessionSourceMetadata::Exec),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(input)?,
|
||||
serde_json::json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": "call-1",
|
||||
"output": "ok",
|
||||
"metadata": {
|
||||
"uuid": "uuid-1",
|
||||
"session_source": "exec"
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_output_roundtrips_metadata() -> Result<()> {
|
||||
let input = ResponseInputItem::McpToolCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
output: CallToolResult {
|
||||
content: vec![serde_json::json!({"type":"text","text":"ok"})],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
},
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
sandbox_policy: Some(SandboxPolicyMetadata::ReadOnly),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
};
|
||||
|
||||
let expected_output = match &input {
|
||||
ResponseInputItem::McpToolCallOutput { output, .. } => {
|
||||
output.as_function_call_output_payload()
|
||||
}
|
||||
other => panic!("unexpected input variant: {other:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
output: expected_output,
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
sandbox_policy: Some(SandboxPolicyMetadata::ReadOnly),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_tool_call_output_roundtrips_metadata() -> Result<()> {
|
||||
let input = ResponseInputItem::CustomToolCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
name: Some("custom_tool".to_string()),
|
||||
output: FunctionCallOutputPayload::from_text("done".to_string()),
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
review_decision: Some(ReviewDecisionMetadata::Approved),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
ResponseItem::CustomToolCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
name: Some("custom_tool".to_string()),
|
||||
output: FunctionCallOutputPayload::from_text("done".to_string()),
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
review_decision: Some(ReviewDecisionMetadata::Approved),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_existing_image_data_urls() -> Result<()> {
|
||||
let call_tool_result = CallToolResult {
|
||||
@@ -3037,6 +3174,12 @@ mod tests {
|
||||
"additionalProperties": false,
|
||||
}
|
||||
})],
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
sandbox_policy: Some(SandboxPolicyMetadata::ReadOnly),
|
||||
session_source: Some(SessionSourceMetadata::Cli),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
};
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
@@ -3058,7 +3201,12 @@ mod tests {
|
||||
"additionalProperties": false,
|
||||
}
|
||||
})],
|
||||
metadata: None,
|
||||
metadata: Some(ResponseItemMetadata {
|
||||
uuid: Some("uuid-1".to_string()),
|
||||
sandbox_policy: Some(SandboxPolicyMetadata::ReadOnly),
|
||||
session_source: Some(SessionSourceMetadata::Cli),
|
||||
..ResponseItemMetadata::default()
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3083,6 +3231,12 @@ mod tests {
|
||||
"additionalProperties": false,
|
||||
}
|
||||
}]
|
||||
,
|
||||
"metadata": {
|
||||
"uuid": "uuid-1",
|
||||
"sandbox_policy": "read_only",
|
||||
"session_source": "cli"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user