Compare commits

...

3 Commits

Author SHA1 Message Date
rafaelj
e963813824 Add onboarding context picker tool 2026-05-21 14:51:51 -04:00
rafaelj
90006cb729 Add onboarding option picker tool 2026-05-21 14:44:57 -04:00
rafaelj
df3e2c3265 Add onboarding interactive tools feature flag 2026-05-21 14:08:50 -04:00
44 changed files with 1535 additions and 5 deletions

View File

@@ -1664,6 +1664,71 @@
"ThreadId": {
"type": "string"
},
"ToolOptionPickerOption": {
"description": "EXPERIMENTAL. Defines a single selectable option for request_option_picker.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"label": {
"type": "string"
}
},
"required": [
"label"
],
"type": "object"
},
"ToolOptionPickerParams": {
"description": "EXPERIMENTAL. Params sent with a request_option_picker event.",
"properties": {
"allowMultiple": {
"type": "boolean"
},
"itemId": {
"type": "string"
},
"options": {
"items": {
"$ref": "#/definitions/ToolOptionPickerOption"
},
"type": "array"
},
"question": {
"type": "string"
},
"skipLabel": {
"type": [
"string",
"null"
]
},
"submitLabel": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"allowMultiple",
"itemId",
"options",
"question",
"threadId",
"turnId"
],
"type": "object"
},
"ToolRequestUserInputOption": {
"description": "EXPERIMENTAL. Defines a single selectable option for request_user_input.",
"properties": {
@@ -1743,6 +1808,26 @@
"question"
],
"type": "object"
},
"ToolSetupCodexContextPickerParams": {
"description": "EXPERIMENTAL. Params sent with a setup_codex_context_picker event.",
"properties": {
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"type": "object"
}
},
"description": "Request initiated from the server and sent to the client.",
@@ -1822,6 +1907,56 @@
"title": "Item/tool/requestUserInputRequest",
"type": "object"
},
{
"description": "EXPERIMENTAL - Request compact option selection from the user for a tool call.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"item/tool/requestOptionPicker"
],
"title": "Item/tool/requestOptionPickerRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ToolOptionPickerParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/tool/requestOptionPickerRequest",
"type": "object"
},
{
"description": "EXPERIMENTAL - Request the onboarding context source picker.",
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"item/tool/requestSetupCodexContextPicker"
],
"title": "Item/tool/requestSetupCodexContextPickerRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ToolSetupCodexContextPickerParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/tool/requestSetupCodexContextPickerRequest",
"type": "object"
},
{
"description": "Request input for an MCP server elicitation.",
"properties": {

View File

@@ -0,0 +1,69 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ToolOptionPickerOption": {
"description": "EXPERIMENTAL. Defines a single selectable option for request_option_picker.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"label": {
"type": "string"
}
},
"required": [
"label"
],
"type": "object"
}
},
"description": "EXPERIMENTAL. Params sent with a request_option_picker event.",
"properties": {
"allowMultiple": {
"type": "boolean"
},
"itemId": {
"type": "string"
},
"options": {
"items": {
"$ref": "#/definitions/ToolOptionPickerOption"
},
"type": "array"
},
"question": {
"type": "string"
},
"skipLabel": {
"type": [
"string",
"null"
]
},
"submitLabel": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"allowMultiple",
"itemId",
"options",
"question",
"threadId",
"turnId"
],
"title": "ToolOptionPickerParams",
"type": "object"
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ToolOptionPickerAction": {
"description": "EXPERIMENTAL. Action selected for request_option_picker.",
"enum": [
"submit",
"skip",
"dismiss"
],
"type": "string"
}
},
"description": "EXPERIMENTAL. Captures a user's response to request_option_picker.",
"properties": {
"action": {
"$ref": "#/definitions/ToolOptionPickerAction"
},
"freeformAnswer": {
"type": [
"string",
"null"
]
},
"selectedOptions": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"action",
"selectedOptions"
],
"title": "ToolOptionPickerResponse",
"type": "object"
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL. Params sent with a setup_codex_context_picker event.",
"properties": {
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"title": "ToolSetupCodexContextPickerParams",
"type": "object"
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ToolSetupCodexContextPickerAction": {
"description": "EXPERIMENTAL. Action selected for setup_codex_context_picker.",
"enum": [
"continue",
"skip",
"dismiss"
],
"type": "string"
}
},
"description": "EXPERIMENTAL. Captures a user's response to setup_codex_context_picker.",
"properties": {
"action": {
"$ref": "#/definitions/ToolSetupCodexContextPickerAction"
},
"selectedSources": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"action",
"selectedSources"
],
"title": "ToolSetupCodexContextPickerResponse",
"type": "object"
}

View File

