Make user input blocking behavior explicit (#36410)

## Why

Clients need to know whether a `request_user_input` request must wait for an
explicit response or may auto-resolve. Using `autoResolutionMs` as that signal
conflated the blocking decision with timeout policy.

## What changed

- Add required `isBlocking` fields to user input protocol and app-server
  request payloads. Plan-mode requests are blocking, while requests from other
  enabled modes are non-blocking.
- Drive the TUI's auto-resolution behavior from `isBlocking` and remove
  `autoResolutionMs` from the model-facing tool schema.
- Deprecate `autoResolutionMs` while retaining it for compatibility, and treat
  legacy payloads without `isBlocking` as blocking.

## Testing

- Cover mode-derived blocking behavior, legacy deserialization, app-server
  forwarding, delegated requests, and TUI auto-resolution.

GitOrigin-RevId: 29aade657ef743065ec264376ba567a9b353d7d7
This commit is contained in:
Shijie Rao authored and copyberry committed 2026-08-01 00:38:10 +00:00
1 parent a850875a8e
commit e2c0837923
36 files changed
+578 -133

No files matched your search

@@ -68,18 +68,10 @@ 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).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_for_mode(
mode: ModeKind,
auto_resolution_ms: Option<u64>,
) -> anyhow::Result<()> {
async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -103,7 +95,8 @@ async fn request_user_input_round_trip_for_mode(
.await?;
let call_id = "user-input-call";
let mut request_args = json!({
let expected_is_blocking = mode == ModeKind::Plan;
let request_args = json!({
"questions": [{
"id": "confirm_path",
"header": "Confirm",
@@ -117,12 +110,6 @@ 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.to_string();
let first_response = sse(vec![
@@ -176,7 +163,8 @@ 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, expected_is_blocking);
assert_eq!(request.auto_resolution_ms, None);
assert_eq!(request.questions[0].is_other, true);
assert!(
timeout(Duration::from_millis(200), async {
@@ -451,7 +439,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).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]