diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index cb9375f2b8..6a96e101e0 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 c7e7930699..b15474ffc7 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 ec97c1f850..47e75d11d5 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 8e83826dbc..fce5c1d1b8 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 6eb695e5b7..f3caf84e96 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 f304246faa..8432eb5437 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 29a2e6db19..7da4ff34d8 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 8d2527a200..e0aa43dd29 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 9ec9797426..d11f47942a 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 52303c2077..576edc360a 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 d9c006b309..6db9cbc2e2 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 0ee3e3e714..8682470d11 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 68e73e7673..030a48b1b8 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 ffb3659ea5..f92e7201a0 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 2afaf9ec79..da1efc5a23 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 26cc859c5d..984dc14b69 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 7f2ece07d0..63cc9d2e9a 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 489c2103ce..c8d1dd0354 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 66371aed4c..04e4301147 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 ac0f055cc7..7db8c18c82 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 1a62b8ad45..9552369480 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 e02f757f99..57052afd9f 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 3aa3191608..0488aa5363 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 900a626ab2..485dfcd211 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 0000000000..bac5531c34 --- /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 d0dbf7c126..ae8c00a720 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 34d4cba17b..fefe723385 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 1da1ee24d3..028069a0f5 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 cc036e2b95..a6d0077770 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 67b8c41d5b..9b24e093ae 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 141f867c1b..971ef366e7 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 cbfde21753..dc084fdfbf 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 68100e6c92..91cda28b0a 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 18b732ae1b..d8613e90c5 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 51e00965ce..74ad7db490 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 e9b8c13adf..c6d21f4cfe 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 eeff9816f4..b7839519dc 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 14aaddae38..d49f66dd6f 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 3300a1da9d..1756bf7c91 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 1d7b857cff..a6f728deb1 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 f661c21baa..d386b08b5d 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 7e0afae7b5..48db0239b3 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 c5f6eb2597..f7c42680c2 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 6f5cc37e29..74cdb32dc3 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 35268fe55f..25a2ae1508 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 0000000000..86895851c7 --- /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 027330cc1d..0000000000 --- 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 d5729387d6..908bb143af 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 c32d86000e..0feb9a19e0 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 5b6961228b..8d43ca03ef 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 fa0d37c13a..fdb6cb93fb 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 ce58e29931..c27c566434 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 22a6a3cb45..c91d1388d1 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 096c3d8cf6..4159cc9ddd 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 c6e40dc733..85de6137b4 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 de6aac43b3..737822d4ef 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 b02f8c7e64..2370bf1fc1 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 d7d649f65d..362854f8db 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 10a807c1cd..2ef68b9b1b 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 4e5b7eed44..ae2107b801 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 b5d3ac9855..e1714f814c 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 e83e7ccd46..34576f7fe5 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 1e12c1db43..9f3f8e13c0 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 2214bbeed6..0440f38f2c 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 295287f065..373419c16e 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 236cfeda63..0ba00c9bc7 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 2cb625fdf8..76fc172d1b 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 b6300ee399..3a9b3db5a9 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 51906ca4da..3bc8ec323a 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 19a420932b..93298bc562 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 7d0e36c0d7..6044fd82a1 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 92adb3eec3..a29cc9a647 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 037b58c25f..cb2c8d84cd 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 d9e085bc50..f983ceb674 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 6cb423661c..ad00a51153 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 c55732bf28..31af6ad260 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 224b55ebdc..3ed5478ecb 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 a549819156..8885451703 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 4da9949591..d08b188ea2 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 b1af68a1e0..55030f7b2c 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(),