@@ -5296,6 +5296,56 @@
"title": "Item/tool/requestUserInputRequest",
"type": "object"
},
{
"description": "EXPERIMENTAL - Request compact option selection from the user for a tool call.",
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"item/tool/requestOptionPicker"
],
"title": "Item/tool/requestOptionPickerRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ToolOptionPickerParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/tool/requestOptionPickerRequest",
"type": "object"
},
{
"description": "EXPERIMENTAL - Request the onboarding context source picker.",
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"item/tool/requestSetupCodexContextPicker"
],
"title": "Item/tool/requestSetupCodexContextPickerRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ToolSetupCodexContextPickerParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Item/tool/requestSetupCodexContextPickerRequest",
"type": "object"
},
{
"description": "Request input for an MCP server elicitation.",
"properties": {
@@ -5473,6 +5523,109 @@
],
"title": "ServerRequest"
},
"ToolOptionPickerAction": {
"description": "EXPERIMENTAL. Action selected for request_option_picker.",
"enum": [
"submit",
"skip",
"dismiss"
],
"type": "string"
},
"ToolOptionPickerOption": {
"description": "EXPERIMENTAL. Defines a single selectable option for request_option_picker.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"label": {
"type": "string"
}
},
"required": [
"label"
],
"type": "object"
},
"ToolOptionPickerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL. Params sent with a request_option_picker event.",
"properties": {
"allowMultiple": {
"type": "boolean"
},
"itemId": {
"type": "string"
},
"options": {
"items": {
"$ref": "#/definitions/ToolOptionPickerOption"
},
"type": "array"
},
"question": {
"type": "string"
},
"skipLabel": {
"type": [
"string",
"null"
]
},
"submitLabel": {
"type": [
"string",
"null"
]
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"allowMultiple",
"itemId",
"options",
"question",
"threadId",
"turnId"
],
"title": "ToolOptionPickerParams",
"type": "object"
},
"ToolOptionPickerResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL. Captures a user's response to request_option_picker.",
"properties": {
"action": {
"$ref": "#/definitions/ToolOptionPickerAction"
},
"freeformAnswer": {
"type": [
"string",
"null"
]
},
"selectedOptions": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"action",
"selectedOptions"
],
"title": "ToolOptionPickerResponse",
"type": "object"
},
"ToolRequestUserInputAnswer": {
"description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.",
"properties": {
@@ -5587,6 +5740,58 @@
"title": "ToolRequestUserInputResponse",
"type": "object"
},
"ToolSetupCodexContextPickerAction": {
"description": "EXPERIMENTAL. Action selected for setup_codex_context_picker.",
"enum": [
"continue",
"skip",
"dismiss"
],
"type": "string"
},
"ToolSetupCodexContextPickerParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL. Params sent with a setup_codex_context_picker event.",
"properties": {
"itemId": {
"type": "string"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"itemId",
"threadId",
"turnId"
],
"title": "ToolSetupCodexContextPickerParams",
"type": "object"
},
"ToolSetupCodexContextPickerResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "EXPERIMENTAL. Captures a user's response to setup_codex_context_picker.",
"properties": {
"action": {
"$ref": "#/definitions/ToolSetupCodexContextPickerAction"
},
"selectedSources": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"action",
"selectedSources"
],
"title": "ToolSetupCodexContextPickerResponse",
"type": "object"
},
"W3cTraceContext": {
"properties": {
"traceparent": {

View File

@@ -11,9 +11,11 @@ import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams";
import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams";
import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams";
import type { PermissionsRequestApprovalParams } from "./v2/PermissionsRequestApprovalParams";
import type { ToolOptionPickerParams } from "./v2/ToolOptionPickerParams";
import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams";
import type { ToolSetupCodexContextPickerParams } from "./v2/ToolSetupCodexContextPickerParams";
/**
* Request initiated from the server and sent to the client.
*/
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };
export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "item/tool/requestOptionPicker", id: RequestId, params: ToolOptionPickerParams, } | { "method": "item/tool/requestSetupCodexContextPicker", id: RequestId, params: ToolSetupCodexContextPickerParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, };

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL. Action selected for request_option_picker.
*/
export type ToolOptionPickerAction = "submit" | "skip" | "dismiss";

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL. Defines a single selectable option for request_option_picker.
*/
export type ToolOptionPickerOption = { label: string, description: string | null, };

View File

@@ -0,0 +1,9 @@
// 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.
import type { ToolOptionPickerOption } from "./ToolOptionPickerOption";
/**
* EXPERIMENTAL. Params sent with a request_option_picker event.
*/
export type ToolOptionPickerParams = { threadId: string, turnId: string, itemId: string, question: string, options: Array<ToolOptionPickerOption>, allowMultiple: boolean, submitLabel: string | null, skipLabel: string | null, };

View File

@@ -0,0 +1,9 @@
// 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.
import type { ToolOptionPickerAction } from "./ToolOptionPickerAction";
/**
* EXPERIMENTAL. Captures a user's response to request_option_picker.
*/
export type ToolOptionPickerResponse = { action: ToolOptionPickerAction, selectedOptions: Array<string>, freeformAnswer: string | null, };

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL. Action selected for setup_codex_context_picker.
*/
export type ToolSetupCodexContextPickerAction = "continue" | "skip" | "dismiss";

View File

@@ -0,0 +1,8 @@
// 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.
/**
* EXPERIMENTAL. Params sent with a setup_codex_context_picker event.
*/
export type ToolSetupCodexContextPickerParams = { threadId: string, turnId: string, itemId: string, };

View File

@@ -0,0 +1,9 @@
// 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.
import type { ToolSetupCodexContextPickerAction } from "./ToolSetupCodexContextPickerAction";
/**
* EXPERIMENTAL. Captures a user's response to setup_codex_context_picker.
*/
export type ToolSetupCodexContextPickerResponse = { action: ToolSetupCodexContextPickerAction, selectedSources: Array<string>, };

View File

@@ -425,11 +425,18 @@ export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams";
export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse";
export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus";
export type { TokenUsageBreakdown } from "./TokenUsageBreakdown";
export type { ToolOptionPickerAction } from "./ToolOptionPickerAction";
export type { ToolOptionPickerOption } from "./ToolOptionPickerOption";
export type { ToolOptionPickerParams } from "./ToolOptionPickerParams";
export type { ToolOptionPickerResponse } from "./ToolOptionPickerResponse";
export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer";
export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption";
export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams";
export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion";
export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse";
export type { ToolSetupCodexContextPickerAction } from "./ToolSetupCodexContextPickerAction";
export type { ToolSetupCodexContextPickerParams } from "./ToolSetupCodexContextPickerParams";
export type { ToolSetupCodexContextPickerResponse } from "./ToolSetupCodexContextPickerResponse";
export type { ToolsV2 } from "./ToolsV2";
export type { Turn } from "./Turn";
export type { TurnCompletedNotification } from "./TurnCompletedNotification";

View File

@@ -1334,6 +1334,18 @@ server_request_definitions! {
response: v2::ToolRequestUserInputResponse,
},
/// EXPERIMENTAL - Request compact option selection from the user for a tool call.
ToolOptionPicker => "item/tool/requestOptionPicker" {
params: v2::ToolOptionPickerParams,
response: v2::ToolOptionPickerResponse,
},
/// EXPERIMENTAL - Request the onboarding context source picker.
ToolSetupCodexContextPicker => "item/tool/requestSetupCodexContextPicker" {
params: v2::ToolSetupCodexContextPickerParams,
response: v2::ToolSetupCodexContextPickerResponse,
},
/// Request input for an MCP server elicitation.
McpServerElicitationRequest => "mcpServer/elicitation/request" {
params: v2::McpServerElicitationRequestParams,

View File

@@ -1447,3 +1447,76 @@ pub struct ToolRequestUserInputAnswer {
pub struct ToolRequestUserInputResponse {
pub answers: HashMap<String, ToolRequestUserInputAnswer>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Defines a single selectable option for request_option_picker.
pub struct ToolOptionPickerOption {
pub label: String,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Params sent with a request_option_picker event.
pub struct ToolOptionPickerParams {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub question: String,
pub options: Vec<ToolOptionPickerOption>,
pub allow_multiple: bool,
pub submit_label: Option<String>,
pub skip_label: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Action selected for request_option_picker.
pub enum ToolOptionPickerAction {
Submit,
Skip,
Dismiss,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Captures a user's response to request_option_picker.
pub struct ToolOptionPickerResponse {
pub action: ToolOptionPickerAction,
pub selected_options: Vec<String>,
pub freeform_answer: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Params sent with a setup_codex_context_picker event.
pub struct ToolSetupCodexContextPickerParams {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Action selected for setup_codex_context_picker.
pub enum ToolSetupCodexContextPickerAction {
Continue,
Skip,
Dismiss,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Captures a user's response to setup_codex_context_picker.
pub struct ToolSetupCodexContextPickerResponse {
pub action: ToolSetupCodexContextPickerAction,
pub selected_sources: Vec<String>,
}

View File

@@ -65,10 +65,17 @@ use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::ThreadTokenUsage;
use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification;
use codex_app_server_protocol::ToolOptionPickerAction;
use codex_app_server_protocol::ToolOptionPickerOption;
use codex_app_server_protocol::ToolOptionPickerParams;
use codex_app_server_protocol::ToolOptionPickerResponse;
use codex_app_server_protocol::ToolRequestUserInputOption;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_app_server_protocol::ToolRequestUserInputQuestion;
use codex_app_server_protocol::ToolRequestUserInputResponse;
use codex_app_server_protocol::ToolSetupCodexContextPickerAction;
use codex_app_server_protocol::ToolSetupCodexContextPickerParams;
use codex_app_server_protocol::ToolSetupCodexContextPickerResponse;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnDiffUpdatedNotification;
@@ -90,6 +97,8 @@ use codex_core::review_prompts;
use codex_protocol::ThreadId;
use codex_protocol::items::parse_hook_prompt_message;
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
use codex_protocol::option_picker::OptionPickerAction as CoreOptionPickerAction;
use codex_protocol::option_picker::OptionPickerResponse as CoreOptionPickerResponse;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_protocol::protocol::Event;
@@ -108,6 +117,8 @@ use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequest
use codex_protocol::request_permissions::RequestPermissionsResponse as CoreRequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse;
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerAction as CoreSetupCodexContextPickerAction;
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerResponse as CoreSetupCodexContextPickerResponse;
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_shell_command::parse_command::shlex_join;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -690,6 +701,67 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
});
}
EventMsg::OptionPicker(request) => {
let user_input_guard = thread_watch_manager
.note_user_input_requested(&conversation_id.to_string())
.await;
let options = request
.options
.into_iter()
.map(|option| ToolOptionPickerOption {
label: option.label,
description: option.description,
})
.collect();
let params = ToolOptionPickerParams {
thread_id: conversation_id.to_string(),
turn_id: request.turn_id,
item_id: request.call_id,
question: request.question,
options,
allow_multiple: request.allow_multiple,
submit_label: request.submit_label,
skip_label: request.skip_label,
};
let (pending_request_id, rx) = outgoing
.send_request(ServerRequestPayload::ToolOptionPicker(params))
.await;
tokio::spawn(async move {
on_option_picker_response(
event_turn_id,
pending_request_id,
rx,
conversation,
thread_state,
user_input_guard,
)
.await;
});
}
EventMsg::SetupCodexContextPicker(request) => {
let user_input_guard = thread_watch_manager
.note_user_input_requested(&conversation_id.to_string())
.await;
let params = ToolSetupCodexContextPickerParams {
thread_id: conversation_id.to_string(),
turn_id: request.turn_id,
item_id: request.call_id,
};
let (pending_request_id, rx) = outgoing
.send_request(ServerRequestPayload::ToolSetupCodexContextPicker(params))
.await;
tokio::spawn(async move {
on_setup_codex_context_picker_response(
event_turn_id,
pending_request_id,
rx,
conversation,
thread_state,
user_input_guard,
)
.await;
});
}
EventMsg::ElicitationRequest(request) => {
let permission_guard = thread_watch_manager
.note_permission_requested(&conversation_id.to_string())
@@ -1599,6 +1671,159 @@ async fn handle_token_count_event(
}
}
async fn on_option_picker_response(
event_turn_id: String,
pending_request_id: RequestId,
receiver: oneshot::Receiver<ClientRequestResult>,
conversation: Arc<CodexThread>,
thread_state: Arc<Mutex<ThreadState>>,
user_input_guard: ThreadWatchActiveGuard,
) {
let response = receiver.await;
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
drop(user_input_guard);
let value = match response {
Ok(Ok(value)) => value,
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
Ok(Err(err)) => {
error!("option picker request failed with client error: {err:?}");
submit_empty_option_picker_response(event_turn_id, conversation).await;
return;
}
Err(err) => {
error!("option picker request failed: {err:?}");
submit_empty_option_picker_response(event_turn_id, conversation).await;
return;
}
};
let response =
serde_json::from_value::<ToolOptionPickerResponse>(value).unwrap_or_else(|err| {
error!("failed to deserialize ToolOptionPickerResponse: {err}");
ToolOptionPickerResponse {
action: ToolOptionPickerAction::Dismiss,
selected_options: Vec::new(),
freeform_answer: None,
}
});
let response = CoreOptionPickerResponse {
action: match response.action {
ToolOptionPickerAction::Submit => CoreOptionPickerAction::Submit,
ToolOptionPickerAction::Skip => CoreOptionPickerAction::Skip,
ToolOptionPickerAction::Dismiss => CoreOptionPickerAction::Dismiss,
},
selected_options: response.selected_options,
freeform_answer: response.freeform_answer,
};
if let Err(err) = conversation
.submit(Op::OptionPickerResponse {
id: event_turn_id,
response,
})
.await
{
error!("failed to submit OptionPickerResponse: {err}");
}
}
async fn submit_empty_option_picker_response(
event_turn_id: String,
conversation: Arc<CodexThread>,
) {
let response = CoreOptionPickerResponse {
action: CoreOptionPickerAction::Dismiss,
selected_options: Vec::new(),
freeform_answer: None,
};
if let Err(err) = conversation
.submit(Op::OptionPickerResponse {
id: event_turn_id,
response,
})
.await
{
error!("failed to submit OptionPickerResponse: {err}");
}
}
async fn on_setup_codex_context_picker_response(
event_turn_id: String,
pending_request_id: RequestId,
receiver: oneshot::Receiver<ClientRequestResult>,
conversation: Arc<CodexThread>,
thread_state: Arc<Mutex<ThreadState>>,
user_input_guard: ThreadWatchActiveGuard,
) {
let response = receiver.await;
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
drop(user_input_guard);
let value = match response {
Ok(Ok(value)) => value,
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
Ok(Err(err)) => {
error!("setup codex context picker request failed with client error: {err:?}");
submit_empty_setup_codex_context_picker_response(event_turn_id, conversation).await;
return;
}
Err(err) => {
error!("setup codex context picker request failed: {err:?}");
submit_empty_setup_codex_context_picker_response(event_turn_id, conversation).await;
return;
}
};
let response = serde_json::from_value::<ToolSetupCodexContextPickerResponse>(value)
.unwrap_or_else(|err| {
error!("failed to deserialize ToolSetupCodexContextPickerResponse: {err}");
ToolSetupCodexContextPickerResponse {
action: ToolSetupCodexContextPickerAction::Dismiss,
selected_sources: Vec::new(),
}
});
let response = CoreSetupCodexContextPickerResponse {
action: match response.action {
ToolSetupCodexContextPickerAction::Continue => {
CoreSetupCodexContextPickerAction::Continue
}
ToolSetupCodexContextPickerAction::Skip => CoreSetupCodexContextPickerAction::Skip,
ToolSetupCodexContextPickerAction::Dismiss => {
CoreSetupCodexContextPickerAction::Dismiss
}
},
selected_source_ids: response.selected_sources,
};
if let Err(err) = conversation
.submit(Op::SetupCodexContextPickerResponse {
id: event_turn_id,
response,
})
.await
{
error!("failed to submit SetupCodexContextPickerResponse: {err}");
}
}
async fn submit_empty_setup_codex_context_picker_response(
event_turn_id: String,
conversation: Arc<CodexThread>,
) {
let response = CoreSetupCodexContextPickerResponse {
action: CoreSetupCodexContextPickerAction::Dismiss,
selected_source_ids: Vec::new(),
};
if let Err(err) = conversation
.submit(Op::SetupCodexContextPickerResponse {
id: event_turn_id,
response,
})
.await
{
error!("failed to submit SetupCodexContextPickerResponse: {err}");
}
}
async fn handle_error(
_conversation_id: ThreadId,
error: TurnError,

View File

@@ -500,6 +500,9 @@
"network_proxy": {
"$ref": "#/definitions/FeatureToml_for_NetworkProxyConfigToml"
},
"onboarding_interactive_tools": {
"type": "boolean"
},
"personality": {
"type": "boolean"
},
@@ -4431,6 +4434,9 @@
"network_proxy": {
"$ref": "#/definitions/FeatureToml_for_NetworkProxyConfigToml"
},
"onboarding_interactive_tools": {
"type": "boolean"
},
"personality": {
"type": "boolean"
},

View File

@@ -26,6 +26,7 @@ use crate::tasks::UserShellCommandTask;
use crate::tasks::execute_user_shell_command;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::option_picker::OptionPickerResponse;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::Event;
@@ -49,6 +50,7 @@ use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerResponse;
use crate::context_manager::is_user_turn_boundary;
use codex_protocol::dynamic_tools::DynamicToolResponse;
@@ -435,6 +437,23 @@ pub async fn request_user_input_response(
sess.notify_user_input_response(&id, response).await;
}
pub async fn option_picker_response(
sess: &Arc<Session>,
id: String,
response: OptionPickerResponse,
) {
sess.notify_option_picker_response(&id, response).await;
}
pub async fn setup_codex_context_picker_response(
sess: &Arc<Session>,
id: String,
response: SetupCodexContextPickerResponse,
) {
sess.notify_setup_codex_context_picker_response(&id, response)
.await;
}
pub async fn request_permissions_response(
sess: &Arc<Session>,
id: String,
@@ -792,6 +811,14 @@ pub(super) async fn submission_loop(
request_user_input_response(&sess, id, response).await;
false
}
Op::OptionPickerResponse { id, response } => {
option_picker_response(&sess, id, response).await;
false
}
Op::SetupCodexContextPickerResponse { id, response } => {
setup_codex_context_picker_response(&sess, id, response).await;
false
}
Op::RequestPermissionsResponse { id, response } => {
request_permissions_response(&sess, id, response).await;
false

View File

@@ -120,6 +120,7 @@ use codex_protocol::request_permissions::RequestPermissionsEvent;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerResponse;
use codex_rmcp_client::ElicitationResponse;
use codex_rollout::state_db;
use codex_rollout_trace::AgentResultTracePayload;
@@ -325,6 +326,8 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::option_picker::OptionPickerArgs;
use codex_protocol::option_picker::OptionPickerResponse;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
@@ -343,12 +346,14 @@ use codex_protocol::protocol::ModelVerificationEvent;
use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::OptionPickerEvent;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RequestUserInputEvent;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionNetworkProxyRuntime;
use codex_protocol::protocol::SetupCodexContextPickerEvent;
use codex_protocol::protocol::StreamErrorEvent;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::ThreadMemoryMode;
@@ -2319,6 +2324,144 @@ impl Session {
rx_response.await.ok()
}
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
pub async fn request_option_picker(
&self,
turn_context: &TurnContext,
call_id: String,
args: OptionPickerArgs,
) -> Option<OptionPickerResponse> {
let sub_id = turn_context.sub_id.clone();
let (tx_response, rx_response) = oneshot::channel();
let event_id = sub_id.clone();
let prev_entry = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.insert_pending_option_picker(sub_id, tx_response)
}
None => None,
}
};
if prev_entry.is_some() {
warn!("Overwriting existing pending option picker for sub_id: {event_id}");
}
let event = EventMsg::OptionPicker(OptionPickerEvent {
call_id,
turn_id: turn_context.sub_id.clone(),
question: args.question,
options: args.options,
allow_multiple: args.allow_multiple,
submit_label: args.submit_label,
skip_label: args.skip_label,
});
turn_context
.turn_metadata_state
.mark_user_input_requested_during_turn();
self.send_event(turn_context, event).await;
rx_response.await.ok()
}
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
pub async fn notify_option_picker_response(
&self,
sub_id: &str,
response: OptionPickerResponse,
) {
let entry = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.remove_pending_option_picker(sub_id)
}
None => None,
}
};
match entry {
Some(tx_response) => {
tx_response.send(response).ok();
}
None => {
warn!("No pending option picker found for sub_id: {sub_id}");
}
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
pub async fn request_setup_codex_context_picker(
&self,
turn_context: &TurnContext,
call_id: String,
) -> Option<SetupCodexContextPickerResponse> {
let sub_id = turn_context.sub_id.clone();
let (tx_response, rx_response) = oneshot::channel();
let event_id = sub_id.clone();
let prev_entry = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.insert_pending_setup_codex_context_picker(sub_id, tx_response)
}
None => None,
}
};
if prev_entry.is_some() {
warn!("Overwriting existing pending setup codex context picker for sub_id: {event_id}");
}
let event = EventMsg::SetupCodexContextPicker(SetupCodexContextPickerEvent {
call_id,
turn_id: turn_context.sub_id.clone(),
});
turn_context
.turn_metadata_state
.mark_user_input_requested_during_turn();
self.send_event(turn_context, event).await;
rx_response.await.ok()
}
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
pub async fn notify_setup_codex_context_picker_response(
&self,
sub_id: &str,
response: SetupCodexContextPickerResponse,
) {
let entry = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.remove_pending_setup_codex_context_picker(sub_id)
}
None => None,
}
};
match entry {
Some(tx_response) => {
tx_response.send(response).ok();
}
None => {
warn!("No pending setup codex context picker found for sub_id: {sub_id}");
}
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"

View File

@@ -1362,6 +1362,8 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
| EventMsg::ExecApprovalRequest(_)
| EventMsg::RequestPermissions(_)
| EventMsg::RequestUserInput(_)
| EventMsg::OptionPicker(_)
| EventMsg::SetupCodexContextPicker(_)
| EventMsg::DynamicToolCallRequest(_)
| EventMsg::DynamicToolCallResponse(_)
| EventMsg::GuardianAssessment(_)

View File

@@ -14,6 +14,7 @@ use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerResponse;
use codex_rmcp_client::ElicitationResponse;
use codex_utils_absolute_path::AbsolutePathBuf;
use rmcp::model::RequestId;
@@ -23,6 +24,7 @@ use crate::session::TurnInputQueue;
use crate::session::turn_context::TurnContext;
use crate::tasks::AnySessionTask;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::option_picker::OptionPickerResponse;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::TokenUsage;
@@ -113,6 +115,9 @@ pub(crate) struct TurnState {
pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
pending_request_permissions: HashMap<String, PendingRequestPermissions>,
pending_user_input: HashMap<String, oneshot::Sender<RequestUserInputResponse>>,
pending_option_picker: HashMap<String, oneshot::Sender<OptionPickerResponse>>,
pending_setup_codex_context_picker:
HashMap<String, oneshot::Sender<SetupCodexContextPickerResponse>>,
pending_elicitations: HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>,
pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
pub(crate) pending_input: TurnInputQueue,
@@ -150,6 +155,8 @@ impl TurnState {
self.pending_approvals.clear();
self.pending_request_permissions.clear();
self.pending_user_input.clear();
self.pending_option_picker.clear();
self.pending_setup_codex_context_picker.clear();
self.pending_elicitations.clear();
self.pending_dynamic_tools.clear();
}
@@ -185,6 +192,36 @@ impl TurnState {
self.pending_user_input.remove(key)
}
pub(crate) fn insert_pending_option_picker(
&mut self,
key: String,
tx: oneshot::Sender<OptionPickerResponse>,
) -> Option<oneshot::Sender<OptionPickerResponse>> {
self.pending_option_picker.insert(key, tx)
}
pub(crate) fn remove_pending_option_picker(
&mut self,
key: &str,
) -> Option<oneshot::Sender<OptionPickerResponse>> {
self.pending_option_picker.remove(key)
}
pub(crate) fn insert_pending_setup_codex_context_picker(
&mut self,
key: String,
tx: oneshot::Sender<SetupCodexContextPickerResponse>,
) -> Option<oneshot::Sender<SetupCodexContextPickerResponse>> {
self.pending_setup_codex_context_picker.insert(key, tx)
}
pub(crate) fn remove_pending_setup_codex_context_picker(
&mut self,
key: &str,
) -> Option<oneshot::Sender<SetupCodexContextPickerResponse>> {
self.pending_setup_codex_context_picker.remove(key)
}
pub(crate) fn insert_pending_elicitation(
&mut self,
server_name: String,

View File

@@ -15,6 +15,7 @@ pub(crate) mod multi_agents;
pub(crate) mod multi_agents_common;
pub(crate) mod multi_agents_spec;
pub(crate) mod multi_agents_v2;
mod option_picker;
mod plan;
pub(crate) mod plan_spec;
mod request_permissions;
@@ -22,6 +23,7 @@ mod request_plugin_install;
pub(crate) mod request_plugin_install_spec;
mod request_user_input;
pub(crate) mod request_user_input_spec;
mod setup_codex_context_picker;
mod shell;
pub(crate) mod shell_spec;
mod test_sync;
@@ -61,10 +63,12 @@ pub use mcp::McpHandler;
pub use mcp_resource::ListMcpResourceTemplatesHandler;
pub use mcp_resource::ListMcpResourcesHandler;
pub use mcp_resource::ReadMcpResourceHandler;
pub use option_picker::OptionPickerHandler;
pub use plan::PlanHandler;
pub use request_permissions::RequestPermissionsHandler;
pub use request_plugin_install::RequestPluginInstallHandler;
pub use request_user_input::RequestUserInputHandler;
pub use setup_codex_context_picker::SetupCodexContextPickerHandler;
pub use shell::ShellCommandHandler;
pub(crate) use shell::ShellCommandHandlerOptions;
pub use test_sync::TestSyncHandler;

View File

@@ -0,0 +1,77 @@
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::option_picker::OptionPickerArgs;
use codex_tools::REQUEST_OPTION_PICKER_TOOL_NAME;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::create_request_option_picker_tool;
pub struct OptionPickerHandler;
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for OptionPickerHandler {
fn tool_name(&self) -> ToolName {
ToolName::plain(REQUEST_OPTION_PICKER_TOOL_NAME)
}
fn spec(&self) -> ToolSpec {
create_request_option_picker_tool()
}
async fn handle(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
let ToolInvocation {
session,
turn,
call_id,
payload,
..
} = invocation;
let arguments = match payload {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(format!(
"{REQUEST_OPTION_PICKER_TOOL_NAME} handler received unsupported payload"
)));
}
};
if turn.session_source.is_non_root_agent() {
return Err(FunctionCallError::RespondToModel(
"request_option_picker can only be used by the root thread".to_string(),
));
}
let args: OptionPickerArgs = parse_arguments(&arguments)?;
let response = session
.request_option_picker(turn.as_ref(), call_id, args)
.await
.ok_or_else(|| {
FunctionCallError::RespondToModel(format!(
"{REQUEST_OPTION_PICKER_TOOL_NAME} was cancelled before receiving a response"
))
})?;
let content = serde_json::to_string(&response).map_err(|err| {
FunctionCallError::Fatal(format!(
"failed to serialize {REQUEST_OPTION_PICKER_TOOL_NAME} response: {err}"
))
})?;
Ok(boxed_tool_output(FunctionToolOutput::from_text(
content,
Some(true),
)))
}
}
impl CoreToolRuntime for OptionPickerHandler {}

View File

@@ -0,0 +1,77 @@
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::setup_codex_context_picker::SetupCodexContextPickerArgs;
use codex_tools::SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::create_setup_codex_context_picker_tool;
pub struct SetupCodexContextPickerHandler;
#[async_trait::async_trait]
impl ToolExecutor<ToolInvocation> for SetupCodexContextPickerHandler {
fn tool_name(&self) -> ToolName {
ToolName::plain(SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME)
}
fn spec(&self) -> ToolSpec {
create_setup_codex_context_picker_tool()
}
async fn handle(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
let ToolInvocation {
session,
turn,
call_id,
payload,
..
} = invocation;
let arguments = match payload {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(format!(
"{SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME} handler received unsupported payload"
)));
}
};
if turn.session_source.is_non_root_agent() {
return Err(FunctionCallError::RespondToModel(
"setup_codex_context_picker can only be used by the root thread".to_string(),
));
}
let _: SetupCodexContextPickerArgs = parse_arguments(&arguments)?;
let response = session
.request_setup_codex_context_picker(turn.as_ref(), call_id)
.await
.ok_or_else(|| {
FunctionCallError::RespondToModel(format!(
"{SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME} was cancelled before receiving a response"
))
})?;
let content = serde_json::to_string(&response).map_err(|err| {
FunctionCallError::Fatal(format!(
"failed to serialize {SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME} response: {err}"
))
})?;
Ok(boxed_tool_output(FunctionToolOutput::from_text(
content,
Some(true),
)))
}
}
impl CoreToolRuntime for SetupCodexContextPickerHandler {}

