diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index fe9fda0f54..02dde191f7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2911,6 +2911,37 @@ }, "ResponseItem": { "oneOf": [ + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "tools": { + "items": true, + "type": "array" + }, + "type": { + "enum": [ + "additional_tools" + ], + "title": "AdditionalToolsResponseItemType", + "type": "string" + } + }, + "required": [ + "role", + "tools", + "type" + ], + "title": "AdditionalToolsResponseItem", + "type": "object" + }, { "properties": { "content": { 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 c0d5261e89..beff01d636 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 @@ -18691,6 +18691,37 @@ }, "ResponseItem": { "oneOf": [ + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "tools": { + "items": true, + "type": "array" + }, + "type": { + "enum": [ + "additional_tools" + ], + "title": "AdditionalToolsResponseItemType", + "type": "string" + } + }, + "required": [ + "role", + "tools", + "type" + ], + "title": "AdditionalToolsResponseItem", + "type": "object" + }, { "properties": { "content": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index ed38115cc3..dca5a9d03b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -14703,6 +14703,37 @@ }, "ResponseItem": { "oneOf": [ + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "tools": { + "items": true, + "type": "array" + }, + "type": { + "enum": [ + "additional_tools" + ], + "title": "AdditionalToolsResponseItemType", + "type": "string" + } + }, + "required": [ + "role", + "tools", + "type" + ], + "title": "AdditionalToolsResponseItem", + "type": "object" + }, { "properties": { "content": { 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 e9c7bf6e43..2b0ac2129c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -482,6 +482,37 @@ }, "ResponseItem": { "oneOf": [ + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "tools": { + "items": true, + "type": "array" + }, + "type": { + "enum": [ + "additional_tools" + ], + "title": "AdditionalToolsResponseItemType", + "type": "string" + } + }, + "required": [ + "role", + "tools", + "type" + ], + "title": "AdditionalToolsResponseItem", + "type": "object" + }, { "properties": { "content": { 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 15f5fa0638..a0831997e8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -553,6 +553,37 @@ }, "ResponseItem": { "oneOf": [ + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "tools": { + "items": true, + "type": "array" + }, + "type": { + "enum": [ + "additional_tools" + ], + "title": "AdditionalToolsResponseItemType", + "type": "string" + } + }, + "required": [ + "role", + "tools", + "type" + ], + "title": "AdditionalToolsResponseItem", + "type": "object" + }, { "properties": { "content": { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index f78ef1f577..dc4c583e0a 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 97809f7e6a..ff6c19540b 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index 80e7095fd2..3e11def4a9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -14,7 +14,7 @@ import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSumm import type { ResponseItemId } from "./ResponseItemId"; import type { WebSearchAction } from "./WebSearchAction"; -export type ResponseItem = { "type": "message", id?: ResponseItemId, role: string, content: Array, phase?: MessagePhase, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "agent_message", id?: ResponseItemId, author: string, recipient: string, content: Array, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "reasoning", id?: ResponseItemId, summary: Array, content?: Array, encrypted_content: string | null, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "local_shell_call", +export type ResponseItem = { "type": "additional_tools", id?: ResponseItemId, role: string, tools: unknown[], } | { "type": "message", id?: ResponseItemId, role: string, content: Array, phase?: MessagePhase, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "agent_message", id?: ResponseItemId, author: string, recipient: string, content: Array, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "reasoning", id?: ResponseItemId, summary: Array, content?: Array, encrypted_content: string | null, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "local_shell_call", /** * Legacy id field retained for compatibility with older payloads. */ diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index bb29f84985..1665c31e8f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1010,12 +1010,12 @@ impl InternalChatMessageMetadataPassthrough { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { - #[schemars(skip)] - #[ts(skip)] AdditionalTools { #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, role: String, + #[ts(type = "unknown[]")] tools: Vec, }, Message { diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index 7eb21511d2..968bfbe73c 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -76,7 +76,8 @@ pub fn into_persisted_rollout_items( #[inline] pub fn should_persist_response_item(item: &ResponseItem) -> bool { match item { - ResponseItem::Message { .. } + ResponseItem::AdditionalTools { .. } + | ResponseItem::Message { .. } | ResponseItem::AgentMessage { .. } | ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } @@ -91,9 +92,7 @@ pub fn should_persist_response_item(item: &ResponseItem) -> bool { | ResponseItem::ConfigurationUpdate { .. } | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::AdditionalTools { .. } - | ResponseItem::CompactionTrigger { .. } - | ResponseItem::Other => false, + ResponseItem::CompactionTrigger { .. } | ResponseItem::Other => false, } } diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index e57b8d99b5..c97800ba4b 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -613,6 +613,58 @@ fn strip_legacy_ghost_snapshot_keeps_checkpoint_metadata_aligned() { ); } +#[tokio::test] +async fn recorder_preserves_additional_tools_in_both_history_modes() -> std::io::Result<()> { + for history_mode in [ThreadHistoryMode::Legacy, ThreadHistoryMode::Paginated] { + let home = TempDir::new().expect("temp dir"); + let config = test_config(home.path()); + let thread_id = ThreadId::new(); + let recorder = RolloutRecorder::new( + &config, + RolloutRecorderParams::new( + thread_id, + /*forked_from_id*/ None, + /*parent_thread_id*/ None, + SessionSource::Exec, + /*thread_source*/ None, + "test_originator".to_string(), + BaseInstructions::default(), + Vec::new(), + ) + .with_history_mode(history_mode), + ) + .await?; + let item = RolloutItem::ResponseItem( + ResponseItem::AdditionalTools { + id: None, + role: "developer".to_string(), + tools: vec![serde_json::json!({ + "type": "namespace", + "name": "functions", + "description": "Available tools.", + "tools": [{"type": "function", "name": "lookup", + "description": "Look up a value.", + "parameters": {"type": "object", "properties": {}}}] + })], + } + .into(), + ); + let persisted = crate::persisted_rollout_items(std::slice::from_ref(&item), history_mode); + recorder.record_canonical_items(&persisted).await?; + recorder.flush().await?; + let (items, loaded_thread_id, parse_errors) = + RolloutRecorder::load_rollout_items(recorder.rollout_path()).await?; + assert_eq!(loaded_thread_id, Some(thread_id)); + assert_eq!(parse_errors, 0); + assert_eq!( + serde_json::to_value(&items[1..])?, + serde_json::to_value([item])? + ); + recorder.shutdown().await?; + } + Ok(()) +} + #[tokio::test] async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<()> { let home = TempDir::new().expect("temp dir"); diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index b580c55d0c..32b5865582 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -3934,6 +3934,16 @@ class ResourceTemplate(BaseModel): uri_template: Annotated[str, Field(alias="uriTemplate")] +class AdditionalToolsResponseItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str | None = None + role: str + tools: list + type: Annotated[Literal["additional_tools"], Field(title="AdditionalToolsResponseItemType")] + + class AgentMessageResponseItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -11147,7 +11157,8 @@ class CustomToolCallOutputResponseItem(BaseModel): class ResponseItem( RootModel[ - MessageResponseItem + AdditionalToolsResponseItem + | MessageResponseItem | AgentMessageResponseItem | ReasoningResponseItem | LocalShellCallResponseItem @@ -11170,7 +11181,8 @@ class ResponseItem( populate_by_name=True, ) root: ( - MessageResponseItem + AdditionalToolsResponseItem + | MessageResponseItem | AgentMessageResponseItem | ReasoningResponseItem | LocalShellCallResponseItem