diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index b7a958eff1..294bfc50c0 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -1909,7 +1909,7 @@ mod tests { is_secret: false, options: Some(vec![]), }], - auto_resolution_ms: None, + is_blocking: true, }) .expect("params should serialize"), ), @@ -1971,7 +1971,7 @@ mod tests { is_secret: false, options: Some(vec![]), }], - auto_resolution_ms: None, + is_blocking: true, }) .expect("params should serialize"), ), diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index c6ac80dee5..396be65af9 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -1722,14 +1722,8 @@ "ToolRequestUserInputParams": { "description": "EXPERIMENTAL. Params sent with a request_user_input event.", "properties": { - "autoResolutionMs": { - "default": null, - "format": "uint64", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] + "isBlocking": { + "type": "boolean" }, "itemId": { "type": "string" @@ -1748,6 +1742,7 @@ } }, "required": [ + "isBlocking", "itemId", "questions", "threadId", diff --git a/codex-rs/app-server-protocol/schema/json/ToolRequestUserInputParams.json b/codex-rs/app-server-protocol/schema/json/ToolRequestUserInputParams.json index 947371fd10..ef44327cba 100644 --- a/codex-rs/app-server-protocol/schema/json/ToolRequestUserInputParams.json +++ b/codex-rs/app-server-protocol/schema/json/ToolRequestUserInputParams.json @@ -57,14 +57,8 @@ }, "description": "EXPERIMENTAL. Params sent with a request_user_input event.", "properties": { - "autoResolutionMs": { - "default": null, - "format": "uint64", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] + "isBlocking": { + "type": "boolean" }, "itemId": { "type": "string" @@ -83,6 +77,7 @@ } }, "required": [ + "isBlocking", "itemId", "questions", "threadId", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 56ea39276a..8359809623 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -5769,14 +5769,8 @@ "$schema": "http://json-schema.org/draft-07/schema#", "description": "EXPERIMENTAL. Params sent with a request_user_input event.", "properties": { - "autoResolutionMs": { - "default": null, - "format": "uint64", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] + "isBlocking": { + "type": "boolean" }, "itemId": { "type": "string" @@ -5795,6 +5789,7 @@ } }, "required": [ + "isBlocking", "itemId", "questions", "threadId", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ToolRequestUserInputParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ToolRequestUserInputParams.ts index 73ff8b6786..7cabae16f7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ToolRequestUserInputParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ToolRequestUserInputParams.ts @@ -6,4 +6,4 @@ import type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestio /** * EXPERIMENTAL. Params sent with a request_user_input event. */ -export type ToolRequestUserInputParams = { threadId: string, turnId: string, itemId: string, questions: Array, autoResolutionMs: number | null, }; +export type ToolRequestUserInputParams = { threadId: string, turnId: string, itemId: string, questions: Array, isBlocking: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index e2a0d45129..2ea0aedbd2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -1502,9 +1502,7 @@ pub struct ToolRequestUserInputParams { pub turn_id: String, pub item_id: String, pub questions: Vec, - #[serde(default)] - #[ts(type = "number | null")] - pub auto_resolution_ms: Option, + pub is_blocking: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a4b488acdf..92e377b439 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1471,6 +1471,8 @@ UI guidance for IDEs: surface an approval dialog as soon as the request arrives. ### request_user_input +`item/tool/requestUserInput` includes `isBlocking`, which is `true` when the agent requires an explicit user answer before continuing and `false` when the client may eventually resolve the prompt without user input. + When the client responds to `item/tool/requestUserInput`, the server emits `serverRequest/resolved` with `{ threadId, requestId }`. If the pending request is cleared by turn start, turn completion, or turn interruption before the client answers, the server emits the same notification for that cleanup. ### Attestation generation diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index bcba603bcd..d3e2275935 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -710,7 +710,7 @@ pub(crate) async fn apply_bespoke_event_handling( turn_id: request.turn_id, item_id: request.call_id, questions, - auto_resolution_ms: request.auto_resolution_ms, + is_blocking: request.is_blocking, }; let (pending_request_id, rx) = outgoing .send_request(ServerRequestPayload::ToolRequestUserInput(params)) diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 9ce815bafe..0eaaebe1d5 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -1260,7 +1260,7 @@ mod tests { turn_id: "turn-1".to_string(), item_id: "call-1".to_string(), questions: vec![], - auto_resolution_ms: None, + is_blocking: true, }, )) .await; @@ -1323,7 +1323,7 @@ mod tests { turn_id: "turn-1".to_string(), item_id: "call-1".to_string(), questions: vec![], - auto_resolution_ms: None, + is_blocking: true, }, )) .await; diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 0d447011b8..ec47ed254a 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -1159,7 +1159,7 @@ mod thread_processor_behavior_tests { turn_id: "turn-1".to_string(), item_id: "call-1".to_string(), questions: vec![], - auto_resolution_ms: None, + is_blocking: true, }, )) .await; diff --git a/codex-rs/app-server/tests/common/responses.rs b/codex-rs/app-server/tests/common/responses.rs index 586d1446c0..779a016359 100644 --- a/codex-rs/app-server/tests/common/responses.rs +++ b/codex-rs/app-server/tests/common/responses.rs @@ -74,7 +74,8 @@ pub fn create_request_user_input_sse_response(call_id: &str) -> anyhow::Result anyhow::Result { +fn create_non_blocking_request_user_input_sse_response(call_id: &str) -> anyhow::Result { let tool_call_arguments = serde_json::to_string(&json!({ "questions": [{ "id": "confirm_path", @@ -40,7 +37,7 @@ fn create_request_user_input_sse_response_with_auto_resolution( "description": "Stop and revisit the approach." }] }], - "autoResolutionMs": auto_resolution_ms + "isBlocking": false }))?; Ok(responses::sse(vec![ @@ -54,9 +51,7 @@ fn create_request_user_input_sse_response_with_auto_resolution( async fn request_user_input_round_trip() -> Result<()> { let codex_home = tempfile::TempDir::new()?; let responses = vec![ - create_request_user_input_sse_response_with_auto_resolution( - "call1", /*auto_resolution_ms*/ 60_000, - )?, + create_non_blocking_request_user_input_sse_response("call1")?, create_final_assistant_message_sse_response("done")?, ]; let server = create_mock_responses_server_sequence(responses).await; @@ -119,7 +114,7 @@ async fn request_user_input_round_trip() -> Result<()> { assert_eq!(params.turn_id, turn.id); assert_eq!(params.item_id, "call1"); assert_eq!(params.questions.len(), 1); - assert_eq!(params.auto_resolution_ms, Some(60_000)); + assert!(!params.is_blocking); let resolved_request_id = request_id.clone(); mcp.send_response( diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index a528298580..a36f2955e7 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1671,6 +1671,8 @@ async fn turn_start_uses_thread_feature_overrides_for_request_user_input_tool_de let request = response_mock.single_request(); let payload_text = request.body_json().to_string(); + assert!(payload_text.contains("In non-Plan modes, always set isBlocking to false.")); + assert!(!payload_text.contains("In Plan mode, set isBlocking to false")); assert!(payload_text.contains("This tool is only available in Default or Plan mode.")); Ok(()) diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 21e8de83ca..353c24bff2 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -667,7 +667,7 @@ async fn handle_request_user_input( let args = RequestUserInputArgs { questions: event.questions, - auto_resolution_ms: event.auto_resolution_ms, + is_blocking: event.is_blocking, }; let response_fut = parent_session.request_user_input(parent_ctx, parent_ctx.sub_id.clone(), args); diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index 6d51a0032b..8316ffa8ce 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -435,7 +435,7 @@ async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() { is_secret: false, options: None, }], - auto_resolution_ms: None, + is_blocking: true, }, &cancel_token, ) @@ -479,7 +479,7 @@ async fn delegated_mcp_user_reviewer_returns_none_without_metadata() { is_secret: false, options: None, }], - auto_resolution_ms: None, + is_blocking: true, }; let response = maybe_auto_review_mcp_request_user_input( &parent_session, diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index 9b2b85aa2d..1e0ec9cda2 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -249,7 +249,7 @@ async fn should_install_mcp_dependencies( }; let args = RequestUserInputArgs { questions: vec![question], - auto_resolution_ms: None, + is_blocking: true, }; let sub_id = &turn_context.sub_id; let call_id = format!("mcp-deps-{sub_id}"); diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index a270af7851..427cd49ddf 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -1340,7 +1340,7 @@ async fn maybe_request_mcp_tool_approval( let args = RequestUserInputArgs { questions: vec![question], - auto_resolution_ms: None, + is_blocking: true, }; let response = sess .request_user_input(turn_context.as_ref(), call_id.to_string(), args) diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 6fe61222cf..115b6d8c99 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2463,7 +2463,7 @@ impl Session { call_id, turn_id: turn_context.sub_id.clone(), questions: args.questions, - auto_resolution_ms: args.auto_resolution_ms, + is_blocking: args.is_blocking, }); turn_context .turn_metadata_state diff --git a/codex-rs/core/src/tools/handlers/request_user_input_spec.rs b/codex-rs/core/src/tools/handlers/request_user_input_spec.rs index 0eba5eefd6..c5afd75873 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input_spec.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input_spec.rs @@ -6,8 +6,6 @@ use codex_tools::ToolSpec; use std::collections::BTreeMap; pub const REQUEST_USER_INPUT_TOOL_NAME: &str = "request_user_input"; -pub const MIN_AUTO_RESOLUTION_MS: u64 = 60_000; -pub const MAX_AUTO_RESOLUTION_MS: u64 = 240_000; pub fn create_request_user_input_tool(description: String) -> ToolSpec { let option_props = BTreeMap::from([ @@ -68,13 +66,13 @@ 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 auto_resolution_ms_schema = JsonSchema::number(Some(format!( - "Optional auto-resolution window in milliseconds, from {MIN_AUTO_RESOLUTION_MS} to {MAX_AUTO_RESOLUTION_MS}. Include this only when the question is useful but non-blocking and continuing with best judgment is acceptable if the user does not answer; omit it when explicit user input is required before continuing. Use {MIN_AUTO_RESOLUTION_MS} for lightly helpful context and up to {MAX_AUTO_RESOLUTION_MS} when the answer would materially unblock better work." - ))); + let is_blocking_schema = JsonSchema::boolean(Some( + "Whether the request requires explicit user input before continuing.".to_string(), + )); let properties = BTreeMap::from([ ("questions".to_string(), questions_schema), - ("autoResolutionMs".to_string(), auto_resolution_ms_schema), + ("isBlocking".to_string(), is_blocking_schema), ]); ToolSpec::Function(ResponsesApiTool { @@ -84,7 +82,7 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec { defer_loading: None, parameters: JsonSchema::object( properties, - Some(vec!["questions".to_string()]), + Some(vec!["questions".to_string(), "isBlocking".to_string()]), Some(false.into()), ), output_schema: None, @@ -120,26 +118,20 @@ pub fn normalize_request_user_input_args( question.is_other = true; } - if let Some(auto_resolution_ms) = args.auto_resolution_ms { - let clamped_auto_resolution_ms = - auto_resolution_ms.clamp(MIN_AUTO_RESOLUTION_MS, MAX_AUTO_RESOLUTION_MS); - if clamped_auto_resolution_ms != auto_resolution_ms { - tracing::warn!( - auto_resolution_ms, - clamped_auto_resolution_ms, - "clamped request_user_input autoResolutionMs to supported range" - ); - args.auto_resolution_ms = Some(clamped_auto_resolution_ms); - } - } - Ok(args) } pub fn request_user_input_tool_description(available_modes: &[ModeKind]) -> String { let allowed_modes = format_allowed_modes(available_modes); + let blocking_policy = if available_modes == [ModeKind::Plan] { + "Set isBlocking to true when explicit user input is required before continuing." + } else if available_modes.contains(&ModeKind::Plan) { + "Set isBlocking to true only in Plan mode when explicit user input is required before continuing. In non-Plan modes, always set isBlocking to false." + } else { + "Set isBlocking to false." + }; format!( - "Request user input for one to three short questions and wait for the response. Set autoResolutionMs, from {MIN_AUTO_RESOLUTION_MS} to {MAX_AUTO_RESOLUTION_MS} milliseconds, only when the question is useful but non-blocking and continuing with best judgment is acceptable if the user does not answer; omit it when explicit user input is required. This tool is only available in {allowed_modes}." + "Request user input for one to three short questions and wait for the response. {blocking_policy} This tool is only available in {allowed_modes}." ) } diff --git a/codex-rs/core/src/tools/handlers/request_user_input_spec_tests.rs b/codex-rs/core/src/tools/handlers/request_user_input_spec_tests.rs index 2b532cd97a..2559309035 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input_spec_tests.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input_spec_tests.rs @@ -30,9 +30,9 @@ fn request_user_input_tool_includes_questions_schema() { defer_loading: None, parameters: JsonSchema::object(BTreeMap::from([ ( - "autoResolutionMs".to_string(), - JsonSchema::number(Some( - "Optional auto-resolution window in milliseconds, from 60000 to 240000. Include this only when the question is useful but non-blocking and continuing with best judgment is acceptable if the user does not answer; omit it when explicit user input is required before continuing. Use 60000 for lightly helpful context and up to 240000 when the answer would materially unblock better work." + "isBlocking".to_string(), + JsonSchema::boolean(Some( + "Whether the request requires explicit user input before continuing." .to_string(), )), ), @@ -107,14 +107,14 @@ fn request_user_input_tool_includes_questions_schema() { ), ), ), - ]), Some(vec!["questions".to_string()]), Some(false.into())), + ]), Some(vec!["questions".to_string(), "isBlocking".to_string()]), Some(false.into())), output_schema: None, }) ); } #[test] -fn normalize_request_user_input_args_clamps_out_of_range_auto_resolution_ms() { +fn normalize_request_user_input_args_sets_other_and_preserves_is_blocking() { let args = RequestUserInputArgs { questions: vec![RequestUserInputQuestion { id: "confirm".to_string(), @@ -127,7 +127,7 @@ fn normalize_request_user_input_args_clamps_out_of_range_auto_resolution_ms() { description: "Continue.".to_string(), }]), }], - auto_resolution_ms: Some(MIN_AUTO_RESOLUTION_MS - 1), + is_blocking: false, }; assert_eq!( @@ -137,63 +137,42 @@ fn normalize_request_user_input_args_clamps_out_of_range_auto_resolution_ms() { is_other: true, ..args.questions[0].clone() }], - auto_resolution_ms: Some(MIN_AUTO_RESOLUTION_MS), - }) - ); - assert_eq!( - normalize_request_user_input_args(RequestUserInputArgs { - auto_resolution_ms: Some(MAX_AUTO_RESOLUTION_MS + 1), - ..args.clone() - }), - Ok(RequestUserInputArgs { - questions: vec![RequestUserInputQuestion { - is_other: true, - ..args.questions[0].clone() - }], - auto_resolution_ms: Some(MAX_AUTO_RESOLUTION_MS), + is_blocking: false, }) ); } #[test] -fn normalize_request_user_input_args_accepts_auto_resolution_boundaries() { - let args = RequestUserInputArgs { - questions: vec![RequestUserInputQuestion { - id: "confirm".to_string(), - header: "Confirm".to_string(), - question: "Proceed?".to_string(), - is_other: false, - is_secret: false, - options: Some(vec![RequestUserInputQuestionOption { - label: "Yes (Recommended)".to_string(), - description: "Continue.".to_string(), - }]), - }], - auto_resolution_ms: Some(MIN_AUTO_RESOLUTION_MS), - }; +fn request_user_input_args_default_to_blocking_when_field_is_omitted() { + let args = serde_json::from_value::(serde_json::json!({ + "questions": [{ + "id": "confirm", + "header": "Confirm", + "question": "Proceed?", + "options": [{ + "label": "Yes (Recommended)", + "description": "Continue." + }] + }] + })) + .expect("request_user_input args should deserialize"); assert_eq!( - normalize_request_user_input_args(args.clone()), - Ok(RequestUserInputArgs { + args, + RequestUserInputArgs { questions: vec![RequestUserInputQuestion { - is_other: true, - ..args.questions[0].clone() + id: "confirm".to_string(), + header: "Confirm".to_string(), + question: "Proceed?".to_string(), + is_other: false, + is_secret: false, + options: Some(vec![RequestUserInputQuestionOption { + label: "Yes (Recommended)".to_string(), + description: "Continue.".to_string(), + }]), }], - auto_resolution_ms: Some(MIN_AUTO_RESOLUTION_MS), - }) - ); - assert_eq!( - normalize_request_user_input_args(RequestUserInputArgs { - auto_resolution_ms: Some(MAX_AUTO_RESOLUTION_MS), - ..args.clone() - }), - Ok(RequestUserInputArgs { - questions: vec![RequestUserInputQuestion { - is_other: true, - ..args.questions[0].clone() - }], - auto_resolution_ms: Some(MAX_AUTO_RESOLUTION_MS), - }) + is_blocking: true, + } ); } @@ -228,13 +207,13 @@ fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() { } #[test] -fn request_user_input_tool_description_mentions_available_modes() { +fn request_user_input_tool_description_mentions_blocking_semantics_and_available_modes() { assert_eq!( request_user_input_tool_description(&default_available_modes()), - "Request user input for one to three short questions and wait for the response. Set autoResolutionMs, from 60000 to 240000 milliseconds, only when the question is useful but non-blocking and continuing with best judgment is acceptable if the user does not answer; omit it when explicit user input is required. This tool is only available in Plan mode.".to_string() + "Request user input for one to three short questions and wait for the response. Set isBlocking to true when explicit user input is required before continuing. This tool is only available in Plan mode.".to_string() ); assert_eq!( request_user_input_tool_description(&default_mode_enabled_available_modes()), - "Request user input for one to three short questions and wait for the response. Set autoResolutionMs, from 60000 to 240000 milliseconds, only when the question is useful but non-blocking and continuing with best judgment is acceptable if the user does not answer; omit it when explicit user input is required. This tool is only available in Default or Plan mode.".to_string() + "Request user input for one to three short questions and wait for the response. Set isBlocking to true only in Plan mode when explicit user input is required before continuing. In non-Plan modes, always set isBlocking to false. This tool is only available in Default or Plan mode.".to_string() ); } diff --git a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs index efea95e9cb..383f9a0c79 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs @@ -52,7 +52,8 @@ async fn multi_agent_v2_request_user_input_rejects_subagent_threads() { "description": "B" } ] - }] + }], + "isBlocking": true }) .to_string(), }, diff --git a/codex-rs/core/tests/suite/mcp_turn_metadata.rs b/codex-rs/core/tests/suite/mcp_turn_metadata.rs index 0d403180d7..f100e0ce38 100644 --- a/codex-rs/core/tests/suite/mcp_turn_metadata.rs +++ b/codex-rs/core/tests/suite/mcp_turn_metadata.rs @@ -353,7 +353,8 @@ async fn mcp_tool_call_metadata_records_prior_request_user_input_tool() -> Resul "label": "No", "description": "Stop and revisit the approach." }] - }] + }], + "isBlocking": true }) .to_string(); let calendar_args = serde_json::to_string(&json!({ diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 1348a87186..af3a9d8a7d 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -68,17 +68,17 @@ fn call_output_content_and_success( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> { - request_user_input_round_trip_for_mode(ModeKind::Plan, /*auto_resolution_ms*/ None).await + request_user_input_round_trip_for_mode(ModeKind::Plan, /*is_blocking*/ true).await } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn request_user_input_round_trip_emits_auto_resolution_ms() -> anyhow::Result<()> { - request_user_input_round_trip_for_mode(ModeKind::Plan, Some(60_000)).await +async fn request_user_input_round_trip_emits_non_blocking() -> anyhow::Result<()> { + request_user_input_round_trip_for_mode(ModeKind::Plan, /*is_blocking*/ false).await } async fn request_user_input_round_trip_for_mode( mode: ModeKind, - auto_resolution_ms: Option, + is_blocking: bool, ) -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -117,12 +117,10 @@ async fn request_user_input_round_trip_for_mode( }] }] }); - if let Some(auto_resolution_ms) = auto_resolution_ms { - let request_args = request_args - .as_object_mut() - .expect("request_user_input args should be a JSON object"); - request_args.insert("autoResolutionMs".to_string(), json!(auto_resolution_ms)); - } + let request_args = request_args + .as_object_mut() + .expect("request_user_input args should be a JSON object"); + request_args.insert("isBlocking".to_string(), json!(is_blocking)); let request_args = request_args.to_string(); let first_response = sse(vec![ @@ -176,7 +174,7 @@ async fn request_user_input_round_trip_for_mode( .await; assert_eq!(request.call_id, call_id); assert_eq!(request.questions.len(), 1); - assert_eq!(request.auto_resolution_ms, auto_resolution_ms); + assert_eq!(request.is_blocking, is_blocking); assert_eq!(request.questions[0].is_other, true); assert!( timeout(Duration::from_millis(200), async { @@ -273,7 +271,8 @@ async fn request_user_input_interrupt_emits_deferred_token_count() -> anyhow::Re "label": "No", "description": "Stop and revisit the approach." }] - }] + }], + "isBlocking": true }) .to_string(); @@ -368,7 +367,8 @@ where "label": "No", "description": "Stop and revisit the approach." }] - }] + }], + "isBlocking": true }) .to_string(); @@ -451,7 +451,7 @@ async fn request_user_input_rejected_in_default_mode_by_default() -> anyhow::Res #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_user_input_round_trip_in_default_mode_with_feature() -> anyhow::Result<()> { - request_user_input_round_trip_for_mode(ModeKind::Default, /*auto_resolution_ms*/ None).await + request_user_input_round_trip_for_mode(ModeKind::Default, /*is_blocking*/ true).await } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/protocol/src/request_user_input.rs b/codex-rs/protocol/src/request_user_input.rs index dd52c5e6f6..8a56af729a 100644 --- a/codex-rs/protocol/src/request_user_input.rs +++ b/codex-rs/protocol/src/request_user_input.rs @@ -5,6 +5,10 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; +fn default_is_blocking() -> bool { + true +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestUserInputQuestionOption { pub label: String, @@ -31,9 +35,9 @@ pub struct RequestUserInputQuestion { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] pub struct RequestUserInputArgs { pub questions: Vec, - #[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")] - #[schemars(rename = "autoResolutionMs")] - pub auto_resolution_ms: Option, + #[serde(rename = "isBlocking", default = "default_is_blocking")] + #[schemars(rename = "isBlocking")] + pub is_blocking: bool, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] @@ -55,7 +59,7 @@ pub struct RequestUserInputEvent { #[serde(default)] pub turn_id: String, pub questions: Vec, - #[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")] - #[schemars(rename = "autoResolutionMs")] - pub auto_resolution_ms: Option, + #[serde(rename = "isBlocking", default = "default_is_blocking")] + #[schemars(rename = "isBlocking")] + pub is_blocking: bool, } diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index 0b72897685..a5e6345ecf 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -579,7 +579,7 @@ mod tests { turn_id: "turn-2".to_string(), item_id: "tool-1".to_string(), questions: Vec::new(), - auto_resolution_ms: None, + is_blocking: true, }, }), None @@ -870,7 +870,7 @@ mod tests { turn_id: "turn-1".to_string(), item_id: "tool-1".to_string(), questions: Vec::new(), - auto_resolution_ms: None, + is_blocking: true, }, }); @@ -893,7 +893,7 @@ mod tests { turn_id: "turn-1".to_string(), item_id: item_id.to_string(), questions: Vec::new(), - auto_resolution_ms: None, + is_blocking: true, }, }); } diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index d204105356..e3be276438 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -597,7 +597,7 @@ mod tests { turn_id: turn_id.to_string(), item_id: call_id.to_string(), questions: Vec::new(), - auto_resolution_ms: None, + is_blocking: true, }, } } diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 29e29541a7..b3c62372aa 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -4722,7 +4722,7 @@ fn request_user_input_request(thread_id: ThreadId, turn_id: &str, item_id: &str) turn_id: turn_id.to_string(), item_id: item_id.to_string(), questions: Vec::new(), - auto_resolution_ms: None, + is_blocking: true, }, } } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index bf1fa5fa8a..c76f5c4994 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -273,15 +273,13 @@ impl RequestUserInputOverlay { } fn snooze_auto_resolution(&mut self) { - if self.request.auto_resolution_ms.is_some() { + if !self.request.is_blocking { self.auto_resolution_snoozed = true; } } fn auto_resolution_timing_at(&self, now: Instant) -> AutoResolutionTiming { - // The TUI currently treats autoResolutionMs as an enable signal. The - // model-provided duration value is reserved for future runtime policy. - if self.request.auto_resolution_ms.is_none() || self.auto_resolution_snoozed { + if self.request.is_blocking || self.auto_resolution_snoozed { return AutoResolutionTiming::Disabled; } @@ -1694,16 +1692,16 @@ mod tests { item_id: "call-1".to_string(), turn_id: turn_id.to_string(), questions, - auto_resolution_ms: None, + is_blocking: true, } } - fn request_event_with_auto_resolution( + fn non_blocking_request_event( turn_id: &str, questions: Vec, ) -> ToolRequestUserInputParams { let mut request = request_event(turn_id, questions); - request.auto_resolution_ms = Some(60_000); + request.is_blocking = false; request } @@ -1770,14 +1768,14 @@ mod tests { item_id: "call-2".to_string(), turn_id: "turn-2".to_string(), questions: vec![question_with_options("q2", "Second")], - auto_resolution_ms: None, + is_blocking: true, }); overlay.try_consume_user_input_request(ToolRequestUserInputParams { thread_id: "thread-1".to_string(), item_id: "call-3".to_string(), turn_id: "turn-3".to_string(), questions: vec![question_with_options("q3", "Third")], - auto_resolution_ms: None, + is_blocking: true, }); overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); @@ -1787,7 +1785,7 @@ mod tests { } #[test] - fn auto_resolution_absent_has_no_timer() { + fn blocking_request_has_no_timer() { let (tx, _rx) = test_sender(); let overlay = RequestUserInputOverlay::new( request_event("turn-1", vec![question_with_options("q1", "First")]), @@ -1810,10 +1808,7 @@ mod tests { fn auto_resolution_hides_timer_during_grace_period() { let (tx, _rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( - "turn-1", - vec![question_with_options("q1", "First")], - ), + non_blocking_request_event("turn-1", vec![question_with_options("q1", "First")]), tx, /*has_input_focus*/ true, /*enhanced_keys_supported*/ false, @@ -1841,7 +1836,7 @@ mod tests { fn auto_resolution_visible_countdown_snapshot() { let (tx, _rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( + non_blocking_request_event( "turn-1", vec![ question_with_options("q1", "First"), @@ -1867,10 +1862,7 @@ mod tests { fn auto_resolution_visible_countdown_is_red() { let (tx, _rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( - "turn-1", - vec![question_with_options("q1", "First")], - ), + non_blocking_request_event("turn-1", vec![question_with_options("q1", "First")]), tx, /*has_input_focus*/ true, /*enhanced_keys_supported*/ false, @@ -1907,10 +1899,7 @@ mod tests { fn auto_resolution_expiry_emits_empty_answer() { let (tx, mut rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( - "turn-1", - vec![question_with_options("q1", "First")], - ), + non_blocking_request_event("turn-1", vec![question_with_options("q1", "First")]), tx, /*has_input_focus*/ true, /*enhanced_keys_supported*/ false, @@ -1941,10 +1930,7 @@ mod tests { fn auto_resolution_key_interaction_snoozes_timer() { let (tx, mut rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( - "turn-1", - vec![question_with_options("q1", "First")], - ), + non_blocking_request_event("turn-1", vec![question_with_options("q1", "First")]), tx, /*has_input_focus*/ true, /*enhanced_keys_supported*/ false, @@ -1968,10 +1954,7 @@ mod tests { fn auto_resolution_paste_interaction_snoozes_timer() { let (tx, mut rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( - "turn-1", - vec![question_with_options("q1", "First")], - ), + non_blocking_request_event("turn-1", vec![question_with_options("q1", "First")]), tx, /*has_input_focus*/ true, /*enhanced_keys_supported*/ false, @@ -1995,16 +1978,13 @@ mod tests { fn auto_resolution_resets_for_queued_request() { let (tx, mut rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( - request_event_with_auto_resolution( - "turn-1", - vec![question_with_options("q1", "First")], - ), + non_blocking_request_event("turn-1", vec![question_with_options("q1", "First")]), tx, /*has_input_focus*/ true, /*enhanced_keys_supported*/ false, /*disable_paste_burst*/ false, ); - overlay.try_consume_user_input_request(request_event_with_auto_resolution( + overlay.try_consume_user_input_request(non_blocking_request_event( "turn-2", vec![question_with_options("q2", "Second")], )); @@ -2035,7 +2015,7 @@ mod tests { item_id: "call-1".to_string(), turn_id: "turn-1".to_string(), questions: vec![question_with_options("q1", "First")], - auto_resolution_ms: None, + is_blocking: true, }, tx, /*has_input_focus*/ true, @@ -2064,7 +2044,7 @@ mod tests { item_id: "call-1".to_string(), turn_id: "turn-1".to_string(), questions: vec![question_with_options("q1", "First")], - auto_resolution_ms: None, + is_blocking: true, }, tx, /*has_input_focus*/ true, @@ -2076,7 +2056,7 @@ mod tests { item_id: "call-2".to_string(), turn_id: "turn-1".to_string(), questions: vec![question_with_options("q2", "Second")], - auto_resolution_ms: None, + is_blocking: true, }); assert!( @@ -2104,7 +2084,7 @@ mod tests { item_id: "call-1".to_string(), turn_id: "turn-1".to_string(), questions: vec![question_with_options("q1", "First")], - auto_resolution_ms: None, + is_blocking: true, }, tx, /*has_input_focus*/ true, @@ -2116,14 +2096,14 @@ mod tests { item_id: "call-2".to_string(), turn_id: "turn-1".to_string(), questions: vec![question_with_options("q2", "Second")], - auto_resolution_ms: None, + is_blocking: true, }); overlay.try_consume_user_input_request(ToolRequestUserInputParams { thread_id: "thread-1".to_string(), item_id: "call-3".to_string(), turn_id: "turn-1".to_string(), questions: vec![question_with_options("q3", "Third")], - auto_resolution_ms: None, + is_blocking: true, }); assert!( diff --git a/codex-rs/tui/src/chatwidget/interrupts.rs b/codex-rs/tui/src/chatwidget/interrupts.rs index 7b4c23e273..8256dfd894 100644 --- a/codex-rs/tui/src/chatwidget/interrupts.rs +++ b/codex-rs/tui/src/chatwidget/interrupts.rs @@ -152,7 +152,7 @@ mod tests { item_id: call_id.to_string(), turn_id: turn_id.to_string(), questions: Vec::new(), - auto_resolution_ms: None, + is_blocking: true, } } diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index fd2b2314e2..649899e2b1 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -597,7 +597,7 @@ async fn request_user_input_notification_overrides_pending_agent_turn_complete_n description: "Update only Plan mode.".to_string(), }]), }], - auto_resolution_ms: None, + is_blocking: true, }); assert_matches!( @@ -627,7 +627,7 @@ async fn handle_request_user_input_sets_pending_notification() { description: "Update only Plan mode.".to_string(), }]), }], - auto_resolution_ms: None, + is_blocking: true, }); assert_matches!(