Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2593,7 +2593,7 @@ pub struct ToolRequestUserInputOption {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Represents one request_user_input question and its optional options.
/// EXPERIMENTAL. Represents one request_user_input question and its required options.
pub struct ToolRequestUserInputQuestion {
pub id: String,
pub header: String,
Expand Down
1 change: 0 additions & 1 deletion codex-rs/app-server/tests/common/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ pub fn create_request_user_input_sse_response(call_id: &str) -> anyhow::Result<S
"id": "confirm_path",
"header": "Confirm",
"question": "Proceed with the plan?",
"isOther": false,
"options": [{
"label": "Yes (Recommended)",
"description": "Continue the current plan."
Expand Down
14 changes: 13 additions & 1 deletion codex-rs/core/src/tools/handlers/request_user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,19 @@ impl ToolHandler for RequestUserInputHandler {
)));
}

let args: RequestUserInputArgs = parse_arguments(&arguments)?;
let mut args: RequestUserInputArgs = parse_arguments(&arguments)?;
let missing_options = args
.questions
.iter()
.any(|question| question.options.as_ref().is_none_or(Vec::is_empty));
if missing_options {
return Err(FunctionCallError::RespondToModel(
"request_user_input requires non-empty options for every question".to_string(),
));
}
for question in &mut args.questions {
question.is_other = true;
}
Comment on lines +62 to +64
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We hardcode the isOther property to true passing it to downstream consumer. isOther itself is removed from the tool spec.

let response = session
.request_user_input(turn.as_ref(), call_id, args)
.await
Expand Down
13 changes: 2 additions & 11 deletions codex-rs/core/src/tools/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ fn create_request_user_input_tool() -> ToolSpec {

let options_schema = JsonSchema::Array {
description: Some(
"Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; use isOther on the question to request a free form choice. If the question is free form in nature, please do not have any option."
"Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; the client will add a free-form \"Other\" option automatically."
.to_string(),
),
items: Box::new(JsonSchema::Object {
Expand Down Expand Up @@ -602,15 +602,6 @@ fn create_request_user_input_tool() -> ToolSpec {
description: Some("Single-sentence prompt shown to the user.".to_string()),
},
);
question_props.insert(
"isOther".to_string(),
JsonSchema::Boolean {
description: Some(
"True when this question should include a free-form \"Other\" option. Otherwise false."
.to_string(),
),
},
);
question_props.insert("options".to_string(), options_schema);

let questions_schema = JsonSchema::Array {
Expand All @@ -621,7 +612,7 @@ fn create_request_user_input_tool() -> ToolSpec {
"id".to_string(),
"header".to_string(),
"question".to_string(),
"isOther".to_string(),
"options".to_string(),
]),
additional_properties: Some(false.into()),
}),
Expand Down
3 changes: 1 addition & 2 deletions codex-rs/core/tests/suite/request_user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
"id": "confirm_path",
"header": "Confirm",
"question": "Proceed with the plan?",
"isOther": false,
"options": [{
"label": "Yes (Recommended)",
"description": "Continue the current plan."
Expand Down Expand Up @@ -153,6 +152,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
.await;
assert_eq!(request.call_id, call_id);
assert_eq!(request.questions.len(), 1);
assert_eq!(request.questions[0].is_other, true);

let mut answers = HashMap::new();
answers.insert(
Expand Down Expand Up @@ -214,7 +214,6 @@ where
"id": "confirm_path",
"header": "Confirm",
"question": "Proceed with the plan?",
"isOther": false,
"options": [{
"label": "Yes (Recommended)",
"description": "Continue the current plan."
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/docs/protocol_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
- `EventMsg`
- `EventMsg::AgentMessage` – Messages from the `Model`
- `EventMsg::ExecApprovalRequest` – Request approval from user to execute a command
- `EventMsg::RequestUserInput` – Request user input for a tool call (questions can include options plus `isOther` to add a free-form choice)
- `EventMsg::RequestUserInput` – Request user input for a tool call (questions must include options; the client always adds a free-form choice)
- `EventMsg::TurnComplete` – A turn completed successfully
- `EventMsg::Error` – A turn stopped with an error
- `EventMsg::Warning` – A non-fatal warning that the client should surface to the user
Expand Down
Loading