mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add request_user_input auto-resolution window contract (#27256)
## Why `request_user_input` is moving beyond its original plan-mode-only workflow, and future default/goal-mode usage needs a way for the model to ask helpful but non-blocking questions without forcing the turn to wait forever. This PR adds an explicit `autoResolutionMs` contract so a later client/runtime change can auto-resolve unanswered prompts after a bounded window while leaving truly blocking questions unchanged. This is contract plumbing only; it does not implement the client-side timer or auto-selection behavior, and the model-facing description treats the field as reserved unless the current runtime explicitly supports auto-resolution. ## What Changed - Added optional `autoResolutionMs` to the model-facing `request_user_input` args and core `RequestUserInputEvent`. - Added model-facing schema text for `autoResolutionMs` while marking it reserved for runtimes that explicitly support auto-resolution. - Bounds `autoResolutionMs` to `60_000..=240_000` ms during argument normalization by clamping out-of-range model-provided values. - Propagated the field through app-server v2 `ToolRequestUserInputParams`, app-server request forwarding, generated TypeScript, and JSON schema fixtures. - Updated app-server, core, protocol, and TUI call sites/tests so omitted values preserve existing `None`/`null` behavior and coverage verifies a `Some(60_000)` round trip. ## Verification - `just test -p codex-app-server-protocol` - `just test -p codex-core request_user_input` - `just test -p codex-app-server request_user_input_round_trip` - `just test -p codex-tui request_user_input` - `just test -p codex-protocol`
This commit is contained in:
committed by
GitHub
Unverified
parent
78bab04116
commit
216ce03031
@@ -1888,6 +1888,7 @@ mod tests {
|
||||
is_secret: false,
|
||||
options: Some(vec![]),
|
||||
}],
|
||||
auto_resolution_ms: None,
|
||||
})
|
||||
.expect("params should serialize"),
|
||||
),
|
||||
@@ -1949,6 +1950,7 @@ mod tests {
|
||||
is_secret: false,
|
||||
options: Some(vec![]),
|
||||
}],
|
||||
auto_resolution_ms: None,
|
||||
})
|
||||
.expect("params should serialize"),
|
||||
),
|
||||
|
||||
@@ -1690,6 +1690,15 @@
|
||||
"ToolRequestUserInputParams": {
|
||||
"description": "EXPERIMENTAL. Params sent with a request_user_input event.",
|
||||
"properties": {
|
||||
"autoResolutionMs": {
|
||||
"default": null,
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -57,6 +57,15 @@
|
||||
},
|
||||
"description": "EXPERIMENTAL. Params sent with a request_user_input event.",
|
||||
"properties": {
|
||||
"autoResolutionMs": {
|
||||
"default": null,
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+9
@@ -5626,6 +5626,15 @@
|
||||
"$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"
|
||||
]
|
||||
},
|
||||
"itemId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+1
-1
@@ -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<ToolRequestUserInputQuestion>, };
|
||||
export type ToolRequestUserInputParams = { threadId: string, turnId: string, itemId: string, questions: Array<ToolRequestUserInputQuestion>, autoResolutionMs: number | null, };
|
||||
|
||||
@@ -1464,6 +1464,9 @@ pub struct ToolRequestUserInputParams {
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
pub questions: Vec<ToolRequestUserInputQuestion>,
|
||||
#[serde(default)]
|
||||
#[ts(type = "number | null")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -687,6 +687,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,
|
||||
};
|
||||
let (pending_request_id, rx) = outgoing
|
||||
.send_request(ServerRequestPayload::ToolRequestUserInput(params))
|
||||
|
||||
@@ -1259,6 +1259,7 @@ mod tests {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-1".to_string(),
|
||||
questions: vec![],
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
@@ -1321,6 +1322,7 @@ mod tests {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-1".to_string(),
|
||||
questions: vec![],
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
|
||||
@@ -1140,6 +1140,7 @@ mod thread_processor_behavior_tests {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-1".to_string(),
|
||||
questions: vec![],
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
|
||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_request_user_input_sse_response;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
@@ -18,15 +17,46 @@ use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use core_test_support::responses;
|
||||
use serde_json::json;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
fn create_request_user_input_sse_response_with_auto_resolution(
|
||||
call_id: &str,
|
||||
auto_resolution_ms: u64,
|
||||
) -> anyhow::Result<String> {
|
||||
let tool_call_arguments = serde_json::to_string(&json!({
|
||||
"questions": [{
|
||||
"id": "confirm_path",
|
||||
"header": "Confirm",
|
||||
"question": "Proceed with the plan?",
|
||||
"options": [{
|
||||
"label": "Yes (Recommended)",
|
||||
"description": "Continue the current plan."
|
||||
}, {
|
||||
"label": "No",
|
||||
"description": "Stop and revisit the approach."
|
||||
}]
|
||||
}],
|
||||
"autoResolutionMs": auto_resolution_ms
|
||||
}))?;
|
||||
|
||||
Ok(responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_function_call(call_id, "request_user_input", &tool_call_arguments),
|
||||
responses::ev_completed("resp-1"),
|
||||
]))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn request_user_input_round_trip() -> Result<()> {
|
||||
let codex_home = tempfile::TempDir::new()?;
|
||||
let responses = vec![
|
||||
create_request_user_input_sse_response("call1")?,
|
||||
create_request_user_input_sse_response_with_auto_resolution(
|
||||
"call1", /*auto_resolution_ms*/ 60_000,
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
@@ -89,6 +119,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));
|
||||
let resolved_request_id = request_id.clone();
|
||||
|
||||
mcp.send_response(
|
||||
@@ -128,7 +159,6 @@ async fn request_user_input_round_trip() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
|
||||
@@ -655,6 +655,7 @@ async fn handle_request_user_input(
|
||||
|
||||
let args = RequestUserInputArgs {
|
||||
questions: event.questions,
|
||||
auto_resolution_ms: event.auto_resolution_ms,
|
||||
};
|
||||
let response_fut =
|
||||
parent_session.request_user_input(parent_ctx, parent_ctx.sub_id.clone(), args);
|
||||
|
||||
@@ -433,6 +433,7 @@ async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
|
||||
is_secret: false,
|
||||
options: None,
|
||||
}],
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
&cancel_token,
|
||||
)
|
||||
@@ -476,6 +477,7 @@ async fn delegated_mcp_user_reviewer_returns_none_without_metadata() {
|
||||
is_secret: false,
|
||||
options: None,
|
||||
}],
|
||||
auto_resolution_ms: None,
|
||||
};
|
||||
let response = maybe_auto_review_mcp_request_user_input(
|
||||
&parent_session,
|
||||
|
||||
@@ -254,6 +254,7 @@ async fn should_install_mcp_dependencies(
|
||||
};
|
||||
let args = RequestUserInputArgs {
|
||||
questions: vec![question],
|
||||
auto_resolution_ms: None,
|
||||
};
|
||||
let sub_id = &turn_context.sub_id;
|
||||
let call_id = format!("mcp-deps-{sub_id}");
|
||||
|
||||
@@ -1297,6 +1297,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
|
||||
let args = RequestUserInputArgs {
|
||||
questions: vec![question],
|
||||
auto_resolution_ms: None,
|
||||
};
|
||||
let response = sess
|
||||
.request_user_input(turn_context.as_ref(), call_id.to_string(), args)
|
||||
|
||||
@@ -2391,6 +2391,7 @@ impl Session {
|
||||
call_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
questions: args.questions,
|
||||
auto_resolution_ms: args.auto_resolution_ms,
|
||||
});
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
|
||||
@@ -6,6 +6,8 @@ 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([
|
||||
@@ -66,7 +68,14 @@ 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 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 properties = BTreeMap::from([
|
||||
("questions".to_string(), questions_schema),
|
||||
("autoResolutionMs".to_string(), auto_resolution_ms_schema),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: REQUEST_USER_INPUT_TOOL_NAME.to_string(),
|
||||
@@ -111,13 +120,26 @@ 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);
|
||||
format!(
|
||||
"Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}."
|
||||
"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}."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::*;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::request_user_input_available_modes;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -26,7 +28,15 @@ 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([
|
||||
(
|
||||
"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."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"questions".to_string(),
|
||||
JsonSchema::array(
|
||||
JsonSchema::object(
|
||||
@@ -96,12 +106,97 @@ 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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_request_user_input_args_clamps_out_of_range_auto_resolution_ms() {
|
||||
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 - 1),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
normalize_request_user_input_args(args.clone()),
|
||||
Ok(RequestUserInputArgs {
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
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),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[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),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
normalize_request_user_input_args(args.clone()),
|
||||
Ok(RequestUserInputArgs {
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
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),
|
||||
..args.clone()
|
||||
}),
|
||||
Ok(RequestUserInputArgs {
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
is_other: true,
|
||||
..args.questions[0].clone()
|
||||
}],
|
||||
auto_resolution_ms: Some(MAX_AUTO_RESOLUTION_MS),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
|
||||
assert_eq!(
|
||||
@@ -136,10 +231,10 @@ fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
|
||||
fn request_user_input_tool_description_mentions_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. 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 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()
|
||||
);
|
||||
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. 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 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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,10 +76,18 @@ 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).await
|
||||
request_user_input_round_trip_for_mode(ModeKind::Plan, /*auto_resolution_ms*/ None).await
|
||||
}
|
||||
|
||||
async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Result<()> {
|
||||
#[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<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -104,7 +112,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
|
||||
.await?;
|
||||
|
||||
let call_id = "user-input-call";
|
||||
let request_args = json!({
|
||||
let mut request_args = json!({
|
||||
"questions": [{
|
||||
"id": "confirm_path",
|
||||
"header": "Confirm",
|
||||
@@ -117,8 +125,14 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
|
||||
"description": "Stop and revisit the approach."
|
||||
}]
|
||||
}]
|
||||
})
|
||||
.to_string();
|
||||
});
|
||||
if let Some(auto_resolution_ms) = auto_resolution_ms {
|
||||
let Some(request_args) = request_args.as_object_mut() else {
|
||||
panic!("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![
|
||||
ev_response_created("resp-1"),
|
||||
@@ -171,6 +185,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
|
||||
.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.questions[0].is_other, true);
|
||||
assert!(
|
||||
timeout(Duration::from_millis(200), async {
|
||||
@@ -445,7 +460,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).await
|
||||
request_user_input_round_trip_for_mode(ModeKind::Default, /*auto_resolution_ms*/ None).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -31,6 +31,9 @@ pub struct RequestUserInputQuestion {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct RequestUserInputArgs {
|
||||
pub questions: Vec<RequestUserInputQuestion>,
|
||||
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "autoResolutionMs")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -52,4 +55,7 @@ pub struct RequestUserInputEvent {
|
||||
#[serde(default)]
|
||||
pub turn_id: String,
|
||||
pub questions: Vec<RequestUserInputQuestion>,
|
||||
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "autoResolutionMs")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
}
|
||||
|
||||
@@ -509,6 +509,7 @@ mod tests {
|
||||
turn_id: "turn-2".to_string(),
|
||||
item_id: "tool-1".to_string(),
|
||||
questions: Vec::new(),
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
}),
|
||||
None
|
||||
@@ -798,6 +799,7 @@ mod tests {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "tool-1".to_string(),
|
||||
questions: Vec::new(),
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -820,6 +822,7 @@ mod tests {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: item_id.to_string(),
|
||||
questions: Vec::new(),
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -597,6 +597,7 @@ mod tests {
|
||||
turn_id: turn_id.to_string(),
|
||||
item_id: call_id.to_string(),
|
||||
questions: Vec::new(),
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4586,6 +4586,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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1538,6 +1538,7 @@ mod tests {
|
||||
item_id: "call-1".to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
questions,
|
||||
auto_resolution_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1600,12 +1601,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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
overlay.handle_key_event(KeyEvent::from(KeyCode::Esc));
|
||||
@@ -1623,6 +1626,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,
|
||||
},
|
||||
tx,
|
||||
/*has_input_focus*/ true,
|
||||
@@ -1651,6 +1655,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,
|
||||
},
|
||||
tx,
|
||||
/*has_input_focus*/ true,
|
||||
@@ -1662,6 +1667,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,
|
||||
});
|
||||
|
||||
assert!(
|
||||
@@ -1689,6 +1695,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,
|
||||
},
|
||||
tx,
|
||||
/*has_input_focus*/ true,
|
||||
@@ -1700,12 +1707,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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -152,6 +152,7 @@ mod tests {
|
||||
item_id: call_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
questions: Vec::new(),
|
||||
auto_resolution_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -597,6 +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,
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
@@ -626,6 +627,7 @@ async fn handle_request_user_input_sets_pending_notification() {
|
||||
description: "Update only Plan mode.".to_string(),
|
||||
}]),
|
||||
}],
|
||||
auto_resolution_ms: None,
|
||||
});
|
||||
|
||||
assert_matches!(
|
||||
|
||||
Reference in New Issue
Block a user