Preserve request_user_input option picker metadata

This commit is contained in:
rafaelj
2026-05-20 01:48:25 -04:00
parent 3d10ba9f36
commit cc6bed0884
21 changed files with 155 additions and 5 deletions
+4
View File
@@ -1736,6 +1736,8 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![ToolRequestUserInputQuestion {
id: "question-1".to_string(),
header: "Mode".to_string(),
@@ -1797,6 +1799,8 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![ToolRequestUserInputQuestion {
id: "question-1".to_string(),
header: "Mode".to_string(),
@@ -78,6 +78,7 @@ 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;
@@ -7556,6 +7557,10 @@ 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>,
}
@@ -840,6 +840,8 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: request.turn_id,
item_id: request.call_id,
input_type: request.input_type,
option_picker_allow_multiple: request.option_picker_allow_multiple,
questions,
};
let (pending_request_id, rx) = outgoing
@@ -10606,6 +10606,8 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![],
},
))
@@ -1139,6 +1139,8 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![],
},
))
@@ -1197,6 +1199,8 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![],
},
))
+2
View File
@@ -644,6 +644,8 @@ async fn handle_request_user_input(
}
let args = RequestUserInputArgs {
input_type: event.input_type,
option_picker_allow_multiple: event.option_picker_allow_multiple,
questions: event.questions,
};
let response_fut =
@@ -416,6 +416,8 @@ 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,
questions: vec![RequestUserInputQuestion {
id: format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
header: "Approve app tool call?".to_string(),
@@ -250,6 +250,8 @@ async fn should_install_mcp_dependencies(
]),
};
let args = RequestUserInputArgs {
input_type: None,
option_picker_allow_multiple: None,
questions: vec![question],
};
let sub_id = &turn_context.sub_id;
+2
View File
@@ -1051,6 +1051,8 @@ async fn maybe_request_mcp_tool_approval(
}
let args = RequestUserInputArgs {
input_type: None,
option_picker_allow_multiple: None,
questions: vec![question],
};
let response = sess
+8 -1
View File
@@ -2135,10 +2135,17 @@ impl Session {
warn!("Overwriting existing pending user input for sub_id: {event_id}");
}
let RequestUserInputArgs {
input_type,
option_picker_allow_multiple,
questions,
} = args;
let event = EventMsg::RequestUserInput(RequestUserInputEvent {
call_id,
turn_id: turn_context.sub_id.clone(),
questions: args.questions,
input_type,
option_picker_allow_multiple,
questions,
});
self.send_event(turn_context, event).await;
rx_response.await.ok()
+5 -1
View File
@@ -139,7 +139,11 @@ async fn request_skill_dependencies(
.request_user_input(
turn_context,
format!("skill-deps-{}", turn_context.sub_id),
RequestUserInputArgs { questions },
RequestUserInputArgs {
input_type: None,
option_picker_allow_multiple: None,
questions,
},
)
.await
.unwrap_or_else(|| RequestUserInputResponse {
@@ -5,6 +5,13 @@ use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum RequestUserInputType {
Structured,
OptionPicker,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputQuestionOption {
pub label: String,
@@ -30,6 +37,22 @@ 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"
)]
#[schemars(rename = "inputType")]
#[ts(rename = "inputType")]
pub input_type: Option<RequestUserInputType>,
#[serde(
rename = "optionPickerAllowMultiple",
default,
skip_serializing_if = "Option::is_none"
)]
#[schemars(rename = "optionPickerAllowMultiple")]
#[ts(rename = "optionPickerAllowMultiple")]
pub option_picker_allow_multiple: Option<bool>,
pub questions: Vec<RequestUserInputQuestion>,
}
@@ -51,5 +74,21 @@ 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"
)]
#[schemars(rename = "inputType")]
#[ts(rename = "inputType")]
pub input_type: Option<RequestUserInputType>,
#[serde(
rename = "optionPickerAllowMultiple",
default,
skip_serializing_if = "Option::is_none"
)]
#[schemars(rename = "optionPickerAllowMultiple")]
#[ts(rename = "optionPickerAllowMultiple")]
pub option_picker_allow_multiple: Option<bool>,
pub questions: Vec<RequestUserInputQuestion>,
}
+21 -1
View File
@@ -4,6 +4,7 @@ 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";
@@ -67,7 +68,26 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
Some("Questions to show the user. Prefer 1 and do not exceed 3".to_string()),
);
let properties = BTreeMap::from([("questions".to_string(), questions_schema)]);
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),
]);
ToolSpec::Function(ResponsesApiTool {
name: REQUEST_USER_INPUT_TOOL_NAME.to_string(),
@@ -13,7 +13,25 @@ fn request_user_input_tool_includes_questions_schema() {
description: "Ask the user to choose.".to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(BTreeMap::from([(
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(
JsonSchema::object(
@@ -83,7 +101,8 @@ fn request_user_input_tool_includes_questions_schema() {
"Questions to show the user. Prefer 1 and do not exceed 3".to_string(),
),
),
)]), Some(vec!["questions".to_string()]), Some(false.into())),
),
]), Some(vec!["questions".to_string()]), Some(false.into())),
output_schema: None,
})
);
@@ -524,6 +524,8 @@ 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,
questions: Vec::new(),
},
}),
@@ -817,6 +819,8 @@ 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,
questions: Vec::new(),
},
});
@@ -839,6 +843,8 @@ 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,
questions: Vec::new(),
},
});
@@ -609,6 +609,8 @@ 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,
questions: Vec::new(),
},
}
+2
View File
@@ -4078,6 +4078,8 @@ 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,
questions: Vec::new(),
},
}
@@ -1487,6 +1487,8 @@ mod tests {
RequestUserInputEvent {
call_id: "call-1".to_string(),
turn_id: turn_id.to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions,
}
}
@@ -1548,11 +1550,15 @@ 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,
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,
questions: vec![question_with_options("q3", "Third")],
});
@@ -1569,6 +1575,8 @@ mod tests {
RequestUserInputEvent {
call_id: "call-1".to_string(),
turn_id: "turn-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![question_with_options("q1", "First")],
},
tx,
@@ -1596,6 +1604,8 @@ mod tests {
RequestUserInputEvent {
call_id: "call-1".to_string(),
turn_id: "turn-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![question_with_options("q1", "First")],
},
tx,
@@ -1606,6 +1616,8 @@ 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,
questions: vec![question_with_options("q2", "Second")],
});
@@ -1632,6 +1644,8 @@ mod tests {
RequestUserInputEvent {
call_id: "call-1".to_string(),
turn_id: "turn-1".to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: vec![question_with_options("q1", "First")],
},
tx,
@@ -1642,11 +1656,15 @@ 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,
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,
questions: vec![question_with_options("q3", "Third")],
});
+2
View File
@@ -1908,6 +1908,8 @@ fn request_user_input_from_params(params: ToolRequestUserInputParams) -> Request
RequestUserInputEvent {
turn_id: params.turn_id,
call_id: params.item_id,
input_type: params.input_type,
option_picker_allow_multiple: params.option_picker_allow_multiple,
questions: params
.questions
.into_iter()
@@ -161,6 +161,8 @@ mod tests {
RequestUserInputEvent {
call_id: call_id.to_string(),
turn_id: turn_id.to_string(),
input_type: None,
option_picker_allow_multiple: None,
questions: Vec::new(),
}
}
@@ -585,6 +585,8 @@ 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,
questions: vec![RequestUserInputQuestion {
id: "reasoning_scope".to_string(),
header: "Reasoning scope".to_string(),
@@ -613,6 +615,8 @@ 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,
questions: vec![RequestUserInputQuestion {
id: "reasoning_scope".to_string(),
header: "Reasoning scope".to_string(),