diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index cb9375f2b878..6a96e101e03e 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -1028,6 +1028,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }, }) } diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index c7e79306997f..b15474ffc7b1 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -522,6 +522,27 @@ } ] }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -4513,6 +4534,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index ec97c1f850d9..47e75d11d590 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 @@ -7734,6 +7734,27 @@ } ] }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -20973,6 +20994,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/v2/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, 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 8e83826dbc7d..fce5c1d1b81f 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 @@ -1225,6 +1225,27 @@ } ] }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -18687,6 +18708,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 6eb695e5b729..f3caf84e9698 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -806,6 +827,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index f304246faa53..8432eb54378b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -806,6 +827,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 29a2e6db1908..7da4ff34d8ae 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -994,6 +1015,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 8d2527a20002..e0aa43dd29a3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -92,6 +92,27 @@ } ] }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1578,6 +1599,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json index 9ec979742604..d11f47942a69 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadItemsListResponse.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -806,6 +827,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 52303c207755..576edc360ade 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index d9c006b30922..6db9cbc2e2cc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 0ee3e3e71434..8682470d11f0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 68e73e7673fd..030a48b1b854 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -92,6 +92,27 @@ } ] }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1578,6 +1599,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json index ffb3659ea5ff..f92e7201a093 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 2afaf9ec7959..da1efc5a235e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 26cc859c5d81..984dc14b69c5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -92,6 +92,27 @@ } ] }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1578,6 +1599,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 7f2ece07d0c5..63cc9d2e9adc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json index 489c2103ce5a..c8d1dd03548c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnsListResponse.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -994,6 +1015,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 66371aed4c3d..04e4301147d7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -14,6 +14,27 @@ "AgentPath": { "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -1369,6 +1390,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index ac0f055cc7de..7db8c18c8232 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -994,6 +1015,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 1a62b8ad45f6..95523694804b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -994,6 +1015,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index e02f757f99b5..57052afd9f76 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -11,6 +11,27 @@ ], "type": "string" }, + "AsyncUserInputQuestion": { + "additionalProperties": false, + "properties": { + "options": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -994,6 +1015,16 @@ ], "default": null }, + "questions": { + "default": null, + "items": { + "$ref": "#/definitions/AsyncUserInputQuestion" + }, + "type": [ + "array", + "null" + ] + }, "text": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 3aa3191608f3..0488aa5363be 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 900a626ab20f..485dfcd21118 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AsyncUserInputQuestion.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AsyncUserInputQuestion.ts new file mode 100644 index 000000000000..bac5531c3458 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AsyncUserInputQuestion.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 AsyncUserInputQuestion = { title: string, options: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index d0dbf7c126e7..ae8c00a7205b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -10,6 +10,7 @@ import type { SleepItem } from "../SleepItem"; import type { WebSearchItem } from "../WebSearchItem"; import type { JsonValue } from "../serde_json/JsonValue"; import type { AgentMessageDelivery } from "./AgentMessageDelivery"; +import type { AsyncUserInputQuestion } from "./AsyncUserInputQuestion"; import type { CollabAgentState } from "./CollabAgentState"; import type { CollabAgentTool } from "./CollabAgentTool"; import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; @@ -29,7 +30,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus"; import type { SubAgentActivityKind } from "./SubAgentActivityKind"; import type { UserInput } from "./UserInput"; -export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, +export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, /** * Trusted first-party plugin id when this command resolves to one plugin script. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 34d4cba17b13..fefe72338585 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -41,6 +41,7 @@ export type { AppsListResponse } from "./AppsListResponse"; export type { AppsReadParams } from "./AppsReadParams"; export type { AppsReadResponse } from "./AppsReadResponse"; export type { AskForApproval } from "./AskForApproval"; +export type { AsyncUserInputQuestion } from "./AsyncUserInputQuestion"; export type { AttestationGenerateParams } from "./AttestationGenerateParams"; export type { AttestationGenerateResponse } from "./AttestationGenerateResponse"; export type { AuthRecoveryNotification } from "./AuthRecoveryNotification"; 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 1da1ee24d3bb..028069a0f563 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -497,6 +497,7 @@ impl ThreadHistoryBuilder { phase: payload.phase.clone(), memory_citation: payload.memory_citation.clone().map(Into::into), delivery: payload.delivery, + questions: payload.questions.clone(), }); } @@ -1657,6 +1658,7 @@ impl From<&PendingTurn> for Turn { mod tests { use super::*; use crate::protocol::v2::AgentMessageDelivery; + use crate::protocol::v2::AsyncUserInputQuestion; use crate::protocol::v2::CommandExecutionSource; use codex_extension_items::ExtensionItem as CoreExtensionItem; use codex_extension_items::sleep::SleepItem as CoreSleepItem; @@ -1759,6 +1761,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::AgentReasoning(AgentReasoningEvent { text: "thinking".into(), @@ -1779,6 +1782,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), ]; @@ -1818,6 +1822,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, } ); assert_eq!( @@ -1852,6 +1857,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, } ); } @@ -2422,12 +2428,23 @@ mod tests { } #[test] - fn preserves_agent_message_phase_and_delivery_in_history() { + fn preserves_agent_message_phase_delivery_and_questions_in_history() { + let questions = vec![ + AsyncUserInputQuestion { + title: "Which environment?".into(), + options: Some(vec!["Staging".into(), "Production".into()]), + }, + AsyncUserInputQuestion { + title: "Anything else?".into(), + options: None, + }, + ]; let events = vec![EventMsg::AgentMessage(AgentMessageEvent { message: "Final reply".into(), phase: Some(CoreMessagePhase::FinalAnswer), memory_citation: None, delivery: Some(AgentMessageDelivery::Async), + questions: Some(questions.clone()), })]; let items = events @@ -2444,6 +2461,7 @@ mod tests { phase: Some(CoreMessagePhase::FinalAnswer), memory_citation: None, delivery: Some(AgentMessageDelivery::Async), + questions: Some(questions), } ); } @@ -2554,6 +2572,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::AgentReasoning(AgentReasoningEvent { text: "second summary".into(), @@ -2603,6 +2622,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some("turn-1".into()), @@ -2624,6 +2644,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), ]; @@ -2656,6 +2677,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, } ); @@ -2681,6 +2703,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, } ); } @@ -2701,6 +2724,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::UserMessage(UserMessageEvent { client_id: None, @@ -2715,6 +2739,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }), EventMsg::UserMessage(UserMessageEvent { @@ -2730,6 +2755,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), ]; @@ -2761,6 +2787,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }, ] ); @@ -2781,6 +2808,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }, ] ); @@ -2802,6 +2830,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::UserMessage(UserMessageEvent { client_id: None, @@ -2816,6 +2845,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }), ]; @@ -3849,6 +3879,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-b".into(), @@ -4028,6 +4059,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), ]; @@ -4295,6 +4327,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), EventMsg::Error(ErrorEvent { misalignment: None, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs index cc036e2b95ec..a6d0077770e9 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs @@ -125,6 +125,7 @@ fn projects_completed_canonical_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }); let user_changes = project(item_completed(thread_id, "turn-1", user_item.clone())); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 67b8c41d5b30..9b24e093aea6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -26,6 +26,7 @@ use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianA use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; pub use codex_protocol::items::AgentMessageDelivery; +pub use codex_protocol::items::AsyncUserInputQuestion; use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool; use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus; use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus; @@ -255,6 +256,8 @@ pub enum ThreadItem { memory_citation: Option, #[serde(default)] delivery: Option, + #[serde(default)] + questions: Option>, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -881,6 +884,7 @@ impl From for ThreadItem { phase: agent.phase, memory_citation: agent.memory_citation.map(Into::into), delivery: agent.delivery, + questions: agent.questions, } } CoreTurnItem::FunctionCallOutput(output) => ThreadItem::FunctionCallOutput { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 141f867c1b33..971ef366e740 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3071,6 +3071,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { phase: None, memory_citation: None, delivery: None, + questions: None, }); assert_eq!( @@ -3081,6 +3082,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { phase: None, memory_citation: None, delivery: None, + questions: None, } ); @@ -3100,6 +3102,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { rollout_ids: vec!["rollout-1".to_string()], }), delivery: None, + questions: None, }); assert_eq!( @@ -3118,9 +3121,42 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { thread_ids: vec!["rollout-1".to_string()], }), delivery: None, + questions: None, } ); + let async_item = ThreadItem::from(TurnItem::AgentMessage(AgentMessageItem { + id: "async-1".to_string(), + content: vec![AgentMessageContent::Text { + text: "Which?".to_string(), + }], + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + delivery: Some(AgentMessageDelivery::Async), + questions: Some(vec![AsyncUserInputQuestion { + title: "Which?".to_string(), + options: None, + }]), + })); + assert_eq!( + serde_json::to_value(&async_item).unwrap(), + json!({ + "type": "agentMessage", "id": "async-1", "text": "Which?", "phase": "final_answer", + "memoryCitation": null, "delivery": "async", "questions": [{"title": "Which?", "options": null}] + }) + ); + let old_item: ThreadItem = serde_json::from_value(json!({ + "type": "agentMessage", "id": "old-1", "text": "An old message" + })) + .unwrap(); + assert!(matches!( + old_item, + ThreadItem::AgentMessage { + questions: None, + .. + } + )); + let reasoning_item = TurnItem::Reasoning(ReasoningItem { id: "reasoning-1".to_string(), summary_text: vec!["line one".to_string(), "line two".to_string()], diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index cbfde217539a..dc084fdfbfc0 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1824,7 +1824,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn - `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, `localImage`, `audio`, or `localAudio`). - `functionCallOutput` — `{id, name, namespace, output}` for a standalone function-call output without a `call_id`. `namespace` is nullable, and `output` is either a string or structured content items. Clients decide whether to render these tool-authority items; ordinary paired function-call outputs are not emitted separately. -- `agentMessage` — `{id, text, phase, memoryCitation, delivery}` containing the accumulated agent reply. `delivery: "async"` identifies a user-visible message sent without ending the current turn; ordinary agent messages have `delivery: null`. +- `agentMessage` — `{id, text, phase, memoryCitation, delivery, questions}` containing the accumulated agent reply. `delivery: "async"` identifies a user-visible message sent without ending the current turn. Async user-input requests also provide `questions`, an ordered array of `{title, options}`; `options: null` means free text only. `text` remains a readable fallback. Replies arrive as ordinary user messages. Ordinary agent messages have `delivery: null` and `questions: null`. - `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental). - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). - `commandExecution` — `{id, pluginId?, scriptPath?, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `pluginId` is present only for commands attributed to a trusted first-party plugin, newly attributed items also include `scriptPath` as a safe `/`-separated path relative to the trusted plugin root, older history may omit `scriptPath`, and `status` is `inProgress`, `completed`, `failed`, or `declined`. Ordinary execution items and their replay expose `command` and `commandActions` as redacted display values, not executable commands. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 68100e6c92bd..91cda28b0a09 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2291,6 +2291,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, })), ]; let stored_thread = StoredThread { @@ -3586,6 +3587,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), started_at_ms: Some(0), completed_at_ms: 0, @@ -3604,6 +3606,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }), started_at_ms: Some(0), completed_at_ms: 0, diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 18b732ae1b26..d8613e90c56e 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -1031,6 +1031,7 @@ mod tests { phase: None, memory_citation: None, delivery: Some(AgentMessageDelivery::Async), + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), diff --git a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs index 51e00965ce66..74ad7db49087 100644 --- a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs +++ b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs @@ -74,6 +74,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }, ThreadItem::McpToolCall { id: "mcp-1".to_string(), @@ -125,6 +126,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, } ); assert_eq!( diff --git a/codex-rs/app-server/src/request_processors/token_usage_replay.rs b/codex-rs/app-server/src/request_processors/token_usage_replay.rs index e9b8c13adfaf..c6d21f4cfe53 100644 --- a/codex-rs/app-server/src/request_processors/token_usage_replay.rs +++ b/codex-rs/app-server/src/request_processors/token_usage_replay.rs @@ -178,6 +178,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, })), RolloutItem::EventMsg(EventMsg::TokenCount(TokenCountEvent { info: None, diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index eeff9816f4bc..b7839519dc08 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -2045,6 +2045,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { phase: None, memory_citation: None, delivery: None, + questions: None, }) ); diff --git a/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs b/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs index 14aaddae384c..d49f66dd6ff7 100644 --- a/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs @@ -260,8 +260,8 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result ), ( "functions", - "send_user_message_async", - json!({"message": "PRIVATE_MESSAGE"}), + "request_user_input_async", + json!({"questions": [{"title": "PRIVATE_MESSAGE"}]}), ), ( "notes", @@ -270,8 +270,8 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result ), ( "functions", - "send_user_message_async", - json!({"message": " "}), + "request_user_input_async", + json!({"questions": [{"title": " "}]}), ), ]; let server = responses::start_mock_server().await; diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 3300a1da9d2f..1756bf7c91f8 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -584,6 +584,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> { phase: Some(MessagePhase::Commentary), memory_citation: None, delivery: None, + questions: None, }), ), paginated_completed_item( @@ -597,6 +598,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> { phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }), ), paginated_turn_completed("turn-1"), @@ -1627,6 +1629,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( phase: None, memory_citation: None, delivery: None, + questions: None, }), ), paginated_completed_item( @@ -1679,6 +1682,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( phase: None, memory_citation: None, delivery: None, + questions: None, }, ], items_view: TurnItemsView::Full, @@ -1865,6 +1869,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( phase: None, memory_citation: None, delivery: None, + questions: None, }, ], items_view: TurnItemsView::Summary, @@ -2111,6 +2116,7 @@ fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Res phase: None, memory_citation: None, delivery: None, + questions: None, }))?, }) )?; 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 1d7b857cffae..a6f728deb176 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -3836,6 +3836,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn() phase: None, memory_citation: None, delivery: None, + questions: None, }))?, }) .to_string(), @@ -3923,6 +3924,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re phase: None, memory_citation: None, delivery: None, + questions: None, }))?, }) .to_string(), @@ -4233,6 +4235,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is phase: None, memory_citation: None, delivery: None, + questions: None, }))?, }) .to_string(), diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index f661c21baa0a..d386b08b5d74 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -174,6 +174,7 @@ fn parse_agent_message( phase, memory_citation: None, delivery: None, + questions: None, } } diff --git a/codex-rs/core/src/realtime_history_tests.rs b/codex-rs/core/src/realtime_history_tests.rs index 7e0afae7b554..48db0239b313 100644 --- a/codex-rs/core/src/realtime_history_tests.rs +++ b/codex-rs/core/src/realtime_history_tests.rs @@ -222,6 +222,7 @@ fn promotes_backing_agent_artifacts_once_without_a_client_request() { phase: None, memory_citation: None, delivery: None, + questions: None, })); assert!(state.observe(&completed).items.is_empty()); @@ -298,6 +299,7 @@ fn promotes_distinct_visualizations_once_and_ignores_markdown_fences() { phase: None, memory_citation: None, delivery: None, + questions: None, })); assert!(state.observe(&completed).items.is_empty()); } diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index c5f6eb2597a9..f7c42680c2bb 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -2088,6 +2088,7 @@ async fn emit_agent_message_in_plan_mode( phase: None, memory_citation: None, delivery: None, + questions: None, }) }); sess.emit_turn_item_started(turn_context, &start_item).await; diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 6f5cc37e2978..74cdb32dc35c 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -2442,6 +2442,7 @@ fn completed_legacy_event_history_is_not_mid_turn() { phase: None, memory_citation: None, delivery: None, + questions: None, })), ]); diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 35268fe55fe2..25a2ae150829 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -22,8 +22,8 @@ mod request_permissions; mod request_plugin_install; pub(crate) mod request_plugin_install_spec; mod request_user_input; +mod request_user_input_async; pub(crate) mod request_user_input_spec; -mod send_user_message_async; pub(crate) mod shell_spec; mod sleep; mod test_sync; @@ -69,7 +69,7 @@ pub use plan::PlanHandler; pub use request_permissions::RequestPermissionsHandler; pub use request_plugin_install::RequestPluginInstallHandler; pub use request_user_input::RequestUserInputHandler; -pub use send_user_message_async::SendUserMessageAsyncHandler; +pub use request_user_input_async::RequestUserInputAsyncHandler; pub use sleep::SleepHandler; pub use test_sync::TestSyncHandler; pub(crate) use tool_search::ToolSearchHandlerCache; diff --git a/codex-rs/core/src/tools/handlers/request_user_input_async.rs b/codex-rs/core/src/tools/handlers/request_user_input_async.rs new file mode 100644 index 000000000000..86895851c750 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/request_user_input_async.rs @@ -0,0 +1,147 @@ +use crate::function_tool::FunctionCallError; +use crate::tools::context::FunctionToolOutput; +use crate::tools::context::ToolInvocation; +use crate::tools::context::ToolPayload; +use crate::tools::context::boxed_tool_output; +use crate::tools::handlers::parse_arguments; +use crate::tools::registry::CoreToolRuntime; +use crate::tools::registry::ToolExecutor; +use codex_protocol::items::AgentMessageContent; +use codex_protocol::items::AgentMessageDelivery; +use codex_protocol::items::AgentMessageItem; +use codex_protocol::items::AsyncUserInputQuestion; +use codex_protocol::items::TurnItem; +use codex_protocol::models::MessagePhase; +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiTool; +use codex_tools::ToolName; +use codex_tools::ToolSpec; +use serde::Deserialize; +use std::collections::BTreeMap; + +const TOOL_NAME: &str = "request_user_input_async"; + +pub struct RequestUserInputAsyncHandler { + pub description: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RequestUserInputAsyncArgs { + questions: Vec, +} + +impl ToolExecutor for RequestUserInputAsyncHandler { + fn tool_name(&self) -> ToolName { + ToolName::plain(TOOL_NAME) + } + + fn spec(&self) -> ToolSpec { + let mut options = JsonSchema::array( + JsonSchema::string(/*description*/ None), + Some("Suggested answers, in display order. Put the recommended answer first; the first option is preselected by default. The user can select one option or enter a free-text answer. Do not include an Other option or a free-text placeholder; the UI provides free-text input automatically. Omit options for a free-text-only question.".to_string()), + ); + options.min_items = Some(1); + let question = JsonSchema::object( + BTreeMap::from([ + ("title".to_string(), JsonSchema::string(Some("The complete question shown to the user, including any context needed to answer it.".to_string()))), + ("options".to_string(), options), + ]), + Some(vec!["title".to_string()]), + /*additional_properties*/ Some(false.into()), + ); + let mut questions = JsonSchema::array( + question, + Some( + "One or more self-contained questions to present together, in display order." + .to_string(), + ), + ); + questions.min_items = Some(1); + let properties = BTreeMap::from([("questions".to_string(), questions)]); + + ToolSpec::Function(ResponsesApiTool { + name: TOOL_NAME.to_string(), + description: self.description.clone().unwrap_or_else(|| { + "Ask the user one or more questions during ongoing work. Use this tool only to request missing information, preferences, constraints, clarification, or approval. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message. Keep questions concise, self-contained, and easy to understand, using a level of detail appropriate to the user and task. The UI always allows a free-text answer, including when suggested options are provided. A preselected option is not submitted automatically." + .to_string() + }), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + Some(vec!["questions".to_string()]), + /*additional_properties*/ Some(false.into()), + ), + output_schema: None, + }) + } + + fn handle<'a>(&'a self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'a> + where + ToolInvocation: 'a, + { + Box::pin(async move { + let ToolInvocation { + session, + turn, + call_id, + payload, + .. + } = invocation; + let ToolPayload::Function { arguments } = payload else { + return Err(FunctionCallError::RespondToModel(format!( + "{TOOL_NAME} handler received unsupported payload" + ))); + }; + let args: RequestUserInputAsyncArgs = parse_arguments(&arguments)?; + if args.questions.is_empty() { + return Err(FunctionCallError::RespondToModel( + "questions must not be empty".to_string(), + )); + } + let mut messages = Vec::with_capacity(args.questions.len()); + for question in &args.questions { + if question.title.trim().is_empty() { + return Err(FunctionCallError::RespondToModel( + "question titles must not be empty".to_string(), + )); + } + let mut lines = vec![question.title.clone()]; + if let Some(options) = &question.options { + if options.is_empty() || options.iter().any(|option| option.trim().is_empty()) { + return Err(FunctionCallError::RespondToModel( + "options must contain at least one non-empty answer".to_string(), + )); + } + lines.extend(options.iter().map(|option| format!("- {option}"))); + } + messages.push(lines.join("\n")); + } + + let item = TurnItem::AgentMessage(AgentMessageItem { + id: call_id, + content: vec![AgentMessageContent::Text { + text: messages.join("\n\n"), + }], + phase: Some(MessagePhase::FinalAnswer), + memory_citation: None, + delivery: Some(AgentMessageDelivery::Async), + questions: Some(args.questions), + }); + session.emit_turn_item_started(turn.as_ref(), &item).await; + session.emit_turn_item_completed(turn.as_ref(), item).await; + + Ok(boxed_tool_output(FunctionToolOutput::from_text( + r#"{"accepted":true}"#.to_string(), + /*success*/ Some(true), + ))) + }) + } +} + +impl CoreToolRuntime for RequestUserInputAsyncHandler { + fn is_builtin_control_tool(&self) -> bool { + true + } +} diff --git a/codex-rs/core/src/tools/handlers/send_user_message_async.rs b/codex-rs/core/src/tools/handlers/send_user_message_async.rs deleted file mode 100644 index 027330cc1d81..000000000000 --- a/codex-rs/core/src/tools/handlers/send_user_message_async.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::function_tool::FunctionCallError; -use crate::tools::context::FunctionToolOutput; -use crate::tools::context::ToolInvocation; -use crate::tools::context::ToolPayload; -use crate::tools::context::boxed_tool_output; -use crate::tools::handlers::parse_arguments; -use crate::tools::registry::CoreToolRuntime; -use crate::tools::registry::ToolExecutor; -use codex_protocol::items::AgentMessageContent; -use codex_protocol::items::AgentMessageDelivery; -use codex_protocol::items::AgentMessageItem; -use codex_protocol::items::TurnItem; -use codex_protocol::models::MessagePhase; -use codex_tools::JsonSchema; -use codex_tools::ResponsesApiTool; -use codex_tools::ToolName; -use codex_tools::ToolSpec; -use serde::Deserialize; -use std::collections::BTreeMap; - -const TOOL_NAME: &str = "send_user_message_async"; - -pub struct SendUserMessageAsyncHandler { - pub description: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct SendUserMessageAsyncArgs { - message: String, -} - -impl ToolExecutor for SendUserMessageAsyncHandler { - fn tool_name(&self) -> ToolName { - ToolName::plain(TOOL_NAME) - } - - fn spec(&self) -> ToolSpec { - let properties = BTreeMap::from([( - "message".to_string(), - JsonSchema::string(Some( - "The concise question to send to the user.".to_string(), - )), - )]); - - ToolSpec::Function(ResponsesApiTool { - name: TOOL_NAME.to_string(), - description: self.description.clone().unwrap_or_else(|| { - "Send a concise message that needs the user's attention during ongoing work. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message.\nOnly use this tool to ask for missing information, preferences, constraints, clarification, or approval. The message should be concise, easy to read and understand, and at the right level of abstraction that is appropriate for the user and task at hand." - .to_string() - }), - strict: false, - defer_loading: None, - parameters: JsonSchema::object( - properties, - Some(vec!["message".to_string()]), - /*additional_properties*/ Some(false.into()), - ), - output_schema: None, - }) - } - - fn handle<'a>(&'a self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'a> - where - ToolInvocation: 'a, - { - Box::pin(async move { - let ToolInvocation { - session, - turn, - call_id, - payload, - .. - } = invocation; - let ToolPayload::Function { arguments } = payload else { - return Err(FunctionCallError::RespondToModel(format!( - "{TOOL_NAME} handler received unsupported payload" - ))); - }; - let args: SendUserMessageAsyncArgs = parse_arguments(&arguments)?; - let message = args.message.trim(); - if message.is_empty() { - return Err(FunctionCallError::RespondToModel( - "message must not be empty".to_string(), - )); - } - - let item = TurnItem::AgentMessage(AgentMessageItem { - id: call_id, - content: vec![AgentMessageContent::Text { - text: message.to_string(), - }], - phase: Some(MessagePhase::FinalAnswer), - memory_citation: None, - delivery: Some(AgentMessageDelivery::Async), - }); - session.emit_turn_item_started(turn.as_ref(), &item).await; - session.emit_turn_item_completed(turn.as_ref(), item).await; - - Ok(boxed_tool_output(FunctionToolOutput::from_text( - r#"{"accepted":true}"#.to_string(), - /*success*/ Some(true), - ))) - }) - } -} - -impl CoreToolRuntime for SendUserMessageAsyncHandler { - fn is_builtin_control_tool(&self) -> bool { - true - } -} diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index d5729387d693..908bb143afcb 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -23,8 +23,8 @@ use crate::tools::handlers::PlanHandler; use crate::tools::handlers::ReadMcpResourceHandler; use crate::tools::handlers::RequestPermissionsHandler; use crate::tools::handlers::RequestPluginInstallHandler; +use crate::tools::handlers::RequestUserInputAsyncHandler; use crate::tools::handlers::RequestUserInputHandler; -use crate::tools::handlers::SendUserMessageAsyncHandler; use crate::tools::handlers::SleepHandler; use crate::tools::handlers::TestSyncHandler; use crate::tools::handlers::ToolSearchHandlerCache; @@ -1163,10 +1163,16 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool .model_info .experimental_supported_tools .iter() - .any(|tool| tool == "send_user_message_async") + // Existing model catalogs still advertise the previous name. + .any(|tool| { + matches!( + tool.as_str(), + "request_user_input_async" | "send_user_message_async" + ) + }) { registry.add_with_exposure( - SendUserMessageAsyncHandler { + RequestUserInputAsyncHandler { description: context .model_messages .and_then(|messages| messages.tools.as_ref()) diff --git a/codex-rs/core/src/turn_timing_tests.rs b/codex-rs/core/src/turn_timing_tests.rs index c32d86000e3a..0feb9a19e039 100644 --- a/codex-rs/core/src/turn_timing_tests.rs +++ b/codex-rs/core/src/turn_timing_tests.rs @@ -66,6 +66,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() { phase: None, memory_citation: None, delivery: None, + questions: None, })) .await .is_some() @@ -78,6 +79,7 @@ async fn turn_timing_state_records_ttfm_independently_of_ttft() { phase: None, memory_citation: None, delivery: None, + questions: None, })) .await, None diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 5b6961228b6f..8d43ca03efd0 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -136,6 +136,7 @@ mod request_permissions; mod request_permissions_tool; mod request_plugin_install; mod request_user_input; +mod request_user_input_async; mod responses_api_proxy_headers; mod responses_lite; #[cfg(target_os = "linux")] @@ -151,7 +152,6 @@ mod rollout_list_find; mod safety_buffering; mod safety_check_downgrade; mod search_tool; -mod send_user_message_async; mod settings_commits; mod settings_constraints; mod shell_snapshot; diff --git a/codex-rs/core/tests/suite/send_user_message_async.rs b/codex-rs/core/tests/suite/request_user_input_async.rs similarity index 61% rename from codex-rs/core/tests/suite/send_user_message_async.rs rename to codex-rs/core/tests/suite/request_user_input_async.rs index fa0d37c13a9f..fdb6cb93fbbe 100644 --- a/codex-rs/core/tests/suite/send_user_message_async.rs +++ b/codex-rs/core/tests/suite/request_user_input_async.rs @@ -4,6 +4,7 @@ use codex_core::TurnInputRequest; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageDelivery; use codex_protocol::items::AgentMessageItem; +use codex_protocol::items::AsyncUserInputQuestion; use codex_protocol::items::TurnItem; use codex_protocol::models::MessagePhase; use codex_protocol::openai_models::ReasoningEffort; @@ -112,26 +113,40 @@ async fn persistent_async_message_guidance_follows_tool_availability( .as_array() .expect("request tools") .iter() - .any(|tool| tool["name"] == "send_user_message_async"), + .any(|tool| tool["name"] == "request_user_input_async"), expect_tool_guidance, ); Ok(()) } -#[test_case(None; "fallback_description")] -#[test_case(Some(ToolMessages { send_user_message_async: None }); "missing_tool")] -#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage::default()) }); "missing_description")] -#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog async message description.".to_string()) }) }); "catalog_description")] -#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()) }) }); "empty_description")] +#[test_case(None, "send_user_message_async"; "fallback_description")] +#[test_case(None, "request_user_input_async"; "current_catalog_name")] +#[test_case(Some(ToolMessages { send_user_message_async: None }), "send_user_message_async"; "missing_tool")] +#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage::default()) }), "send_user_message_async"; "missing_description")] +#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog async message description.".to_string()) }) }), "send_user_message_async"; "catalog_description")] +#[test_case(Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()) }) }), "send_user_message_async"; "empty_description")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn send_user_message_async_emits_item_and_does_not_end_the_turn( +async fn request_user_input_async_emits_item_and_does_not_end_the_turn( tool_messages: Option, + catalog_tool_name: &'static str, ) -> Result<()> { skip_if_no_network!(Ok(())); const CALL_ID: &str = "async-message-call"; - const MESSAGE: &str = "Still investigating."; + const TITLE: &str = "Which environment should I use?"; + const MESSAGE: &str = + "Which environment should I use?\n- Staging\n- Production\n\nWhat deadline should I use?"; + let questions = vec![ + AsyncUserInputQuestion { + title: TITLE.to_string(), + options: Some(vec!["Staging".to_string(), "Production".to_string()]), + }, + AsyncUserInputQuestion { + title: "What deadline should I use?".to_string(), + options: None, + }, + ]; let server = start_mock_server().await; let responses = mount_sse_sequence( @@ -142,8 +157,12 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn( ev_function_call_with_namespace( CALL_ID, "functions", - "send_user_message_async", - &json!({ "message": MESSAGE }).to_string(), + "request_user_input_async", + &json!({ "questions": [ + {"title": TITLE, "options": ["Staging", "Production"]}, + {"title": "What deadline should I use?"} + ] }) + .to_string(), ), ev_completed("resp-1"), ]), @@ -160,15 +179,18 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn( .and_then(|tools| tools.send_user_message_async.as_ref()) .and_then(|tool| tool.description.as_deref()) .unwrap_or( - "Send a concise message that needs the user's attention during ongoing work. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message.\nOnly use this tool to ask for missing information, preferences, constraints, clarification, or approval. The message should be concise, easy to read and understand, and at the right level of abstraction that is appropriate for the user and task at hand.", + "Ask the user one or more questions during ongoing work. Use this tool only to request missing information, preferences, constraints, clarification, or approval. The tool returns immediately without ending the turn or waiting for a reply; any reply arrives asynchronously as a new user message. Keep questions concise, self-contained, and easy to understand, using a level of detail appropriate to the user and task. The UI always allows a free-text answer, including when suggested options are provided. A preselected option is not submitted automatically.", ) .to_string(); let test = test_codex() .with_model_info_override("gpt-5.2", move |model| { model.tool_mode = Some(ToolMode::CodeModeOnly); + model.experimental_supported_tools.retain(|tool| { + tool != "send_user_message_async" && tool != "request_user_input_async" + }); model .experimental_supported_tools - .push("send_user_message_async".to_string()); + .push(catalog_tool_name.to_string()); model .model_messages .as_mut() @@ -208,6 +230,7 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn( phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: Some(AgentMessageDelivery::Async), + questions: Some(questions), })? ); @@ -238,22 +261,36 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn( assert_eq!(requests.len(), 2); for request in &requests { let body = request.body_json(); - let tool = body["tools"] - .as_array() - .expect("request tools") + let tools = body["tools"].as_array().expect("request tools"); + let tool = tools .iter() - .find(|tool| tool["type"] == "function" && tool["name"] == "send_user_message_async") + .find(|tool| tool["type"] == "function" && tool["name"] == "request_user_input_async") .expect("the async message tool should be directly visible to the model"); assert_eq!(tool["description"], expected_description); + assert_eq!(tool["strict"], false); + assert_eq!(tool["parameters"]["required"], json!(["questions"])); + assert_eq!(tool["parameters"]["additionalProperties"], false); + let schema = &tool["parameters"]["properties"]["questions"]; + assert_eq!(schema["minItems"], 1); + assert_eq!(schema["items"]["required"], json!(["title"])); + assert_eq!(schema["items"]["additionalProperties"], false); + assert_eq!(schema["items"]["properties"]["options"]["minItems"], 1); + assert_eq!( + schema["items"]["properties"]["options"]["items"]["type"], + "string" + ); + assert!( + tools + .iter() + .all(|tool| tool["name"] != "send_user_message_async") + ); } assert_eq!( requests[1].function_call_output_text(CALL_ID), Some(r#"{"accepted":true}"#.to_string()) ); let has_synthetic_assistant_message = requests[1].input().into_iter().any(|item| { - item["type"] == "message" - && item["role"] == "assistant" - && item.to_string().contains(MESSAGE) + item["type"] == "message" && item["role"] == "assistant" && item.to_string().contains(TITLE) }); assert!( !has_synthetic_assistant_message, @@ -262,3 +299,62 @@ async fn send_user_message_async_emits_item_and_does_not_end_the_turn( Ok(()) } + +#[test_case(json!({"questions": []}), "questions must not be empty"; "no_questions")] +#[test_case(json!({"questions": [{"title": " "}]}), "question titles must not be empty"; "blank_title")] +#[test_case(json!({"questions": [{"title": "Which?", "options": []}]}), "options must contain at least one non-empty answer"; "empty_options")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn invalid_async_questions_do_not_emit_an_item( + arguments: serde_json::Value, + expected_error: &str, +) -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_function_call_with_namespace( + "invalid", + "functions", + "request_user_input_async", + &arguments.to_string(), + ), + ev_completed("resp-1"), + ]), + sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), + ], + ) + .await; + let test = test_codex() + .with_model_info_override("gpt-5.2", |model| { + model.tool_mode = Some(ToolMode::CodeModeOnly); + model + .experimental_supported_tools + .push("request_user_input_async".to_string()); + }) + .build_with_auto_env(&server) + .await?; + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "Get clarification.".to_string(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(test.codex.as_ref(), |event| { + let item = match event { + EventMsg::ItemStarted(event) => Some(&event.item), + EventMsg::ItemCompleted(event) => Some(&event.item), + _ => None, + }; + assert!(!matches!(item, Some(TurnItem::AgentMessage(message)) if message.delivery == Some(AgentMessageDelivery::Async))); + matches!(event, EventMsg::TurnComplete(_)) + }).await; + assert_eq!( + responses.requests()[1].function_call_output_text("invalid"), + Some(expected_error.to_string()) + ); + Ok(()) +} diff --git a/codex-rs/core/tests/suite/step_settings.rs b/codex-rs/core/tests/suite/step_settings.rs index ce58e29931b4..c27c56643495 100644 --- a/codex-rs/core/tests/suite/step_settings.rs +++ b/codex-rs/core/tests/suite/step_settings.rs @@ -1134,7 +1134,7 @@ async fn model_activation_uses_destination_metadata_defaults( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn send_user_message_async_description_follows_mid_turn_model_changes() -> Result<()> { +async fn request_user_input_async_description_follows_mid_turn_model_changes() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -1201,7 +1201,7 @@ async fn send_user_message_async_description_follows_mid_turn_model_changes() -> .as_array() .expect("request tools") .iter() - .find(|tool| tool["name"] == "send_user_message_async") + .find(|tool| tool["name"] == "request_user_input_async") .expect("async message tool"); json!({"model": body["model"], "description": tool["description"]}) }) diff --git a/codex-rs/exec/src/event_processor_with_human_output_tests.rs b/codex-rs/exec/src/event_processor_with_human_output_tests.rs index 22a6a3cb4562..c91d1388d178 100644 --- a/codex-rs/exec/src/event_processor_with_human_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_human_output_tests.rs @@ -251,6 +251,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() { phase: None, memory_citation: None, delivery: None, + questions: None, }, ThreadItem::Plan { id: "plan-1".to_string(), @@ -262,6 +263,7 @@ fn final_message_from_turn_items_uses_latest_agent_message() { phase: None, memory_citation: None, delivery: None, + questions: None, }, ]); @@ -321,6 +323,7 @@ fn turn_completed_recovers_final_message_from_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }], status: TurnStatus::Completed, error: None, @@ -370,6 +373,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }], status: TurnStatus::Completed, error: None, diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs index 096c3d8cf6df..4159cc9ddded 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output_tests.rs @@ -19,6 +19,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index c6e40dc733a8..85de6137b4ab 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -389,6 +389,7 @@ fn turn_items_for_thread_returns_matching_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }], status: codex_app_server_protocol::TurnStatus::Completed, error: None, @@ -420,6 +421,7 @@ fn turn_items_for_thread_returns_matching_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }]) ); assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None); diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index de6aac43b33f..737822d4ef0f 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -306,6 +306,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -928,6 +929,7 @@ fn agent_message_item_updates_final_message() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -964,6 +966,7 @@ fn agent_message_item_started_is_ignored() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -1315,6 +1318,7 @@ fn turn_completion_recovers_final_message_from_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }], status: TurnStatus::Completed, error: None, @@ -1443,6 +1447,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -1462,6 +1467,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }], status: TurnStatus::Completed, error: None, @@ -1495,6 +1501,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), @@ -1542,6 +1549,7 @@ fn failed_turn_clears_stale_final_message() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: "turn-1".to_string(), diff --git a/codex-rs/external-agent-migration/src/sessions/export.rs b/codex-rs/external-agent-migration/src/sessions/export.rs index b02f8c7e642b..2370bf1fc1b1 100644 --- a/codex-rs/external-agent-migration/src/sessions/export.rs +++ b/codex-rs/external-agent-migration/src/sessions/export.rs @@ -136,6 +136,7 @@ pub(super) fn rollout_items_from_messages(messages: Vec) -> phase: None, memory_citation: None, delivery: None, + questions: None, }, ))); items.push(RolloutItem::ResponseItem(response_item(message).into())); @@ -158,6 +159,7 @@ fn external_session_imported_marker_item() -> RolloutItem { phase: None, memory_citation: None, delivery: None, + questions: None, })) } @@ -254,6 +256,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, } ); } @@ -287,6 +290,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }) ); let last_turn_complete = imported diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index d7d649f65db4..362854f8dbe9 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -133,6 +133,14 @@ pub enum AgentMessageDelivery { Async, } +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct AsyncUserInputQuestion { + pub title: String, + pub options: Option>, +} + #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] /// Assistant-authored message payload used in turn-item streams. /// @@ -155,6 +163,9 @@ pub struct AgentMessageItem { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub delivery: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub questions: Option>, } #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] diff --git a/codex-rs/protocol/src/legacy_events.rs b/codex-rs/protocol/src/legacy_events.rs index 10a807c1cd7d..2ef68b9b1bfd 100644 --- a/codex-rs/protocol/src/legacy_events.rs +++ b/codex-rs/protocol/src/legacy_events.rs @@ -106,6 +106,7 @@ impl AgentMessageItem { phase: self.phase.clone(), memory_citation: self.memory_citation.clone(), delivery: self.delivery, + questions: self.questions.clone(), }), }) .collect() diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 4e5b7eed444b..ae2107b801b7 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -34,6 +34,7 @@ use crate::dynamic_tools::DynamicToolResponse; use crate::dynamic_tools::DynamicToolSpec; use crate::error::Result as CodexResult; use crate::items::AgentMessageDelivery; +use crate::items::AsyncUserInputQuestion; use crate::items::TurnItem; use crate::mcp::CallToolResult; use crate::mcp::RequestId; @@ -2496,6 +2497,9 @@ pub struct AgentMessageEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub delivery: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub questions: Option>, } #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index b5d3ac985536..e1714f814cca 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -67,6 +67,7 @@ fn agent_message_item(message: &str) -> RolloutItem { phase: None, memory_citation: None, delivery: None, + questions: None, })) } @@ -650,6 +651,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result< phase: None, memory_citation: None, delivery: None, + questions: None, }, ))]) .await?; @@ -905,6 +907,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i phase: None, memory_citation: None, delivery: None, + questions: None, }, ))]) .await?; @@ -957,6 +960,7 @@ async fn writer_state_retries_write_error_before_reporting_flush_success() -> st phase: None, memory_citation: None, delivery: None, + questions: None, }, ))]); diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index e83e7ccd4663..34576f7fe56d 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -1043,6 +1043,7 @@ mod tests { phase: Some(MessagePhase::Commentary), memory_citation: None, delivery: None, + questions: None, })), RolloutItem::ResponseItem( ResponseItem::FunctionCallOutput { diff --git a/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs b/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs index 1e12c1db43dd..9f3f8e13c0bb 100644 --- a/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs +++ b/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs @@ -95,6 +95,7 @@ pub(super) fn completed_item( phase: event.phase.clone(), memory_citation: event.memory_citation.clone(), delivery: event.delivery, + questions: event.questions.clone(), }), None, )), diff --git a/codex-rs/thread-store/src/local/rollout_migration_tests.rs b/codex-rs/thread-store/src/local/rollout_migration_tests.rs index 2214bbeed64c..0440f38f2cae 100644 --- a/codex-rs/thread-store/src/local/rollout_migration_tests.rs +++ b/codex-rs/thread-store/src/local/rollout_migration_tests.rs @@ -160,6 +160,7 @@ fn agent_message(text: &str) -> RolloutItem { phase: None, memory_citation: None, delivery: None, + questions: None, })) } diff --git a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs index 295287f0652d..373419c16eda 100644 --- a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs +++ b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs @@ -232,6 +232,7 @@ async fn split_homes_support_backfill_listing_and_paginated_history() { phase: None, memory_citation: None, delivery: None, + questions: None, }), ), turn_completed("turn-1"), @@ -325,6 +326,7 @@ async fn paginated_live_append_materializes_turn_items_and_state() { phase: None, memory_citation: None, delivery: None, + questions: None, }), ), turn_completed("turn-1"), @@ -480,6 +482,7 @@ async fn paginated_realtime_items_materialize_separately_in_rollout_order() { phase: None, memory_citation: None, delivery: None, + questions: None, }), ), RolloutItem::RealtimeItem(RealtimeItem { @@ -2669,6 +2672,7 @@ fn agent_message(id: &str, phase: MessagePhase) -> TurnItem { phase: Some(phase), memory_citation: None, delivery: None, + questions: None, }) } diff --git a/codex-rs/tools/src/json_schema/types.rs b/codex-rs/tools/src/json_schema/types.rs index 236cfeda634e..0ba00c9bc766 100644 --- a/codex-rs/tools/src/json_schema/types.rs +++ b/codex-rs/tools/src/json_schema/types.rs @@ -48,6 +48,8 @@ pub struct JsonSchema { pub enum_values: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, + #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")] + pub min_items: Option, #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/tui/src/app/agent_status_feed_tests.rs b/codex-rs/tui/src/app/agent_status_feed_tests.rs index 2cb625fdf874..76fc172d1b32 100644 --- a/codex-rs/tui/src/app/agent_status_feed_tests.rs +++ b/codex-rs/tui/src/app/agent_status_feed_tests.rs @@ -38,6 +38,7 @@ fn agent_status_uses_bounded_buffered_activity() { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-child".to_string(), turn_id: "turn-1".to_string(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b6300ee3997e..3a9b3db5a946 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3733,6 +3733,7 @@ async fn active_thread_file_change_approval_recovers_buffered_changes() { phase: None, memory_citation: None, delivery: None, + questions: None, }, }), /*replay_kind*/ None, @@ -7552,6 +7553,7 @@ async fn replay_thread_snapshot_replays_turn_history_in_order() { phase: None, memory_citation: None, delivery: None, + questions: None, }, ], status: TurnStatus::Completed, diff --git a/codex-rs/tui/src/app/tests/patch_approval_tests.rs b/codex-rs/tui/src/app/tests/patch_approval_tests.rs index 51906ca4da4c..3bc8ec323ace 100644 --- a/codex-rs/tui/src/app/tests/patch_approval_tests.rs +++ b/codex-rs/tui/src/app/tests/patch_approval_tests.rs @@ -271,6 +271,7 @@ async fn active_patch_approval_preserves_deferred_startup_protection() -> Result phase: None, memory_citation: None, delivery: None, + questions: None, }, }), /*replay_kind*/ None, diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index 19a420932b2f..93298bc5627c 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -1515,6 +1515,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() -> phase: None, memory_citation: None, delivery: None, + questions: None, }) })); items.extend([ @@ -1704,6 +1705,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> { phase: None, memory_citation: None, delivery: None, + questions: None, }), started_at_ms: None, completed_at_ms: 0, @@ -2102,6 +2104,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri phase: None, memory_citation: None, delivery: None, + questions: None, }), started_at_ms: None, completed_at_ms: 0, diff --git a/codex-rs/tui/src/app/thread_title_tests.rs b/codex-rs/tui/src/app/thread_title_tests.rs index 7d0e36c0d7be..6044fd82a1cb 100644 --- a/codex-rs/tui/src/app/thread_title_tests.rs +++ b/codex-rs/tui/src/app/thread_title_tests.rs @@ -479,5 +479,6 @@ fn title_agent_message(id: &str, text: &str, phase: Option) -> Thr phase, memory_citation: None, delivery: None, + questions: None, } } diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 92adb3eec378..a29cc9a6479f 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -3674,6 +3674,7 @@ mod tests { phase: None, memory_citation: None, delivery: None, + questions: None, }, ], status: TurnStatus::Completed, diff --git a/codex-rs/tui/src/chatwidget/replay.rs b/codex-rs/tui/src/chatwidget/replay.rs index 037b58c25f38..cb2c8d84cd5d 100644 --- a/codex-rs/tui/src/chatwidget/replay.rs +++ b/codex-rs/tui/src/chatwidget/replay.rs @@ -95,6 +95,7 @@ impl ChatWidget { phase, memory_citation, delivery, + questions, .. } => { self.on_agent_message_item_completed( @@ -120,6 +121,7 @@ impl ChatWidget { } }), delivery, + questions, }, &turn_id, from_replay, diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index d9e085bc50a8..f983ceb674c3 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -625,6 +625,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item( phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }; chat.handle_server_notification( ServerNotification::ItemCompleted(ItemCompletedNotification { @@ -1302,6 +1303,7 @@ async fn live_app_server_turn_completion_repairs_dropped_message_deltas() { phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }]; chat.handle_server_notification( ServerNotification::TurnCompleted(TurnCompletedNotification { diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 6cb423661c86..ad00a5115362 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -771,6 +771,7 @@ pub(super) fn replay_agent_message( phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }, "turn-1".to_string(), replay_kind, @@ -924,6 +925,7 @@ pub(super) fn complete_assistant_message( phase, memory_citation: None, delivery: None, + questions: None, }, }), /*replay_kind*/ None, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index c55732bf2814..31af6ad260e1 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -378,6 +378,7 @@ async fn replayed_nested_review_prompts_do_not_render_or_seed_composer_history() phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }, ], ..app_server_turn( diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 224b55ebdc7d..3ed5478ecbbc 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -832,6 +832,7 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() { phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }], status: AppServerTurnStatus::Completed, error: None, @@ -871,6 +872,7 @@ async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_com phase: Some(MessagePhase::FinalAnswer), memory_citation: None, delivery: None, + questions: None, }], status: AppServerTurnStatus::Completed, error: None, diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index a549819156c0..888545170379 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -6338,6 +6338,7 @@ session_picker_view = "dense" phase: None, memory_citation: None, delivery: None, + questions: None, }, ThreadItem::Plan { id: String::from("plan-1"), diff --git a/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs b/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs index 4da9949591f9..d08b188ea259 100644 --- a/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs +++ b/codex-rs/tui/src/resume_picker_transcript_preview_tests.rs @@ -42,6 +42,7 @@ fn rollout_agent_message(text: &str) -> RolloutItem { phase: None, memory_citation: None, delivery: None, + questions: None, })) } @@ -269,6 +270,7 @@ fn transcript_preview_reverse_scan_stops_before_older_items() { phase: None, memory_citation: None, delivery: None, + questions: None, }; let mut lines = Vec::new(); diff --git a/codex-rs/tui/src/temporary_structured_request_tests.rs b/codex-rs/tui/src/temporary_structured_request_tests.rs index b1af68a1e0b8..55030f7b2c3c 100644 --- a/codex-rs/tui/src/temporary_structured_request_tests.rs +++ b/codex-rs/tui/src/temporary_structured_request_tests.rs @@ -21,6 +21,7 @@ fn agent_message_notification(turn_id: &str, text: &str) -> ServerNotification { phase: None, memory_citation: None, delivery: None, + questions: None, }, thread_id: "thread-1".to_string(), turn_id: turn_id.to_string(),