View File

@@ -13,11 +13,13 @@ use crate::tools::handlers::ListAvailablePluginsToInstallHandler;
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
use crate::tools::handlers::ListMcpResourcesHandler;
use crate::tools::handlers::McpHandler;
use crate::tools::handlers::OptionPickerHandler;
use crate::tools::handlers::PlanHandler;
use crate::tools::handlers::ReadMcpResourceHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::SetupCodexContextPickerHandler;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::TestSyncHandler;
@@ -571,6 +573,10 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
planned_tools.add(RequestUserInputHandler {
available_modes: request_user_input_available_modes(features),
});
if features.enabled(Feature::OnboardingInteractiveTools) {
planned_tools.add(OptionPickerHandler);
planned_tools.add(SetupCodexContextPickerHandler);
}
if features.enabled(Feature::RequestPermissionsTool) {
planned_tools.add(RequestPermissionsHandler);

View File

@@ -1596,6 +1596,30 @@ async fn handle_server_request(
)
.await
}
ServerRequest::ToolOptionPicker { request_id, params } => {
reject_server_request(
client,
request_id,
&method,
format!(
"request_option_picker is not supported in exec mode for thread `{}`",
params.thread_id
),
)
.await
}
ServerRequest::ToolSetupCodexContextPicker { request_id, params } => {
reject_server_request(
client,
request_id,
&method,
format!(
"setup_codex_context_picker is not supported in exec mode for thread `{}`",
params.thread_id
),
)
.await
}
ServerRequest::DynamicToolCall { request_id, params } => {
reject_server_request(
client,

View File

@@ -174,6 +174,8 @@ pub enum Feature {
MentionsV2,
/// Allow request_user_input in Default collaboration mode.
DefaultModeRequestUserInput,
/// Allow onboarding-only interactive tools.
OnboardingInteractiveTools,
/// Enable automatic review for approval prompts.
GuardianApproval,
/// Enable persisted thread goals and automatic goal continuation.
@@ -1068,6 +1070,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::OnboardingInteractiveTools,
key: "onboarding_interactive_tools",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::GuardianApproval,
key: "guardian_approval",

View File

@@ -369,6 +369,8 @@ async fn run_codex_tool_session_inner(
| EventMsg::ReasoningRawContentDelta(_)
| EventMsg::ExitedReviewMode(_)
| EventMsg::RequestUserInput(_)
| EventMsg::OptionPicker(_)
| EventMsg::SetupCodexContextPicker(_)
| EventMsg::RequestPermissions(_)
| EventMsg::DynamicToolCallRequest(_)
| EventMsg::DynamicToolCallResponse(_)

View File

@@ -21,11 +21,13 @@ pub mod models;
pub mod network_policy;
pub mod num_format;
pub mod openai_models;
pub mod option_picker;
pub mod parse_command;
pub mod permissions;
pub mod plan_tool;
pub mod protocol;
pub mod request_permissions;
pub mod request_user_input;
pub mod setup_codex_context_picker;
pub mod shell_environment;
pub mod user_input;

View File

@@ -0,0 +1,58 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct OptionPickerOption {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct OptionPickerArgs {
pub question: String,
pub options: Vec<OptionPickerOption>,
#[serde(default)]
pub allow_multiple: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submit_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skip_label: Option<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum OptionPickerAction {
Submit,
Skip,
Dismiss,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct OptionPickerResponse {
pub action: OptionPickerAction,
pub selected_options: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub freeform_answer: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct OptionPickerEvent {
/// Responses API call id for the associated tool call, if available.
pub call_id: String,
/// Turn ID that this request belongs to.
pub turn_id: String,
pub question: String,
pub options: Vec<OptionPickerOption>,
pub allow_multiple: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submit_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skip_label: Option<String>,
}

View File

@@ -43,11 +43,13 @@ use crate::models::SandboxEnforcement;
use crate::models::WebSearchAction;
use crate::num_format::format_with_separators;
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
use crate::option_picker::OptionPickerResponse;
use crate::parse_command::ParsedCommand;
use crate::plan_tool::UpdatePlanArgs;
use crate::request_permissions::RequestPermissionsEvent;
use crate::request_permissions::RequestPermissionsResponse;
use crate::request_user_input::RequestUserInputResponse;
use crate::setup_codex_context_picker::SetupCodexContextPickerResponse;
use crate::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
@@ -75,6 +77,7 @@ pub use crate::approvals::NetworkApprovalContext;
pub use crate::approvals::NetworkApprovalProtocol;
pub use crate::approvals::NetworkPolicyAmendment;
pub use crate::approvals::NetworkPolicyRuleAction;
pub use crate::option_picker::OptionPickerEvent;
pub use crate::permissions::FileSystemAccessMode;
pub use crate::permissions::FileSystemPath;
pub use crate::permissions::FileSystemSandboxEntry;
@@ -85,6 +88,7 @@ pub use crate::permissions::NetworkSandboxPolicy;
use crate::permissions::default_read_only_subpaths_for_writable_root;
pub use crate::request_permissions::RequestPermissionsArgs;
pub use crate::request_user_input::RequestUserInputEvent;
pub use crate::setup_codex_context_picker::SetupCodexContextPickerEvent;
/// Open/close tags for special user-input blocks. Used across crates to avoid
/// duplicated hardcoded strings.
@@ -579,6 +583,22 @@ pub enum Op {
response: RequestUserInputResponse,
},
/// Resolve a request_option_picker tool call.
OptionPickerResponse {
/// Turn id for the in-flight request.
id: String,
/// User-provided selection.
response: OptionPickerResponse,
},
/// Resolve a setup_codex_context_picker tool call.
SetupCodexContextPickerResponse {
/// Turn id for the in-flight request.
id: String,
/// User-selected onboarding context sources.
response: SetupCodexContextPickerResponse,
},
/// Resolve a request_permissions tool call.
RequestPermissionsResponse {
/// Call id for the in-flight request.
@@ -728,6 +748,8 @@ impl Op {
Self::PatchApproval { .. } => "patch_approval",
Self::ResolveElicitation { .. } => "resolve_elicitation",
Self::UserInputAnswer { .. } => "user_input_answer",
Self::OptionPickerResponse { .. } => "option_picker_response",
Self::SetupCodexContextPickerResponse { .. } => "setup_codex_context_picker_response",
Self::RequestPermissionsResponse { .. } => "request_permissions_response",
Self::DynamicToolResponse { .. } => "dynamic_tool_response",
Self::RefreshMcpServers { .. } => "refresh_mcp_servers",
@@ -1246,6 +1268,10 @@ pub enum EventMsg {
RequestUserInput(RequestUserInputEvent),
OptionPicker(OptionPickerEvent),
SetupCodexContextPicker(SetupCodexContextPickerEvent),
DynamicToolCallRequest(DynamicToolCallRequest),
DynamicToolCallResponse(DynamicToolCallResponseEvent),

View File

@@ -0,0 +1,32 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct SetupCodexContextPickerArgs {}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum SetupCodexContextPickerAction {
Continue,
Skip,
Dismiss,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct SetupCodexContextPickerResponse {
pub action: SetupCodexContextPickerAction,
pub selected_source_ids: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct SetupCodexContextPickerEvent {
/// Responses API call id for the associated tool call, if available.
pub call_id: String,
/// Turn ID that this request belongs to.
pub turn_id: String,
}

View File

@@ -251,6 +251,8 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTr
| EventMsg::ExecApprovalRequest(_)
| EventMsg::RequestPermissions(_)
| EventMsg::RequestUserInput(_)
| EventMsg::OptionPicker(_)
| EventMsg::SetupCodexContextPicker(_)
| EventMsg::DynamicToolCallRequest(_)
| EventMsg::DynamicToolCallResponse(_)
| EventMsg::ElicitationRequest(_)
@@ -322,6 +324,8 @@ pub(crate) fn wrapped_protocol_event_type(event: &EventMsg) -> Option<&'static s
| EventMsg::ExecApprovalRequest(_)
| EventMsg::RequestPermissions(_)
| EventMsg::RequestUserInput(_)
| EventMsg::OptionPicker(_)
| EventMsg::SetupCodexContextPicker(_)
| EventMsg::DynamicToolCallRequest(_)
| EventMsg::DynamicToolCallResponse(_)
| EventMsg::ElicitationRequest(_)

View File

@@ -191,6 +191,8 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
| EventMsg::ExecApprovalRequest(_)
| EventMsg::RequestPermissions(_)
| EventMsg::RequestUserInput(_)
| EventMsg::OptionPicker(_)
| EventMsg::SetupCodexContextPicker(_)
| EventMsg::ElicitationRequest(_)
| EventMsg::ApplyPatchApprovalRequest(_)
| EventMsg::StreamError(_)

View File

@@ -7,8 +7,10 @@ mod function_call_error;
mod image_detail;
mod json_schema;
mod mcp_tool;
mod option_picker_tool;
mod request_plugin_install;
mod responses_api;
mod setup_codex_context_picker_tool;
mod tool_call;
mod tool_config;
mod tool_definition;
@@ -36,6 +38,8 @@ pub use json_schema::JsonSchemaType;
pub use json_schema::parse_tool_input_schema;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool;
pub use option_picker_tool::REQUEST_OPTION_PICKER_TOOL_NAME;
pub use option_picker_tool::create_request_option_picker_tool;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_PERSIST_KEY;
@@ -57,6 +61,8 @@ pub use responses_api::dynamic_tool_to_responses_api_tool;
pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
pub use responses_api::mcp_tool_to_responses_api_tool;
pub use responses_api::tool_definition_to_responses_api_tool;
pub use setup_codex_context_picker_tool::SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME;
pub use setup_codex_context_picker_tool::create_setup_codex_context_picker_tool;
pub use tool_call::ToolCall;
pub use tool_config::ShellCommandBackendConfig;
pub use tool_config::ToolEnvironmentMode;

View File

@@ -0,0 +1,65 @@
use crate::JsonSchema;
use crate::ResponsesApiTool;
use crate::ToolSpec;
use std::collections::BTreeMap;
pub const REQUEST_OPTION_PICKER_TOOL_NAME: &str = "request_option_picker";
pub fn create_request_option_picker_tool() -> ToolSpec {
let option_props = BTreeMap::from([
(
"label".to_string(),
JsonSchema::string(Some("User-facing option label.".to_string())),
),
(
"description".to_string(),
JsonSchema::string(Some(
"Optional short description for the option.".to_string(),
)),
),
]);
let properties = BTreeMap::from([
(
"question".to_string(),
JsonSchema::string(Some("Question to show the user.".to_string())),
),
(
"options".to_string(),
JsonSchema::array(
JsonSchema::object(
option_props,
Some(vec!["label".to_string()]),
Some(false.into()),
),
Some("Selectable options to show in the picker.".to_string()),
),
),
(
"allowMultiple".to_string(),
JsonSchema::boolean(Some(
"Set true when the user may choose more than one option.".to_string(),
)),
),
(
"submitLabel".to_string(),
JsonSchema::string(Some("Optional label for the submit button.".to_string())),
),
(
"skipLabel".to_string(),
JsonSchema::string(Some("Optional label for the skip button.".to_string())),
),
]);
ToolSpec::Function(ResponsesApiTool {
name: REQUEST_OPTION_PICKER_TOOL_NAME.to_string(),
description: "Ask the user to choose from a compact list of onboarding options. Use only when setting up Codex from the onboarding tutorial.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(
properties,
Some(vec!["question".to_string(), "options".to_string()]),
Some(false.into()),
),
output_schema: None,
})
}

View File

@@ -0,0 +1,17 @@
use crate::JsonSchema;
use crate::ResponsesApiTool;
use crate::ToolSpec;
use std::collections::BTreeMap;
pub const SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME: &str = "setup_codex_context_picker";
pub fn create_setup_codex_context_picker_tool() -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME.to_string(),
description: "Open Codex's onboarding context source picker so the user can connect work apps or add a folder before the assistant continues setup.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(BTreeMap::new(), Some(Vec::new()), Some(false.into())),
output_schema: None,
})
}

View File

@@ -27,8 +27,9 @@ pub fn request_user_input_available_modes(features: &Features) -> Vec<ModeKind>
.into_iter()
.filter(|mode| {
mode.allows_request_user_input()
|| (features.enabled(Feature::DefaultModeRequestUserInput)
&& *mode == ModeKind::Default)
|| (*mode == ModeKind::Default
&& (features.enabled(Feature::DefaultModeRequestUserInput)
|| features.enabled(Feature::OnboardingInteractiveTools)))
})
.collect()
}

View File

@@ -15,6 +15,12 @@ pub(super) fn server_request_thread_id(request: &ServerRequest) -> Option<Thread
ServerRequest::ToolRequestUserInput { params, .. } => {
ThreadId::from_string(&params.thread_id).ok()
}
ServerRequest::ToolOptionPicker { params, .. } => {
ThreadId::from_string(&params.thread_id).ok()
}
ServerRequest::ToolSetupCodexContextPicker { params, .. } => {
ThreadId::from_string(&params.thread_id).ok()
}
ServerRequest::McpServerElicitationRequest { params, .. } => {
ThreadId::from_string(&params.thread_id).ok()
}

View File

@@ -127,6 +127,19 @@ impl PendingAppServerRequests {
);
None
}
ServerRequest::ToolOptionPicker { request_id, .. } => {
Some(UnsupportedAppServerRequest {
request_id: request_id.clone(),
message: "Option picker requests are not available in TUI yet.".to_string(),
})
}
ServerRequest::ToolSetupCodexContextPicker { request_id, .. } => {
Some(UnsupportedAppServerRequest {
request_id: request_id.clone(),
message: "Setup Codex context picker requests are not available in TUI yet."
.to_string(),
})
}
ServerRequest::DynamicToolCall { request_id, .. } => {
Some(UnsupportedAppServerRequest {
request_id: request_id.clone(),
@@ -336,7 +349,9 @@ impl PendingAppServerRequests {
.mcp_requests
.values()
.any(|pending_request_id| pending_request_id == request_id),
ServerRequest::DynamicToolCall { .. }
ServerRequest::ToolOptionPicker { .. }
| ServerRequest::ToolSetupCodexContextPicker { .. }
| ServerRequest::DynamicToolCall { .. }
| ServerRequest::ChatgptAuthTokensRefresh { .. }
| ServerRequest::AttestationGenerate { .. }
| ServerRequest::ApplyPatchApproval { .. }

View File

@@ -85,6 +85,8 @@ impl SideParentStatus {
pub(super) fn for_request(request: &ServerRequest) -> Option<Self> {
match request {
ServerRequest::ToolRequestUserInput { .. } => Some(SideParentStatus::NeedsInput),
ServerRequest::ToolOptionPicker { .. } => Some(SideParentStatus::NeedsInput),
ServerRequest::ToolSetupCodexContextPicker { .. } => Some(SideParentStatus::NeedsInput),
ServerRequest::CommandExecutionRequestApproval { .. }
| ServerRequest::FileChangeRequestApproval { .. }
| ServerRequest::McpServerElicitationRequest { .. }

View File

@@ -35,7 +35,9 @@ impl ChatWidget {
ServerRequest::ToolRequestUserInput { params, .. } => {
self.on_request_user_input(params);
}
ServerRequest::DynamicToolCall { .. }
ServerRequest::ToolOptionPicker { .. }
| ServerRequest::ToolSetupCodexContextPicker { .. }
| ServerRequest::DynamicToolCall { .. }
| ServerRequest::AttestationGenerate { .. }
| ServerRequest::ChatgptAuthTokensRefresh { .. }
| ServerRequest::ApplyPatchApproval { .. }