mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Add onboarding interactive tools
This commit is contained in:
@@ -1742,6 +1742,10 @@ mod tests {
|
||||
id: "question-1".to_string(),
|
||||
header: "Mode".to_string(),
|
||||
question: "Pick one".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![]),
|
||||
@@ -1805,6 +1809,10 @@ mod tests {
|
||||
id: "question-1".to_string(),
|
||||
header: "Mode".to_string(),
|
||||
question: "Pick one".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![]),
|
||||
|
||||
@@ -1100,6 +1100,18 @@ server_request_definitions! {
|
||||
response: v2::ToolRequestUserInputResponse,
|
||||
},
|
||||
|
||||
/// EXPERIMENTAL - Request an onboarding option picker.
|
||||
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,
|
||||
|
||||
@@ -49,6 +49,7 @@ use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
use codex_protocol::option_picker::OptionPickerAction as CoreOptionPickerAction;
|
||||
use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
|
||||
use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath;
|
||||
@@ -78,7 +79,6 @@ use codex_protocol::protocol::HookSource as CoreHookSource;
|
||||
use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
|
||||
use codex_protocol::protocol::ModelVerification as CoreModelVerification;
|
||||
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||
use codex_protocol::request_user_input::RequestUserInputType;
|
||||
use codex_protocol::protocol::NonSteerableTurnKind as CoreNonSteerableTurnKind;
|
||||
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
|
||||
use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType;
|
||||
@@ -102,6 +102,8 @@ use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
|
||||
use codex_protocol::request_user_input::RequestUserInputType;
|
||||
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerAction as CoreSetupCodexContextPickerAction;
|
||||
use codex_protocol::user_input::ByteRange as CoreByteRange;
|
||||
use codex_protocol::user_input::TextElement as CoreTextElement;
|
||||
use codex_protocol::user_input::UserInput as CoreUserInput;
|
||||
@@ -7542,6 +7544,10 @@ pub struct ToolRequestUserInputQuestion {
|
||||
pub id: String,
|
||||
pub header: String,
|
||||
pub question: String,
|
||||
pub input_type: Option<RequestUserInputType>,
|
||||
pub allow_multiple: Option<bool>,
|
||||
pub optional: Option<bool>,
|
||||
pub placeholder: Option<String>,
|
||||
#[serde(default)]
|
||||
pub is_other: bool,
|
||||
#[serde(default)]
|
||||
@@ -7557,9 +7563,7 @@ pub struct ToolRequestUserInputParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub input_type: Option<RequestUserInputType>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub option_picker_allow_multiple: Option<bool>,
|
||||
pub questions: Vec<ToolRequestUserInputQuestion>,
|
||||
}
|
||||
@@ -7580,6 +7584,97 @@ 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 = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ToolOptionPickerAction {
|
||||
Submit,
|
||||
Skip,
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
impl From<ToolOptionPickerAction> for CoreOptionPickerAction {
|
||||
fn from(value: ToolOptionPickerAction) -> Self {
|
||||
match value {
|
||||
ToolOptionPickerAction::Submit => Self::Submit,
|
||||
ToolOptionPickerAction::Skip => Self::Skip,
|
||||
ToolOptionPickerAction::Dismiss => Self::Dismiss,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL. Response payload from 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 = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ToolSetupCodexContextPickerAction {
|
||||
Continue,
|
||||
Skip,
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
impl From<ToolSetupCodexContextPickerAction> for CoreSetupCodexContextPickerAction {
|
||||
fn from(value: ToolSetupCodexContextPickerAction) -> Self {
|
||||
match value {
|
||||
ToolSetupCodexContextPickerAction::Continue => Self::Continue,
|
||||
ToolSetupCodexContextPickerAction::Skip => Self::Skip,
|
||||
ToolSetupCodexContextPickerAction::Dismiss => Self::Dismiss,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL. Response payload from the setup Codex context picker.
|
||||
pub struct ToolSetupCodexContextPickerResponse {
|
||||
pub action: ToolSetupCodexContextPickerAction,
|
||||
pub selected_sources: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -91,10 +91,15 @@ use codex_app_server_protocol::ThreadRealtimeTranscriptDoneNotification;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification;
|
||||
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::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;
|
||||
@@ -123,6 +128,7 @@ use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynam
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
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;
|
||||
@@ -143,6 +149,7 @@ 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::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;
|
||||
@@ -823,6 +830,10 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
id: question.id,
|
||||
header: question.header,
|
||||
question: question.question,
|
||||
input_type: question.input_type,
|
||||
allow_multiple: question.allow_multiple,
|
||||
optional: question.optional,
|
||||
placeholder: question.placeholder,
|
||||
is_other: question.is_other,
|
||||
is_secret: question.is_secret,
|
||||
options: question.options.map(|options| {
|
||||
@@ -877,6 +888,99 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::OptionPicker(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let params = ToolOptionPickerParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: request.turn_id,
|
||||
item_id: request.call_id,
|
||||
question: request.question,
|
||||
options: request
|
||||
.options
|
||||
.into_iter()
|
||||
.map(|option| ToolOptionPickerOption {
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
})
|
||||
.collect(),
|
||||
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,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
} else {
|
||||
error!(
|
||||
"request_option_picker is only supported on api v2 (call_id: {})",
|
||||
request.call_id
|
||||
);
|
||||
let empty = CoreOptionPickerResponse {
|
||||
action: codex_protocol::option_picker::OptionPickerAction::Dismiss,
|
||||
selected_options: Vec::new(),
|
||||
freeform_answer: None,
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::OptionPickerResponse {
|
||||
id: event_turn_id,
|
||||
response: empty,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit OptionPickerResponse: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::SetupCodexContextPicker(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
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,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
} else {
|
||||
error!(
|
||||
"setup_codex_context_picker is only supported on api v2 (call_id: {})",
|
||||
request.call_id
|
||||
);
|
||||
let empty = CoreSetupCodexContextPickerResponse {
|
||||
action: codex_protocol::setup_codex_context_picker::SetupCodexContextPickerAction::Dismiss,
|
||||
selected_source_ids: Vec::new(),
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::SetupCodexContextPickerResponse {
|
||||
id: event_turn_id,
|
||||
response: empty,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit SetupCodexContextPickerResponse: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::ElicitationRequest(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let permission_guard = thread_watch_manager
|
||||
@@ -2597,6 +2701,145 @@ async fn on_request_user_input_response(
|
||||
}
|
||||
}
|
||||
|
||||
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>>,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||
let empty = || CoreOptionPickerResponse {
|
||||
action: codex_protocol::option_picker::OptionPickerAction::Dismiss,
|
||||
selected_options: Vec::new(),
|
||||
freeform_answer: None,
|
||||
};
|
||||
let value = match response {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::OptionPickerResponse {
|
||||
id: event_turn_id,
|
||||
response: empty(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit OptionPickerResponse: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("request failed: {err:?}");
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::OptionPickerResponse {
|
||||
id: event_turn_id,
|
||||
response: empty(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit OptionPickerResponse: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response =
|
||||
serde_json::from_value::<ToolOptionPickerResponse>(value).unwrap_or_else(|err| {
|
||||
error!("failed to deserialize ToolOptionPickerResponse: {err}");
|
||||
ToolOptionPickerResponse {
|
||||
action: codex_app_server_protocol::ToolOptionPickerAction::Dismiss,
|
||||
selected_options: Vec::new(),
|
||||
freeform_answer: None,
|
||||
}
|
||||
});
|
||||
let response = CoreOptionPickerResponse {
|
||||
action: response.action.into(),
|
||||
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 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>>,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||
let empty = || CoreSetupCodexContextPickerResponse {
|
||||
action: codex_protocol::setup_codex_context_picker::SetupCodexContextPickerAction::Dismiss,
|
||||
selected_source_ids: Vec::new(),
|
||||
};
|
||||
let value = match response {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::SetupCodexContextPickerResponse {
|
||||
id: event_turn_id,
|
||||
response: empty(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit SetupCodexContextPickerResponse: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("request failed: {err:?}");
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::SetupCodexContextPickerResponse {
|
||||
id: event_turn_id,
|
||||
response: empty(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit SetupCodexContextPickerResponse: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response = serde_json::from_value::<ToolSetupCodexContextPickerResponse>(value)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("failed to deserialize ToolSetupCodexContextPickerResponse: {err}");
|
||||
ToolSetupCodexContextPickerResponse {
|
||||
action: codex_app_server_protocol::ToolSetupCodexContextPickerAction::Dismiss,
|
||||
selected_sources: Vec::new(),
|
||||
}
|
||||
});
|
||||
let response = CoreSetupCodexContextPickerResponse {
|
||||
action: response.action.into(),
|
||||
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 on_mcp_server_elicitation_response(
|
||||
server_name: String,
|
||||
request_id: codex_protocol::mcp::RequestId,
|
||||
|
||||
@@ -416,12 +416,18 @@ async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
|
||||
&RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question: "Allow this app tool?".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: None,
|
||||
|
||||
@@ -233,6 +233,10 @@ async fn should_install_mcp_dependencies(
|
||||
question: format!(
|
||||
"The following MCP servers are required by the selected skills but are not installed yet: {server_list}. Install them now?"
|
||||
),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
|
||||
@@ -1355,6 +1355,10 @@ fn build_mcp_tool_approval_question(
|
||||
id: question_id,
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question,
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(options),
|
||||
|
||||
@@ -36,6 +36,7 @@ use codex_mcp::collect_mcp_snapshot_from_manager;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
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;
|
||||
@@ -457,6 +458,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: codex_protocol::setup_codex_context_picker::SetupCodexContextPickerResponse,
|
||||
) {
|
||||
sess.notify_setup_codex_context_picker_response(&id, response)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn request_permissions_response(
|
||||
sess: &Arc<Session>,
|
||||
id: String,
|
||||
@@ -1086,6 +1104,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
|
||||
|
||||
@@ -97,6 +97,9 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::models::format_allow_prefixes;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::option_picker::OptionPickerArgs;
|
||||
use codex_protocol::option_picker::OptionPickerEvent;
|
||||
use codex_protocol::option_picker::OptionPickerResponse;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
@@ -121,6 +124,8 @@ 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::SetupCodexContextPickerEvent;
|
||||
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerResponse;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rollout::state_db;
|
||||
use codex_rollout_trace::AgentResultTracePayload;
|
||||
@@ -2180,6 +2185,138 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
});
|
||||
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(),
|
||||
});
|
||||
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"
|
||||
|
||||
@@ -1470,6 +1470,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(_)
|
||||
|
||||
@@ -125,6 +125,10 @@ async fn request_skill_dependencies(
|
||||
question: format!(
|
||||
"{requirement} This is an experimental internal feature. The value is stored in memory for this session only."
|
||||
),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: true,
|
||||
options: None,
|
||||
|
||||
@@ -11,9 +11,11 @@ use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::option_picker::OptionPickerResponse;
|
||||
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;
|
||||
@@ -111,6 +113,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>>,
|
||||
pending_input: Vec<ResponseInputItem>,
|
||||
@@ -148,6 +153,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();
|
||||
self.pending_input.clear();
|
||||
@@ -184,6 +191,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,
|
||||
|
||||
@@ -8,9 +8,11 @@ mod mcp_resource;
|
||||
pub(crate) mod multi_agents;
|
||||
pub(crate) mod multi_agents_common;
|
||||
pub(crate) mod multi_agents_v2;
|
||||
mod option_picker;
|
||||
mod plan;
|
||||
mod request_permissions;
|
||||
mod request_user_input;
|
||||
mod setup_codex_context_picker;
|
||||
mod shell;
|
||||
mod test_sync;
|
||||
mod tool_search;
|
||||
@@ -41,9 +43,11 @@ pub use goal::GoalHandler;
|
||||
pub use list_dir::ListDirHandler;
|
||||
pub use mcp::McpHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
pub use option_picker::OptionPickerHandler;
|
||||
pub use plan::PlanHandler;
|
||||
pub use request_permissions::RequestPermissionsHandler;
|
||||
pub use request_user_input::RequestUserInputHandler;
|
||||
pub use setup_codex_context_picker::SetupCodexContextPickerHandler;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub use shell::ShellHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::option_picker::OptionPickerArgs;
|
||||
use codex_tools::REQUEST_OPTION_PICKER_TOOL_NAME;
|
||||
|
||||
pub struct OptionPickerHandler;
|
||||
|
||||
impl ToolHandler for OptionPickerHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, 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(FunctionToolOutput::from_text(content, Some(true)))
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ async fn multi_agent_v2_request_user_input_rejects_subagent_threads() {
|
||||
|
||||
let result = RequestUserInputHandler {
|
||||
default_mode_request_user_input: true,
|
||||
option_picker_enabled: false,
|
||||
}
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::new(session),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::setup_codex_context_picker::SetupCodexContextPickerArgs;
|
||||
use codex_tools::SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME;
|
||||
|
||||
pub struct SetupCodexContextPickerHandler;
|
||||
|
||||
impl ToolHandler for SetupCodexContextPickerHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, 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"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
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(FunctionToolOutput::from_text(content, Some(true)))
|
||||
}
|
||||
}
|
||||
@@ -84,9 +84,11 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
use crate::tools::handlers::ListDirHandler;
|
||||
use crate::tools::handlers::McpHandler;
|
||||
use crate::tools::handlers::McpResourceHandler;
|
||||
use crate::tools::handlers::OptionPickerHandler;
|
||||
use crate::tools::handlers::PlanHandler;
|
||||
use crate::tools::handlers::RequestPermissionsHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
use crate::tools::handlers::SetupCodexContextPickerHandler;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::handlers::TestSyncHandler;
|
||||
@@ -168,6 +170,8 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
let request_user_input_handler = Arc::new(RequestUserInputHandler {
|
||||
default_mode_request_user_input: config.default_mode_request_user_input,
|
||||
});
|
||||
let option_picker_handler = Arc::new(OptionPickerHandler);
|
||||
let setup_codex_context_picker_handler = Arc::new(SetupCodexContextPickerHandler);
|
||||
let deferred_dynamic_tools = dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
|
||||
@@ -241,6 +245,12 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
ToolHandlerKind::RequestPermissions => {
|
||||
builder.register_handler(handler.name, request_permissions_handler.clone());
|
||||
}
|
||||
ToolHandlerKind::OptionPicker => {
|
||||
builder.register_handler(handler.name, option_picker_handler.clone());
|
||||
}
|
||||
ToolHandlerKind::SetupCodexContextPicker => {
|
||||
builder.register_handler(handler.name, setup_codex_context_picker_handler.clone());
|
||||
}
|
||||
ToolHandlerKind::RequestUserInput => {
|
||||
builder.register_handler(handler.name, request_user_input_handler.clone());
|
||||
}
|
||||
|
||||
@@ -1486,6 +1486,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,
|
||||
|
||||
@@ -188,6 +188,8 @@ pub enum Feature {
|
||||
Steer,
|
||||
/// 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.
|
||||
@@ -948,6 +950,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",
|
||||
|
||||
@@ -381,6 +381,8 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::UndoCompleted(_)
|
||||
| EventMsg::ExitedReviewMode(_)
|
||||
| EventMsg::RequestUserInput(_)
|
||||
| EventMsg::OptionPicker(_)
|
||||
| EventMsg::SetupCodexContextPicker(_)
|
||||
| EventMsg::RequestPermissions(_)
|
||||
| EventMsg::DynamicToolCallRequest(_)
|
||||
| EventMsg::DynamicToolCallResponse(_)
|
||||
|
||||
@@ -19,11 +19,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;
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -43,6 +43,7 @@ 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;
|
||||
@@ -75,6 +76,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 +87,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.
|
||||
@@ -698,6 +701,22 @@ pub enum Op {
|
||||
response: RequestUserInputResponse,
|
||||
},
|
||||
|
||||
/// Resolve a request_option_picker tool call.
|
||||
OptionPickerResponse {
|
||||
/// Turn id for the in-flight request.
|
||||
id: String,
|
||||
/// User-selected option picker response.
|
||||
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: crate::setup_codex_context_picker::SetupCodexContextPickerResponse,
|
||||
},
|
||||
|
||||
/// Resolve a request_permissions tool call.
|
||||
RequestPermissionsResponse {
|
||||
/// Call id for the in-flight request.
|
||||
@@ -891,6 +910,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::AddToHistory { .. } => "add_to_history",
|
||||
@@ -1422,6 +1443,8 @@ pub enum EventMsg {
|
||||
RequestPermissions(RequestPermissionsEvent),
|
||||
|
||||
RequestUserInput(RequestUserInputEvent),
|
||||
OptionPicker(OptionPickerEvent),
|
||||
SetupCodexContextPicker(SetupCodexContextPickerEvent),
|
||||
|
||||
DynamicToolCallRequest(DynamicToolCallRequest),
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ pub enum RequestUserInputType {
|
||||
OptionPicker,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum RequestUserInputPlacement {
|
||||
Composer,
|
||||
Inline,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct RequestUserInputQuestionOption {
|
||||
pub label: String,
|
||||
@@ -23,6 +30,22 @@ pub struct RequestUserInputQuestion {
|
||||
pub id: String,
|
||||
pub header: String,
|
||||
pub question: String,
|
||||
#[serde(rename = "inputType", default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "inputType")]
|
||||
#[ts(rename = "inputType")]
|
||||
pub input_type: Option<RequestUserInputType>,
|
||||
#[serde(
|
||||
rename = "allowMultiple",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[schemars(rename = "allowMultiple")]
|
||||
#[ts(rename = "allowMultiple")]
|
||||
pub allow_multiple: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub optional: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub placeholder: Option<String>,
|
||||
#[serde(rename = "isOther", default)]
|
||||
#[schemars(rename = "isOther")]
|
||||
#[ts(rename = "isOther")]
|
||||
@@ -37,11 +60,7 @@ pub struct RequestUserInputQuestion {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct RequestUserInputArgs {
|
||||
#[serde(
|
||||
rename = "inputType",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[serde(rename = "inputType", default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "inputType")]
|
||||
#[ts(rename = "inputType")]
|
||||
pub input_type: Option<RequestUserInputType>,
|
||||
@@ -74,11 +93,7 @@ pub struct RequestUserInputEvent {
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
#[serde(
|
||||
rename = "inputType",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[serde(rename = "inputType", default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "inputType")]
|
||||
#[ts(rename = "inputType")]
|
||||
pub input_type: Option<RequestUserInputType>,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
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)]
|
||||
pub struct SetupCodexContextPickerResponse {
|
||||
pub action: SetupCodexContextPickerAction,
|
||||
pub selected_source_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
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,
|
||||
}
|
||||
@@ -254,6 +254,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(_)
|
||||
@@ -335,6 +337,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(_)
|
||||
|
||||
@@ -152,6 +152,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::BackgroundEvent(_)
|
||||
|
||||
@@ -12,9 +12,11 @@ mod json_schema;
|
||||
mod local_tool;
|
||||
mod mcp_resource_tool;
|
||||
mod mcp_tool;
|
||||
mod option_picker_tool;
|
||||
mod plan_tool;
|
||||
mod request_user_input_tool;
|
||||
mod responses_api;
|
||||
mod setup_codex_context_picker_tool;
|
||||
mod tool_config;
|
||||
mod tool_definition;
|
||||
mod tool_discovery;
|
||||
@@ -79,6 +81,8 @@ pub use mcp_resource_tool::create_list_mcp_resources_tool;
|
||||
pub use mcp_resource_tool::create_read_mcp_resource_tool;
|
||||
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 plan_tool::create_update_plan_tool;
|
||||
pub use request_user_input_tool::REQUEST_USER_INPUT_TOOL_NAME;
|
||||
pub use request_user_input_tool::create_request_user_input_tool;
|
||||
@@ -98,6 +102,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_config::ShellCommandBackendConfig;
|
||||
pub use tool_config::ToolUserShellType;
|
||||
pub use tool_config::ToolsConfig;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,6 @@ use crate::ToolSpec;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub const REQUEST_USER_INPUT_TOOL_NAME: &str = "request_user_input";
|
||||
@@ -53,7 +52,6 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
|
||||
),
|
||||
("options".to_string(), options_schema),
|
||||
]);
|
||||
|
||||
let questions_schema = JsonSchema::array(
|
||||
JsonSchema::object(
|
||||
question_props,
|
||||
@@ -61,33 +59,13 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
|
||||
"id".to_string(),
|
||||
"header".to_string(),
|
||||
"question".to_string(),
|
||||
"options".to_string(),
|
||||
]),
|
||||
Some(false.into()),
|
||||
),
|
||||
Some("Questions to show the user. Prefer 1 and do not exceed 3".to_string()),
|
||||
);
|
||||
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"inputType".to_string(),
|
||||
JsonSchema::string_enum(
|
||||
vec![json!("structured"), json!("option_picker")],
|
||||
Some(
|
||||
"Optional UI presentation hint. Use `option_picker` when the prompt should render as a selectable option picker; otherwise omit this or use `structured`."
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"optionPickerAllowMultiple".to_string(),
|
||||
JsonSchema::boolean(Some(
|
||||
"Optional. For `inputType: option_picker`, set true when multiple choices may be selected."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
("questions".to_string(), questions_schema),
|
||||
]);
|
||||
let properties = BTreeMap::from([("questions".to_string(), questions_schema)]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: REQUEST_USER_INPUT_TOOL_NAME.to_string(),
|
||||
@@ -120,14 +98,6 @@ pub fn request_user_input_unavailable_message(
|
||||
pub fn normalize_request_user_input_args(
|
||||
mut args: RequestUserInputArgs,
|
||||
) -> Result<RequestUserInputArgs, String> {
|
||||
let missing_options = args
|
||||
.questions
|
||||
.iter()
|
||||
.any(|question| question.options.as_ref().is_none_or(Vec::is_empty));
|
||||
if missing_options {
|
||||
return Err("request_user_input requires non-empty options for every question".to_string());
|
||||
}
|
||||
|
||||
for question in &mut args.questions {
|
||||
question.is_other = true;
|
||||
}
|
||||
|
||||
@@ -14,23 +14,6 @@ fn request_user_input_tool_includes_questions_schema() {
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(BTreeMap::from([
|
||||
(
|
||||
"inputType".to_string(),
|
||||
JsonSchema::string_enum(
|
||||
vec![json!("structured"), json!("option_picker")],
|
||||
Some(
|
||||
"Optional UI presentation hint. Use `option_picker` when the prompt should render as a selectable option picker; otherwise omit this or use `structured`."
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"optionPickerAllowMultiple".to_string(),
|
||||
JsonSchema::boolean(Some(
|
||||
"Optional. For `inputType: option_picker`, set true when multiple choices may be selected."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"questions".to_string(),
|
||||
JsonSchema::array(
|
||||
@@ -93,7 +76,6 @@ fn request_user_input_tool_includes_questions_schema() {
|
||||
"id".to_string(),
|
||||
"header".to_string(),
|
||||
"question".to_string(),
|
||||
"options".to_string(),
|
||||
]),
|
||||
Some(false.into()),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -110,6 +110,7 @@ pub struct ToolsConfig {
|
||||
pub max_concurrent_threads_per_session: Option<usize>,
|
||||
pub wait_agent_min_timeout_ms: Option<i64>,
|
||||
pub default_mode_request_user_input: bool,
|
||||
pub onboarding_interactive_tools_enabled: bool,
|
||||
pub experimental_supported_tools: Vec<String>,
|
||||
pub agent_jobs_tools: bool,
|
||||
pub agent_jobs_worker_tools: bool,
|
||||
@@ -148,6 +149,8 @@ impl ToolsConfig {
|
||||
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
|
||||
let include_default_mode_request_user_input =
|
||||
features.enabled(Feature::DefaultModeRequestUserInput);
|
||||
let include_onboarding_interactive_tools =
|
||||
features.enabled(Feature::OnboardingInteractiveTools);
|
||||
let include_search_tool =
|
||||
model_info.supports_search_tool && features.enabled(Feature::ToolSearch);
|
||||
let include_tool_suggest = features.enabled(Feature::ToolSuggest)
|
||||
@@ -231,6 +234,7 @@ impl ToolsConfig {
|
||||
max_concurrent_threads_per_session: None,
|
||||
wait_agent_min_timeout_ms: None,
|
||||
default_mode_request_user_input: include_default_mode_request_user_input,
|
||||
onboarding_interactive_tools_enabled: include_onboarding_interactive_tools,
|
||||
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
||||
agent_jobs_tools: include_agent_jobs,
|
||||
agent_jobs_worker_tools,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::CommandToolOptions;
|
||||
use crate::REQUEST_OPTION_PICKER_TOOL_NAME;
|
||||
use crate::REQUEST_USER_INPUT_TOOL_NAME;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME;
|
||||
use crate::ShellToolOptions;
|
||||
use crate::SpawnAgentToolOptions;
|
||||
use crate::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
@@ -38,11 +40,13 @@ use crate::create_list_mcp_resources_tool;
|
||||
use crate::create_local_shell_tool;
|
||||
use crate::create_read_mcp_resource_tool;
|
||||
use crate::create_report_agent_job_result_tool;
|
||||
use crate::create_request_option_picker_tool;
|
||||
use crate::create_request_permissions_tool;
|
||||
use crate::create_request_user_input_tool;
|
||||
use crate::create_resume_agent_tool;
|
||||
use crate::create_send_input_tool_v1;
|
||||
use crate::create_send_message_tool;
|
||||
use crate::create_setup_codex_context_picker_tool;
|
||||
use crate::create_shell_command_tool;
|
||||
use crate::create_shell_tool;
|
||||
use crate::create_spawn_agent_tool_v1;
|
||||
@@ -251,6 +255,28 @@ pub fn build_tool_registry_plan(
|
||||
ToolHandlerKind::RequestUserInput,
|
||||
);
|
||||
|
||||
if config.onboarding_interactive_tools_enabled {
|
||||
plan.push_spec(
|
||||
create_request_option_picker_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler(
|
||||
REQUEST_OPTION_PICKER_TOOL_NAME,
|
||||
ToolHandlerKind::OptionPicker,
|
||||
);
|
||||
|
||||
plan.push_spec(
|
||||
create_setup_codex_context_picker_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler(
|
||||
SETUP_CODEX_CONTEXT_PICKER_TOOL_NAME,
|
||||
ToolHandlerKind::SetupCodexContextPicker,
|
||||
);
|
||||
}
|
||||
|
||||
if config.request_permissions_tool_enabled {
|
||||
plan.push_spec(
|
||||
create_request_permissions_tool(request_permissions_tool_description()),
|
||||
|
||||
@@ -88,7 +88,10 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
}),
|
||||
create_write_stdin_tool(),
|
||||
create_update_plan_tool(),
|
||||
request_user_input_tool_spec(/*default_mode_request_user_input*/ false),
|
||||
request_user_input_tool_spec(
|
||||
/*default_mode_request_user_input*/ false,
|
||||
/*onboarding_interactive_tools_enabled*/ false,
|
||||
),
|
||||
create_apply_patch_freeform_tool(),
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
@@ -586,7 +589,10 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
|
||||
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
||||
assert_eq!(
|
||||
request_user_input_tool.spec,
|
||||
request_user_input_tool_spec(/*default_mode_request_user_input*/ false)
|
||||
request_user_input_tool_spec(
|
||||
/*default_mode_request_user_input*/ false,
|
||||
/*onboarding_interactive_tools_enabled*/ false,
|
||||
)
|
||||
);
|
||||
|
||||
features.enable(Feature::DefaultModeRequestUserInput);
|
||||
@@ -609,7 +615,43 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
|
||||
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
||||
assert_eq!(
|
||||
request_user_input_tool.spec,
|
||||
request_user_input_tool_spec(/*default_mode_request_user_input*/ true)
|
||||
request_user_input_tool_spec(
|
||||
/*default_mode_request_user_input*/ true,
|
||||
/*onboarding_interactive_tools_enabled*/ false,
|
||||
)
|
||||
);
|
||||
|
||||
features.enable(Feature::OnboardingInteractiveTools);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
permission_profile: &PermissionProfile::Disabled,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
|
||||
assert_eq!(
|
||||
request_user_input_tool.spec,
|
||||
request_user_input_tool_spec(
|
||||
/*default_mode_request_user_input*/ true,
|
||||
/*onboarding_interactive_tools_enabled*/ true,
|
||||
)
|
||||
);
|
||||
assert_contains_tool_names(
|
||||
&tools,
|
||||
&[
|
||||
REQUEST_OPTION_PICKER_TOOL_NAME,
|
||||
REQUEST_LAUNCH_BRIEF_QUESTIONNAIRE_TOOL_NAME,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2343,7 +2385,10 @@ fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
|
||||
);
|
||||
}
|
||||
|
||||
fn request_user_input_tool_spec(default_mode_request_user_input: bool) -> ToolSpec {
|
||||
fn request_user_input_tool_spec(
|
||||
default_mode_request_user_input: bool,
|
||||
_onboarding_interactive_tools_enabled: bool,
|
||||
) -> ToolSpec {
|
||||
create_request_user_input_tool(request_user_input_tool_description(
|
||||
default_mode_request_user_input,
|
||||
))
|
||||
|
||||
@@ -24,7 +24,9 @@ pub enum ToolHandlerKind {
|
||||
Mcp,
|
||||
McpResource,
|
||||
Plan,
|
||||
OptionPicker,
|
||||
RequestPermissions,
|
||||
SetupCodexContextPicker,
|
||||
RequestUserInput,
|
||||
ResumeAgentV1,
|
||||
SendInputV1,
|
||||
|
||||
@@ -310,6 +310,12 @@ fn server_request_thread_id(request: &ServerRequest) -> Option<ThreadId> {
|
||||
ServerRequest::ToolRequestUserInput { params, .. } => {
|
||||
ThreadId::from_string(¶ms.thread_id).ok()
|
||||
}
|
||||
ServerRequest::ToolOptionPicker { params, .. } => {
|
||||
ThreadId::from_string(¶ms.thread_id).ok()
|
||||
}
|
||||
ServerRequest::ToolSetupCodexContextPicker { params, .. } => {
|
||||
ThreadId::from_string(¶ms.thread_id).ok()
|
||||
}
|
||||
ServerRequest::McpServerElicitationRequest { params, .. } => {
|
||||
ThreadId::from_string(¶ms.thread_id).ok()
|
||||
}
|
||||
|
||||
@@ -99,6 +99,18 @@ impl PendingAppServerRequests {
|
||||
});
|
||||
None
|
||||
}
|
||||
ServerRequest::ToolOptionPicker { request_id, .. } => {
|
||||
Some(UnsupportedAppServerRequest {
|
||||
request_id: request_id.clone(),
|
||||
message: "Option picker is not available in TUI yet.".to_string(),
|
||||
})
|
||||
}
|
||||
ServerRequest::ToolSetupCodexContextPicker { request_id, .. } => {
|
||||
Some(UnsupportedAppServerRequest {
|
||||
request_id: request_id.clone(),
|
||||
message: "Setup Codex context picker is not available in TUI yet.".to_string(),
|
||||
})
|
||||
}
|
||||
ServerRequest::McpServerElicitationRequest { request_id, params } => {
|
||||
self.mcp_requests.insert(
|
||||
McpLegacyRequestKey {
|
||||
@@ -333,6 +345,8 @@ impl PendingAppServerRequests {
|
||||
.values()
|
||||
.any(|pending_request_id| pending_request_id == request_id),
|
||||
ServerRequest::DynamicToolCall { .. }
|
||||
| ServerRequest::ToolOptionPicker { .. }
|
||||
| ServerRequest::ToolSetupCodexContextPicker { .. }
|
||||
| ServerRequest::ChatgptAuthTokensRefresh { .. }
|
||||
| ServerRequest::ApplyPatchApproval { .. }
|
||||
| ServerRequest::ExecCommandApproval { .. } => true,
|
||||
@@ -524,8 +538,10 @@ mod tests {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
item_id: "tool-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: Vec::new(),
|
||||
},
|
||||
}),
|
||||
@@ -819,8 +835,10 @@ mod tests {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "tool-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: Vec::new(),
|
||||
},
|
||||
});
|
||||
@@ -843,8 +861,10 @@ mod tests {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: item_id.to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: Vec::new(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -609,8 +609,10 @@ mod tests {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
item_id: call_id.to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: Vec::new(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(super) fn snapshot_has_pending_interactive_request(snapshot: &ThreadEventSna
|
||||
| ServerRequest::FileChangeRequestApproval { .. }
|
||||
| ServerRequest::McpServerElicitationRequest { .. }
|
||||
| ServerRequest::PermissionsRequestApproval { .. }
|
||||
| ServerRequest::ToolOptionPicker { .. }
|
||||
| ServerRequest::ToolRequestUserInput { .. }
|
||||
)
|
||||
)
|
||||
|
||||
@@ -92,6 +92,8 @@ impl SideParentStatus {
|
||||
| ServerRequest::ApplyPatchApproval { .. }
|
||||
| ServerRequest::ExecCommandApproval { .. } => Some(SideParentStatus::NeedsApproval),
|
||||
ServerRequest::DynamicToolCall { .. }
|
||||
| ServerRequest::ToolOptionPicker { .. }
|
||||
| ServerRequest::ToolSetupCodexContextPicker { .. }
|
||||
| ServerRequest::ChatgptAuthTokensRefresh { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4078,8 +4078,10 @@ fn request_user_input_request(thread_id: ThreadId, turn_id: &str, item_id: &str)
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
item_id: item_id.to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: Vec::new(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1345,6 +1345,10 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
header: header.to_string(),
|
||||
question: "Choose an option.".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
@@ -1369,6 +1373,10 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
header: header.to_string(),
|
||||
question: "Choose an option.".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: true,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
@@ -1393,6 +1401,10 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
header: header.to_string(),
|
||||
question: "Choose the next step for this task.".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
@@ -1423,6 +1435,10 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
header: header.to_string(),
|
||||
question: "Choose one option.".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
@@ -1445,6 +1461,10 @@ mod tests {
|
||||
question:
|
||||
"Choose one option; each hint is intentionally very long to test wrapped scrolling."
|
||||
.to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
@@ -1474,6 +1494,10 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
header: header.to_string(),
|
||||
question: "Share details.".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: None,
|
||||
@@ -1487,8 +1511,10 @@ mod tests {
|
||||
RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions,
|
||||
}
|
||||
}
|
||||
@@ -1550,15 +1576,19 @@ mod tests {
|
||||
overlay.try_consume_user_input_request(RequestUserInputEvent {
|
||||
call_id: "call-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q2", "Second")],
|
||||
});
|
||||
overlay.try_consume_user_input_request(RequestUserInputEvent {
|
||||
call_id: "call-3".to_string(),
|
||||
turn_id: "turn-3".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q3", "Third")],
|
||||
});
|
||||
|
||||
@@ -1575,8 +1605,10 @@ mod tests {
|
||||
RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q1", "First")],
|
||||
},
|
||||
tx,
|
||||
@@ -1604,8 +1636,10 @@ mod tests {
|
||||
RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q1", "First")],
|
||||
},
|
||||
tx,
|
||||
@@ -1616,8 +1650,10 @@ mod tests {
|
||||
overlay.try_consume_user_input_request(RequestUserInputEvent {
|
||||
call_id: "call-2".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q2", "Second")],
|
||||
});
|
||||
|
||||
@@ -1644,8 +1680,10 @@ mod tests {
|
||||
RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q1", "First")],
|
||||
},
|
||||
tx,
|
||||
@@ -1656,15 +1694,19 @@ mod tests {
|
||||
overlay.try_consume_user_input_request(RequestUserInputEvent {
|
||||
call_id: "call-2".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q2", "Second")],
|
||||
});
|
||||
overlay.try_consume_user_input_request(RequestUserInputEvent {
|
||||
call_id: "call-3".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![question_with_options("q3", "Third")],
|
||||
});
|
||||
|
||||
@@ -2873,6 +2915,10 @@ mod tests {
|
||||
id: "q1".to_string(),
|
||||
header: "Next Step".to_string(),
|
||||
question: "What would you like to do next?".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
@@ -2925,6 +2971,10 @@ mod tests {
|
||||
id: "q1".to_string(),
|
||||
header: "Next Step".to_string(),
|
||||
question: "What would you like to do next?".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
|
||||
@@ -1918,6 +1918,10 @@ fn request_user_input_from_params(params: ToolRequestUserInputParams) -> Request
|
||||
id: question.id,
|
||||
header: question.header,
|
||||
question: question.question,
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: question.is_other,
|
||||
is_secret: question.is_secret,
|
||||
options: question.options.map(|options| {
|
||||
@@ -6990,6 +6994,8 @@ impl ChatWidget {
|
||||
self.on_request_user_input(request_user_input_from_params(params));
|
||||
}
|
||||
ServerRequest::DynamicToolCall { .. }
|
||||
| ServerRequest::ToolOptionPicker { .. }
|
||||
| ServerRequest::ToolSetupCodexContextPicker { .. }
|
||||
| ServerRequest::ChatgptAuthTokensRefresh { .. }
|
||||
| ServerRequest::ApplyPatchApproval { .. }
|
||||
| ServerRequest::ExecCommandApproval { .. } => {
|
||||
@@ -7771,6 +7777,7 @@ impl ChatWidget {
|
||||
EventMsg::RequestUserInput(ev) => {
|
||||
self.on_request_user_input(ev);
|
||||
}
|
||||
EventMsg::OptionPicker(_) => {}
|
||||
EventMsg::RequestPermissions(ev) => {
|
||||
self.on_request_permissions(ev);
|
||||
}
|
||||
|
||||
@@ -161,8 +161,10 @@ mod tests {
|
||||
RequestUserInputEvent {
|
||||
call_id: call_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,12 +585,18 @@ async fn request_user_input_notification_overrides_pending_agent_turn_complete_n
|
||||
chat.handle_request_user_input_now(RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: "reasoning_scope".to_string(),
|
||||
header: "Reasoning scope".to_string(),
|
||||
question: "Which reasoning scope should I use?".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![RequestUserInputQuestionOption {
|
||||
@@ -615,12 +621,18 @@ async fn handle_request_user_input_sets_pending_notification() {
|
||||
chat.handle_request_user_input_now(RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
input_type: None,
|
||||
option_picker_allow_multiple: None,
|
||||
title: None,
|
||||
submit_label: None,
|
||||
skip_label: None,
|
||||
placement: None,
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: "reasoning_scope".to_string(),
|
||||
header: "Reasoning scope".to_string(),
|
||||
question: "Which reasoning scope should I use?".to_string(),
|
||||
input_type: None,
|
||||
allow_multiple: None,
|
||||
optional: None,
|
||||
placeholder: None,
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![RequestUserInputQuestionOption {
|
||||
|
||||
Reference in New Issue
Block a user