diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 0be0c7243..b275969a9 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2285,6 +2285,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "phase": { "anyOf": [ { @@ -2325,6 +2335,16 @@ }, "type": "array" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "recipient": { "type": "string" }, @@ -2363,6 +2383,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -2404,6 +2434,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "$ref": "#/definitions/LocalShellStatus" }, @@ -2438,6 +2478,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -2483,6 +2533,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -2510,6 +2570,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -2544,6 +2614,16 @@ "input": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -2575,6 +2655,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -2611,6 +2701,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -2654,6 +2754,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -2679,6 +2789,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -2713,6 +2833,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -2730,6 +2860,16 @@ }, { "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction_trigger" @@ -2752,6 +2892,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "context_compaction" @@ -2784,6 +2934,17 @@ } ] }, + "ResponseItemMetadata": { + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ResponsesApiWebSearchAction": { "oneOf": [ { 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 cad6515cf..7752b7b1a 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 @@ -14639,6 +14639,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "phase": { "anyOf": [ { @@ -14679,6 +14689,16 @@ }, "type": "array" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "recipient": { "type": "string" }, @@ -14717,6 +14737,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/v2/ReasoningItemReasoningSummary" @@ -14758,6 +14788,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "$ref": "#/definitions/v2/LocalShellStatus" }, @@ -14792,6 +14832,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -14837,6 +14887,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -14864,6 +14924,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/v2/FunctionCallOutputBody" }, @@ -14898,6 +14968,16 @@ "input": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -14929,6 +15009,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -14965,6 +15055,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -15008,6 +15108,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -15033,6 +15143,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -15067,6 +15187,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -15084,6 +15214,16 @@ }, { "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction_trigger" @@ -15106,6 +15246,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "context_compaction" @@ -15138,6 +15288,17 @@ } ] }, + "ResponseItemMetadata": { + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ResponsesApiWebSearchAction": { "oneOf": [ { 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 33390a98a..49e926755 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 @@ -11112,6 +11112,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "phase": { "anyOf": [ { @@ -11152,6 +11162,16 @@ }, "type": "array" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "recipient": { "type": "string" }, @@ -11190,6 +11210,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -11231,6 +11261,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "$ref": "#/definitions/LocalShellStatus" }, @@ -11265,6 +11305,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -11310,6 +11360,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -11337,6 +11397,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -11371,6 +11441,16 @@ "input": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -11402,6 +11482,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -11438,6 +11528,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -11481,6 +11581,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -11506,6 +11616,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -11540,6 +11660,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -11557,6 +11687,16 @@ }, { "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction_trigger" @@ -11579,6 +11719,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "context_compaction" @@ -11611,6 +11761,17 @@ } ] }, + "ResponseItemMetadata": { + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ResponsesApiWebSearchAction": { "oneOf": [ { 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 2d954061f..fdd40dae6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -384,6 +384,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "phase": { "anyOf": [ { @@ -424,6 +434,16 @@ }, "type": "array" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "recipient": { "type": "string" }, @@ -462,6 +482,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -503,6 +533,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "$ref": "#/definitions/LocalShellStatus" }, @@ -537,6 +577,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -582,6 +632,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -609,6 +669,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -643,6 +713,16 @@ "input": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -674,6 +754,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -710,6 +800,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -753,6 +853,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -778,6 +888,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -812,6 +932,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -829,6 +959,16 @@ }, { "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction_trigger" @@ -851,6 +991,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "context_compaction" @@ -883,6 +1033,17 @@ } ] }, + "ResponseItemMetadata": { + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ResponsesApiWebSearchAction": { "oneOf": [ { 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 42505ac24..7ba2a0a64 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -455,6 +455,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "phase": { "anyOf": [ { @@ -495,6 +505,16 @@ }, "type": "array" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "recipient": { "type": "string" }, @@ -533,6 +553,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "summary": { "items": { "$ref": "#/definitions/ReasoningItemReasoningSummary" @@ -574,6 +604,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "$ref": "#/definitions/LocalShellStatus" }, @@ -608,6 +648,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -653,6 +703,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -680,6 +740,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" }, @@ -714,6 +784,16 @@ "input": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": "string" }, @@ -745,6 +825,16 @@ "call_id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "name": { "type": [ "string", @@ -781,6 +871,16 @@ "execution": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": "string" }, @@ -824,6 +924,16 @@ ], "writeOnly": true }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "status": { "type": [ "string", @@ -849,6 +959,16 @@ "id": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "result": { "type": "string" }, @@ -883,6 +1003,16 @@ "encrypted_content": { "type": "string" }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction" @@ -900,6 +1030,16 @@ }, { "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "compaction_trigger" @@ -922,6 +1062,16 @@ "null" ] }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, "type": { "enum": [ "context_compaction" @@ -954,6 +1104,17 @@ } ] }, + "ResponseItemMetadata": { + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ResponsesApiWebSearchAction": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index b90963db8..5bd9a1032 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -9,10 +9,11 @@ import type { LocalShellStatus } from "./LocalShellStatus"; import type { MessagePhase } from "./MessagePhase"; import type { ReasoningItemContent } from "./ReasoningItemContent"; import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary"; +import type { ResponseItemMetadata } from "./ResponseItemMetadata"; import type { WebSearchAction } from "./WebSearchAction"; -export type ResponseItem = { "type": "message", role: string, content: Array, phase?: MessagePhase, } | { "type": "agent_message", author: string, recipient: string, content: Array, } | { "type": "reasoning", summary: Array, content?: Array, encrypted_content: string | null, } | { "type": "local_shell_call", +export type ResponseItem = { "type": "message", role: string, content: Array, phase?: MessagePhase, metadata?: ResponseItemMetadata, } | { "type": "agent_message", author: string, recipient: string, content: Array, metadata?: ResponseItemMetadata, } | { "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, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "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, } | { "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": "compaction", encrypted_content: string, } | { "type": "compaction_trigger" } | { "type": "context_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": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_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 new file mode 100644 index 000000000..f7b69a6df --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.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 ResponseItemMetadata = { turn_id?: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index ad6376fa5..c9ad437e5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -66,6 +66,7 @@ export type { Resource } from "./Resource"; export type { ResourceContent } from "./ResourceContent"; export type { ResourceTemplate } from "./ResourceTemplate"; export type { ResponseItem } from "./ResponseItem"; +export type { ResponseItemMetadata } from "./ResponseItemMetadata"; export type { ReviewDecision } from "./ReviewDecision"; export type { ServerNotification } from "./ServerNotification"; export type { ServerRequest } from "./ServerRequest"; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index f499e568f..e19a19e1e 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -3381,6 +3381,7 @@ mod tests { text: "plain text".into(), }], phase: None, + metadata: None, }), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-a".into(), diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 08ddfa4c6..e949daa33 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -136,9 +136,11 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() text: "REMOTE_COMPACT_SUMMARY".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = responses::mount_compact_json_once( diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 809358cc8..21dae50d6 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -60,6 +60,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu text: injected_text.to_string(), }], phase: None, + metadata: None, }; let inject_req = mcp @@ -197,6 +198,7 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() text: "Injected after first turn".to_string(), }], phase: None, + metadata: None, }; let injected_value = serde_json::to_value(&injected_item)?; 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 cf797ff6b..e67184bf0 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -2449,6 +2449,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { text: "history override".to_string(), }], phase: None, + metadata: None, }]), ..Default::default() }) @@ -3456,6 +3457,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> { text: history_text.to_string(), }], phase: None, + metadata: None, }]; // Resume with explicit history and override the model. diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index 202a57416..4ea564f13 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -815,6 +815,7 @@ mod tests { text: "hello".to_string(), }], phase: None, + metadata: None, }], tools: vec![json!({ "type": "function", diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs index 860f4f283..1c231d6be 100644 --- a/codex-rs/codex-api/src/endpoint/search.rs +++ b/codex-rs/codex-api/src/endpoint/search.rs @@ -161,6 +161,7 @@ mod tests { }, ], phase: None, + metadata: None, }])), commands: Some(SearchCommands { search_query: Some(vec![SearchQuery { diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index ae34accb6..624d008e5 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -514,6 +514,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { role: "user".into(), content: vec![ContentItem::InputText { text: "hi".into() }], phase: None, + metadata: None, }], tools: Vec::new(), tool_choice: "auto".into(), diff --git a/codex-rs/context-fragments/src/fragment.rs b/codex-rs/context-fragments/src/fragment.rs index bd0e06772..38714e251 100644 --- a/codex-rs/context-fragments/src/fragment.rs +++ b/codex-rs/context-fragments/src/fragment.rs @@ -83,6 +83,7 @@ pub trait ContextualUserFragment { text: self.render(), }], phase: None, + metadata: None, } } @@ -94,6 +95,7 @@ pub trait ContextualUserFragment { text: self.render(), }], phase: None, + metadata: None, } } diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index dcfa1b83a..9c12d60e6 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -51,7 +51,7 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item: | ResponseItem::WebSearchCall { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other, ) => false, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index ac42984b5..410e1beaf 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -71,6 +71,7 @@ fn assistant_message(text: &str, phase: Option) -> ResponseItem { text: text.to_string(), }], phase, + metadata: None, } } @@ -90,6 +91,7 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem { namespace: None, arguments: "{}".to_string(), call_id: call_id.to_string(), + metadata: None, } } @@ -861,6 +863,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { text: "Parent root guidance.".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -869,6 +872,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { text: "Parent subagent guidance.".to_string(), }], phase: None, + metadata: None, }, assistant_message("parent commentary", Some(MessagePhase::Commentary)), assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)), @@ -878,6 +882,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { summary: Vec::new(), content: None, encrypted_content: None, + metadata: None, }, trigger_message.to_response_input_item().into(), spawn_agent_call(&parent_spawn_call_id), @@ -903,7 +908,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .flush_rollout() .await .expect("parent rollout should flush"); - let child_thread_id = harness .control .spawn_agent_with_metadata( @@ -941,6 +945,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { text: "parent seed context".to_string(), }], phase: None, + metadata: None, }, assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)), ResponseItem::Message { @@ -950,6 +955,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { text: "Child subagent guidance.".to_string(), }], phase: None, + metadata: None, }, ]; assert_eq!( @@ -1080,6 +1086,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() { text: "compacted parent summary".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -1088,6 +1095,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() { text: "Parent root guidance.".to_string(), }], phase: None, + metadata: None, }, ]; parent_thread @@ -1385,6 +1393,7 @@ async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_li text: "parent startup developer context".to_string(), }], phase: None, + metadata: None, }], ) .await; @@ -1506,6 +1515,7 @@ async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() { text: "Parent root guidance.".to_string(), }], phase: None, + metadata: None, }, spawn_agent_call(&parent_spawn_call_id), ], diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 2c8b380ba..4ea8c0ff1 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -782,7 +782,10 @@ impl ModelClient { responses_metadata: &CodexResponsesMetadata, ) -> Result { let instructions = &prompt.base_instructions.text; - let input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); + let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); + if !self.state.provider.info().is_openai() { + input.iter_mut().for_each(ResponseItem::clear_metadata); + } let tools = create_tools_json_for_responses_api(&prompt.tools)?; let reasoning = Self::build_reasoning(model_info, effort, summary); let include = if reasoning.is_some() { @@ -1057,9 +1060,15 @@ impl ModelClientSession { trace!("incremental request failed, items didn't match"); return None; }; - let response_items = - last_response.map_or(&[][..], |response| response.items_added.as_slice()); - let Some(incremental_items) = after_previous_input.strip_prefix(response_items) else { + let mut response_items = + last_response.map_or_else(Vec::new, |response| response.items_added.clone()); + if !self.client.state.provider.info().is_openai() { + response_items + .iter_mut() + .for_each(ResponseItem::clear_metadata); + } + let Some(incremental_items) = after_previous_input.strip_prefix(response_items.as_slice()) + else { trace!("incremental request failed, items didn't match"); return None; }; diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 71d102598..624501db4 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -98,7 +98,7 @@ fn strip_image_details(items: &mut [ResponseItem]) { | ResponseItem::WebSearchCall { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => {} } diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 4b218e490..b71acc398 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -20,6 +20,7 @@ fn prompt_with_image_outputs() -> Prompt { detail: Some(ImageDetail::Original), }], phase: None, + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "function-call".to_string(), @@ -29,6 +30,7 @@ fn prompt_with_image_outputs() -> Prompt { detail: Some(ImageDetail::High), }, ]), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "custom-call".to_string(), @@ -39,6 +41,7 @@ fn prompt_with_image_outputs() -> Prompt { detail: Some(ImageDetail::Auto), }, ]), + metadata: None, }, ], ..Default::default() @@ -63,6 +66,7 @@ fn responses_lite_request_copies_strip_image_details() { detail: None, }], phase: None, + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "function-call".to_string(), @@ -72,6 +76,7 @@ fn responses_lite_request_copies_strip_image_details() { detail: None, }, ]), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "custom-call".to_string(), @@ -82,6 +87,7 @@ fn responses_lite_request_copies_strip_image_details() { detail: None, }, ]), + metadata: None, }, ] ); diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 29f397d5c..06074a5d2 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -225,6 +225,7 @@ fn output_message(id: &str, text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index c881823d8..f97e81c6e 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -81,6 +81,7 @@ async fn forward_events_cancelled_while_send_blocked_shuts_down_delegate() { call_id: "call-1".to_string(), name: "tool".to_string(), input: "{}".to_string(), + metadata: None, }, }), }) diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 3b8d972d7..451b49782 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -437,6 +437,7 @@ impl CodexThread { role: "user".to_string(), content: vec![ContentItem::InputText { text: message }], phase: None, + metadata: None, }; self.codex .session diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 6ffb779d3..52b2ef4ef 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -32,6 +32,7 @@ use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_protocol::models::ResponseItemMetadata; use codex_protocol::protocol::CompactedItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TurnStartedEvent; @@ -443,7 +444,13 @@ pub fn content_items_to_text(content: &[ContentItem]) -> Option { } } -pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec { +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct CompactedUserMessage { + message: String, + metadata: Option, +} + +pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec { items .iter() .filter_map(|item| match crate::event_mapping::parse_turn_item(item) { @@ -451,7 +458,13 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec { if is_summary_message(&user.message()) { None } else { - Some(user.message()) + Some(CompactedUserMessage { + message: user.message(), + metadata: match item { + ResponseItem::Message { metadata, .. } => metadata.clone(), + _ => None, + }, + }) } } _ => None, @@ -522,7 +535,7 @@ pub(crate) fn insert_initial_context_before_last_real_user_or_summary( pub(crate) fn build_compacted_history( initial_context: Vec, - user_messages: &[String], + user_messages: &[CompactedUserMessage], summary_text: &str, ) -> Vec { build_compacted_history_with_limit( @@ -535,24 +548,28 @@ pub(crate) fn build_compacted_history( fn build_compacted_history_with_limit( mut history: Vec, - user_messages: &[String], + user_messages: &[CompactedUserMessage], summary_text: &str, max_tokens: usize, ) -> Vec { - let mut selected_messages: Vec = Vec::new(); + let mut selected_messages: Vec = Vec::new(); if max_tokens > 0 { let mut remaining = max_tokens; for message in user_messages.iter().rev() { if remaining == 0 { break; } - let tokens = approx_token_count(message); + let tokens = approx_token_count(&message.message); if tokens <= remaining { selected_messages.push(message.clone()); remaining = remaining.saturating_sub(tokens); } else { - let truncated = truncate_text(message, TruncationPolicy::Tokens(remaining)); - selected_messages.push(truncated); + let truncated = + truncate_text(&message.message, TruncationPolicy::Tokens(remaining)); + selected_messages.push(CompactedUserMessage { + message: truncated, + metadata: message.metadata.clone(), + }); break; } } @@ -564,9 +581,10 @@ fn build_compacted_history_with_limit( id: None, role: "user".to_string(), content: vec![ContentItem::InputText { - text: message.clone(), + text: message.message.clone(), }], phase: None, + metadata: message.metadata.clone(), }); } @@ -581,6 +599,7 @@ fn build_compacted_history_with_limit( role: "user".to_string(), content: vec![ContentItem::InputText { text: summary_text }], phase: None, + metadata: None, }); history diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index a3da98cc8..eca14af80 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -344,7 +344,7 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool { ResponseItem::Message { .. } => false, ResponseItem::AgentMessage { .. } => true, ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::CompactionTrigger => false, + ResponseItem::CompactionTrigger { .. } => false, ResponseItem::Reasoning { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } @@ -403,29 +403,38 @@ pub(crate) fn trim_function_call_history_to_fit_context_window( fn rewritten_output_for_context_window(item: &ResponseItem) -> Option { Some(match item { - ResponseItem::FunctionCallOutput { call_id, output } => ResponseItem::FunctionCallOutput { + ResponseItem::FunctionCallOutput { + call_id, + output, + metadata, + } => ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: truncated_output_payload(output), + metadata: metadata.clone(), }, ResponseItem::CustomToolCallOutput { call_id, name, output, + metadata, } => ResponseItem::CustomToolCallOutput { call_id: call_id.clone(), name: name.clone(), output: truncated_output_payload(output), + metadata: metadata.clone(), }, ResponseItem::ToolSearchOutput { call_id, status, execution, + metadata, .. } => ResponseItem::ToolSearchOutput { call_id: call_id.clone(), status: status.clone(), execution: execution.clone(), tools: Vec::new(), + metadata: metadata.clone(), }, _ => return None, }) diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 047c2527c..d13aa9406 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -231,7 +231,7 @@ async fn run_remote_compact_task_inner_impl( ) .await?; let mut input = prompt_input.clone(); - input.push(ResponseItem::CompactionTrigger); + input.push(ResponseItem::CompactionTrigger { metadata: None }); let prompt = Prompt { input, tools: tool_router.model_visible_specs(), @@ -515,6 +515,7 @@ fn truncate_message_text_to_token_budget( role, content, phase, + metadata, } = item else { return Some(item); @@ -553,6 +554,7 @@ fn truncate_message_text_to_token_budget( role, content: truncated_content, phase, + metadata, }) } @@ -573,6 +575,7 @@ mod tests { text: text.to_string(), }], phase, + metadata: None, } } @@ -604,13 +607,16 @@ mod tests { namespace: None, arguments: "{}".to_string(), call_id: "call_1".to_string(), + metadata: None, }, ResponseItem::Compaction { encrypted_content: "old".to_string(), + metadata: None, }, ]; let output = ResponseItem::Compaction { encrypted_content: "new".to_string(), + metadata: None, }; let (history, _) = build_v2_compacted_history(&input, output.clone()); @@ -638,6 +644,7 @@ mod tests { ]; let output = ResponseItem::Compaction { encrypted_content: "new".to_string(), + metadata: None, }; let (history, _) = build_v2_compacted_history(&input, output.clone()); @@ -664,9 +671,11 @@ mod tests { }, ], phase: None, + metadata: None, }]; let output = ResponseItem::Compaction { encrypted_content: "new".to_string(), + metadata: None, }; let (_, retained_image_count) = build_v2_compacted_history(&input, output); @@ -714,6 +723,7 @@ mod tests { }, ], phase: None, + metadata: None, }; let truncated = @@ -737,6 +747,7 @@ mod tests { }, ], phase: None, + metadata: None, }] ); } @@ -751,6 +762,7 @@ mod tests { detail: None, }], phase: None, + metadata: None, }; let newest = message("user", "new", /*phase*/ None); let retained = vec![ @@ -775,6 +787,7 @@ mod tests { detail: None, }], phase: None, + metadata: None, }; let newest = message("user", "new", /*phase*/ None); let retained = vec![image_only_message, newest.clone()]; @@ -789,6 +802,7 @@ mod tests { async fn collect_compaction_output_accepts_additional_output_items() { let compaction = ResponseItem::Compaction { encrypted_content: "encrypted".to_string(), + metadata: None, }; let stream = response_stream(vec![ Ok(ResponseEvent::OutputItemDone(message( diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index 550a7ec84..a52e70a7b 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -2,6 +2,7 @@ use super::*; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_protocol::models::DEFAULT_IMAGE_DETAIL; +use codex_protocol::models::ResponseItemMetadata; use pretty_assertions::assert_eq; async fn process_compacted_history_with_test_session( @@ -31,6 +32,14 @@ fn user_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, + } +} + +fn compacted_user_message(text: &str) -> CompactedUserMessage { + CompactedUserMessage { + message: text.to_string(), + metadata: None, } } @@ -75,6 +84,7 @@ fn collect_user_messages_extracts_user_text_only() { text: "ignored".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: Some("user".to_string()), @@ -83,13 +93,14 @@ fn collect_user_messages_extracts_user_text_only() { text: "first".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Other, ]; let collected = collect_user_messages(&items); - assert_eq!(vec!["first".to_string()], collected); + assert_eq!(vec![compacted_user_message("first")], collected); } #[test] @@ -107,6 +118,7 @@ do things .to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -115,6 +127,7 @@ do things text: "cwd=/tmp".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -123,12 +136,13 @@ do things text: "real user message".to_string(), }], phase: None, + metadata: None, }, ]; let collected = collect_user_messages(&items); - assert_eq!(vec!["real user message".to_string()], collected); + assert_eq!(vec![compacted_user_message("real user message")], collected); } #[test] @@ -148,7 +162,7 @@ fn collect_user_messages_filters_legacy_warnings() { let collected = collect_user_messages(&items); - assert_eq!(vec!["real user message".to_string()], collected); + assert_eq!(vec![compacted_user_message("real user message")], collected); } #[test] @@ -157,9 +171,10 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() { // that oversized user content is truncated. let max_tokens = 16; let big = "word ".repeat(200); + let user_message = compacted_user_message(&big); let history = super::build_compacted_history_with_limit( Vec::new(), - std::slice::from_ref(&big), + std::slice::from_ref(&user_message), "SUMMARY", max_tokens, ); @@ -196,7 +211,7 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() { #[test] fn build_token_limited_compacted_history_appends_summary_message() { let initial_context: Vec = Vec::new(); - let user_messages = vec!["first user message".to_string()]; + let user_messages = vec![compacted_user_message("first user message")]; let summary_text = "summary text"; let history = build_compacted_history(initial_context, &user_messages, summary_text); @@ -215,6 +230,23 @@ fn build_token_limited_compacted_history_appends_summary_message() { assert_eq!(summary, summary_text); } +#[test] +fn build_compacted_history_preserves_user_message_metadata() { + let history = build_compacted_history( + Vec::new(), + &[CompactedUserMessage { + message: "first user message".to_string(), + metadata: Some(ResponseItemMetadata { + turn_id: Some("turn-1".to_string()), + }), + }], + "summary text", + ); + + assert_eq!(history[0].turn_id(), Some("turn-1")); + assert_eq!(history[1].turn_id(), None); +} + #[test] fn should_use_remote_compact_task_for_azure_provider() { let provider = ModelProviderInfo { @@ -249,6 +281,7 @@ async fn process_compacted_history_replaces_developer_messages() { text: "stale permissions".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -257,6 +290,7 @@ async fn process_compacted_history_replaces_developer_messages() { text: "summary".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -265,6 +299,7 @@ async fn process_compacted_history_replaces_developer_messages() { text: "stale personality".to_string(), }], phase: None, + metadata: None, }, ]; let (refreshed, mut expected) = process_compacted_history_with_test_session( @@ -279,6 +314,7 @@ async fn process_compacted_history_replaces_developer_messages() { text: "summary".to_string(), }], phase: None, + metadata: None, }); assert_eq!(refreshed, expected); } @@ -292,6 +328,7 @@ async fn process_compacted_history_reinjects_full_initial_context() { text: "summary".to_string(), }], phase: None, + metadata: None, }]; let (refreshed, mut expected) = process_compacted_history_with_test_session( compacted_history, @@ -305,6 +342,7 @@ async fn process_compacted_history_reinjects_full_initial_context() { text: "summary".to_string(), }], phase: None, + metadata: None, }); assert_eq!(refreshed, expected); } @@ -324,6 +362,7 @@ keep me updated .to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -336,6 +375,7 @@ keep me updated .to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -348,6 +388,7 @@ keep me updated .to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -356,6 +397,7 @@ keep me updated text: "summary".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -364,6 +406,7 @@ keep me updated text: "stale developer instructions".to_string(), }], phase: None, + metadata: None, }, ]; let (refreshed, mut expected) = process_compacted_history_with_test_session( @@ -378,6 +421,7 @@ keep me updated text: "summary".to_string(), }], phase: None, + metadata: None, }); assert_eq!(refreshed, expected); } @@ -417,6 +461,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message text: "older user".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -425,6 +470,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message text: format!("{SUMMARY_PREFIX}\nsummary text"), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -433,6 +479,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message text: "latest user".to_string(), }], phase: None, + metadata: None, }, ]; @@ -449,6 +496,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message text: "older user".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -457,6 +505,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message text: format!("{SUMMARY_PREFIX}\nsummary text"), }], phase: None, + metadata: None, }, ]; expected.extend(initial_context); @@ -467,6 +516,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message text: "latest user".to_string(), }], phase: None, + metadata: None, }); assert_eq!(refreshed, expected); } @@ -480,6 +530,7 @@ async fn process_compacted_history_reinjects_model_switch_message() { text: "summary".to_string(), }], phase: None, + metadata: None, }]; let previous_turn_settings = PreviousTurnSettings { model: "previous-regular-model".to_string(), @@ -510,6 +561,7 @@ async fn process_compacted_history_reinjects_model_switch_message() { text: "summary".to_string(), }], phase: None, + metadata: None, }); assert_eq!(refreshed, expected); } @@ -524,6 +576,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: "older user".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -532,6 +585,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: "latest user".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -540,6 +594,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: format!("{SUMMARY_PREFIX}\nsummary text"), }], phase: None, + metadata: None, }, ]; let initial_context = vec![ResponseItem::Message { @@ -549,6 +604,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: "fresh permissions".to_string(), }], phase: None, + metadata: None, }]; let refreshed = @@ -561,6 +617,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: "older user".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -569,6 +626,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: "fresh permissions".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -577,6 +635,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: "latest user".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -585,6 +644,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() text: format!("{SUMMARY_PREFIX}\nsummary text"), }], phase: None, + metadata: None, }, ]; assert_eq!(refreshed, expected); @@ -594,6 +654,7 @@ 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, @@ -602,6 +663,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last text: "fresh permissions".to_string(), }], phase: None, + metadata: None, }]; let refreshed = @@ -614,9 +676,11 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last text: "fresh permissions".to_string(), }], phase: None, + metadata: None, }, 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 479ccc89c..47f37a32f 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -338,23 +338,25 @@ 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::AgentMessage { .. } @@ -367,7 +369,7 @@ impl ContextManager { | ResponseItem::ImageGenerationCall { .. } | ResponseItem::CustomToolCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => item.clone(), } @@ -459,7 +461,7 @@ fn is_api_message(message: &ResponseItem) -> bool { | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::CompactionTrigger => false, + ResponseItem::CompactionTrigger { .. } => false, ResponseItem::Other => false, } } @@ -511,9 +513,11 @@ fn estimate_response_item_model_visible_bytes(item: &ResponseItem) -> i64 { } | ResponseItem::Compaction { encrypted_content: content, + .. } | ResponseItem::ContextCompaction { encrypted_content: Some(content), + .. } => i64::try_from(estimate_reasoning_length(content.len())).unwrap_or(i64::MAX), item => { let raw = serde_json::to_string(item) @@ -689,7 +693,7 @@ fn is_model_generated_item(item: &ResponseItem) -> bool { | ResponseItem::LocalShellCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::CompactionTrigger => false, + ResponseItem::CompactionTrigger { .. } => false, ResponseItem::FunctionCallOutput { .. } | ResponseItem::ToolSearchOutput { .. } | ResponseItem::CustomToolCallOutput { .. } diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index f4bc87f5b..a98061b5b 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -14,6 +14,7 @@ use codex_protocol::models::LocalShellExecAction; use codex_protocol::models::LocalShellStatus; use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; +use codex_protocol::models::ResponseItemMetadata; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::protocol::AskForApproval; @@ -41,6 +42,7 @@ fn assistant_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -59,6 +61,7 @@ fn inter_agent_assistant_msg(text: &str) -> ResponseItem { text: serde_json::to_string(&communication).unwrap(), }], phase: None, + metadata: None, } } @@ -78,6 +81,7 @@ fn user_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -89,6 +93,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -100,6 +105,7 @@ fn developer_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -114,6 +120,7 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem { }) .collect(), phase: None, + metadata: None, } } @@ -145,6 +152,7 @@ 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, } } @@ -158,6 +166,7 @@ fn reasoning_msg(text: &str) -> ResponseItem { text: text.to_string(), }]), encrypted_content: None, + metadata: None, } } @@ -169,6 +178,7 @@ fn reasoning_with_encrypted_content(len: usize) -> ResponseItem { }], content: None, encrypted_content: Some("a".repeat(len)), + metadata: None, } } @@ -192,6 +202,7 @@ fn filters_non_api_messages() { text: "ignored".to_string(), }], phase: None, + metadata: None, }; let reasoning = reasoning_msg("thinking..."); h.record_items([&system, &reasoning, &ResponseItem::Other], policy); @@ -214,6 +225,7 @@ fn filters_non_api_messages() { text: "thinking...".to_string(), }]), encrypted_content: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -222,6 +234,7 @@ fn filters_non_api_messages() { text: "hi".to_string() }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -230,6 +243,7 @@ fn filters_non_api_messages() { text: "hello".to_string() }], phase: None, + metadata: None, } ] ); @@ -379,6 +393,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ], phase: None, + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -386,6 +401,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), @@ -398,6 +414,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -405,6 +422,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { call_id: "tool-1".to_string(), name: "js_repl".to_string(), input: "view_image".to_string(), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "tool-1".to_string(), @@ -418,6 +436,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), + metadata: None, }, ]; let history = create_history_with_items(items); @@ -441,6 +460,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ], phase: None, + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -448,6 +468,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), @@ -460,6 +481,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { .to_string(), }, ]), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -467,6 +489,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { call_id: "tool-1".to_string(), name: "js_repl".to_string(), input: "view_image".to_string(), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "tool-1".to_string(), @@ -480,6 +503,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { .to_string(), }, ]), + metadata: None, }, ]; assert_eq!(stripped, expected); @@ -499,6 +523,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ], phase: None, + metadata: None, }]); let preserved = with_images.for_prompt(&modalities); assert_eq!(preserved.len(), 1); @@ -518,6 +543,7 @@ 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, @@ -526,6 +552,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() { text: "hi".to_string(), }], phase: None, + metadata: None, }, ]); @@ -537,6 +564,7 @@ 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, @@ -545,6 +573,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() { text: "hi".to_string(), }], phase: None, + metadata: None, } ] ); @@ -560,12 +589,14 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() { text: "generate a lobster".to_string(), }], phase: None, + metadata: None, }, ResponseItem::ImageGenerationCall { id: "ig_123".to_string(), status: "completed".to_string(), revised_prompt: Some("lobster".to_string()), result: "Zm9v".to_string(), + metadata: None, }, ]); @@ -579,12 +610,14 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() { text: "generate a lobster".to_string(), }], phase: None, + metadata: None, }, ResponseItem::ImageGenerationCall { id: "ig_123".to_string(), status: "completed".to_string(), revised_prompt: Some("lobster".to_string()), result: String::new(), + metadata: None, }, ] ); @@ -621,10 +654,12 @@ fn remove_first_item_removes_matching_output_for_function_call() { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, }, 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); @@ -638,6 +673,7 @@ 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, @@ -645,6 +681,7 @@ fn remove_first_item_removes_matching_call_for_output() { namespace: None, arguments: "{}".to_string(), call_id: "call-2".to_string(), + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -667,6 +704,7 @@ fn replace_last_turn_images_replaces_tool_output_images() { ]), success: Some(true), }, + metadata: None, }, ]; let mut history = create_history_with_items(items); @@ -687,6 +725,7 @@ fn replace_last_turn_images_replaces_tool_output_images() { ]), success: Some(true), }, + metadata: None, }, ] ); @@ -702,6 +741,7 @@ fn replace_last_turn_images_does_not_touch_user_images() { detail: Some(DEFAULT_IMAGE_DETAIL), }], phase: None, + metadata: None, }]; let mut history = create_history_with_items(items.clone()); @@ -723,10 +763,12 @@ fn remove_first_item_handles_local_shell_pair() { env: None, user: None, }), + metadata: None, }, 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); @@ -948,11 +990,13 @@ fn remove_first_item_handles_custom_tool_pair() { call_id: "tool-1".to_string(), name: "my_tool".to_string(), input: "{}".to_string(), + metadata: None, }, ResponseItem::CustomToolCallOutput { 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); @@ -974,10 +1018,12 @@ fn normalization_retains_local_shell_outputs() { env: None, user: None, }), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "shell-1".to_string(), output: FunctionCallOutputPayload::from_text("Total output lines: 1\n\nok".to_string()), + metadata: None, }, ]; @@ -1001,6 +1047,9 @@ fn record_items_truncates_function_call_output_content() { body: FunctionCallOutputBody::Text(long_output.clone()), success: Some(true), }, + metadata: Some(ResponseItemMetadata { + turn_id: Some("turn-1".to_string()), + }), }; history.record_items([&item], policy); @@ -1021,6 +1070,7 @@ fn record_items_truncates_function_call_output_content() { } other => panic!("unexpected history item: {other:?}"), } + assert_eq!(history.items[0].turn_id(), Some("turn-1")); } #[test] @@ -1033,6 +1083,7 @@ 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); @@ -1066,6 +1117,7 @@ fn record_items_respects_custom_token_limit() { body: FunctionCallOutputBody::Text(long_output), success: Some(true), }, + metadata: None, }; history.record_items([&item], policy); @@ -1185,6 +1237,7 @@ fn normalize_adds_missing_output_for_function_call() { namespace: None, arguments: "{}".to_string(), call_id: "call-x".to_string(), + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1199,10 +1252,12 @@ fn normalize_adds_missing_output_for_function_call() { namespace: None, arguments: "{}".to_string(), call_id: "call-x".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "call-x".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ] ); @@ -1217,6 +1272,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() { call_id: "tool-x".to_string(), name: "custom".to_string(), input: "{}".to_string(), + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1231,11 +1287,13 @@ fn normalize_adds_missing_output_for_custom_tool_call() { call_id: "tool-x".to_string(), name: "custom".to_string(), input: "{}".to_string(), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "tool-x".to_string(), name: None, output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ] ); @@ -1255,6 +1313,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() { env: None, user: None, }), + metadata: None, }]; let mut h = create_history_with_items(items); @@ -1274,10 +1333,12 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() { env: None, user: None, }), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "shell-1".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ] ); @@ -1289,6 +1350,7 @@ 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); @@ -1304,6 +1366,7 @@ 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); @@ -1323,11 +1386,13 @@ fn normalize_mixed_inserts_and_removals() { namespace: None, arguments: "{}".to_string(), call_id: "c1".to_string(), + metadata: None, }, // Orphan output that should be removed 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 { @@ -1336,6 +1401,7 @@ fn normalize_mixed_inserts_and_removals() { call_id: "t1".to_string(), name: "tool".to_string(), input: "{}".to_string(), + metadata: None, }, // Local shell call also gets an inserted function call output ResponseItem::LocalShellCall { @@ -1349,6 +1415,7 @@ fn normalize_mixed_inserts_and_removals() { env: None, user: None, }), + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -1364,10 +1431,12 @@ fn normalize_mixed_inserts_and_removals() { namespace: None, arguments: "{}".to_string(), call_id: "c1".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "c1".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -1375,11 +1444,13 @@ fn normalize_mixed_inserts_and_removals() { call_id: "t1".to_string(), name: "tool".to_string(), input: "{}".to_string(), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "t1".to_string(), name: None, output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ResponseItem::LocalShellCall { id: None, @@ -1392,10 +1463,12 @@ fn normalize_mixed_inserts_and_removals() { env: None, user: None, }), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "s1".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ] ); @@ -1409,6 +1482,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() { namespace: None, arguments: "{}".to_string(), call_id: "call-x".to_string(), + metadata: None, }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1421,10 +1495,12 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() { namespace: None, arguments: "{}".to_string(), call_id: "call-x".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "call-x".to_string(), output: FunctionCallOutputPayload::from_text("aborted".to_string()), + metadata: None, }, ] ); @@ -1438,6 +1514,7 @@ 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); @@ -1452,12 +1529,14 @@ 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, }, ] ); @@ -1473,6 +1552,7 @@ fn normalize_adds_missing_output_for_custom_tool_call_panics_in_debug() { call_id: "tool-x".to_string(), name: "custom".to_string(), input: "{}".to_string(), + metadata: None, }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1493,6 +1573,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id_panics_in_debug() env: None, user: None, }), + metadata: None, }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1505,6 +1586,7 @@ 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()); @@ -1518,6 +1600,7 @@ 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()); @@ -1531,6 +1614,7 @@ 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); @@ -1548,6 +1632,7 @@ 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()); @@ -1560,6 +1645,7 @@ 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); @@ -1572,6 +1658,7 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() { status: "completed".to_string(), execution: "server".to_string(), tools: Vec::new(), + metadata: None, }] ); } @@ -1587,10 +1674,12 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() { namespace: None, arguments: "{}".to_string(), call_id: "c1".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "c2".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -1598,6 +1687,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() { call_id: "t1".to_string(), name: "tool".to_string(), input: "{}".to_string(), + metadata: None, }, ResponseItem::LocalShellCall { id: None, @@ -1610,6 +1700,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() { env: None, user: None, }), + metadata: None, }, ]; let mut h = create_history_with_items(items); @@ -1633,6 +1724,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() { }, ], phase: None, + metadata: None, }; let text_only_item = ResponseItem::Message { id: None, @@ -1641,6 +1733,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() { text: "Here is the screenshot".to_string(), }], phase: None, + metadata: None, }; let raw_len = serde_json::to_string(&image_item).unwrap().len() as i64; @@ -1668,6 +1761,7 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() { detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1694,6 +1788,7 @@ fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() { detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1714,6 +1809,7 @@ fn non_base64_image_urls_are_unchanged() { detail: Some(DEFAULT_IMAGE_DETAIL), }], phase: None, + metadata: None, }; let function_output_item = ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), @@ -1723,6 +1819,7 @@ fn non_base64_image_urls_are_unchanged() { detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), + metadata: None, }; assert_eq!( @@ -1745,6 +1842,7 @@ fn encrypted_function_output_uses_plaintext_byte_estimate() { encrypted_content: encrypted_content.clone(), }, ]), + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1765,6 +1863,7 @@ fn data_url_without_base64_marker_is_unchanged() { detail: Some(DEFAULT_IMAGE_DETAIL), }], phase: None, + metadata: None, }; assert_eq!( @@ -1785,6 +1884,7 @@ fn non_image_base64_data_url_is_unchanged() { detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1805,6 +1905,7 @@ fn mixed_case_data_url_markers_are_adjusted() { detail: Some(DEFAULT_IMAGE_DETAIL), }], phase: None, + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1837,6 +1938,7 @@ fn multiple_inline_images_apply_multiple_fixed_costs() { }, ], phase: None, + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1870,6 +1972,7 @@ 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; @@ -1900,6 +2003,7 @@ fn original_detail_images_are_capped_at_max_patch_count() { detail: Some(ImageDetail::Original), }, ]), + metadata: None, }; let raw_len = serde_json::to_string(&item).unwrap().len() as i64; @@ -1933,6 +2037,7 @@ 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; @@ -1951,6 +2056,7 @@ fn text_only_items_unchanged() { text: "Hello world, this is a response.".to_string(), }], phase: None, + metadata: None, }; let estimated = estimate_response_item_model_visible_bytes(&item); diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index cbfe6f305..1da58b619 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -49,6 +49,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, }, )); } @@ -64,6 +65,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { status: "completed".to_string(), execution: "client".to_string(), tools: Vec::new(), + metadata: None, }, )); } @@ -79,6 +81,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, }, )); } @@ -95,6 +98,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/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index d7302bbbf..0ce02240b 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -203,6 +203,7 @@ fn build_text_message(role: &str, text_sections: Vec) -> Option 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 6f9dad64b..ad5b4c877 100644 --- a/codex-rs/core/src/event_mapping_tests.rs +++ b/codex-rs/core/src/event_mapping_tests.rs @@ -61,6 +61,7 @@ fn parses_user_message_with_text_and_two_images() { }, ], phase: None, + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected user message turn item"); @@ -110,6 +111,7 @@ fn skips_local_image_label_text() { }, ], phase: None, + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected user message turn item"); @@ -142,6 +144,7 @@ fn parses_assistant_message_input_text_for_backward_compatibility() { .to_string(), }], phase: None, + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected assistant message turn item"); @@ -191,6 +194,7 @@ fn skips_unnamed_image_label_text() { }, ], phase: None, + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected user message turn item"); @@ -223,7 +227,7 @@ fn skips_user_instructions_and_env() { text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "user".to_string(), @@ -231,7 +235,7 @@ fn skips_user_instructions_and_env() { text: "test_text".to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "user".to_string(), @@ -239,7 +243,7 @@ fn skips_user_instructions_and_env() { text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "user".to_string(), @@ -248,7 +252,7 @@ fn skips_user_instructions_and_env() { .to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "user".to_string(), @@ -256,7 +260,7 @@ fn skips_user_instructions_and_env() { text: "echo 42".to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "user".to_string(), @@ -271,7 +275,7 @@ fn skips_user_instructions_and_env() { }, ], phase: None, - }, + metadata: None,}, ]; for item in items { @@ -321,7 +325,7 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() { }, ], phase: None, - }; + metadata: None,}; let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item"); @@ -353,6 +357,7 @@ fn internal_model_context_does_not_parse_as_visible_turn_item() { .render(), }], phase: None, + metadata: None, }; assert!(parse_turn_item(&item).is_none()); @@ -367,6 +372,7 @@ fn parses_agent_message() { text: "Hello from Codex".to_string(), }], phase: None, + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected agent message turn item"); @@ -398,6 +404,7 @@ 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"); @@ -430,6 +437,7 @@ fn parses_reasoning_including_raw_content() { }, ]), encrypted_content: None, + metadata: None, }; let turn_item = parse_turn_item(&item).expect("expected reasoning turn item"); @@ -455,6 +463,7 @@ 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"); @@ -483,6 +492,7 @@ 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"); @@ -511,6 +521,7 @@ 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"); @@ -537,6 +548,7 @@ 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 f984cb87e..da03eb95a 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -298,6 +298,7 @@ async fn seed_guardian_parent_history(session: &Arc, turn: &Arc, turn: &Arc, turn: &Arc anyh text: "Please also push the second docs fix.".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -534,6 +539,7 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh text: "I need approval for the second push.".to_string(), }], phase: None, + metadata: None, }, ], ) @@ -656,6 +662,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() - text: "Compacted retained user request.".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -664,6 +671,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() - text: "Compacted summary of earlier guardian context.".to_string(), }], phase: None, + metadata: None, }, ], /*reference_context_item*/ None, @@ -680,6 +688,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() - text: "Please push after the compaction.".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -688,6 +697,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() - text: "I need approval for the post-compaction push.".to_string(), }], phase: None, + metadata: None, }, ], ) @@ -735,6 +745,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() { text: "\n/tmp\n".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -743,6 +754,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() { text: "hello".to_string(), }], phase: None, + metadata: None, }, ]; @@ -770,6 +782,7 @@ fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message() text: "ordinary developer context".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -778,6 +791,7 @@ fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message() text: approval_text.clone(), }], phase: None, + metadata: None, }, ]; @@ -802,6 +816,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() { text: "check the repo".to_string(), }], phase: None, + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -809,12 +824,14 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() { namespace: None, arguments: "{\"path\":\"README.md\"}".to_string(), call_id: "call-1".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: codex_protocol::models::FunctionCallOutputPayload::from_text( "repo is public".to_string(), ), + metadata: None, }, ResponseItem::Message { id: None, @@ -823,6 +840,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() { text: "I need to push a fix".to_string(), }], phase: None, + metadata: None, }, ]; @@ -1909,6 +1927,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: text: "Please push the second docs fix too.".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -1917,6 +1936,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: text: "I need approval for the second docs fix.".to_string(), }], phase: None, + metadata: None, }, ], ) @@ -1954,6 +1974,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: text: "Please push the third docs fix too.".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -1962,6 +1983,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow: text: "I need approval for the third docs fix.".to_string(), }], phase: None, + metadata: None, }, ], ) @@ -2701,7 +2723,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> text: "Please inspect pending changes before pushing.".to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "assistant".to_string(), @@ -2709,7 +2731,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> text: "I need approval to run git diff.".to_string(), }], phase: None, - }, + metadata: None,}, ], ) .await; @@ -2768,7 +2790,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> text: "Now inspect whether pushing is safe.".to_string(), }], phase: None, - }, + metadata: None,}, ResponseItem::Message { id: None, role: "assistant".to_string(), @@ -2776,7 +2798,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() -> text: "I need approval to push after the diff check.".to_string(), }], phase: None, - }, + metadata: None,}, ], ) .await; diff --git a/codex-rs/core/src/image_preparation.rs b/codex-rs/core/src/image_preparation.rs index d992adb80..d30dfbb5c 100644 --- a/codex-rs/core/src/image_preparation.rs +++ b/codex-rs/core/src/image_preparation.rs @@ -62,7 +62,7 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) { | ResponseItem::WebSearchCall { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => {} } diff --git a/codex-rs/core/src/image_preparation_tests.rs b/codex-rs/core/src/image_preparation_tests.rs index 1d54973cc..01d512c75 100644 --- a/codex-rs/core/src/image_preparation_tests.rs +++ b/codex-rs/core/src/image_preparation_tests.rs @@ -49,6 +49,7 @@ fn preparation_preserves_small_image_bytes_and_non_data_urls() { }, ], phase: None, + metadata: None, }]; prepare_response_items(&mut items); @@ -85,6 +86,7 @@ fn detail_policies_apply_the_expected_budgets() { role: "user".to_string(), content: vec![ContentItem::InputImage { image_url, detail }], phase: None, + metadata: None, }]; prepare_response_items(&mut items); @@ -130,6 +132,7 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() { ]), success: Some(true), }, + metadata: None, }]; prepare_response_items(&mut items); @@ -160,6 +163,7 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() { ]), success: Some(true), }, + metadata: None, }] ); } diff --git a/codex-rs/core/src/realtime_context_tests.rs b/codex-rs/core/src/realtime_context_tests.rs index 8c3aa8d97..f04282a65 100644 --- a/codex-rs/core/src/realtime_context_tests.rs +++ b/codex-rs/core/src/realtime_context_tests.rs @@ -74,6 +74,7 @@ fn message(role: &str, content: ContentItem) -> ResponseItem { role: role.to_string(), content: vec![content], phase: None, + metadata: None, } } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index ed9e58113..d73d476e0 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -181,6 +181,7 @@ use uuid::Uuid; use crate::client::ModelClient; use crate::codex_thread::ThreadConfigSnapshot; +#[cfg(test)] use crate::compact::collect_user_messages; use crate::config::Config; use crate::config::Constrained; diff --git a/codex-rs/core/src/session/rollout_reconstruction.rs b/codex-rs/core/src/session/rollout_reconstruction.rs index c844a630f..34e7807a9 100644 --- a/codex-rs/core/src/session/rollout_reconstruction.rs +++ b/codex-rs/core/src/session/rollout_reconstruction.rs @@ -296,7 +296,7 @@ impl Session { // prompt shape. // TODO(ccunningham): if we drop support for None replacement_history compaction items, // we can get rid of this second loop entirely and just build `history` directly in the first loop. - let user_messages = collect_user_messages(history.raw_items()); + let user_messages = compact::collect_user_messages(history.raw_items()); let rebuilt = compact::build_compacted_history( Vec::new(), &user_messages, diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index d24394676..d53e3866c 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -20,6 +20,7 @@ fn user_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -31,6 +32,7 @@ fn assistant_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -49,6 +51,7 @@ fn inter_agent_assistant_message(text: &str) -> ResponseItem { text: serde_json::to_string(&communication).unwrap(), }], phase: None, + metadata: None, } } diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 9eee4da39..80037ce49 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -103,6 +103,7 @@ use codex_protocol::config_types::Settings; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; +use codex_protocol::models::ResponseItemMetadata; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::CompactedItem; @@ -189,6 +190,7 @@ fn user_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -200,6 +202,7 @@ fn assistant_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -259,6 +262,7 @@ fn skill_message(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -1598,6 +1602,9 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { text: "summary".to_string(), }], phase: None, + metadata: Some(ResponseItemMetadata { + turn_id: Some("compact-turn".to_string()), + }), }; let replacement_history = vec![ summary_item.clone(), @@ -1608,6 +1615,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { text: "stale developer instructions".to_string(), }], phase: None, + metadata: None, }, ]; let rollout_items = vec![RolloutItem::Compacted(CompactedItem { @@ -1669,32 +1677,35 @@ async fn resize_all_images_prepares_failures_before_history_insertion() { ]), success: Some(true), }, + metadata: None, }; session .record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&item)) .await; + let expected = vec![ResponseItem::FunctionCallOutput { + call_id: "call-1".to_string(), + output: FunctionCallOutputPayload { + body: FunctionCallOutputBody::ContentItems(vec![ + FunctionCallOutputContentItem::InputText { + text: "before".to_string(), + }, + FunctionCallOutputContentItem::InputText { + text: "image content omitted because it could not be processed".to_string(), + }, + FunctionCallOutputContentItem::InputImage { + image_url: "https://example.com/image.png".to_string(), + detail: Some(ImageDetail::High), + }, + ]), + success: Some(true), + }, + metadata: None, + }]; assert_eq!( session.state.lock().await.clone_history().raw_items(), - &[ResponseItem::FunctionCallOutput { - call_id: "call-1".to_string(), - output: FunctionCallOutputPayload { - body: FunctionCallOutputBody::ContentItems(vec![ - FunctionCallOutputContentItem::InputText { - text: "before".to_string(), - }, - FunctionCallOutputContentItem::InputText { - text: "image content omitted because it could not be processed".to_string(), - }, - FunctionCallOutputContentItem::InputImage { - image_url: "https://example.com/image.png".to_string(), - detail: Some(ImageDetail::High), - }, - ]), - success: Some(true), - }, - }] + expected.as_slice() ); } @@ -1721,6 +1732,7 @@ async fn resize_all_images_prepares_resumed_history_before_installing_it() { }, ], phase: None, + metadata: None, }; session @@ -1745,6 +1757,7 @@ async fn resize_all_images_prepares_resumed_history_before_installing_it() { }, ], phase: None, + metadata: None, }] ); } @@ -1860,7 +1873,8 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only() session .record_context_updates_and_set_reference_context_item(&turn_context) .await; - expected.extend(session.build_initial_context(&turn_context).await); + let initial_context = session.build_initial_context(&turn_context).await; + expected.extend(initial_context); let history_after_seed = session.clone_history().await; assert_eq!(expected, history_after_seed.raw_items()); @@ -7662,6 +7676,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, }], /*reference_context_item*/ None, ) @@ -7914,6 +7929,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 { @@ -7944,7 +7960,8 @@ async fn handle_output_item_done_records_image_save_history_message() { image_output_path.display(), ), ); - assert_eq!(history.raw_items(), &[image_message, item]); + let expected = vec![image_message, item]; + assert_eq!(history.raw_items(), expected.as_slice()); assert_eq!( std::fs::read(&expected_saved_path).expect("saved file"), b"foo" @@ -7969,6 +7986,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 { @@ -7985,7 +8003,8 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() { .expect("image generation item should still complete"); let history = session.clone_history().await; - assert_eq!(history.raw_items(), &[item]); + let expected = vec![item]; + assert_eq!(history.raw_items(), expected.as_slice()); assert!(!expected_saved_path.exists()); } @@ -8128,6 +8147,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co text: format!("{}\nsummary", crate::compact::SUMMARY_PREFIX), }], phase: None, + metadata: None, }; session .record_conversation_items(&turn_context, std::slice::from_ref(&compacted_summary)) @@ -8152,7 +8172,8 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co let history = session.clone_history().await; let mut expected_history = vec![compacted_summary]; - expected_history.extend(session.build_initial_context(&turn_context).await); + let initial_context = session.build_initial_context(&turn_context).await; + expected_history.extend(initial_context); assert_eq!(history.raw_items().to_vec(), expected_history); } @@ -8725,6 +8746,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() text: "late pending input".to_string(), }], phase: None, + metadata: None, }; assert!( history.raw_items().iter().any(|item| item == &expected), @@ -9157,6 +9179,7 @@ async fn abort_empty_active_turn_preserves_pending_input() { text: "late pending input".to_string(), }], phase: None, + metadata: None, }; let turn_state = { let mut active = sess.active_turn.lock().await; @@ -9415,6 +9438,7 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, }; let mut ctx = HandleOutputCtx { sess: Arc::clone(&sess), @@ -9542,6 +9566,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() { call_id: "call-1".to_string(), name: "shell_command".to_string(), input: "{}".to_string(), + metadata: None, }; let call = ToolRouter::build_tool_call(item.clone()) @@ -9626,6 +9651,7 @@ async fn sample_rollout( text: "first user".to_string(), }], phase: None, + metadata: None, }; live_history.record_items( std::iter::once(&user1), @@ -9640,6 +9666,7 @@ async fn sample_rollout( text: "assistant reply one".to_string(), }], phase: None, + metadata: None, }; live_history.record_items( std::iter::once(&assistant1), @@ -9667,6 +9694,7 @@ async fn sample_rollout( text: "second user".to_string(), }], phase: None, + metadata: None, }; live_history.record_items( std::iter::once(&user2), @@ -9681,6 +9709,7 @@ async fn sample_rollout( text: "assistant reply two".to_string(), }], phase: None, + metadata: None, }; live_history.record_items( std::iter::once(&assistant2), @@ -9708,6 +9737,7 @@ async fn sample_rollout( text: "third user".to_string(), }], phase: None, + metadata: None, }; live_history.record_items( std::iter::once(&user3), @@ -9722,6 +9752,7 @@ async fn sample_rollout( text: "assistant reply three".to_string(), }], phase: None, + metadata: None, }; live_history.record_items( std::iter::once(&assistant3), @@ -9867,6 +9898,7 @@ while :; do sleep 1; done"#, }) .to_string(), call_id: "shell-cleanup-call".to_string(), + metadata: None, }; let call = ToolRouter::build_tool_call(item)? .expect("shell command response item should build a tool call"); diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 0d3c9c033..a2cffe2de 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -536,6 +536,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message text: "stale developer message".to_string(), }], phase: None, + metadata: None, }, ResponseItem::Message { id: None, @@ -544,6 +545,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message text: "summary".to_string(), }], phase: None, + metadata: None, }, ], InitialContextInjection::BeforeLastUserMessage, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 3df795296..3c783eb48 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1978,7 +1978,7 @@ async fn try_run_sampling_request( | ResponseItem::WebSearchCall { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => false, }; diff --git a/codex-rs/core/src/session/turn_tests.rs b/codex-rs/core/src/session/turn_tests.rs index 3511c03bd..7c9f003cb 100644 --- a/codex-rs/core/src/session/turn_tests.rs +++ b/codex-rs/core/src/session/turn_tests.rs @@ -33,6 +33,7 @@ fn assistant_output_text(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 9ae20df74..265d7b450 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -626,6 +626,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti Some(ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: output.clone(), + metadata: None, }) } ResponseInputItem::CustomToolCallOutput { @@ -636,12 +637,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 { @@ -654,6 +657,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/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs index 560f917fb..fdb8cddf4 100644 --- a/codex-rs/core/src/stream_events_utils_tests.rs +++ b/codex-rs/core/src/stream_events_utils_tests.rs @@ -42,6 +42,7 @@ fn assistant_output_text_with_phase(text: &str, phase: Option) -> text: text.to_string(), }], phase, + metadata: None, } } @@ -52,6 +53,7 @@ fn external_context_pollution_items_include_web_search_and_tool_search() { id: None, status: Some("completed".to_string()), action: None, + metadata: None, }, ResponseItem::ToolSearchCall { id: None, @@ -59,12 +61,14 @@ fn external_context_pollution_items_include_web_search_and_tool_search() { status: None, execution: "client".to_string(), arguments: serde_json::json!({"query": "calendar"}), + metadata: None, }, ResponseItem::ToolSearchOutput { call_id: Some("search-1".to_string()), status: "completed".to_string(), execution: "client".to_string(), tools: Vec::new(), + metadata: None, }, ]; @@ -89,6 +93,7 @@ fn external_context_pollution_items_exclude_local_tool_calls() { env: None, user: None, }), + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -96,10 +101,12 @@ fn external_context_pollution_items_exclude_local_tool_calls() { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -107,11 +114,13 @@ fn external_context_pollution_items_exclude_local_tool_calls() { call_id: "custom-1".to_string(), name: "apply_patch".to_string(), input: "*** Begin Patch\n*** End Patch\n".to_string(), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "custom-1".to_string(), name: Some("apply_patch".to_string()), output: FunctionCallOutputPayload::from_text("ok".to_string()), + metadata: None, }, assistant_output_text("plain assistant text"), ]; @@ -413,6 +422,7 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() { status: "completed".to_string(), revised_prompt: None, result: "Zm9v".to_string(), + metadata: None, }; assert!(completed_item_defers_mailbox_delivery_to_next_turn( diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 61f698015..b2f337ec4 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -109,6 +109,7 @@ pub(crate) fn interrupted_turn_history_marker( text: marker.render(), }], phase: None, + metadata: None, }) } } diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 0d9e3521d..bb8670237 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -247,6 +247,7 @@ pub(crate) async fn exit_review_mode( role: "user".to_string(), content: vec![ContentItem::InputText { text: user_message }], phase: None, + metadata: None, }], ) .await; @@ -267,6 +268,7 @@ pub(crate) async fn exit_review_mode( text: assistant_message, }], phase: None, + metadata: None, }, ) .await; diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index a5670b053..4a7890511 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -42,6 +42,7 @@ fn user_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } fn assistant_msg(text: &str) -> ResponseItem { @@ -52,6 +53,7 @@ fn assistant_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -80,6 +82,7 @@ fn truncates_before_requested_user_message() { }], content: None, encrypted_content: None, + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -87,6 +90,7 @@ fn truncates_before_requested_user_message() { name: "tool".to_string(), namespace: None, arguments: "{}".to_string(), + metadata: None, }, assistant_msg("a4"), ]; diff --git a/codex-rs/core/src/thread_rollout_truncation_tests.rs b/codex-rs/core/src/thread_rollout_truncation_tests.rs index 652682776..bb6c3ac03 100644 --- a/codex-rs/core/src/thread_rollout_truncation_tests.rs +++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs @@ -15,6 +15,7 @@ fn user_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -26,6 +27,7 @@ fn assistant_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -37,6 +39,7 @@ fn developer_msg(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } @@ -76,6 +79,7 @@ fn truncates_rollout_from_start_before_nth_user_only() { }], content: None, encrypted_content: None, + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -83,6 +87,7 @@ fn truncates_rollout_from_start_before_nth_user_only() { name: "tool".to_string(), namespace: None, arguments: "{}".to_string(), + metadata: None, }, assistant_msg("a4"), ]; diff --git a/codex-rs/core/src/tools/code_mode/delegate.rs b/codex-rs/core/src/tools/code_mode/delegate.rs index 06bb71a28..15177944c 100644 --- a/codex-rs/core/src/tools/code_mode/delegate.rs +++ b/codex-rs/core/src/tools/code_mode/delegate.rs @@ -302,6 +302,7 @@ impl CoreTurnHost { call_id, name: Some(PUBLIC_TOOL_NAME.to_string()), output: FunctionCallOutputPayload::from_text(text), + metadata: None, }]) .await .map_err(|_| { diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index 33625317f..f0522c7c1 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -324,6 +324,7 @@ mod tests { text: "extension history".to_string(), }], phase: None, + metadata: None, }; session .record_conversation_items(&turn, std::slice::from_ref(&history_item)) diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 88a8f07dc..8c3ea1432 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2797,6 +2797,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() { text: "materialized".to_string(), }], phase: None, + metadata: None, })]), AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")), /*parent_trace*/ None, diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs index a41bab91a..0a7345a84 100644 --- a/codex-rs/core/src/tools/router_tests.rs +++ b/codex-rs/core/src/tools/router_tests.rs @@ -157,6 +157,7 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<() namespace: Some("mcp__codex_apps__calendar".to_string()), arguments: "{}".to_string(), call_id: "call-namespace".to_string(), + metadata: None, })? .expect("function_call should produce a tool call"); @@ -339,6 +340,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow text: "extension history".to_string(), }], phase: None, + metadata: None, }; session .record_conversation_items(&turn, std::slice::from_ref(&history_item)) @@ -374,6 +376,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow namespace: Some("extension/".to_string()), arguments: json!({ "message": "hello" }).to_string(), call_id: "call-extension".to_string(), + metadata: None, })? .expect("function_call should produce a tool call"); let result = router diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index 380e246fa..8a93a9183 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -378,7 +378,7 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool { | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::CompactionTrigger => false, + ResponseItem::CompactionTrigger { .. } => false, ResponseItem::FunctionCallOutput { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::ToolSearchOutput { .. } diff --git a/codex-rs/core/src/turn_timing_tests.rs b/codex-rs/core/src/turn_timing_tests.rs index 7146f537b..af5a54fec 100644 --- a/codex-rs/core/src/turn_timing_tests.rs +++ b/codex-rs/core/src/turn_timing_tests.rs @@ -112,6 +112,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, } )); assert!(response_item_records_turn_ttft( @@ -121,6 +122,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() { call_id: "call-2".to_string(), name: "custom".to_string(), input: "echo hi".to_string(), + metadata: None, } )); assert!(response_item_records_turn_ttft(&ResponseItem::Message { @@ -130,6 +132,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() { text: "hello".to_string(), }], phase: None, + metadata: None, })); } @@ -142,11 +145,13 @@ fn response_item_records_turn_ttft_ignores_empty_non_output_items() { text: String::new(), }], phase: None, + metadata: None, })); assert!(!response_item_records_turn_ttft( &ResponseItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_text("ok".to_string()), + metadata: None, } )); } diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index c917436b6..6e5405e4b 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -685,6 +685,7 @@ pub fn user_message_item(text: &str) -> ResponseItem { text: text.to_string(), }], phase: None, + metadata: None, } } diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index cda5ffcdb..b5bc2b0d4 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -140,6 +140,7 @@ async fn responses_stream_includes_subagent_header_on_review() { text: "hello".into(), }], phase: None, + metadata: None, }]; let mut stream = client_session @@ -270,6 +271,7 @@ async fn responses_stream_includes_subagent_header_on_other() { text: "hello".into(), }], phase: None, + metadata: None, }]; let mut stream = client_session @@ -386,6 +388,7 @@ async fn responses_respects_model_info_overrides_from_config() { text: "hello".into(), }], phase: None, + metadata: None, }]; let mut stream = client_session diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index c41593f51..144daeffd 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -169,6 +169,57 @@ fn assert_codex_client_metadata( ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn non_openai_responses_requests_omit_item_turn_metadata() { + let server = MockServer::start().await; + let response_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), + ) + .await; + let mut provider = + built_in_model_providers(/* openai_base_url */ /*openai_base_url*/ None)["openai"].clone(); + provider.name = "Test Responses".to_string(); + provider.base_url = Some(format!("{}/v1", server.uri())); + provider.supports_websockets = false; + let codex = test_codex() + .with_config(move |config| { + config.model_provider_id = provider.name.clone(); + config.model_provider = provider; + }) + .build(&server) + .await + .unwrap() + .codex; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await + .unwrap(); + wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; + + let body = response_mock.single_request().body_json(); + let input = body["input"] + .as_array() + .expect("request should include input items"); + assert!(!input.is_empty(), "request should include input items"); + for item in input { + assert!( + item.get("metadata").is_none(), + "input item should omit metadata: {item}" + ); + } +} + /// Writes an `auth.json` into the provided `codex_home` with the specified parameters. /// Returns the fake JWT string written to `tokens.id_token`. #[expect(clippy::unwrap_used)] @@ -352,6 +403,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text: "resumed user message".to_string(), }], phase: None, + metadata: None, }; let prior_user_json = serde_json::to_value(&prior_user).unwrap(); writeln!( @@ -373,6 +425,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text: "resumed system instruction".to_string(), }], phase: None, + metadata: None, }; let prior_system_json = serde_json::to_value(&prior_system).unwrap(); writeln!( @@ -394,6 +447,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text: "resumed assistant message".to_string(), }], phase: Some(MessagePhase::Commentary), + metadata: None, }; let prior_item_json = serde_json::to_value(&prior_item).unwrap(); writeln!( @@ -536,6 +590,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() { call_id: "legacy-js-call".to_string(), name: "js_repl".to_string(), input: "console.log('legacy image flow')".to_string(), + metadata: None, }; let legacy_image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="; let rollout = vec![ @@ -565,6 +620,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 { @@ -577,6 +633,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() { detail: Some(DEFAULT_IMAGE_DETAIL), }], phase: None, + metadata: None, }), }, ]; @@ -694,6 +751,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { namespace: None, arguments: "{\"path\":\"/tmp/example.webp\"}".to_string(), call_id: function_call_id.to_string(), + metadata: None, }), }, RolloutLine { @@ -706,6 +764,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { detail: Some(ImageDetail::Original), }, ]), + metadata: None, }), }, RolloutLine { @@ -716,6 +775,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { call_id: custom_call_id.to_string(), name: "js_repl".to_string(), input: "console.log('image flow')".to_string(), + metadata: None, }), }, RolloutLine { @@ -729,6 +789,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { detail: Some(ImageDetail::Original), }, ]), + metadata: None, }), }, ]; @@ -976,6 +1037,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth text: "hello".to_string(), }], phase: None, + metadata: None, }); let mut stream = client_session @@ -2474,6 +2536,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()), @@ -2482,6 +2545,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { text: "message".into(), }], phase: None, + metadata: None, }); prompt.input.push(ResponseItem::WebSearchCall { id: Some("web-search-id".into()), @@ -2490,6 +2554,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()), @@ -2497,10 +2562,12 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { namespace: None, arguments: "{}".into(), call_id: "function-call-id".into(), + metadata: None, }); 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()), @@ -2513,6 +2580,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { env: None, user: None, }), + metadata: None, }); prompt.input.push(ResponseItem::CustomToolCall { id: Some("custom-tool-id".into()), @@ -2520,11 +2588,13 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { call_id: "custom-tool-call-id".into(), name: "custom_tool".into(), input: "{}".into(), + metadata: None, }); prompt.input.push(ResponseItem::CustomToolCallOutput { 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/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 4aac3dcbd..1f9175d7a 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -2057,6 +2057,7 @@ fn message_item(text: &str) -> ResponseItem { role: "user".into(), content: vec![ContentItem::InputText { text: text.into() }], phase: None, + metadata: None, } } @@ -2066,6 +2067,7 @@ fn assistant_message_item(id: &str, text: &str) -> ResponseItem { role: "assistant".into(), content: vec![ContentItem::OutputText { text: text.into() }], phase: None, + metadata: None, } } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 1d1d387f2..56738a09a 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -2003,9 +2003,11 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { text: remote_summary.to_string(), }], phase: None, + metadata: None, }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = @@ -4009,9 +4011,11 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], phase: None, + metadata: None, }, codex_protocol::models::ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = @@ -4134,9 +4138,11 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], phase: None, + metadata: None, }, 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 6bca81bfd..b6cbb31c4 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -161,6 +161,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, }] } @@ -329,6 +330,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(), @@ -2355,6 +2357,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, @@ -2363,6 +2366,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> text: "COMPACTED_ASSISTANT_NOTE".to_string(), }], phase: None, + metadata: None, }, ]; let compact_mock = responses::mount_compact_json_once( @@ -2411,7 +2415,9 @@ 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" ) }); @@ -2502,9 +2508,11 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text: stale_developer_message.to_string(), }], phase: None, + metadata: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = responses::mount_compact_json_once( @@ -2642,9 +2650,11 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() text: stale_developer_message.to_string(), }], phase: None, + metadata: None, }, ResponseItem::Compaction { encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + metadata: None, }, ]; let compact_mock = responses::mount_compact_json_once( @@ -4050,6 +4060,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/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 199a7d1db..2b1855907 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -178,6 +178,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu }, ], phase: None, + metadata: None, }; assert_eq!(actual, expected); @@ -268,6 +269,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> }, ], phase: None, + metadata: None, }; assert_eq!(actual, expected); diff --git a/codex-rs/core/tests/suite/prompt_debug_tests.rs b/codex-rs/core/tests/suite/prompt_debug_tests.rs index c2ebe3999..5883a8564 100644 --- a/codex-rs/core/tests/suite/prompt_debug_tests.rs +++ b/codex-rs/core/tests/suite/prompt_debug_tests.rs @@ -49,6 +49,7 @@ async fn build_prompt_input_includes_context_and_user_message() -> Result<()> { text: "hello from debug prompt".to_string(), }], phase: None, + metadata: None, }; assert_eq!(input.last(), Some(&expected_user_message)); assert!(input.iter().any(|item| { diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 85bc1827f..fc05e98e0 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -2129,6 +2129,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge role: "user".to_string(), content: vec![ContentItem::InputText { text: user_turn }], phase: None, + metadata: None, }), RolloutItem::ResponseItem(ResponseItem::Message { id: None, @@ -2137,6 +2138,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge text: assistant_turn, }], phase: None, + metadata: None, }), ] }) diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 3fdd5f83e..6a3887297 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -531,6 +531,7 @@ async fn review_input_isolated_from_parent_history() { text: "parent: earlier user message".to_string(), }], phase: None, + metadata: None, }; let user_json = serde_json::to_value(&user).unwrap(); let user_line = serde_json::json!({ @@ -550,6 +551,7 @@ async fn review_input_isolated_from_parent_history() { text: "parent: assistant reply".to_string(), }], phase: None, + metadata: None, }; let assistant_json = serde_json::to_value(&assistant).unwrap(); let assistant_line = serde_json::json!({ diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 929137101..ac1aac0b1 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -174,6 +174,7 @@ async fn wait_for_raw_unified_exec_output( ResponseItem::FunctionCallOutput { call_id: output_call_id, output, + .. } if output_call_id == call_id => output.text_content().map(str::to_string), _ => None, }, diff --git a/codex-rs/ext/image-generation/src/tests.rs b/codex-rs/ext/image-generation/src/tests.rs index f0f483a2a..449c7beb0 100644 --- a/codex-rs/ext/image-generation/src/tests.rs +++ b/codex-rs/ext/image-generation/src/tests.rs @@ -76,6 +76,7 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() { }, ], phase: None, + metadata: None, }, ResponseItem::FunctionCall { id: None, @@ -83,10 +84,12 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() { namespace: None, arguments: "{}".to_string(), call_id: "mcp-call".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "mcp-call".to_string(), output: image_output("mcp"), + metadata: None, }, ResponseItem::CustomToolCall { id: None, @@ -94,21 +97,25 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() { call_id: "code-mode-call".to_string(), name: "exec".to_string(), input: String::new(), + metadata: None, }, ResponseItem::CustomToolCallOutput { call_id: "code-mode-call".to_string(), name: Some("exec".to_string()), output: image_output("code-mode"), + metadata: None, }, ResponseItem::ImageGenerationCall { id: "generated-call".to_string(), status: "completed".to_string(), revised_prompt: None, result: "generated".to_string(), + metadata: None, }, ResponseItem::FunctionCallOutput { call_id: "orphan-call".to_string(), output: image_output("orphan"), + metadata: None, }, ]; @@ -196,6 +203,7 @@ async fn recent_image_fallback_requires_requested_count() { role: "user".to_string(), content: vec![input_image("only-image")], phase: None, + metadata: None, }], &[], ) diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index 66bb9ec71..62688b47d 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -267,7 +267,7 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec { | ResponseItem::WebSearchCall { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => {} } @@ -283,9 +283,9 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec { ContentItem::InputText { .. } | ContentItem::OutputText { .. } => None, })); } - ResponseItem::FunctionCallOutput { call_id, output } - if function_call_ids.contains(call_id.as_str()) => - { + ResponseItem::FunctionCallOutput { + call_id, output, .. + } if function_call_ids.contains(call_id.as_str()) => { image_urls.extend(output_image_urls(output)); } ResponseItem::CustomToolCallOutput { @@ -308,7 +308,7 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec { | ResponseItem::WebSearchCall { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => {} } diff --git a/codex-rs/ext/web-search/src/history.rs b/codex-rs/ext/web-search/src/history.rs index 94f9f3977..6417a705f 100644 --- a/codex-rs/ext/web-search/src/history.rs +++ b/codex-rs/ext/web-search/src/history.rs @@ -32,7 +32,10 @@ fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { messages.push(item.clone()); } ResponseItem::AgentMessage { - author, content, .. + author, + content, + metadata, + .. } => { let text = content .iter() @@ -50,6 +53,7 @@ fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { text: format!("Agent message from {author}:\n{text}"), }], phase: None, + metadata: metadata.clone(), }); } } @@ -58,6 +62,7 @@ fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { role, content, phase, + metadata, } if role == USER_ROLE && matches!(parse_turn_item(item), Some(TurnItem::UserMessage(_))) => { @@ -72,6 +77,7 @@ fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { role: role.clone(), content, phase: phase.clone(), + metadata: metadata.clone(), }); } } @@ -104,6 +110,7 @@ mod tests { } }], phase: None, + metadata: None, } } @@ -120,6 +127,7 @@ mod tests { namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), + metadata: None, }, message(ASSISTANT_ROLE, "previous assistant"), message("developer", "developer"), @@ -152,6 +160,7 @@ mod tests { }, ], phase: None, + metadata: None, }; let items = vec![ previous_user, diff --git a/codex-rs/external-agent-sessions/src/export.rs b/codex-rs/external-agent-sessions/src/export.rs index 05a3004ee..2990c0f5e 100644 --- a/codex-rs/external-agent-sessions/src/export.rs +++ b/codex-rs/external-agent-sessions/src/export.rs @@ -141,6 +141,7 @@ fn response_item(message: ConversationMessage) -> ResponseItem { }, content: vec![content], phase: None, + metadata: None, } } diff --git a/codex-rs/memories/write/src/phase1.rs b/codex-rs/memories/write/src/phase1.rs index 3edad7ff7..775e89887 100644 --- a/codex-rs/memories/write/src/phase1.rs +++ b/codex-rs/memories/write/src/phase1.rs @@ -303,6 +303,7 @@ mod job { )?, }], phase: None, + metadata: None, }]; prompt.base_instructions = BaseInstructions { text: crate::stage_one::PROMPT.to_string(), @@ -428,6 +429,7 @@ mod job { role, content, phase, + metadata, } = item else { return should_persist_response_item_for_memories(item).then(|| item.clone()); @@ -455,6 +457,7 @@ mod job { role: role.clone(), content, phase: phase.clone(), + metadata: metadata.clone(), }) } @@ -684,6 +687,7 @@ mod tests { }, ], phase: None, + metadata: None, }; let skill_message = ResponseItem::Message { id: None, @@ -694,6 +698,7 @@ mod tests { .to_string(), }], phase: None, + metadata: None, }; let subagent_message = ResponseItem::Message { id: None, @@ -703,6 +708,7 @@ mod tests { .to_string(), }], phase: None, + metadata: None, }; let serialized = job::serialize_filtered_rollout_response_items(&[ @@ -724,6 +730,7 @@ mod tests { .to_string(), }], phase: None, + metadata: None, }, subagent_message, ] @@ -742,6 +749,7 @@ mod tests { ), success: Some(true), }, + metadata: None, }, )]) .expect("serialize"); diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index 66b035ed0..13ba9da3b 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -685,6 +685,7 @@ async fn seed_stage1_candidate( text: "remember this startup test conversation".to_string(), }], phase: None, + metadata: None, }), }; let jsonl = serde_json::to_string(&line)?; diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index 00c13046d..8f0471088 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -1230,7 +1230,7 @@ impl SessionTelemetry { ResponseItem::WebSearchCall { .. } => "web_search_call".into(), ResponseItem::ImageGenerationCall { .. } => "image_generation_call".into(), ResponseItem::Compaction { .. } => "compaction".into(), - ResponseItem::CompactionTrigger => "compaction_trigger".into(), + ResponseItem::CompactionTrigger { .. } => "compaction_trigger".into(), ResponseItem::ContextCompaction { .. } => "context_compaction".into(), ResponseItem::Other => "other".into(), } diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index ee61dc394..267f73fbd 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -391,6 +391,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { @@ -765,11 +772,17 @@ pub enum ResponseItem { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] phase: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, AgentMessage { author: String, recipient: String, content: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, Reasoning { #[serde(default, skip_serializing)] @@ -781,6 +794,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. @@ -791,6 +807,9 @@ pub enum ResponseItem { call_id: Option, status: LocalShellStatus, action: LocalShellAction, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, FunctionCall { #[serde(default, skip_serializing)] @@ -805,6 +824,9 @@ pub enum ResponseItem { // Session::handle_function_call parse it into a Value. arguments: String, call_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, ToolSearchCall { #[serde(default, skip_serializing)] @@ -817,6 +839,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: @@ -828,6 +853,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)] @@ -840,6 +868,9 @@ pub enum ResponseItem { call_id: String, name: String, input: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, // `custom_tool_call_output.output` uses the same wire encoding as // `function_call_output.output` so freeform tools can return either plain @@ -852,6 +883,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, @@ -859,6 +893,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`): @@ -878,6 +915,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: @@ -895,16 +935,29 @@ pub enum ResponseItem { #[ts(optional)] revised_prompt: Option, result: String, + #[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, + }, + CompactionTrigger { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, - CompactionTrigger, ContextCompaction { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] encrypted_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, }, #[serde(other)] Other, @@ -915,6 +968,75 @@ impl ResponseItem { pub fn is_user_message(&self) -> bool { matches!(self, Self::Message { role, .. } if role == "user") } + + /// Returns the non-empty turn ID stamped onto this item, if present. + pub fn turn_id(&self) -> Option<&str> { + self.metadata() + .and_then(|metadata| metadata.turn_id.as_deref()) + .filter(|turn_id| !turn_id.is_empty()) + } + + /// Stamps the item with `turn_id` unless it already has a non-empty turn ID. + pub fn stamp_turn_id_if_missing(&mut self, turn_id: &str) { + if turn_id.is_empty() || self.turn_id().is_some() { + return; + } + let Some(metadata) = self.metadata_mut() else { + return; + }; + metadata + .get_or_insert_with(ResponseItemMetadata::default) + .turn_id = Some(turn_id.to_string()); + } + + /// Removes Responses API item metadata before sending to a provider that does not accept it. + pub fn clear_metadata(&mut self) { + if let Some(metadata) = self.metadata_mut() { + *metadata = None; + } + } + + fn metadata(&self) -> Option<&ResponseItemMetadata> { + match self { + Self::Message { metadata, .. } + | Self::AgentMessage { metadata, .. } + | Self::Reasoning { metadata, .. } + | Self::LocalShellCall { metadata, .. } + | Self::FunctionCall { metadata, .. } + | Self::ToolSearchCall { metadata, .. } + | Self::FunctionCallOutput { metadata, .. } + | Self::CustomToolCall { metadata, .. } + | Self::CustomToolCallOutput { metadata, .. } + | Self::ToolSearchOutput { metadata, .. } + | Self::WebSearchCall { metadata, .. } + | Self::ImageGenerationCall { metadata, .. } + | Self::Compaction { metadata, .. } + | Self::CompactionTrigger { metadata } + | Self::ContextCompaction { metadata, .. } => metadata.as_ref(), + Self::Other => None, + } + } + + fn metadata_mut(&mut self) -> Option<&mut Option> { + match self { + Self::Message { metadata, .. } + | Self::AgentMessage { metadata, .. } + | Self::Reasoning { metadata, .. } + | Self::LocalShellCall { metadata, .. } + | Self::FunctionCall { metadata, .. } + | Self::ToolSearchCall { metadata, .. } + | Self::FunctionCallOutput { metadata, .. } + | Self::CustomToolCall { metadata, .. } + | Self::CustomToolCallOutput { metadata, .. } + | Self::ToolSearchOutput { metadata, .. } + | Self::WebSearchCall { metadata, .. } + | Self::ImageGenerationCall { metadata, .. } + | Self::Compaction { metadata, .. } + | Self::CompactionTrigger { metadata } + | Self::ContextCompaction { metadata, .. } => Some(metadata), + Self::Other => None, + } + } } pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md"); @@ -1153,13 +1275,20 @@ impl From for ResponseItem { content, id: None, phase, + metadata: None, + }, + ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput { + call_id, + output, + metadata: None, }, - ResponseInputItem::FunctionCallOutput { call_id, output } => { - Self::FunctionCallOutput { call_id, output } - } 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, @@ -1169,6 +1298,7 @@ impl From for ResponseItem { call_id, name, output, + metadata: None, }, ResponseInputItem::ToolSearchOutput { call_id, @@ -1180,6 +1310,7 @@ impl From for ResponseItem { status, execution, tools, + metadata: None, }, } } @@ -1718,10 +1849,64 @@ mod tests { text: "still working".to_string(), }], phase: Some(MessagePhase::Commentary), + metadata: None, } ); } + #[test] + fn response_item_metadata_round_trips_and_stamps_turn_ids() -> Result<()> { + let mut item = response_item_with_metadata(Some(response_item_metadata("turn-1"))); + let round_trip: ResponseItem = serde_json::from_value(serde_json::to_value(&item)?)?; + assert_eq!(round_trip, item); + + let unknown_metadata: ResponseItem = serde_json::from_value(serde_json::json!({ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "hello"}], + "metadata": { + "turn_id": "turn-1", + "other": "ignored", + }, + }))?; + assert_eq!(unknown_metadata, item); + + item.stamp_turn_id_if_missing("turn-2"); + assert_eq!(item.turn_id(), Some("turn-1")); + + let mut empty_turn_id = response_item_with_metadata(Some(response_item_metadata(""))); + empty_turn_id.stamp_turn_id_if_missing("turn-1"); + assert_eq!(empty_turn_id.turn_id(), Some("turn-1")); + + let mut missing_turn_id = response_item_with_metadata(/*metadata*/ None); + missing_turn_id.stamp_turn_id_if_missing(""); + missing_turn_id.stamp_turn_id_if_missing("turn-1"); + assert_eq!(missing_turn_id.turn_id(), Some("turn-1")); + + let mut other = ResponseItem::Other; + other.stamp_turn_id_if_missing("turn-1"); + assert_eq!(other.turn_id(), None); + Ok(()) + } + + fn response_item_with_metadata(metadata: Option) -> ResponseItem { + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "hello".to_string(), + }], + phase: None, + metadata, + } + } + + fn response_item_metadata(turn_id: &str) -> ResponseItemMetadata { + ResponseItemMetadata { + turn_id: Some(turn_id.to_string()), + } + } + #[test] fn image_detail_roundtrips_all_wire_values() -> Result<()> { assert_eq!( @@ -1823,6 +2008,7 @@ mod tests { status: "completed".to_string(), revised_prompt: Some("A small blue square".to_string()), result: "Zm9v".to_string(), + metadata: None, } ); } @@ -1844,6 +2030,7 @@ mod tests { status: "completed".to_string(), revised_prompt: None, result: "Zm9v".to_string(), + metadata: None, } ); } @@ -2195,6 +2382,7 @@ mod tests { namespace: Some("mcp__codex_apps__gmail".to_string()), arguments: "{\"top_k\":5}".to_string(), call_id: "call-1".to_string(), + metadata: None, } ); } @@ -2541,6 +2729,7 @@ mod tests { item, ResponseItem::Compaction { encrypted_content: "abc".into(), + metadata: None, } ); Ok(()) @@ -2556,6 +2745,7 @@ mod tests { item, ResponseItem::ContextCompaction { encrypted_content: Some("abc".into()), + metadata: None, } ); Ok(()) @@ -2563,7 +2753,7 @@ mod tests { #[test] fn serializes_compaction_trigger_without_payload() -> Result<()> { - let item = ResponseItem::CompactionTrigger; + let item = ResponseItem::CompactionTrigger { metadata: None }; assert_eq!( serde_json::to_value(item)?, @@ -2574,13 +2764,30 @@ mod tests { Ok(()) } + #[test] + fn serializes_stamped_compaction_trigger_metadata() -> Result<()> { + let mut item = ResponseItem::CompactionTrigger { metadata: None }; + item.stamp_turn_id_if_missing("turn-1"); + + assert_eq!( + serde_json::to_value(item)?, + serde_json::json!({ + "type": "compaction_trigger", + "metadata": { + "turn_id": "turn-1", + }, + }) + ); + Ok(()) + } + #[test] fn deserializes_compaction_trigger_without_payload() -> Result<()> { let json = r#"{"type":"compaction_trigger"}"#; let item: ResponseItem = serde_json::from_str(json)?; - assert_eq!(item, ResponseItem::CompactionTrigger); + assert_eq!(item, ResponseItem::CompactionTrigger { metadata: None }); Ok(()) } @@ -2677,6 +2884,7 @@ mod tests { id: expected_id.clone(), status: expected_status.clone(), action: expected_action.clone(), + metadata: None, }; assert_eq!(parsed, expected); @@ -2764,6 +2972,7 @@ mod tests { "query": "calendar create", "limit": 1, }), + metadata: None, } ); @@ -2824,6 +3033,7 @@ mod tests { "additionalProperties": false, } })], + metadata: None, } ); @@ -2877,6 +3087,7 @@ mod tests { arguments: serde_json::json!({ "paths": ["crm"], }), + metadata: None, } ); @@ -2896,6 +3107,7 @@ mod tests { status: "completed".to_string(), execution: "server".to_string(), tools: vec![], + metadata: None, } ); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index be1914e6d..6cf6466d0 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -41,6 +41,7 @@ use crate::models::MessagePhase; use crate::models::PermissionProfile; use crate::models::ResponseInputItem; use crate::models::ResponseItem; +use crate::models::ResponseItemMetadata; use crate::models::SandboxEnforcement; use crate::models::WebSearchAction; use crate::num_format::format_with_separators; @@ -669,6 +670,9 @@ pub struct InterAgentCommunication { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub encrypted_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub metadata: Option, pub trigger_turn: bool, } @@ -686,6 +690,7 @@ impl InterAgentCommunication { other_recipients, content, encrypted_content: None, + metadata: None, trigger_turn, } } @@ -703,6 +708,7 @@ impl InterAgentCommunication { other_recipients, content: String::new(), encrypted_content: Some(encrypted_content), + metadata: None, trigger_turn, } } @@ -730,6 +736,7 @@ impl InterAgentCommunication { author: self.author.to_string(), recipient: self.recipient.to_string(), content: vec![content], + metadata: self.metadata.clone(), } } @@ -2932,6 +2939,7 @@ impl From for ResponseItem { text: value.message, }], phase: None, + metadata: None, } } } @@ -4217,6 +4225,7 @@ mod tests { other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")], content: "review the diff".to_string(), encrypted_content: None, + metadata: None, trigger_turn: true, }; diff --git a/codex-rs/rollout-trace/src/inference.rs b/codex-rs/rollout-trace/src/inference.rs index a4977b0dc..f826d9f47 100644 --- a/codex-rs/rollout-trace/src/inference.rs +++ b/codex-rs/rollout-trace/src/inference.rs @@ -504,6 +504,7 @@ mod tests { text: "raw reasoning".to_string(), }]), encrypted_content: Some("encoded".to_string()), + metadata: None, }; let normal = serde_json::to_value(&item).expect("response item serializes"); diff --git a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs index e67673b96..e7a3253f6 100644 --- a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs +++ b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs @@ -194,6 +194,7 @@ fn normalize_agent_message_item( author, recipient, content, + .. } = response_item else { bail!("item in payload {raw_payload_id} was not an agent_message"); diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index 40ca77c97..ba2ec23c2 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -44,7 +44,7 @@ pub fn should_persist_response_item(item: &ResponseItem) -> bool { | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true, - ResponseItem::CompactionTrigger => false, + ResponseItem::CompactionTrigger { .. } => false, ResponseItem::Other => false, } } @@ -66,7 +66,7 @@ pub fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool { | ResponseItem::Reasoning { .. } | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } - | ResponseItem::CompactionTrigger + | ResponseItem::CompactionTrigger { .. } | ResponseItem::ContextCompaction { .. } | ResponseItem::Other => false, } diff --git a/codex-rs/rollout/src/tests.rs b/codex-rs/rollout/src/tests.rs index e489eaba5..3c1fc8bb5 100644 --- a/codex-rs/rollout/src/tests.rs +++ b/codex-rs/rollout/src/tests.rs @@ -1305,6 +1305,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { text: format!("reply-{idx}"), }], phase: None, + metadata: None, }), }; writeln!(file, "{}", serde_json::to_string(&response_line)?)?; diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index fbb63dcea..3e41b44aa 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -191,6 +191,7 @@ mod tests { text: "hello from response item".to_string(), }], phase: None, + metadata: None, }); apply_rollout_item(&mut metadata, &item, "test-provider"); diff --git a/codex-rs/tools/src/response_history.rs b/codex-rs/tools/src/response_history.rs index cee6f438c..4196d7c3e 100644 --- a/codex-rs/tools/src/response_history.rs +++ b/codex-rs/tools/src/response_history.rs @@ -95,6 +95,7 @@ mod tests { } }], phase: None, + metadata: None, } } diff --git a/codex-rs/tui/src/app/side.rs b/codex-rs/tui/src/app/side.rs index 04831e16e..61fed2815 100644 --- a/codex-rs/tui/src/app/side.rs +++ b/codex-rs/tui/src/app/side.rs @@ -453,6 +453,7 @@ impl App { text: SIDE_BOUNDARY_PROMPT.to_string(), }], phase: None, + metadata: None, } }