diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 85e5940bc..6f5ebcdf0 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1038,6 +1038,13 @@ ], "type": "string" }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -1317,6 +1324,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 9e3cbb415..3e73bdc8b 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -3111,6 +3111,13 @@ } ] }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -3596,6 +3603,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 6df9a010b..5d9e719d8 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3890,6 +3890,13 @@ ], "type": "string" }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -4636,6 +4643,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index e92d46e7b..571079c7c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -5839,6 +5839,13 @@ } ] }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -6536,6 +6543,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, @@ -11731,6 +11748,13 @@ ], "type": "string" }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -12449,6 +12473,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/v2/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index ae6e70522..ad251a461 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -3111,6 +3111,13 @@ } ] }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -3596,6 +3603,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json index e1cc626cc..41284b009 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationParams.json @@ -266,6 +266,13 @@ ], "type": "string" }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "NewConversationParams": { "properties": { "approvalPolicy": { @@ -437,6 +444,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index cdad2018c..abd5beb21 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -3111,6 +3111,13 @@ } ] }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -3596,6 +3603,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index e6ed4d042..69c2c4b8b 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -3111,6 +3111,13 @@ } ] }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ModeKind": { "description": "Initial collaboration mode to use when the TUI starts.", "enum": [ @@ -3596,6 +3603,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index 47039df05..aecb8c2b5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -233,6 +233,13 @@ ], "type": "string" }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "ReasoningItemContent": { "oneOf": [ { @@ -324,6 +331,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 3da1e3c74..52567cec8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -242,6 +242,13 @@ ], "type": "string" }, + "MessagePhase": { + "enum": [ + "commentary", + "final_answer" + ], + "type": "string" + }, "Personality": { "enum": [ "friendly", @@ -340,6 +347,16 @@ ], "writeOnly": true }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, "role": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/MessagePhase.ts b/codex-rs/app-server-protocol/schema/typescript/MessagePhase.ts new file mode 100644 index 000000000..d339c0fa8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/MessagePhase.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type MessagePhase = "commentary" | "final_answer"; diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index d72002161..611c7fb22 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -6,11 +6,12 @@ import type { FunctionCallOutputPayload } from "./FunctionCallOutputPayload"; import type { GhostCommit } from "./GhostCommit"; import type { LocalShellAction } from "./LocalShellAction"; import type { LocalShellStatus } from "./LocalShellStatus"; +import type { MessagePhase } from "./MessagePhase"; import type { ReasoningItemContent } from "./ReasoningItemContent"; import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; import type { WebSearchAction } from "./WebSearchAction"; -export type ResponseItem = { "type": "message", role: string, content: Array, end_turn?: boolean, } | { "type": "reasoning", summary: Array, content?: Array, encrypted_content: string | null, } | { "type": "local_shell_call", +export type ResponseItem = { "type": "message", role: string, content: Array, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array, content?: Array, encrypted_content: string | null, } | { "type": "local_shell_call", /** * Set when using the Responses API. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 191f99386..b24e42a83 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -115,6 +115,7 @@ export type { McpStartupStatus } from "./McpStartupStatus"; export type { McpStartupUpdateEvent } from "./McpStartupUpdateEvent"; export type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent"; export type { McpToolCallEndEvent } from "./McpToolCallEndEvent"; +export type { MessagePhase } from "./MessagePhase"; export type { ModeKind } from "./ModeKind"; export type { NetworkAccess } from "./NetworkAccess"; export type { NewConversationParams } from "./NewConversationParams"; diff --git a/codex-rs/app-server/tests/suite/list_resume.rs b/codex-rs/app-server/tests/suite/list_resume.rs index f2b82c611..efa90ea35 100644 --- a/codex-rs/app-server/tests/suite/list_resume.rs +++ b/codex-rs/app-server/tests/suite/list_resume.rs @@ -308,6 +308,7 @@ async fn test_list_and_resume_conversations() -> Result<()> { text: fork_history_text.to_string(), }], end_turn: None, + phase: None, }]; let resume_with_history_req_id = mcp .send_resume_conversation_request(ResumeConversationParams { diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 66cf43bc0..eeedcb286 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -126,6 +126,7 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() text: "REMOTE_COMPACT_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index efa51573c..358fec351 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -338,6 +338,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> { text: history_text.to_string(), }], end_turn: None, + phase: None, }]; // Resume with explicit history and override the model. diff --git a/codex-rs/codex-api/src/endpoint/chat.rs b/codex-rs/codex-api/src/endpoint/chat.rs index 2148a5ad9..b183597d6 100644 --- a/codex-rs/codex-api/src/endpoint/chat.rs +++ b/codex-rs/codex-api/src/endpoint/chat.rs @@ -194,6 +194,7 @@ impl Stream for AggregatedStream { text: std::mem::take(&mut this.cumulative), }], end_turn: None, + phase: None, }; this.pending .push_back(ResponseEvent::OutputItemDone(aggregated_message)); diff --git a/codex-rs/codex-api/src/requests/chat.rs b/codex-rs/codex-api/src/requests/chat.rs index 5c16a5fb5..60a591e5c 100644 --- a/codex-rs/codex-api/src/requests/chat.rs +++ b/codex-rs/codex-api/src/requests/chat.rs @@ -387,6 +387,7 @@ mod tests { text: "hi".to_string(), }], end_turn: None, + phase: None, }]; let req = ChatRequestBuilder::new("gpt-test", "inst", &prompt_input, &[]) .conversation_id(Some("conv-1".into())) @@ -414,6 +415,7 @@ mod tests { text: "read these".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::FunctionCall { id: None, diff --git a/codex-rs/codex-api/src/requests/responses.rs b/codex-rs/codex-api/src/requests/responses.rs index 73a413dd9..a0e90db06 100644 --- a/codex-rs/codex-api/src/requests/responses.rs +++ b/codex-rs/codex-api/src/requests/responses.rs @@ -224,12 +224,14 @@ mod tests { role: "assistant".into(), content: Vec::new(), end_turn: None, + phase: None, }, ResponseItem::Message { id: None, role: "assistant".into(), content: Vec::new(), end_turn: None, + phase: None, }, ]; diff --git a/codex-rs/codex-api/src/sse/chat.rs b/codex-rs/codex-api/src/sse/chat.rs index a3effce2e..4b7884528 100644 --- a/codex-rs/codex-api/src/sse/chat.rs +++ b/codex-rs/codex-api/src/sse/chat.rs @@ -331,6 +331,7 @@ async fn append_assistant_text( role: "assistant".to_string(), content: vec![], end_turn: None, + phase: None, }; *assistant_item = Some(item.clone()); let _ = tx_event diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index b363671a1..2c9110472 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -429,6 +429,7 @@ mod tests { use super::*; use assert_matches::assert_matches; use bytes::Bytes; + use codex_protocol::models::MessagePhase; use codex_protocol::models::ResponseItem; use futures::stream; use pretty_assertions::assert_eq; @@ -492,7 +493,8 @@ mod tests { "item": { "type": "message", "role": "assistant", - "content": [{"type": "output_text", "text": "Hello"}] + "content": [{"type": "output_text", "text": "Hello"}], + "phase": "commentary" } }) .to_string(); @@ -523,8 +525,11 @@ mod tests { assert_matches!( &events[0], - Ok(ResponseEvent::OutputItemDone(ResponseItem::Message { role, .. })) - if role == "assistant" + Ok(ResponseEvent::OutputItemDone(ResponseItem::Message { + role, + phase: Some(MessagePhase::Commentary), + .. + })) if role == "assistant" ); assert_matches!( diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index b71edf324..3040fb872 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -309,6 +309,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { text: "hi".to_string(), }], end_turn: None, + phase: None, }], tools: Vec::::new(), parallel_tool_calls: false, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6cc389b53..7c9909355 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1834,6 +1834,7 @@ impl Session { text: format!("Warning: {}", message.into()), }], end_turn: None, + phase: None, }; self.record_conversation_items(ctx, &[item]).await; @@ -4542,6 +4543,7 @@ mod tests { text: text.to_string(), }], end_turn: None, + phase: None, } } @@ -4823,6 +4825,7 @@ mod tests { text: "turn 1 user".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -4831,6 +4834,7 @@ mod tests { text: "turn 1 assistant".to_string(), }], end_turn: None, + phase: None, }, ]; sess.record_into_history(&turn_1, tc.as_ref()).await; @@ -4843,6 +4847,7 @@ mod tests { text: "turn 2 user".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -4851,6 +4856,7 @@ mod tests { text: "turn 2 assistant".to_string(), }], end_turn: None, + phase: None, }, ]; sess.record_into_history(&turn_2, tc.as_ref()).await; @@ -4883,6 +4889,7 @@ mod tests { text: "turn 1 user".to_string(), }], end_turn: None, + phase: None, }]; sess.record_into_history(&turn_1, tc.as_ref()).await; @@ -5832,6 +5839,7 @@ mod tests { text: "first user".to_string(), }], end_turn: None, + phase: None, }; live_history.record_items(std::iter::once(&user1), turn_context.truncation_policy); rollout_items.push(RolloutItem::ResponseItem(user1.clone())); @@ -5843,6 +5851,7 @@ mod tests { text: "assistant reply one".to_string(), }], end_turn: None, + phase: None, }; live_history.record_items(std::iter::once(&assistant1), turn_context.truncation_policy); rollout_items.push(RolloutItem::ResponseItem(assistant1.clone())); @@ -5868,6 +5877,7 @@ mod tests { text: "second user".to_string(), }], end_turn: None, + phase: None, }; live_history.record_items(std::iter::once(&user2), turn_context.truncation_policy); rollout_items.push(RolloutItem::ResponseItem(user2.clone())); @@ -5879,6 +5889,7 @@ mod tests { text: "assistant reply two".to_string(), }], end_turn: None, + phase: None, }; live_history.record_items(std::iter::once(&assistant2), turn_context.truncation_policy); rollout_items.push(RolloutItem::ResponseItem(assistant2.clone())); @@ -5904,6 +5915,7 @@ mod tests { text: "third user".to_string(), }], end_turn: None, + phase: None, }; live_history.record_items(std::iter::once(&user3), turn_context.truncation_policy); rollout_items.push(RolloutItem::ResponseItem(user3)); @@ -5915,6 +5927,7 @@ mod tests { text: "assistant reply three".to_string(), }], end_turn: None, + phase: None, }; live_history.record_items(std::iter::once(&assistant3), turn_context.truncation_policy); rollout_items.push(RolloutItem::ResponseItem(assistant3)); diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index b44d43f42..ee94e4994 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -311,6 +311,7 @@ fn build_compacted_history_with_limit( text: message.clone(), }], end_turn: None, + phase: None, }); } @@ -325,6 +326,7 @@ fn build_compacted_history_with_limit( role: "user".to_string(), content: vec![ContentItem::InputText { text: summary_text }], end_turn: None, + phase: None, }); history @@ -416,6 +418,7 @@ mod tests { text: "ignored".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: Some("user".to_string()), @@ -424,6 +427,7 @@ mod tests { text: "first".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Other, ]; @@ -444,6 +448,7 @@ mod tests { .to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -452,6 +457,7 @@ mod tests { text: "cwd=/tmp".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -460,6 +466,7 @@ mod tests { text: "real user message".to_string(), }], end_turn: None, + phase: None, }, ]; @@ -545,6 +552,7 @@ mod tests { text: marker.clone(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -553,6 +561,7 @@ mod tests { text: "real user message".to_string(), }], end_turn: None, + phase: None, }, ]; diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 50ee98e5b..d31f731a9 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -24,6 +24,7 @@ fn assistant_msg(text: &str) -> ResponseItem { text: text.to_string(), }], end_turn: None, + phase: None, } } @@ -43,6 +44,7 @@ fn user_msg(text: &str) -> ResponseItem { text: text.to_string(), }], end_turn: None, + phase: None, } } @@ -54,6 +56,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem { text: text.to_string(), }], end_turn: None, + phase: None, } } @@ -97,6 +100,7 @@ fn filters_non_api_messages() { text: "ignored".to_string(), }], end_turn: None, + phase: None, }; let reasoning = reasoning_msg("thinking..."); h.record_items([&system, &reasoning, &ResponseItem::Other], policy); @@ -127,6 +131,7 @@ fn filters_non_api_messages() { text: "hi".to_string() }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -135,6 +140,7 @@ fn filters_non_api_messages() { text: "hello".to_string() }], end_turn: None, + phase: None, } ] ); @@ -262,6 +268,7 @@ fn replace_last_turn_images_does_not_touch_user_images() { image_url: "data:image/png;base64,AAA".to_string(), }], end_turn: None, + phase: None, }]; let mut history = create_history_with_items(items.clone()); diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index 87692a2f0..9f5455a69 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -81,6 +81,7 @@ impl From for ResponseItem { text: ec.serialize_to_xml(), }], end_turn: None, + phase: None, } } } diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 35804fd47..2ad19d3df 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -177,6 +177,7 @@ mod tests { }, ], end_turn: None, + phase: None, }; let turn_item = parse_turn_item(&item).expect("expected user message turn item"); @@ -219,6 +220,7 @@ mod tests { }, ], end_turn: None, + phase: None, }; let turn_item = parse_turn_item(&item).expect("expected user message turn item"); @@ -260,6 +262,7 @@ mod tests { }, ], end_turn: None, + phase: None, }; let turn_item = parse_turn_item(&item).expect("expected user message turn item"); @@ -289,6 +292,7 @@ mod tests { text: "test_text".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -297,6 +301,7 @@ mod tests { text: "test_text".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -305,6 +310,7 @@ mod tests { text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -314,6 +320,7 @@ mod tests { .to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -322,6 +329,7 @@ mod tests { text: "echo 42".to_string(), }], end_turn: None, + phase: None, }, ]; @@ -340,6 +348,7 @@ mod tests { text: "Hello from Codex".to_string(), }], end_turn: None, + phase: None, }; let turn_item = parse_turn_item(&item).expect("expected agent message turn item"); diff --git a/codex-rs/core/src/instructions/user_instructions.rs b/codex-rs/core/src/instructions/user_instructions.rs index 611bf4cbf..525834847 100644 --- a/codex-rs/core/src/instructions/user_instructions.rs +++ b/codex-rs/core/src/instructions/user_instructions.rs @@ -39,6 +39,7 @@ impl From for ResponseItem { ), }], end_turn: None, + phase: None, } } } @@ -73,6 +74,7 @@ impl From for ResponseItem { ), }], end_turn: None, + phase: None, } } } diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index cdf16291d..ee750b126 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -902,6 +902,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { text: format!("reply-{idx}"), }], end_turn: None, + phase: None, }), }; writeln!(file, "{}", serde_json::to_string(&response_line)?)?; diff --git a/codex-rs/core/src/rollout/truncation.rs b/codex-rs/core/src/rollout/truncation.rs index e6a84628f..c50eacc48 100644 --- a/codex-rs/core/src/rollout/truncation.rs +++ b/codex-rs/core/src/rollout/truncation.rs @@ -86,6 +86,7 @@ mod tests { text: text.to_string(), }], end_turn: None, + phase: None, } } @@ -97,6 +98,7 @@ mod tests { text: text.to_string(), }], end_turn: None, + phase: None, } } diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index a487041eb..d5d6f6058 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -265,6 +265,7 @@ impl Session { ), }], end_turn: None, + phase: None, }; self.record_into_history(std::slice::from_ref(&marker), task.turn_context.as_ref()) .await; diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index d156d3e0d..cfc554f85 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -222,6 +222,7 @@ pub(crate) async fn exit_review_mode( role: "user".to_string(), content: vec![ContentItem::InputText { text: user_message }], end_turn: None, + phase: None, }], ) .await; @@ -241,6 +242,7 @@ pub(crate) async fn exit_review_mode( text: assistant_message, }], end_turn: None, + phase: None, }, ) .await; diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 9b633ded3..37bd0efab 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -462,6 +462,7 @@ mod tests { text: text.to_string(), }], end_turn: None, + phase: None, } } fn assistant_msg(text: &str) -> ResponseItem { @@ -472,6 +473,7 @@ mod tests { text: text.to_string(), }], end_turn: None, + phase: None, } } diff --git a/codex-rs/core/src/user_shell_command.rs b/codex-rs/core/src/user_shell_command.rs index 566f39958..80128df00 100644 --- a/codex-rs/core/src/user_shell_command.rs +++ b/codex-rs/core/src/user_shell_command.rs @@ -63,6 +63,7 @@ pub fn user_shell_command_record_item( text: format_user_shell_command_record(command, exec_output, turn_context), }], end_turn: None, + phase: None, } } diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index 5406b44ad..277209144 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -139,6 +139,7 @@ fn user_message(text: &str) -> ResponseItem { text: text.to_string(), }], end_turn: None, + phase: None, } } @@ -150,6 +151,7 @@ fn assistant_message(text: &str) -> ResponseItem { text: text.to_string(), }], end_turn: None, + phase: None, } } diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 836023775..98e2fd9f0 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -113,6 +113,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { text: "hello".to_string(), }], end_turn: None, + phase: None, }]; let mut stream = match client.stream(&prompt).await { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index a40c92d02..86e511c47 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -110,6 +110,7 @@ async fn responses_stream_includes_subagent_header_on_review() { text: "hello".into(), }], end_turn: None, + phase: None, }]; let mut stream = client_session.stream(&prompt).await.expect("stream failed"); @@ -209,6 +210,7 @@ async fn responses_stream_includes_subagent_header_on_other() { text: "hello".into(), }], end_turn: None, + phase: None, }]; let mut stream = client_session.stream(&prompt).await.expect("stream failed"); @@ -366,6 +368,7 @@ async fn responses_respects_model_info_overrides_from_config() { text: "hello".into(), }], end_turn: None, + phase: None, }]; let mut stream = client.stream(&prompt).await.expect("stream failed"); @@ -473,6 +476,7 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e() text: "hello".into(), }], end_turn: None, + phase: None, }]; let first_request = responses::mount_sse_once(&server, response_body.clone()).await; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index d79ef22b5..d0f2ba1ae 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -29,6 +29,7 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::config_types::Verbosity; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::MessagePhase; use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::WebSearchAction; @@ -197,6 +198,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text: "resumed user message".to_string(), }], end_turn: None, + phase: None, }; let prior_user_json = serde_json::to_value(&prior_user).unwrap(); writeln!( @@ -218,6 +220,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text: "resumed system instruction".to_string(), }], end_turn: None, + phase: None, }; let prior_system_json = serde_json::to_value(&prior_system).unwrap(); writeln!( @@ -239,6 +242,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text: "resumed assistant message".to_string(), }], end_turn: None, + phase: Some(MessagePhase::Commentary), }; let prior_item_json = serde_json::to_value(&prior_item).unwrap(); writeln!( @@ -318,6 +322,25 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { .iter() .position(|(role, text)| role == "assistant" && text == "resumed assistant message") .expect("prior assistant message"); + let prior_assistant = input + .iter() + .find(|item| { + item.get("role").and_then(|role| role.as_str()) == Some("assistant") + && item + .get("content") + .and_then(|content| content.as_array()) + .and_then(|content| content.first()) + .and_then(|entry| entry.get("text")) + .and_then(|text| text.as_str()) + == Some("resumed assistant message") + }) + .expect("resumed assistant message request item"); + assert_eq!( + prior_assistant + .get("phase") + .and_then(|phase| phase.as_str()), + Some("commentary") + ); let pos_permissions = messages .iter() .position(|(role, text)| role == "developer" && text.contains("")) @@ -1210,6 +1233,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { text: "message".into(), }], end_turn: None, + phase: None, }); prompt.input.push(ResponseItem::WebSearchCall { id: Some("web-search-id".into()), diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 1acc3756d..51c94edd7 100644 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -211,6 +211,7 @@ fn message_item(text: &str) -> ResponseItem { role: "user".into(), content: vec![ContentItem::InputText { text: text.into() }], end_turn: None, + phase: None, } } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index fa07bf7f9..ea9d2f92f 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1452,6 +1452,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { text: remote_summary.to_string(), }], end_turn: None, + phase: None, }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), @@ -2275,6 +2276,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), @@ -2395,6 +2397,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 563aff782..e5446f4a3 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -62,6 +62,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { text: "REMOTE_COMPACTED_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), @@ -184,6 +185,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { text: "REMOTE_COMPACTED_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), @@ -251,6 +253,7 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> { text: "REMOTE_COMPACTED_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), @@ -352,6 +355,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> text: "COMPACTED_USER_SUMMARY".to_string(), }], end_turn: None, + phase: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), @@ -363,6 +367,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> text: "COMPACTED_ASSISTANT_NOTE".to_string(), }], end_turn: None, + phase: None, }, ]; let compact_mock = responses::mount_compact_json_once( diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 6aebe6630..691531e04 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -158,6 +158,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu }, ], end_turn: None, + phase: None, }; assert_eq!(actual, expected); @@ -239,6 +240,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> }, ], end_turn: None, + phase: None, }; assert_eq!(actual, expected); diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index cfc4a8048..1c9c3adf7 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -530,6 +530,7 @@ async fn review_input_isolated_from_parent_history() { text: "parent: earlier user message".to_string(), }], end_turn: None, + phase: None, }; let user_json = serde_json::to_value(&user).unwrap(); let user_line = serde_json::json!({ @@ -549,6 +550,7 @@ async fn review_input_isolated_from_parent_history() { text: "parent: assistant reply".to_string(), }], end_turn: None, + phase: None, }; let assistant_json = serde_json::to_value(&assistant).unwrap(); let assistant_line = serde_json::json!({ diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 07c8856fe..74cc9a53f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -72,6 +72,13 @@ pub enum ContentItem { OutputText { text: String }, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum MessagePhase { + Commentary, + FinalAnswer, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { @@ -85,6 +92,11 @@ pub enum ResponseItem { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] end_turn: Option, + // Optional output-message phase (for example: "commentary", "final_answer"). + // Do not use directly; availability can vary by provider and model. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + phase: Option, }, Reasoning { #[serde(default, skip_serializing)] @@ -439,6 +451,7 @@ impl From for ResponseItem { text: di.into_text(), }], end_turn: None, + phase: None, } } } @@ -596,6 +609,7 @@ impl From for ResponseItem { content, id: None, end_turn: None, + phase: None, }, ResponseInputItem::FunctionCallOutput { call_id, output } => { Self::FunctionCallOutput { call_id, output } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 71a7973c6..bf193cca9 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1691,6 +1691,7 @@ impl From for ResponseItem { text: value.message, }], end_turn: None, + phase: None, } } } diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 8d327a4d1..ad4011808 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -151,6 +151,7 @@ mod tests { }, ], end_turn: None, + phase: None, }; let actual = extract_user_message_text(&item); assert_eq!(actual.as_deref(), Some("actual question"));