diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a6fe45cc53..3e2517a891 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1557,6 +1557,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -1697,6 +1707,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -1724,6 +1744,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -1799,6 +1829,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -1835,6 +1875,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -1878,6 +1928,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -1903,6 +1963,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -1937,6 +2007,16 @@ "ghost_commit": { "$ref": "#/definitions/GhostCommit" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "ghost_snapshot" @@ -1957,6 +2037,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -2018,6 +2108,16 @@ } ] }, + "session_source": { + "anyOf": [ + { + "$ref": "#/definitions/SessionSourceMetadata" + }, + { + "type": "null" + } + ] + }, "user_message_type": { "anyOf": [ { @@ -2418,6 +2518,21 @@ ], "type": "string" }, + "SessionSourceMetadata": { + "enum": [ + "cli", + "vscode", + "exec", + "mcp", + "subagent_review", + "subagent_compact", + "subagent_memory_consolidation", + "subagent_thread_spawn", + "subagent_other", + "unknown" + ], + "type": "string" + }, "Settings": { "description": "Settings for a collaboration mode.", "properties": { 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 47faa5b18d..6849e2431a 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 @@ -10224,6 +10224,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/v2/ReasoningItemReasoningSummary" @@ -10364,6 +10374,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -10391,6 +10411,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/v2/FunctionCallOutputBody" }, @@ -10466,6 +10496,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -10502,6 +10542,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -10545,6 +10595,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -10570,6 +10630,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -10604,6 +10674,16 @@ "ghost_commit": { "$ref": "#/definitions/v2/GhostCommit" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "ghost_snapshot" @@ -10624,6 +10704,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -10685,6 +10775,16 @@ } ] }, + "session_source": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SessionSourceMetadata" + }, + { + "type": "null" + } + ] + }, "user_message_type": { "anyOf": [ { @@ -11186,6 +11286,21 @@ } ] }, + "SessionSourceMetadata": { + "enum": [ + "cli", + "vscode", + "exec", + "mcp", + "subagent_review", + "subagent_compact", + "subagent_memory_consolidation", + "subagent_thread_spawn", + "subagent_other", + "unknown" + ], + "type": "string" + }, "Settings": { "description": "Settings for a collaboration mode.", "properties": { 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 87b70f1f93..4dc4d029ff 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 @@ -7012,6 +7012,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -7152,6 +7162,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -7179,6 +7199,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -7254,6 +7284,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -7290,6 +7330,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -7333,6 +7383,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -7358,6 +7418,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -7392,6 +7462,16 @@ "ghost_commit": { "$ref": "#/definitions/GhostCommit" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "ghost_snapshot" @@ -7412,6 +7492,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -7473,6 +7563,16 @@ } ] }, + "session_source": { + "anyOf": [ + { + "$ref": "#/definitions/SessionSourceMetadata" + }, + { + "type": "null" + } + ] + }, "user_message_type": { "anyOf": [ { @@ -8946,6 +9046,21 @@ } ] }, + "SessionSourceMetadata": { + "enum": [ + "cli", + "vscode", + "exec", + "mcp", + "subagent_review", + "subagent_compact", + "subagent_memory_consolidation", + "subagent_thread_spawn", + "subagent_other", + "unknown" + ], + "type": "string" + }, "Settings": { "description": "Settings for a collaboration mode.", "properties": { 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 1cbc1f6337..617179ec6c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -405,6 +405,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -545,6 +555,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -572,6 +592,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -647,6 +677,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -683,6 +723,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -726,6 +776,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -751,6 +811,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -785,6 +855,16 @@ "ghost_commit": { "$ref": "#/definitions/GhostCommit" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "ghost_snapshot" @@ -805,6 +885,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -866,6 +956,16 @@ } ] }, + "session_source": { + "anyOf": [ + { + "$ref": "#/definitions/SessionSourceMetadata" + }, + { + "type": "null" + } + ] + }, "user_message_type": { "anyOf": [ { @@ -1007,6 +1107,21 @@ ], "type": "string" }, + "SessionSourceMetadata": { + "enum": [ + "cli", + "vscode", + "exec", + "mcp", + "subagent_review", + "subagent_compact", + "subagent_memory_consolidation", + "subagent_thread_spawn", + "subagent_other", + "unknown" + ], + "type": "string" + }, "UserMessageType": { "enum": [ "prompt", 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 a397730faa..6739a8e985 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -471,6 +471,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -611,6 +621,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -638,6 +658,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -713,6 +743,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -749,6 +789,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -792,6 +842,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -817,6 +877,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -851,6 +921,16 @@ "ghost_commit": { "$ref": "#/definitions/GhostCommit" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "ghost_snapshot" @@ -871,6 +951,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -932,6 +1022,16 @@ } ] }, + "session_source": { + "anyOf": [ + { + "$ref": "#/definitions/SessionSourceMetadata" + }, + { + "type": "null" + } + ] + }, "user_message_type": { "anyOf": [ { @@ -1088,6 +1188,21 @@ ], "type": "string" }, + "SessionSourceMetadata": { + "enum": [ + "cli", + "vscode", + "exec", + "mcp", + "subagent_review", + "subagent_compact", + "subagent_memory_consolidation", + "subagent_thread_spawn", + "subagent_other", + "unknown" + ], + "type": "string" + }, "UserMessageType": { "enum": [ "prompt", diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index 3917c03776..ab6413b700 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -12,8 +12,8 @@ import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSumm import type { ResponseItemMetadata } from "./ResponseItemMetadata"; import type { WebSearchAction } from "./WebSearchAction"; -export type ResponseItem = { "type": "message", role: string, content: Array, metadata?: ResponseItemMetadata, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array, content?: Array, encrypted_content: string | null, } | { "type": "local_shell_call", +export type ResponseItem = { "type": "message", role: string, content: Array, metadata?: ResponseItemMetadata, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array, content?: Array, encrypted_content: string | null, metadata?: ResponseItemMetadata, } | { "type": "local_shell_call", /** * Set when using the Responses API. */ -call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, } | { "type": "compaction", encrypted_content: string, } | { "type": "other" }; +call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, metadata?: ResponseItemMetadata, } | { "type": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "other" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts index deb3c845bd..a53c8d59c8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts @@ -3,10 +3,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ReviewDecisionMetadata } from "./ReviewDecisionMetadata"; import type { SandboxPolicyMetadata } from "./SandboxPolicyMetadata"; +import type { SessionSourceMetadata } from "./SessionSourceMetadata"; import type { UserMessageType } from "./UserMessageType"; export type ResponseItemMetadata = { user_message_type?: UserMessageType, /** * Client-visible UUID generated by Codex for this item. */ -uuid?: string, sandbox_policy?: SandboxPolicyMetadata, is_tool_call_escalated?: boolean, review_decision?: ReviewDecisionMetadata, }; +uuid?: string, sandbox_policy?: SandboxPolicyMetadata, is_tool_call_escalated?: boolean, review_decision?: ReviewDecisionMetadata, session_source?: SessionSourceMetadata, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/SessionSourceMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/SessionSourceMetadata.ts new file mode 100644 index 0000000000..54b6c35a8c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/SessionSourceMetadata.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 SessionSourceMetadata = "cli" | "vscode" | "exec" | "mcp" | "subagent_review" | "subagent_compact" | "subagent_memory_consolidation" | "subagent_thread_spawn" | "subagent_other" | "unknown"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index c3aa4a74aa..eec3f3be3b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -67,6 +67,7 @@ export type { ServerNotification } from "./ServerNotification"; export type { ServerRequest } from "./ServerRequest"; export type { ServiceTier } from "./ServiceTier"; export type { SessionSource } from "./SessionSource"; +export type { SessionSourceMetadata } from "./SessionSourceMetadata"; export type { Settings } from "./Settings"; export type { SubAgentSource } from "./SubAgentSource"; export type { ThreadId } from "./ThreadId"; diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 83af6258fb..7d49c0daa7 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -187,6 +187,7 @@ impl AgentControl { ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output, + metadata: None, }, )); let initial_history = InitialHistory::Forked(forked_rollout_items); diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 879c790b89..2a69c95e4f 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -551,11 +551,9 @@ async fn spawn_agent_fork_injects_output_for_parent_spawn_call() { .expect("child thread should be registered"); let history = child_thread.codex.session.clone_history().await; let injected_output = history.raw_items().iter().find_map(|item| match item { - ResponseItem::FunctionCallOutput { call_id, output } - if call_id == &parent_spawn_call_id => - { - Some(output) - } + ResponseItem::FunctionCallOutput { + call_id, output, .. + } if call_id == &parent_spawn_call_id => Some(output), _ => None, }); let injected_output = diff --git a/codex-rs/core/src/arc_monitor_tests.rs b/codex-rs/core/src/arc_monitor_tests.rs index 0a6fd3caa2..d31db32ea1 100644 --- a/codex-rs/core/src/arc_monitor_tests.rs +++ b/codex-rs/core/src/arc_monitor_tests.rs @@ -145,6 +145,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies() summary: Vec::new(), content: None, encrypted_content: Some("encrypted-old".to_string()), + metadata: None, }], &turn_context, ) @@ -174,6 +175,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies() summary: Vec::new(), content: None, encrypted_content: Some("encrypted-latest".to_string()), + metadata: None, }], &turn_context, ) diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 8e3211b3a6..f8359ec305 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -155,6 +155,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_text(raw_output.to_string()), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -168,6 +169,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { call_id: "call-2".to_string(), name: None, output: FunctionCallOutputPayload::from_text(raw_output.to_string()), + metadata: None, }, ]; @@ -187,6 +189,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_text(expected_output.to_string()), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -200,6 +203,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { call_id: "call-2".to_string(), name: None, output: FunctionCallOutputPayload::from_text(expected_output.to_string()), + metadata: None, }, ] ); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8e26c0c88a..9b4f216ccd 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -334,6 +334,7 @@ use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItemMetadata; use codex_protocol::models::SandboxPolicyMetadata; +use codex_protocol::models::SessionSourceMetadata; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::InitialHistory; @@ -1043,39 +1044,29 @@ fn user_message_metadata_patch( } } -fn stamp_message_metadata_on_response_item( - item: ResponseItem, - patch: Option, -) -> ResponseItem { - let Some(patch) = patch else { - return item; - }; - if patch.is_empty() { - return item; - } - - match item { - ResponseItem::Message { - id, - role, - content, - metadata, - end_turn, - phase, - } if role == "user" => { - let mut metadata_value = metadata.unwrap_or_default(); - metadata_value.merge_from(patch); - let metadata = (!metadata_value.is_empty()).then_some(metadata_value); - ResponseItem::Message { - id, - role, - content, - metadata, - end_turn, - phase, - } +fn session_source_to_metadata(session_source: &SessionSource) -> SessionSourceMetadata { + match session_source { + SessionSource::Cli => SessionSourceMetadata::Cli, + SessionSource::VSCode => SessionSourceMetadata::Vscode, + SessionSource::Exec => SessionSourceMetadata::Exec, + SessionSource::Mcp => SessionSourceMetadata::Mcp, + SessionSource::SubAgent(SubAgentSource::Review) => SessionSourceMetadata::SubagentReview, + SessionSource::SubAgent(SubAgentSource::Compact) => SessionSourceMetadata::SubagentCompact, + SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => { + SessionSourceMetadata::SubagentMemoryConsolidation } - other => other, + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. }) => { + SessionSourceMetadata::SubagentThreadSpawn + } + SessionSource::SubAgent(SubAgentSource::Other(_)) => SessionSourceMetadata::SubagentOther, + SessionSource::Unknown => SessionSourceMetadata::Unknown, + } +} + +fn session_source_metadata_patch(session_source: &SessionSource) -> ResponseItemMetadata { + ResponseItemMetadata { + session_source: Some(session_source_to_metadata(session_source)), + ..ResponseItemMetadata::default() } } @@ -1091,25 +1082,35 @@ fn response_item_tool_call_id(item: &ResponseItem) -> Option<&str> { } } -fn tool_call_metadata_slot_mut( +fn response_item_metadata_slot_mut( item: &mut ResponseItem, ) -> Option<&mut Option> { match item { - ResponseItem::LocalShellCall { metadata, .. } + ResponseItem::Message { metadata, .. } + | ResponseItem::Reasoning { metadata, .. } + | ResponseItem::LocalShellCall { metadata, .. } | ResponseItem::FunctionCall { metadata, .. } - | ResponseItem::CustomToolCall { metadata, .. } => Some(metadata), + | ResponseItem::ToolSearchCall { metadata, .. } + | ResponseItem::FunctionCallOutput { metadata, .. } + | ResponseItem::CustomToolCall { metadata, .. } + | ResponseItem::CustomToolCallOutput { metadata, .. } + | ResponseItem::ToolSearchOutput { metadata, .. } + | ResponseItem::WebSearchCall { metadata, .. } + | ResponseItem::ImageGenerationCall { metadata, .. } + | ResponseItem::GhostSnapshot { metadata, .. } + | ResponseItem::Compaction { metadata, .. } => Some(metadata), _ => None, } } -fn stamp_tool_metadata_on_response_item( +fn stamp_response_item_metadata_on_response_item( mut item: ResponseItem, patch: ResponseItemMetadata, ) -> ResponseItem { if patch.is_empty() { return item; } - let Some(metadata_slot) = tool_call_metadata_slot_mut(&mut item) else { + let Some(metadata_slot) = response_item_metadata_slot_mut(&mut item) else { return item; }; let mut metadata = metadata_slot.take().unwrap_or_default(); @@ -1118,17 +1119,47 @@ fn stamp_tool_metadata_on_response_item( item } -fn tool_call_metadata_or_default(item: &ResponseItem) -> Option { +fn response_item_metadata_or_default(item: &ResponseItem) -> Option { match item { - ResponseItem::LocalShellCall { metadata, .. } + ResponseItem::Message { metadata, .. } + | ResponseItem::Reasoning { metadata, .. } + | ResponseItem::LocalShellCall { metadata, .. } | ResponseItem::FunctionCall { metadata, .. } - | ResponseItem::CustomToolCall { metadata, .. } => { - Some(metadata.clone().unwrap_or_default()) - } + | ResponseItem::ToolSearchCall { metadata, .. } + | ResponseItem::FunctionCallOutput { metadata, .. } + | ResponseItem::CustomToolCall { metadata, .. } + | ResponseItem::CustomToolCallOutput { metadata, .. } + | ResponseItem::ToolSearchOutput { metadata, .. } + | ResponseItem::WebSearchCall { metadata, .. } + | ResponseItem::ImageGenerationCall { metadata, .. } + | ResponseItem::GhostSnapshot { metadata, .. } + | ResponseItem::Compaction { metadata, .. } => Some(metadata.clone().unwrap_or_default()), _ => None, } } +fn review_decision_to_metadata(decision: &ReviewDecision) -> ReviewDecisionMetadata { + match decision { + ReviewDecision::Approved => ReviewDecisionMetadata::Approved, + ReviewDecision::ApprovedExecpolicyAmendment { .. } => { + ReviewDecisionMetadata::ApprovedWithAmendment + } + ReviewDecision::ApprovedForSession => ReviewDecisionMetadata::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => match network_policy_amendment.action { + codex_protocol::protocol::NetworkPolicyRuleAction::Allow => { + ReviewDecisionMetadata::ApprovedWithNetworkPolicyAllow + } + codex_protocol::protocol::NetworkPolicyRuleAction::Deny => { + ReviewDecisionMetadata::DeniedWithNetworkPolicyDeny + } + }, + ReviewDecision::Denied => ReviewDecisionMetadata::Denied, + ReviewDecision::Abort => ReviewDecisionMetadata::Abort, + } +} + #[derive(Clone, Debug, Default)] struct ToolApprovalMetadataSnapshot { approval_outcomes_by_call_id: HashMap, @@ -1150,7 +1181,7 @@ fn stamp_tool_approval_metadata_with_snapshot( let outcome = snapshot.approval_outcomes_by_call_id.get(call_id).cloned(); let has_pending_approval = snapshot.pending_approval_call_ids.contains(call_id); - let mut metadata = match tool_call_metadata_or_default(&response_item) { + let mut metadata = match response_item_metadata_or_default(&response_item) { Some(metadata) => metadata, None => return response_item, }; @@ -1168,11 +1199,11 @@ fn stamp_tool_approval_metadata_with_snapshot( metadata.review_decision = None; } None => { - return stamp_tool_metadata_on_response_item(response_item, metadata); + return stamp_response_item_metadata_on_response_item(response_item, metadata); } } - stamp_tool_metadata_on_response_item(response_item, metadata) + stamp_response_item_metadata_on_response_item(response_item, metadata) } #[derive(Clone)] @@ -4033,6 +4064,14 @@ impl Session { turn_context: &TurnContext, response_item: ResponseItem, ) { + let response_item = if self.enabled(Feature::ItemMetadata) { + stamp_response_item_metadata_on_response_item( + response_item, + session_source_metadata_patch(&turn_context.session_source), + ) + } else { + response_item + }; let response_item = self .stamp_tool_approval_metadata(turn_context, response_item) .await; @@ -4060,11 +4099,15 @@ impl Session { patch.sandbox_policy = Some(sandbox_policy_to_metadata( turn_context.sandbox_policy.get(), )); + patch.merge_from(session_source_metadata_patch(&turn_context.session_source)); Some(patch) } else { None }; - let response_item = stamp_message_metadata_on_response_item(response_item, metadata_patch); + let response_item = match metadata_patch { + Some(patch) => stamp_response_item_metadata_on_response_item(response_item, patch), + None => response_item, + }; // Persist the user message to history, but emit the turn item from `UserInput` so // UI-only `text_elements` are preserved. `ResponseItem::Message` does not carry @@ -4165,11 +4208,17 @@ impl Session { } let mut input_item: ResponseInputItem = input.into(); - let metadata = Some(user_message_metadata_patch( + let mut metadata = user_message_metadata_patch( UserMessageType::PromptSteering, self.enabled(Feature::ItemMetadata) .then(|| sandbox_policy_to_metadata(active_task.turn_context.sandbox_policy.get())), - )); + ); + if self.enabled(Feature::ItemMetadata) { + metadata.merge_from(session_source_metadata_patch( + &active_task.turn_context.session_source, + )); + } + let metadata = Some(metadata); if self.enabled(Feature::ItemMetadata) && let Some(metadata_patch) = metadata.as_ref() { @@ -4189,20 +4238,31 @@ impl Session { let mut active = self.active_turn.lock().await; match active.as_mut() { Some(at) => { - let sandbox_policy = at.tasks.first().map(|(_, turn_context)| { - sandbox_policy_to_metadata(turn_context.turn_context.sandbox_policy.get()) + let first_turn_context = at.tasks.first().map(|(_, task)| &task.turn_context); + let sandbox_policy = first_turn_context.map(|turn_context| { + sandbox_policy_to_metadata(turn_context.sandbox_policy.get()) }); + let session_source = + first_turn_context.map(|turn_context| turn_context.session_source.clone()); let mut ts = at.turn_state.lock().await; for mut item in input { let metadata = match &item { - ResponseInputItem::Message { .. } => Some(user_message_metadata_patch( - UserMessageType::PromptQueued, - if self.enabled(Feature::ItemMetadata) { - sandbox_policy.clone() - } else { - None - }, - )), + ResponseInputItem::Message { .. } => { + let mut metadata = user_message_metadata_patch( + UserMessageType::PromptQueued, + if self.enabled(Feature::ItemMetadata) { + sandbox_policy.clone() + } else { + None + }, + ); + if self.enabled(Feature::ItemMetadata) + && let Some(session_source) = session_source.as_ref() + { + metadata.merge_from(session_source_metadata_patch(session_source)); + } + Some(metadata) + } _ => None, }; @@ -5920,11 +5980,16 @@ pub(crate) async fn run_turn( .await; let mut initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input.clone()); - let initial_message_metadata = Some(user_message_metadata_patch( + let mut initial_message_metadata = user_message_metadata_patch( UserMessageType::Prompt, sess.enabled(Feature::ItemMetadata) .then(|| sandbox_policy_to_metadata(turn_context.sandbox_policy.get())), - )); + ); + if sess.enabled(Feature::ItemMetadata) { + initial_message_metadata + .merge_from(session_source_metadata_patch(&turn_context.session_source)); + } + let initial_message_metadata = Some(initial_message_metadata); if sess.enabled(Feature::ItemMetadata) && let Some(metadata_patch) = initial_message_metadata.as_ref() { diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 0533a41a96..478d206d06 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -17,6 +17,9 @@ use crate::tools::format_exec_output_str; use codex_protocol::ThreadId; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::LocalShellAction; +use codex_protocol::models::LocalShellExecAction; +use codex_protocol::models::LocalShellStatus; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -61,6 +64,7 @@ use codex_app_server_protocol::AppInfo; use codex_execpolicy::Decision; use codex_execpolicy::NetworkRuleProtocol; use codex_execpolicy::Policy; +use codex_git::GhostCommit; use codex_network_proxy::NetworkProxyConfig; use codex_otel::TelemetryAuthMode; use codex_protocol::models::BaseInstructions; @@ -68,6 +72,7 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_protocol::models::WebSearchAction; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::ConversationAudioParams; use codex_protocol::protocol::RealtimeAudioFrame; @@ -3679,6 +3684,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist status: "completed".to_string(), revised_prompt: Some("a tiny blue square".to_string()), result: "Zm9v".to_string(), + metadata: None, }], None, ) @@ -3722,6 +3728,7 @@ async fn handle_output_item_done_records_image_save_history_message() { status: "completed".to_string(), revised_prompt: Some("a tiny blue square".to_string()), result: "Zm9v".to_string(), + metadata: None, }; let mut ctx = HandleOutputCtx { @@ -3762,6 +3769,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() { status: "completed".to_string(), revised_prompt: Some("broken payload".to_string()), result: "_-8".to_string(), + metadata: None, }; let mut ctx = HandleOutputCtx { @@ -4355,6 +4363,8 @@ async fn task_finish_emits_prompt_queued_metadata_for_injected_user_input_when_f .. } if metadata.user_message_type == Some(codex_protocol::models::UserMessageType::PromptQueued) + && metadata.session_source + == Some(codex_protocol::models::SessionSourceMetadata::Exec) ) )); @@ -4389,6 +4399,203 @@ async fn task_finish_emits_prompt_queued_metadata_for_injected_user_input_when_f )); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn record_response_item_stamps_session_source_on_all_supported_variants() { + let (mut sess, tc, _rx) = make_session_and_context_with_rx().await; + Arc::get_mut(&mut sess) + .expect("session should be uniquely owned in this test") + .features + .enable(crate::features::Feature::ItemMetadata) + .expect("feature flag should be enabled for this test"); + + sess.spawn_task( + Arc::clone(&tc), + vec![UserInput::Text { + text: "start".to_string(), + text_elements: Vec::new(), + }], + NeverEndingTask { + kind: TaskKind::Regular, + listen_to_cancellation_token: false, + }, + ) + .await; + + let items = vec![ + ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: "hello".to_string(), + }], + metadata: None, + end_turn: None, + phase: None, + }, + ResponseItem::Reasoning { + id: "reasoning-1".to_string(), + summary: vec![], + content: None, + encrypted_content: None, + metadata: None, + }, + ResponseItem::LocalShellCall { + id: None, + call_id: Some("shell-1".to_string()), + status: LocalShellStatus::Completed, + action: LocalShellAction::Exec(LocalShellExecAction { + command: vec!["echo".to_string(), "hello".to_string()], + timeout_ms: None, + working_directory: None, + env: None, + user: None, + }), + metadata: None, + }, + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + namespace: None, + arguments: "{}".to_string(), + call_id: "call-1".to_string(), + metadata: None, + }, + ResponseItem::ToolSearchCall { + id: None, + call_id: Some("search-1".to_string()), + status: None, + execution: "client".to_string(), + arguments: serde_json::json!({"query": "calendar"}), + metadata: None, + }, + ResponseItem::FunctionCallOutput { + call_id: "call-output-1".to_string(), + output: FunctionCallOutputPayload { + body: FunctionCallOutputBody::Text("ok".to_string()), + success: None, + }, + metadata: None, + }, + ResponseItem::CustomToolCall { + id: None, + status: Some("completed".to_string()), + call_id: "custom-1".to_string(), + name: "custom".to_string(), + input: "payload".to_string(), + metadata: None, + }, + ResponseItem::CustomToolCallOutput { + call_id: "custom-output-1".to_string(), + output: FunctionCallOutputPayload { + body: FunctionCallOutputBody::Text("done".to_string()), + success: None, + }, + metadata: None, + }, + ResponseItem::ToolSearchOutput { + call_id: Some("search-out-1".to_string()), + status: "completed".to_string(), + execution: "client".to_string(), + tools: vec![], + metadata: None, + }, + ResponseItem::WebSearchCall { + id: None, + status: Some("completed".to_string()), + action: Some(WebSearchAction::Search { + query: Some("weather".to_string()), + queries: None, + }), + metadata: None, + }, + ResponseItem::ImageGenerationCall { + id: "image-1".to_string(), + status: "completed".to_string(), + revised_prompt: Some("cat".to_string()), + result: "image-result".to_string(), + metadata: None, + }, + ResponseItem::GhostSnapshot { + ghost_commit: GhostCommit::new("deadbeef".to_string(), None, Vec::new(), Vec::new()), + metadata: None, + }, + ResponseItem::Compaction { + encrypted_content: "blob".to_string(), + metadata: None, + }, + ]; + + for item in items { + sess.record_response_item_and_emit_turn_item(tc.as_ref(), item) + .await; + } + + let expected_session_source = codex_protocol::models::SessionSourceMetadata::Exec; + let stamped_count = sess + .clone_history() + .await + .raw_items() + .iter() + .filter(|item| match item { + ResponseItem::Message { + metadata: Some(metadata), + .. + } + | ResponseItem::Reasoning { + metadata: Some(metadata), + .. + } + | ResponseItem::LocalShellCall { + metadata: Some(metadata), + .. + } + | ResponseItem::FunctionCall { + metadata: Some(metadata), + .. + } + | ResponseItem::ToolSearchCall { + metadata: Some(metadata), + .. + } + | ResponseItem::FunctionCallOutput { + metadata: Some(metadata), + .. + } + | ResponseItem::CustomToolCall { + metadata: Some(metadata), + .. + } + | ResponseItem::CustomToolCallOutput { + metadata: Some(metadata), + .. + } + | ResponseItem::ToolSearchOutput { + metadata: Some(metadata), + .. + } + | ResponseItem::WebSearchCall { + metadata: Some(metadata), + .. + } + | ResponseItem::ImageGenerationCall { + metadata: Some(metadata), + .. + } + | ResponseItem::GhostSnapshot { + metadata: Some(metadata), + .. + } + | ResponseItem::Compaction { + metadata: Some(metadata), + .. + } => metadata.session_source == Some(expected_session_source.clone()), + _ => false, + }) + .count(); + + assert_eq!(stamped_count, 13); +} + #[test] fn review_decision_metadata_mapping_is_stable() { assert_eq!( @@ -4454,6 +4661,59 @@ fn sandbox_policy_metadata_mapping_is_stable() { ); } +#[test] +fn session_source_metadata_mapping_is_stable() { + assert_eq!( + session_source_to_metadata(&SessionSource::Cli), + codex_protocol::models::SessionSourceMetadata::Cli + ); + assert_eq!( + session_source_to_metadata(&SessionSource::VSCode), + codex_protocol::models::SessionSourceMetadata::Vscode + ); + assert_eq!( + session_source_to_metadata(&SessionSource::Exec), + codex_protocol::models::SessionSourceMetadata::Exec + ); + assert_eq!( + session_source_to_metadata(&SessionSource::Mcp), + codex_protocol::models::SessionSourceMetadata::Mcp + ); + assert_eq!( + session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::Review)), + codex_protocol::models::SessionSourceMetadata::SubagentReview + ); + assert_eq!( + session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::Compact)), + codex_protocol::models::SessionSourceMetadata::SubagentCompact + ); + assert_eq!( + session_source_to_metadata(&SessionSource::SubAgent( + SubAgentSource::MemoryConsolidation + )), + codex_protocol::models::SessionSourceMetadata::SubagentMemoryConsolidation + ); + assert_eq!( + session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: ThreadId::default(), + depth: 2, + agent_nickname: Some("mini".to_string()), + agent_role: Some("reviewer".to_string()), + })), + codex_protocol::models::SessionSourceMetadata::SubagentThreadSpawn + ); + assert_eq!( + session_source_to_metadata(&SessionSource::SubAgent(SubAgentSource::Other( + "custom".to_string() + ))), + codex_protocol::models::SessionSourceMetadata::SubagentOther + ); + assert_eq!( + session_source_to_metadata(&SessionSource::Unknown), + codex_protocol::models::SessionSourceMetadata::Unknown + ); +} + async fn setup_tool_call_metadata_runtime_test() -> ( Arc, Arc, diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index 762fa60020..a45f44dca2 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -563,6 +563,8 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last() { let compacted_history = vec![ResponseItem::Compaction { encrypted_content: "encrypted".to_string(), + + metadata: None, }]; let initial_context = vec![ResponseItem::Message { id: None, @@ -590,6 +592,8 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last }, ResponseItem::Compaction { encrypted_content: "encrypted".to_string(), + + metadata: None, }, ]; assert_eq!(refreshed, expected); diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index f990a80dce..3820d5fc39 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -348,23 +348,27 @@ impl ContextManager { fn process_item(&self, item: &ResponseItem, policy: TruncationPolicy) -> ResponseItem { let policy_with_serialization_budget = policy * 1.2; match item { - ResponseItem::FunctionCallOutput { call_id, output } => { - ResponseItem::FunctionCallOutput { - call_id: call_id.clone(), - output: truncate_function_output_payload( - output, - policy_with_serialization_budget, - ), - } - } + ResponseItem::FunctionCallOutput { + call_id, + output, + metadata, + .. + } => ResponseItem::FunctionCallOutput { + call_id: call_id.clone(), + output: truncate_function_output_payload(output, policy_with_serialization_budget), + metadata: metadata.clone(), + }, ResponseItem::CustomToolCallOutput { call_id, name, output, + metadata, + .. } => ResponseItem::CustomToolCallOutput { call_id: call_id.clone(), name: name.clone(), output: truncate_function_output_payload(output, policy_with_serialization_budget), + metadata: metadata.clone(), }, ResponseItem::Message { .. } | ResponseItem::Reasoning { .. } @@ -463,6 +467,7 @@ pub(crate) fn estimate_response_item_model_visible_bytes(item: &ResponseItem) -> } | ResponseItem::Compaction { encrypted_content: content, + .. } => i64::try_from(estimate_reasoning_length(content.len())).unwrap_or(i64::MAX), item => { let raw = serde_json::to_string(item) diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index ddbb5b8c62..9a50bd9fd8 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -78,6 +78,8 @@ fn custom_tool_call_output(call_id: &str, output: &str) -> ResponseItem { call_id: call_id.to_string(), name: None, output: FunctionCallOutputPayload::from_text(output.to_string()), + + metadata: None, } } @@ -91,6 +93,8 @@ fn reasoning_msg(text: &str) -> ResponseItem { text: text.to_string(), }]), encrypted_content: None, + + metadata: None, } } @@ -102,6 +106,8 @@ fn reasoning_with_encrypted_content(len: usize) -> ResponseItem { }], content: None, encrypted_content: Some("a".repeat(len)), + + metadata: None, } } @@ -149,6 +155,8 @@ fn filters_non_api_messages() { text: "thinking...".to_string(), }]), encrypted_content: None, + + metadata: None, }, ResponseItem::Message { id: None, @@ -295,6 +303,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { detail: None, }, ]), + + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -316,6 +326,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { detail: None, }, ]), + + metadata: None, }, ]; let history = create_history_with_items(items); @@ -361,6 +373,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { .to_string(), }, ]), + + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -382,6 +396,8 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { .to_string(), }, ]), + + metadata: None, }, ]; assert_eq!(stripped, expected); @@ -421,6 +437,8 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() { status: "generating".to_string(), revised_prompt: Some("lobster".to_string()), result: "Zm9v".to_string(), + + metadata: None, }, ResponseItem::Message { id: None, @@ -442,6 +460,8 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() { status: "generating".to_string(), revised_prompt: Some("lobster".to_string()), result: "Zm9v".to_string(), + + metadata: None, }, ResponseItem::Message { id: None, @@ -475,6 +495,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() { status: "completed".to_string(), revised_prompt: Some("lobster".to_string()), result: "Zm9v".to_string(), + + metadata: None, }, ]); @@ -496,6 +518,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() { status: "completed".to_string(), revised_prompt: Some("lobster".to_string()), result: String::new(), + + metadata: None, }, ] ); @@ -505,6 +529,8 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() { fn get_history_for_prompt_drops_ghost_commits() { let items = vec![ResponseItem::GhostSnapshot { ghost_commit: GhostCommit::new("ghost-1".to_string(), None, Vec::new(), Vec::new()), + + metadata: None, }]; let history = create_history_with_items(items); let modalities = default_input_modalities(); @@ -548,6 +574,8 @@ fn remove_first_item_removes_matching_output_for_function_call() { ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -561,6 +589,8 @@ fn remove_first_item_removes_matching_call_for_output() { ResponseItem::FunctionCallOutput { call_id: "call-2".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -591,6 +621,8 @@ fn remove_last_item_removes_matching_call_for_output() { ResponseItem::FunctionCallOutput { call_id: "call-delete-last".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -614,6 +646,8 @@ fn replace_last_turn_images_replaces_tool_output_images() { ]), success: Some(true), }, + + metadata: None, }, ]; let mut history = create_history_with_items(items); @@ -634,6 +668,8 @@ fn replace_last_turn_images_replaces_tool_output_images() { ]), success: Some(true), }, + + metadata: None, }, ] ); @@ -676,6 +712,8 @@ fn remove_first_item_handles_local_shell_pair() { ResponseItem::FunctionCallOutput { call_id: "call-3".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -834,6 +872,8 @@ fn remove_first_item_handles_custom_tool_pair() { call_id: "tool-1".to_string(), name: None, output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -860,6 +900,8 @@ fn normalization_retains_local_shell_outputs() { ResponseItem::FunctionCallOutput { call_id: "shell-1".to_string(), output: FunctionCallOutputPayload::from_text("Total output lines: 1\n\nok".to_string()), + + metadata: None, }, ]; @@ -883,6 +925,8 @@ fn record_items_truncates_function_call_output_content() { body: FunctionCallOutputBody::Text(long_output.clone()), success: Some(true), }, + + metadata: None, }; history.record_items([&item], policy); @@ -915,6 +959,8 @@ fn record_items_truncates_custom_tool_call_output_content() { call_id: "tool-200".to_string(), name: None, output: FunctionCallOutputPayload::from_text(long_output.clone()), + + metadata: None, }; history.record_items([&item], policy); @@ -948,6 +994,8 @@ fn record_items_respects_custom_token_limit() { body: FunctionCallOutputBody::Text(long_output), success: Some(true), }, + + metadata: None, }; history.record_items([&item], policy); @@ -1087,6 +1135,8 @@ fn normalize_adds_missing_output_for_function_call() { ResponseItem::FunctionCallOutput { call_id: "call-x".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ] ); @@ -1122,6 +1172,8 @@ fn normalize_adds_missing_output_for_custom_tool_call() { call_id: "tool-x".to_string(), name: None, output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ] ); @@ -1166,6 +1218,8 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() { ResponseItem::FunctionCallOutput { call_id: "shell-1".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ] ); @@ -1177,6 +1231,8 @@ fn normalize_removes_orphan_function_call_output() { let items = vec![ResponseItem::FunctionCallOutput { call_id: "orphan-1".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1192,6 +1248,8 @@ fn normalize_removes_orphan_custom_tool_call_output() { call_id: "orphan-2".to_string(), name: None, output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1217,6 +1275,8 @@ fn normalize_mixed_inserts_and_removals() { ResponseItem::FunctionCallOutput { call_id: "c2".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, // Will get an inserted custom tool output ResponseItem::CustomToolCall { @@ -1260,6 +1320,8 @@ fn normalize_mixed_inserts_and_removals() { ResponseItem::FunctionCallOutput { call_id: "c1".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -1273,6 +1335,8 @@ fn normalize_mixed_inserts_and_removals() { call_id: "t1".to_string(), name: None, output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ResponseItem::LocalShellCall { id: None, @@ -1289,6 +1353,8 @@ fn normalize_mixed_inserts_and_removals() { ResponseItem::FunctionCallOutput { call_id: "s1".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ] ); @@ -1320,6 +1386,8 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() { ResponseItem::FunctionCallOutput { call_id: "call-x".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + + metadata: None, }, ] ); @@ -1333,6 +1401,8 @@ fn normalize_adds_missing_output_for_tool_search_call() { status: Some("completed".to_string()), execution: "client".to_string(), arguments: "{}".into(), + + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1347,12 +1417,16 @@ fn normalize_adds_missing_output_for_tool_search_call() { status: Some("completed".to_string()), execution: "client".to_string(), arguments: "{}".into(), + + metadata: None, }, ResponseItem::ToolSearchOutput { call_id: Some("search-call-x".to_string()), status: "completed".to_string(), execution: "client".to_string(), tools: Vec::new(), + + metadata: None, }, ] ); @@ -1402,6 +1476,8 @@ fn normalize_removes_orphan_function_call_output_panics_in_debug() { let items = vec![ResponseItem::FunctionCallOutput { call_id: "orphan-1".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1415,6 +1491,8 @@ fn normalize_removes_orphan_custom_tool_call_output_panics_in_debug() { call_id: "orphan-2".to_string(), name: None, output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1428,6 +1506,8 @@ fn normalize_removes_orphan_client_tool_search_output() { status: "completed".to_string(), execution: "client".to_string(), tools: Vec::new(), + + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1445,6 +1525,8 @@ fn normalize_removes_orphan_client_tool_search_output_panics_in_debug() { status: "completed".to_string(), execution: "client".to_string(), tools: Vec::new(), + + metadata: None, }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1457,6 +1539,8 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() { status: "completed".to_string(), execution: "server".to_string(), tools: Vec::new(), + + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1469,6 +1553,8 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() { status: "completed".to_string(), execution: "server".to_string(), tools: Vec::new(), + + metadata: None, }] ); } @@ -1489,6 +1575,8 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() { ResponseItem::FunctionCallOutput { call_id: "c2".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -1569,6 +1657,8 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() { detail: None, }, ]), + + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1595,6 +1685,8 @@ fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() { detail: None, }, ]), + + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1625,6 +1717,8 @@ fn non_base64_image_urls_are_unchanged() { detail: None, }, ]), + + metadata: None, }; assert_eq!( @@ -1668,6 +1762,8 @@ fn non_image_base64_data_url_is_unchanged() { detail: None, }, ]), + + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1752,6 +1848,8 @@ fn original_detail_images_scale_with_dimensions() { detail: Some(ImageDetail::Original), }, ]), + + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1783,6 +1881,8 @@ fn original_detail_webp_images_scale_with_dimensions() { detail: Some(ImageDetail::Original), }, ]), + + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index 839bae331e..f2bf371762 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -34,6 +34,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, )); } @@ -59,6 +60,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { status: "completed".to_string(), execution: "client".to_string(), tools: Vec::new(), + metadata: None, }, )); } @@ -81,6 +83,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { call_id: call_id.clone(), name: None, output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, )); } @@ -104,6 +107,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, )); } diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index ad776d1424..a00765c78c 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -155,6 +155,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option { status, revised_prompt, result, + .. } => Some(TurnItem::ImageGeneration( codex_protocol::items::ImageGenerationItem { id: id.clone(), diff --git a/codex-rs/core/src/event_mapping_tests.rs b/codex-rs/core/src/event_mapping_tests.rs index befd9463bd..daeda30e61 100644 --- a/codex-rs/core/src/event_mapping_tests.rs +++ b/codex-rs/core/src/event_mapping_tests.rs @@ -323,6 +323,8 @@ fn parses_reasoning_summary_and_raw_content() { text: "raw details".to_string(), }]), encrypted_content: None, + + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected reasoning turn item"); @@ -355,6 +357,8 @@ fn parses_reasoning_including_raw_content() { }, ]), encrypted_content: None, + + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected reasoning turn item"); @@ -380,6 +384,8 @@ fn parses_web_search_call() { query: Some("weather".to_string()), queries: None, }), + + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected web search turn item"); @@ -408,6 +414,8 @@ fn parses_web_search_open_page_call() { action: Some(WebSearchAction::OpenPage { url: Some("https://example.com".to_string()), }), + + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected web search turn item"); @@ -436,6 +444,8 @@ fn parses_web_search_find_in_page_call() { url: Some("https://example.com".to_string()), pattern: Some("needle".to_string()), }), + + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected web search turn item"); @@ -462,6 +472,8 @@ fn parses_partial_web_search_call_without_action_as_other() { id: Some("ws_partial".to_string()), status: Some("in_progress".to_string()), action: None, + + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected web search turn item"); diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 30647f1a81..184be2593f 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -103,6 +103,7 @@ async fn seed_guardian_parent_history(session: &Arc, turn: &Arc Opti Some(ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: output.clone(), + metadata: None, }) } ResponseInputItem::CustomToolCallOutput { @@ -396,12 +397,14 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti call_id: call_id.clone(), name: name.clone(), output: output.clone(), + metadata: None, }), ResponseInputItem::McpToolCallOutput { call_id, output } => { let output = output.as_function_call_output_payload(); Some(ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output, + metadata: None, }) } ResponseInputItem::ToolSearchOutput { @@ -414,6 +417,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti status: status.clone(), execution: execution.clone(), tools: tools.clone(), + metadata: None, }), _ => None, } diff --git a/codex-rs/core/src/tasks/ghost_snapshot.rs b/codex-rs/core/src/tasks/ghost_snapshot.rs index 01aa9758f7..c407f45fea 100644 --- a/codex-rs/core/src/tasks/ghost_snapshot.rs +++ b/codex-rs/core/src/tasks/ghost_snapshot.rs @@ -112,6 +112,7 @@ impl SessionTask for GhostSnapshotTask { .session .record_conversation_items(&ctx, &[ResponseItem::GhostSnapshot { ghost_commit: ghost_commit.clone(), + metadata: None, }]) .await; info!("ghost commit captured: {}", ghost_commit.id()); diff --git a/codex-rs/core/src/tasks/undo.rs b/codex-rs/core/src/tasks/undo.rs index 9d899cb30d..9a6ca368bc 100644 --- a/codex-rs/core/src/tasks/undo.rs +++ b/codex-rs/core/src/tasks/undo.rs @@ -81,7 +81,7 @@ impl SessionTask for UndoTask { .enumerate() .rev() .find_map(|(idx, item)| match item { - ResponseItem::GhostSnapshot { ghost_commit } => { + ResponseItem::GhostSnapshot { ghost_commit, .. } => { Some((idx, ghost_commit.clone())) } _ => None, diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index ae389b49e0..6e8660a2b8 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -54,6 +54,8 @@ fn drops_from_last_user_only() { }], content: None, encrypted_content: None, + + metadata: None, }, ResponseItem::FunctionCall { id: None, diff --git a/codex-rs/core/src/turn_timing_tests.rs b/codex-rs/core/src/turn_timing_tests.rs index 77b2c80f77..7806dfb78c 100644 --- a/codex-rs/core/src/turn_timing_tests.rs +++ b/codex-rs/core/src/turn_timing_tests.rs @@ -126,6 +126,8 @@ fn response_item_records_turn_ttft_ignores_empty_non_output_items() { &ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + + metadata: None, } )); } diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 721d056e71..8ee00a63e2 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -395,6 +395,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() { call_id: "legacy-js-call".to_string(), name: None, output: FunctionCallOutputPayload::from_text("legacy js_repl stdout".to_string()), + metadata: None, }), }, RolloutLine { @@ -537,6 +538,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { detail: Some(ImageDetail::Original), }, ]), + metadata: None, }), }, RolloutLine { @@ -561,6 +563,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { detail: Some(ImageDetail::Original), }, ]), + metadata: None, }), }, ]; @@ -1857,6 +1860,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { text: "content".into(), }]), encrypted_content: None, + metadata: None, }); prompt.input.push(ResponseItem::Message { id: Some("message-id".into()), @@ -1875,6 +1879,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { query: Some("weather".into()), queries: None, }), + metadata: None, }); prompt.input.push(ResponseItem::FunctionCall { id: Some("function-id".into()), @@ -1887,6 +1892,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { prompt.input.push(ResponseItem::FunctionCallOutput { call_id: "function-call-id".into(), output: FunctionCallOutputPayload::from_text("ok".into()), + metadata: None, }); prompt.input.push(ResponseItem::LocalShellCall { id: Some("local-shell-id".into()), @@ -1913,6 +1919,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { call_id: "custom-tool-call-id".into(), name: None, output: FunctionCallOutputPayload::from_text("ok".into()), + metadata: None, }); let mut stream = client_session diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index d4c3d59d71..ae305b234d 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1594,6 +1594,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = @@ -2804,6 +2805,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = @@ -2925,6 +2927,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 9544f6e56f..45e1df685d 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -79,6 +79,7 @@ fn format_labeled_requests_snapshot( fn compacted_summary_only_output(summary: &str) -> Vec { vec![ResponseItem::Compaction { encrypted_content: summary_with_prefix(summary), + metadata: None, }] } @@ -221,6 +222,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { let compacted_history = vec![ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }]; let compact_mock = responses::mount_compact_json_once( harness.server(), @@ -1117,6 +1119,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> let compacted_history = vec![ ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ResponseItem::Message { id: None, @@ -1172,7 +1175,10 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> let has_compaction_item = replacement_history.iter().any(|item| { matches!( item, - ResponseItem::Compaction { encrypted_content } + ResponseItem::Compaction { + encrypted_content, + .. + } if encrypted_content == "ENCRYPTED_COMPACTION_SUMMARY" ) }); @@ -1268,6 +1274,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = responses::mount_compact_json_once( @@ -1401,6 +1408,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = responses::mount_compact_json_once( @@ -2412,6 +2420,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject let compacted_history = vec![ResponseItem::Compaction { encrypted_content: summary_with_prefix("REMOTE_SUMMARY_ONLY"), + metadata: None, }]; let compact_mock = responses::mount_compact_json_once( harness.server(), diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index e231df8edc..6fec0365df 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -215,6 +215,13 @@ async fn user_message_type_metadata_is_emitted_when_feature_enabled() -> anyhow: .and_then(Value::as_str), Some("prompt") ); + assert_eq!( + user_message + .get("metadata") + .and_then(|metadata| metadata.get("session_source")) + .and_then(Value::as_str), + Some("exec") + ); Ok(()) } @@ -344,6 +351,13 @@ async fn user_message_type_prompt_steering_metadata_is_emitted_when_feature_enab .and_then(Value::as_str), Some("prompt_steering") ); + assert_eq!( + steered_message + .get("metadata") + .and_then(|metadata| metadata.get("session_source")) + .and_then(Value::as_str), + Some("exec") + ); Ok(()) } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index cc6c9535bb..a02686be29 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -291,6 +291,22 @@ pub enum SandboxPolicyMetadata { FullAccess, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case")] +pub enum SessionSourceMetadata { + Cli, + Vscode, + Exec, + Mcp, + SubagentReview, + SubagentCompact, + SubagentMemoryConsolidation, + SubagentThreadSpawn, + SubagentOther, + Unknown, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] pub struct ResponseItemMetadata { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -309,6 +325,9 @@ pub struct ResponseItemMetadata { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub review_decision: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub session_source: Option, } impl ResponseItemMetadata { @@ -318,6 +337,7 @@ impl ResponseItemMetadata { && self.sandbox_policy.is_none() && self.is_tool_call_escalated.is_none() && self.review_decision.is_none() + && self.session_source.is_none() } pub fn merge_from(&mut self, other: Self) { @@ -336,6 +356,9 @@ impl ResponseItemMetadata { if other.review_decision.is_some() { self.review_decision = other.review_decision; } + if other.session_source.is_some() { + self.session_source = other.session_source; + } } } @@ -442,6 +465,9 @@ pub enum ResponseItem { #[ts(optional)] content: Option>, encrypted_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, LocalShellCall { /// Legacy id field retained for compatibility with older payloads. @@ -484,6 +510,9 @@ pub enum ResponseItem { execution: String, #[ts(type = "unknown")] arguments: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, // NOTE: The `output` field for `function_call_output` uses a dedicated payload type with // custom serialization. On the wire it is either: @@ -495,6 +524,9 @@ pub enum ResponseItem { #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, CustomToolCall { #[serde(default, skip_serializing)] @@ -522,6 +554,9 @@ pub enum ResponseItem { #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, ToolSearchOutput { call_id: Option, @@ -529,6 +564,9 @@ pub enum ResponseItem { execution: String, #[ts(type = "unknown[]")] tools: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, // Emitted by the Responses API when the agent triggers a web search. // Example payload (from SSE `response.output_item.done`): @@ -548,6 +586,9 @@ pub enum ResponseItem { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] action: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, // Emitted by the Responses API when the agent triggers image generation. // Example payload: @@ -565,14 +606,23 @@ pub enum ResponseItem { #[ts(optional)] revised_prompt: Option, result: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, // Generated by the harness but considered exactly as a model response. GhostSnapshot { ghost_commit: GhostCommit, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, #[serde(alias = "compaction_summary")] Compaction { encrypted_content: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, #[serde(other)] Other, @@ -1136,12 +1186,18 @@ impl From for ResponseItem { end_turn: None, phase: None, }, - ResponseInputItem::FunctionCallOutput { call_id, output } => { - Self::FunctionCallOutput { call_id, output } - } + ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput { + call_id, + output, + metadata: None, + }, ResponseInputItem::McpToolCallOutput { call_id, output } => { let output = output.into_function_call_output_payload(); - Self::FunctionCallOutput { call_id, output } + Self::FunctionCallOutput { + call_id, + output, + metadata: None, + } } ResponseInputItem::CustomToolCallOutput { call_id, @@ -1151,6 +1207,7 @@ impl From for ResponseItem { call_id, name, output, + metadata: None, }, ResponseInputItem::ToolSearchOutput { call_id, @@ -1162,6 +1219,7 @@ impl From for ResponseItem { status, execution, tools, + metadata: None, }, } } @@ -1729,6 +1787,7 @@ mod tests { status: "completed".to_string(), revised_prompt: Some("A small blue square".to_string()), result: "Zm9v".to_string(), + metadata: None, } ); } @@ -1750,6 +1809,7 @@ mod tests { status: "completed".to_string(), revised_prompt: None, result: "Zm9v".to_string(), + metadata: None, } ); } @@ -2623,6 +2683,7 @@ mod tests { item, ResponseItem::Compaction { encrypted_content: "abc".into(), + metadata: None, } ); Ok(()) @@ -2703,6 +2764,7 @@ mod tests { id: expected_id.clone(), status: expected_status.clone(), action: expected_action.clone(), + metadata: None, }; assert_eq!(parsed, expected); @@ -2793,6 +2855,7 @@ mod tests { "query": "calendar create", "limit": 1, }), + metadata: None, } ); @@ -2853,6 +2916,7 @@ mod tests { "additionalProperties": false, } })], + metadata: None, } ); @@ -2906,6 +2970,7 @@ mod tests { arguments: serde_json::json!({ "paths": ["crm"], }), + metadata: None, } ); @@ -2925,6 +2990,7 @@ mod tests { status: "completed".to_string(), execution: "server".to_string(), tools: vec![], + metadata: None, } ); @@ -3135,4 +3201,35 @@ mod tests { Ok(()) } + + #[test] + fn response_item_metadata_helpers_include_session_source() { + let mut metadata = ResponseItemMetadata::default(); + assert!(metadata.is_empty()); + + metadata.merge_from(ResponseItemMetadata { + session_source: Some(SessionSourceMetadata::Exec), + ..Default::default() + }); + + assert_eq!(metadata.session_source, Some(SessionSourceMetadata::Exec)); + assert!(!metadata.is_empty()); + } + + #[test] + fn session_source_metadata_serializes_snake_case() -> Result<()> { + assert_eq!( + serde_json::to_value(SessionSourceMetadata::Cli)?, + serde_json::Value::String("cli".to_string()) + ); + assert_eq!( + serde_json::to_value(SessionSourceMetadata::Vscode)?, + serde_json::Value::String("vscode".to_string()) + ); + assert_eq!( + serde_json::to_value(SessionSourceMetadata::SubagentThreadSpawn)?, + serde_json::Value::String("subagent_thread_spawn".to_string()) + ); + Ok(()) + } }