mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(core): store turn_id on ResponseItem metadata (#28360)
## Description This PR is a followup to https://github.com/openai/codex/pull/28355 and starts assigning `internal_chat_message_metadata_passthrough.turn_id` to durable Responses API items created during a turn. The goal is that those items keep the `turn_id` that introduced them when Codex resends stateless HTTP context, reconstructs history for resume/fork paths, or reuses websocket response state. ## What changed - Set `internal_chat_message_metadata_passthrough.turn_id` when missing as response items enter durable history, initial/replacement history, inter-agent communication history, and local compaction summaries. - Preserve existing item turn IDs instead of overwriting them during persistence, resume reconstruction, compaction, forked history, and websocket incremental reuse. - Keep `compaction_trigger` fieldless because it is a request control, not a durable response item. - Update focused history/request assertions and fixtures for stateless requests, websocket incrementals, compaction, thread injection, prompt debug, and related CI coverage.
This commit is contained in:
committed by
GitHub
Unverified
parent
7153affa0f
commit
4a82ecc3c9
@@ -2922,16 +2922,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InternalChatMessageMetadataPassthrough"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"compaction_trigger"
|
||||
|
||||
-10
@@ -15594,16 +15594,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/InternalChatMessageMetadataPassthrough"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"compaction_trigger"
|
||||
|
||||
-10
@@ -11998,16 +11998,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InternalChatMessageMetadataPassthrough"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"compaction_trigger"
|
||||
|
||||
-10
@@ -1003,16 +1003,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InternalChatMessageMetadataPassthrough"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"compaction_trigger"
|
||||
|
||||
@@ -1074,16 +1074,6 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/InternalChatMessageMetadataPassthrough"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"compaction_trigger"
|
||||
|
||||
@@ -20,4 +20,4 @@ id?: string,
|
||||
/**
|
||||
* Set when using the Responses API.
|
||||
*/
|
||||
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
|
||||
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
|
||||
|
||||
@@ -86,7 +86,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
resumed_history
|
||||
.history
|
||||
.iter()
|
||||
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if response_item == &injected_item)),
|
||||
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if responses::strip_metadata(response_item.clone()) == injected_item)),
|
||||
"injected item should be persisted in rollout history"
|
||||
);
|
||||
|
||||
|
||||
@@ -156,8 +156,10 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
search_body["input"]
|
||||
.as_array()
|
||||
.context("search input should be an array")?
|
||||
.last(),
|
||||
Some(&json!({
|
||||
.last()
|
||||
.cloned()
|
||||
.map(responses::strip_metadata_from_json),
|
||||
Some(json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "Search the web"}],
|
||||
@@ -165,7 +167,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
requests[1].function_call_output(call_id),
|
||||
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)),
|
||||
json!({
|
||||
"type": "function_call_output",
|
||||
"call_id": call_id,
|
||||
|
||||
@@ -887,6 +887,15 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
parent_thread
|
||||
.inject_user_message_without_turn("parent seed context".to_string())
|
||||
.await;
|
||||
let expected_parent_seed = parent_thread
|
||||
.codex
|
||||
.session
|
||||
.clone_history()
|
||||
.await
|
||||
.raw_items()
|
||||
.first()
|
||||
.cloned()
|
||||
.expect("parent seed should be recorded");
|
||||
let turn_context = parent_thread.codex.session.new_default_turn().await;
|
||||
let parent_spawn_call_id = "spawn-call-history".to_string();
|
||||
let trigger_message = InterAgentCommunication::new(
|
||||
@@ -988,17 +997,12 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
);
|
||||
assert_ne!(child_thread_id, parent_thread_id);
|
||||
let history = child_thread.codex.session.clone_history().await;
|
||||
let mut expected_final_answer =
|
||||
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer));
|
||||
expected_final_answer.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
let expected_history = [
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "parent seed context".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
|
||||
expected_parent_seed,
|
||||
expected_final_answer,
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
|
||||
@@ -302,6 +302,11 @@ async fn run_compact_task_inner_impl(
|
||||
let user_messages = collect_user_messages(history_items);
|
||||
|
||||
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
|
||||
if let Some(summary_item) = new_history.last_mut() {
|
||||
// This replacement history skips `record_conversation_items`; only the appended summary
|
||||
// belongs to this compaction turn.
|
||||
summary_item.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
}
|
||||
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
|
||||
|
||||
if matches!(
|
||||
|
||||
@@ -234,9 +234,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
)
|
||||
.await?;
|
||||
let mut input = prompt_input.clone();
|
||||
input.push(ResponseItem::CompactionTrigger {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
input.push(ResponseItem::CompactionTrigger {});
|
||||
let prompt = Prompt {
|
||||
input,
|
||||
tools: tool_router.model_visible_specs(),
|
||||
|
||||
@@ -2693,6 +2693,10 @@ impl Session {
|
||||
) -> Cow<'a, [ResponseItem]> {
|
||||
let mut items = Cow::Borrowed(items);
|
||||
prepare_response_items(items.to_mut());
|
||||
// Most response items get their passthrough turn ID at the durable history boundary.
|
||||
for item in items.to_mut() {
|
||||
item.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
}
|
||||
if turn_context.config.features.enabled(Feature::ItemIds) {
|
||||
Self::assign_missing_response_item_ids(items)
|
||||
} else {
|
||||
@@ -2784,8 +2788,9 @@ impl Session {
|
||||
pub(crate) async fn record_inter_agent_communication(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
communication: InterAgentCommunication,
|
||||
mut communication: InterAgentCommunication,
|
||||
) {
|
||||
communication.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
let response_item = communication.to_model_input_item();
|
||||
let items = self.prepare_conversation_items_for_history(
|
||||
turn_context,
|
||||
@@ -3355,6 +3360,10 @@ impl Session {
|
||||
{
|
||||
items.push(guardian_developer_message);
|
||||
}
|
||||
// New context windows and compaction install these items directly into replacement history.
|
||||
for item in &mut items {
|
||||
item.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::responses::strip_metadata_from_items;
|
||||
use core_test_support::test_codex::local;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_path_buf;
|
||||
@@ -1692,6 +1693,67 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
|
||||
assert_eq!(expected, history.raw_items());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let fresh_item = user_message("fresh");
|
||||
let mut existing_item = assistant_message("existing");
|
||||
existing_item.set_turn_id_if_missing("older-turn");
|
||||
|
||||
session
|
||||
.record_conversation_items(&turn_context, &[fresh_item.clone(), existing_item.clone()])
|
||||
.await;
|
||||
|
||||
let mut expected_fresh_item = fresh_item;
|
||||
expected_fresh_item.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
let expected_items = vec![expected_fresh_item, existing_item];
|
||||
assert_eq!(
|
||||
session.clone_history().await.raw_items(),
|
||||
expected_items.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
|
||||
let (mut session, turn_context) = make_session_and_context().await;
|
||||
let rollout_path = attach_thread_persistence(&mut session).await;
|
||||
let communication = InterAgentCommunication::new(
|
||||
AgentPath::root().join("worker").expect("worker path"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"child done".to_string(),
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
let mut expected_item = communication.to_model_input_item();
|
||||
expected_item.set_turn_id_if_missing(&turn_context.sub_id);
|
||||
|
||||
session
|
||||
.record_inter_agent_communication(&turn_context, communication)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
session.clone_history().await.raw_items(),
|
||||
std::slice::from_ref(&expected_item)
|
||||
);
|
||||
|
||||
session.flush_rollout().await.expect("rollout should flush");
|
||||
let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path)
|
||||
.await
|
||||
.expect("read rollout history")
|
||||
else {
|
||||
panic!("expected resumed rollout history");
|
||||
};
|
||||
|
||||
let (resumed_session, _resumed_turn_context) = make_session_and_context().await;
|
||||
resumed_session
|
||||
.record_initial_history(InitialHistory::Resumed(resumed))
|
||||
.await;
|
||||
assert_eq!(
|
||||
resumed_session.clone_history().await.raw_items(),
|
||||
std::slice::from_ref(&expected_item)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepares_image_failures_before_history_insertion() {
|
||||
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
@@ -1758,7 +1820,7 @@ async fn prepares_image_failures_before_history_insertion() {
|
||||
},
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}];
|
||||
assert_eq!(history.raw_items(), expected.as_slice());
|
||||
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -8224,7 +8286,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
|
||||
),
|
||||
);
|
||||
let expected = vec![image_message, item];
|
||||
assert_eq!(history.raw_items(), expected.as_slice());
|
||||
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
|
||||
assert_eq!(
|
||||
std::fs::read(&expected_saved_path).expect("saved file"),
|
||||
b"foo"
|
||||
@@ -8267,7 +8329,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let expected = vec![item];
|
||||
assert_eq!(history.raw_items(), expected.as_slice());
|
||||
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
|
||||
assert!(!expected_saved_path.exists());
|
||||
}
|
||||
|
||||
@@ -9019,7 +9081,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
assert!(
|
||||
history.raw_items().iter().any(|item| item == &expected),
|
||||
strip_metadata_from_items(history.raw_items()).contains(&expected),
|
||||
"expected pending input to be persisted into history on turn completion"
|
||||
);
|
||||
|
||||
|
||||
@@ -334,11 +334,13 @@ mod tests {
|
||||
session
|
||||
.record_conversation_items(&turn, std::slice::from_ref(&history_item))
|
||||
.await;
|
||||
let mut expected_history_item = history_item.clone();
|
||||
expected_history_item.set_turn_id_if_missing(&turn_id);
|
||||
let raw_history_event = rx.recv().await.expect("history raw response item event");
|
||||
let EventMsg::RawResponseItem(raw_history_item) = raw_history_event.msg else {
|
||||
panic!("expected raw response item event");
|
||||
};
|
||||
assert_eq!(raw_history_item.item, history_item);
|
||||
assert_eq!(raw_history_item.item, expected_history_item);
|
||||
let invocation = ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
@@ -377,7 +379,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
captured_call.conversation_history.items(),
|
||||
std::slice::from_ref(&history_item)
|
||||
std::slice::from_ref(&expected_history_item)
|
||||
);
|
||||
match captured_call.payload {
|
||||
ToolPayload::Function { arguments } => {
|
||||
|
||||
@@ -345,6 +345,8 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
|
||||
session
|
||||
.record_conversation_items(&turn, std::slice::from_ref(&history_item))
|
||||
.await;
|
||||
let mut expected_history_item = history_item.clone();
|
||||
expected_history_item.set_turn_id_if_missing(&turn.sub_id);
|
||||
|
||||
let router = ToolRouter::from_turn_context(
|
||||
&turn,
|
||||
@@ -404,7 +406,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
|
||||
json!({
|
||||
"arguments": { "message": "hello" },
|
||||
"callId": "call-extension",
|
||||
"conversationHistory": [history_item],
|
||||
"conversationHistory": [expected_history_item],
|
||||
"ok": true,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -266,7 +266,7 @@ fn format_request_body_snapshot(
|
||||
request: &ResponsesRequest,
|
||||
options: &ContextSnapshotOptions,
|
||||
) -> String {
|
||||
let mut body = request.body_json();
|
||||
let mut body = crate::responses::strip_metadata_from_json(request.body_json());
|
||||
canonicalize_json_snapshot_value(&mut body, options);
|
||||
serde_json::to_string_pretty(&body).expect("request body should serialize")
|
||||
}
|
||||
|
||||
@@ -98,6 +98,35 @@ fn decode_body_bytes(body: &[u8], content_encoding: Option<&str>) -> Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a response item without internal transport metadata for semantic assertions.
|
||||
pub fn strip_metadata(mut item: ResponseItem) -> ResponseItem {
|
||||
item.clear_internal_chat_message_metadata_passthrough();
|
||||
item
|
||||
}
|
||||
|
||||
/// Returns response items without internal transport metadata for semantic assertions.
|
||||
pub fn strip_metadata_from_items(items: &[ResponseItem]) -> Vec<ResponseItem> {
|
||||
items.iter().cloned().map(strip_metadata).collect()
|
||||
}
|
||||
|
||||
/// Returns JSON without internal transport metadata for semantic assertions.
|
||||
pub fn strip_metadata_from_json(value: Value) -> Value {
|
||||
match value {
|
||||
Value::Array(values) => {
|
||||
Value::Array(values.into_iter().map(strip_metadata_from_json).collect())
|
||||
}
|
||||
Value::Object(mut map) => {
|
||||
map.remove("internal_chat_message_metadata_passthrough");
|
||||
Value::Object(
|
||||
map.into_iter()
|
||||
.map(|(key, value)| (key, strip_metadata_from_json(value)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
value => value,
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponsesRequest {
|
||||
pub fn body_json(&self) -> Value {
|
||||
let body = decode_body_bytes(
|
||||
@@ -1092,11 +1121,17 @@ pub async fn mount_compact_user_history_with_summary_sequence(
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
// Append a synthetic compaction item as the newest item.
|
||||
output.push(serde_json::json!({
|
||||
let compaction_turn_id = body_json["client_metadata"]["turn_id"].as_str();
|
||||
// Match Responses API: generated compaction items inherit the compact request turn.
|
||||
let mut compaction_item = serde_json::json!({
|
||||
"type": "compaction",
|
||||
"encrypted_content": summary_text,
|
||||
}));
|
||||
});
|
||||
if let Some(turn_id) = compaction_turn_id {
|
||||
compaction_item["internal_chat_message_metadata_passthrough"] =
|
||||
serde_json::json!({ "turn_id": turn_id });
|
||||
}
|
||||
output.push(compaction_item);
|
||||
ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "application/json")
|
||||
.set_body_json(serde_json::json!({ "output": output }))
|
||||
|
||||
@@ -175,11 +175,14 @@ async fn websocket_v2_test_codex_shell_chain() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let call_id = "shell-command-call";
|
||||
let mut shell_command_call = ev_shell_command_call(call_id, "echo websocket");
|
||||
shell_command_call["item"]["internal_chat_message_metadata_passthrough"] =
|
||||
serde_json::json!({"turn_id": "turn-123"});
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_shell_command_call(call_id, "echo websocket"),
|
||||
shell_command_call,
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![
|
||||
|
||||
@@ -66,6 +66,7 @@ use core_test_support::responses::mount_sse_once_match;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::sse_failed;
|
||||
use core_test_support::responses::strip_metadata_from_json;
|
||||
use core_test_support::responses_metadata as test_responses_metadata;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
@@ -185,6 +186,65 @@ fn assert_codex_client_metadata(
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn openai_stateless_responses_requests_preserve_item_turn_metadata_across_turns() {
|
||||
let server = MockServer::start().await;
|
||||
let response_mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp1"),
|
||||
ev_assistant_message("msg-1", "first answer"),
|
||||
ev_completed("resp1"),
|
||||
]),
|
||||
sse(vec![ev_response_created("resp2"), ev_completed("resp2")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let test = test_codex().build(&server).await.unwrap();
|
||||
|
||||
test.submit_turn("turn one").await.unwrap();
|
||||
test.submit_turn("turn two").await.unwrap();
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let first = requests[0].body_json();
|
||||
let second = requests[1].body_json();
|
||||
let first_turn_id = first["client_metadata"]["turn_id"]
|
||||
.as_str()
|
||||
.expect("first request should include turn id");
|
||||
let second_turn_id = second["client_metadata"]["turn_id"]
|
||||
.as_str()
|
||||
.expect("second request should include turn id");
|
||||
assert_ne!(first_turn_id, second_turn_id);
|
||||
|
||||
let first_input = first["input"].as_array().expect("first input");
|
||||
let second_input = second["input"].as_array().expect("second input");
|
||||
assert_eq!(&second_input[..first_input.len()], first_input.as_slice());
|
||||
for item in first_input {
|
||||
assert_eq!(
|
||||
item["internal_chat_message_metadata_passthrough"]["turn_id"].as_str(),
|
||||
Some(first_turn_id)
|
||||
);
|
||||
}
|
||||
|
||||
let item_turn_id = |text: &str| {
|
||||
second_input
|
||||
.iter()
|
||||
.find(|item| {
|
||||
item["content"].as_array().is_some_and(|content| {
|
||||
content
|
||||
.iter()
|
||||
.any(|content_item| content_item["text"].as_str() == Some(text))
|
||||
})
|
||||
})
|
||||
.and_then(|item| item["internal_chat_message_metadata_passthrough"]["turn_id"].as_str())
|
||||
};
|
||||
assert_eq!(item_turn_id("turn one"), Some(first_turn_id));
|
||||
assert_eq!(item_turn_id("first answer"), Some(first_turn_id));
|
||||
assert_eq!(item_turn_id("turn two"), Some(second_turn_id));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn non_openai_responses_requests_omit_item_passthrough_metadata() {
|
||||
let server = MockServer::start().await;
|
||||
@@ -3612,7 +3672,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
|
||||
let tail_len = r3_tail_expected.as_array().unwrap().len();
|
||||
let actual_tail = &r3_input_array[r3_input_array.len() - tail_len..];
|
||||
assert_eq!(
|
||||
serde_json::Value::Array(actual_tail.to_vec()),
|
||||
strip_metadata_from_json(serde_json::Value::Array(actual_tail.to_vec())),
|
||||
r3_tail_expected,
|
||||
"request 3 tail mismatch",
|
||||
);
|
||||
|
||||
@@ -1564,10 +1564,13 @@ async fn responses_websocket_uses_incremental_create_on_prefix() {
|
||||
async fn responses_websocket_forwards_turn_metadata_on_initial_and_incremental_create() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let mut first_output_item = ev_assistant_message("msg-1", "assistant output");
|
||||
first_output_item["item"]["internal_chat_message_metadata_passthrough"] =
|
||||
json!({"turn_id": "turn-123"});
|
||||
let server = start_websocket_server(vec![vec![
|
||||
vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "assistant output"),
|
||||
first_output_item,
|
||||
ev_completed("resp-1"),
|
||||
],
|
||||
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||
@@ -1577,9 +1580,11 @@ async fn responses_websocket_forwards_turn_metadata_on_initial_and_incremental_c
|
||||
let harness = websocket_harness(&server).await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let mut prior_assistant_output = assistant_message_item("msg-1", "assistant output");
|
||||
prior_assistant_output.set_turn_id_if_missing("turn-123");
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
assistant_message_item("msg-1", "assistant output"),
|
||||
prior_assistant_output,
|
||||
message_item("second"),
|
||||
]);
|
||||
let first_responses_metadata = turn_metadata(&harness, Some("turn-123"));
|
||||
|
||||
@@ -924,6 +924,7 @@ fn normalize_value(value: Value) -> Value {
|
||||
Value::Array(values) => Value::Array(values.into_iter().map(normalize_value).collect()),
|
||||
Value::Object(map) => Value::Object(
|
||||
map.into_iter()
|
||||
.filter(|(key, _value)| key != "internal_chat_message_metadata_passthrough")
|
||||
.map(|(key, value)| (key, normalize_value(value)))
|
||||
.collect(),
|
||||
),
|
||||
|
||||
@@ -181,7 +181,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(responses::strip_metadata(actual), expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -272,7 +272,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(responses::strip_metadata(actual), expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::responses::strip_metadata_from_json;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
@@ -54,6 +55,13 @@ fn text_user_input_parts(texts: Vec<String>) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_eq_without_metadata(left: serde_json::Value, right: serde_json::Value) {
|
||||
assert_eq!(
|
||||
strip_metadata_from_json(left),
|
||||
strip_metadata_from_json(right)
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_default_env_context(text: &str, cwd: &str) {
|
||||
assert_env_context_fragment(text);
|
||||
assert!(
|
||||
@@ -384,16 +392,18 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
Some("input_text"),
|
||||
"expected environment context bundled after UI message in cached contextual message"
|
||||
);
|
||||
assert_eq!(input1[2], text_user_input("hello 1".to_string()));
|
||||
assert_eq_without_metadata(input1[2].clone(), text_user_input("hello 1".to_string()));
|
||||
|
||||
let body2 = req2.single_request().body_json();
|
||||
let input2 = body2["input"].as_array().expect("input array");
|
||||
assert_eq!(
|
||||
&input2[..input1.len()],
|
||||
input1.as_slice(),
|
||||
"expected cached prefix to be reused"
|
||||
assert_eq_without_metadata(
|
||||
serde_json::Value::Array(input2[..input1.len()].to_vec()),
|
||||
serde_json::Value::Array(input1.to_vec()),
|
||||
);
|
||||
assert_eq_without_metadata(
|
||||
input2[input1.len()].clone(),
|
||||
text_user_input("hello 2".to_string()),
|
||||
);
|
||||
assert_eq!(input2[input1.len()], text_user_input("hello 2".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -523,7 +533,10 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
|
||||
expected_body2.push(expected_permissions_msg_2);
|
||||
expected_body2.push(expected_env_msg_2);
|
||||
expected_body2.push(expected_user_message_2);
|
||||
assert_eq!(body2["input"], serde_json::Value::Array(expected_body2));
|
||||
assert_eq_without_metadata(
|
||||
body2["input"].clone(),
|
||||
serde_json::Value::Array(expected_body2),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -810,7 +823,10 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
expected_body2.push(expected_settings_update_msg);
|
||||
expected_body2.push(expected_env_msg_2);
|
||||
expected_body2.push(expected_user_message_2);
|
||||
assert_eq!(body2["input"], serde_json::Value::Array(expected_body2));
|
||||
assert_eq_without_metadata(
|
||||
body2["input"].clone(),
|
||||
serde_json::Value::Array(expected_body2),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -818,7 +834,6 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let req1 = mount_sse_once(
|
||||
@@ -940,7 +955,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
expected_contextual_user_msg_1.clone(),
|
||||
expected_user_message_1.clone(),
|
||||
]);
|
||||
assert_eq!(body1["input"], expected_input_1);
|
||||
assert_eq_without_metadata(body1["input"].clone(), expected_input_1);
|
||||
|
||||
let expected_user_message_2 = text_user_input("hello 2".to_string());
|
||||
let expected_input_2 = serde_json::Value::Array(vec![
|
||||
@@ -949,7 +964,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
expected_user_message_1,
|
||||
expected_user_message_2,
|
||||
]);
|
||||
assert_eq!(body2["input"], expected_input_2);
|
||||
assert_eq_without_metadata(body2["input"].clone(), expected_input_2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1079,7 +1094,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
expected_contextual_user_msg_1.clone(),
|
||||
expected_user_message_1.clone(),
|
||||
]);
|
||||
assert_eq!(body1["input"], expected_input_1);
|
||||
assert_eq_without_metadata(body1["input"].clone(), expected_input_1);
|
||||
|
||||
let body1_input = body1["input"].as_array().expect("input array");
|
||||
let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
|
||||
@@ -1118,7 +1133,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
expected_env_update_msg,
|
||||
expected_user_message_2,
|
||||
]);
|
||||
assert_eq!(body2["input"], expected_input_2);
|
||||
assert_eq_without_metadata(body2["input"].clone(), expected_input_2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_home::CodexHomeUserInstructionsProvider;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::strip_metadata;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -51,7 +52,10 @@ async fn build_prompt_input_includes_context_and_user_message() -> Result<()> {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
assert_eq!(input.last(), Some(&expected_user_message));
|
||||
assert_eq!(
|
||||
input.last().cloned().map(strip_metadata),
|
||||
Some(expected_user_message)
|
||||
);
|
||||
assert!(input.iter().any(|item| {
|
||||
let ResponseItem::Message { content, .. } = item else {
|
||||
return false;
|
||||
|
||||
@@ -28,6 +28,7 @@ use core_test_support::responses::namespace_child_tool;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::sse_response;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::responses::strip_metadata_from_json;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
@@ -1085,8 +1086,8 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
|
||||
.pop()
|
||||
.expect("child request");
|
||||
assert_eq!(
|
||||
child_request.inputs_of_type("agent_message"),
|
||||
vec![json!({
|
||||
strip_metadata_from_json(Value::Array(child_request.inputs_of_type("agent_message"))),
|
||||
Value::Array(vec![json!({
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/worker",
|
||||
@@ -1100,7 +1101,7 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
|
||||
"encrypted_content": encrypted_message,
|
||||
},
|
||||
],
|
||||
})]
|
||||
})])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -1228,8 +1229,8 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
||||
.pop()
|
||||
.expect("agent message request");
|
||||
assert_eq!(
|
||||
request.inputs_of_type("agent_message"),
|
||||
vec![json!({
|
||||
strip_metadata_from_json(Value::Array(request.inputs_of_type("agent_message"))),
|
||||
Value::Array(vec![json!({
|
||||
"type": "agent_message",
|
||||
"author": "/root/worker",
|
||||
"recipient": "/root",
|
||||
@@ -1237,7 +1238,7 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message(
|
||||
"type": "input_text",
|
||||
"text": notification,
|
||||
}],
|
||||
})]
|
||||
})])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -915,6 +915,20 @@ pub struct InternalChatMessageMetadataPassthrough {
|
||||
pub turn_id: Option<String>,
|
||||
}
|
||||
|
||||
impl InternalChatMessageMetadataPassthrough {
|
||||
pub(crate) fn set_turn_id_if_missing(metadata: &mut Option<Self>, turn_id: &str) {
|
||||
if turn_id.is_empty()
|
||||
|| metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.turn_id.as_deref())
|
||||
.is_some_and(|turn_id| !turn_id.is_empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
metadata.get_or_insert_with(Self::default).turn_id = Some(turn_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ResponseItem {
|
||||
@@ -1120,13 +1134,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
|
||||
},
|
||||
// Compaction triggers are request controls, and the Responses API does not
|
||||
// accept an `id` field for them.
|
||||
CompactionTrigger {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
|
||||
},
|
||||
// Compaction triggers are request controls, not durable response items.
|
||||
CompactionTrigger {},
|
||||
ContextCompaction {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1198,16 +1207,11 @@ impl ResponseItem {
|
||||
}
|
||||
|
||||
/// Stamps the item with `turn_id` unless it already has a non-empty turn ID.
|
||||
pub fn stamp_turn_id_if_missing(&mut self, turn_id: &str) {
|
||||
if turn_id.is_empty() || self.turn_id().is_some() {
|
||||
return;
|
||||
}
|
||||
pub fn set_turn_id_if_missing(&mut self, turn_id: &str) {
|
||||
let Some(metadata) = self.internal_chat_message_metadata_passthrough_mut() else {
|
||||
return;
|
||||
};
|
||||
metadata
|
||||
.get_or_insert_with(InternalChatMessageMetadataPassthrough::default)
|
||||
.turn_id = Some(turn_id.to_string());
|
||||
InternalChatMessageMetadataPassthrough::set_turn_id_if_missing(metadata, turn_id);
|
||||
}
|
||||
|
||||
/// Removes internal chat message metadata passthrough before sending to a provider that does
|
||||
@@ -1274,15 +1278,11 @@ impl ResponseItem {
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
}
|
||||
| Self::CompactionTrigger {
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
}
|
||||
| Self::ContextCompaction {
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
} => metadata.as_ref(),
|
||||
Self::Other => None,
|
||||
Self::CompactionTrigger { .. } | Self::Other => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,15 +1342,11 @@ impl ResponseItem {
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
}
|
||||
| Self::CompactionTrigger {
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
}
|
||||
| Self::ContextCompaction {
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
} => Some(metadata),
|
||||
Self::Other => None,
|
||||
Self::CompactionTrigger { .. } | Self::Other => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2206,23 +2202,23 @@ mod tests {
|
||||
}))?;
|
||||
assert_eq!(unknown_metadata, item);
|
||||
|
||||
item.stamp_turn_id_if_missing("turn-2");
|
||||
item.set_turn_id_if_missing("turn-2");
|
||||
assert_eq!(item.turn_id(), Some("turn-1"));
|
||||
|
||||
let mut empty_turn_id =
|
||||
response_item_with_passthrough_metadata(Some(passthrough_metadata("")));
|
||||
empty_turn_id.stamp_turn_id_if_missing("turn-1");
|
||||
empty_turn_id.set_turn_id_if_missing("turn-1");
|
||||
assert_eq!(empty_turn_id.turn_id(), Some("turn-1"));
|
||||
|
||||
let mut missing_turn_id = response_item_with_passthrough_metadata(
|
||||
/*internal_chat_message_metadata_passthrough*/ None,
|
||||
);
|
||||
missing_turn_id.stamp_turn_id_if_missing("");
|
||||
missing_turn_id.stamp_turn_id_if_missing("turn-1");
|
||||
missing_turn_id.set_turn_id_if_missing("");
|
||||
missing_turn_id.set_turn_id_if_missing("turn-1");
|
||||
assert_eq!(missing_turn_id.turn_id(), Some("turn-1"));
|
||||
|
||||
let mut other = ResponseItem::Other;
|
||||
other.stamp_turn_id_if_missing("turn-1");
|
||||
other.set_turn_id_if_missing("turn-1");
|
||||
assert_eq!(other.turn_id(), None);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3111,9 +3107,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn serializes_compaction_trigger_without_payload() -> Result<()> {
|
||||
let item = ResponseItem::CompactionTrigger {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
let item = ResponseItem::CompactionTrigger {};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(item)?,
|
||||
@@ -3124,37 +3118,13 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_stamped_compaction_trigger_passthrough_metadata() -> Result<()> {
|
||||
let mut item = ResponseItem::CompactionTrigger {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
item.stamp_turn_id_if_missing("turn-1");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(item)?,
|
||||
serde_json::json!({
|
||||
"type": "compaction_trigger",
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn-1",
|
||||
},
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_compaction_trigger_without_payload() -> Result<()> {
|
||||
let json = r#"{"type":"compaction_trigger"}"#;
|
||||
|
||||
let item: ResponseItem = serde_json::from_str(json)?;
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::CompactionTrigger {
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(item, ResponseItem::CompactionTrigger {});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use crate::models::AgentMessageInputContent;
|
||||
use crate::models::BaseInstructions;
|
||||
use crate::models::ContentItem;
|
||||
use crate::models::ImageDetail;
|
||||
use crate::models::InternalChatMessageMetadataPassthrough;
|
||||
use crate::models::MessagePhase;
|
||||
use crate::models::PermissionProfile;
|
||||
use crate::models::ResponseInputItem;
|
||||
@@ -698,6 +699,9 @@ pub struct InterAgentCommunication {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub encrypted_content: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
|
||||
pub trigger_turn: bool,
|
||||
}
|
||||
|
||||
@@ -715,6 +719,7 @@ impl InterAgentCommunication {
|
||||
other_recipients,
|
||||
content,
|
||||
encrypted_content: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
trigger_turn,
|
||||
}
|
||||
}
|
||||
@@ -732,15 +737,25 @@ impl InterAgentCommunication {
|
||||
other_recipients,
|
||||
content: String::new(),
|
||||
encrypted_content: Some(encrypted_content),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
trigger_turn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_turn_id_if_missing(&mut self, turn_id: &str) {
|
||||
InternalChatMessageMetadataPassthrough::set_turn_id_if_missing(
|
||||
&mut self.internal_chat_message_metadata_passthrough,
|
||||
turn_id,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn to_response_input_item(&self) -> ResponseInputItem {
|
||||
let mut communication = self.clone();
|
||||
communication.internal_chat_message_metadata_passthrough = None;
|
||||
ResponseInputItem::Message {
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: serde_json::to_string(self).unwrap_or_default(),
|
||||
text: serde_json::to_string(&communication).unwrap_or_default(),
|
||||
}],
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
}
|
||||
@@ -775,7 +790,9 @@ impl InterAgentCommunication {
|
||||
author: self.author.to_string(),
|
||||
recipient: self.recipient.to_string(),
|
||||
content,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
internal_chat_message_metadata_passthrough: self
|
||||
.internal_chat_message_metadata_passthrough
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4339,21 +4356,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn inter_agent_communication_response_input_item_preserves_commentary_phase() {
|
||||
let communication = InterAgentCommunication {
|
||||
let mut communication = InterAgentCommunication {
|
||||
author: AgentPath::root(),
|
||||
recipient: AgentPath::root().join("reviewer").expect("recipient path"),
|
||||
other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
|
||||
content: "review the diff".to_string(),
|
||||
encrypted_content: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
trigger_turn: true,
|
||||
};
|
||||
communication.set_turn_id_if_missing("turn-1");
|
||||
let mut serialized_communication = communication.clone();
|
||||
serialized_communication.internal_chat_message_metadata_passthrough = None;
|
||||
|
||||
assert_eq!(
|
||||
communication.to_response_input_item(),
|
||||
ResponseInputItem::Message {
|
||||
role: "assistant".to_string(),
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: serde_json::to_string(&communication).expect("serialize communication"),
|
||||
text: serde_json::to_string(&serialized_communication)
|
||||
.expect("serialize communication"),
|
||||
}],
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user