mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Replace request_user_input autoResolutionMs with isBlocking
This commit is contained in:
@@ -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"),
|
||||
),
|
||||
|
||||
+3
-8
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+3
-8
@@ -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",
|
||||
|
||||
+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>, autoResolutionMs: number | null, };
|
||||
export type ToolRequestUserInputParams = { threadId: string, turnId: string, itemId: string, questions: Array<ToolRequestUserInputQuestion>, isBlocking: boolean, };
|
||||
|
||||
@@ -1502,9 +1502,7 @@ 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>,
|
||||
pub is_blocking: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -74,7 +74,8 @@ pub fn create_request_user_input_sse_response(call_id: &str) -> anyhow::Result<S
|
||||
"label": "No",
|
||||
"description": "Stop and revisit the approach."
|
||||
}]
|
||||
}]
|
||||
}],
|
||||
"isBlocking": true
|
||||
}))?;
|
||||
|
||||
Ok(responses::sse(vec![
|
||||
|
||||
@@ -23,10 +23,7 @@ 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> {
|
||||
fn create_non_blocking_request_user_input_sse_response(call_id: &str) -> anyhow::Result<String> {
|
||||
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(
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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::<RequestUserInputArgs>(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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ async fn multi_agent_v2_request_user_input_rejects_subagent_threads() {
|
||||
"description": "B"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}],
|
||||
"isBlocking": true
|
||||
})
|
||||
.to_string(),
|
||||
},
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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<u64>,
|
||||
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)]
|
||||
|
||||
@@ -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<RequestUserInputQuestion>,
|
||||
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "autoResolutionMs")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
#[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<RequestUserInputQuestion>,
|
||||
#[serde(rename = "autoResolutionMs", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(rename = "autoResolutionMs")]
|
||||
pub auto_resolution_ms: Option<u64>,
|
||||
#[serde(rename = "isBlocking", default = "default_is_blocking")]
|
||||
#[schemars(rename = "isBlocking")]
|
||||
pub is_blocking: bool,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ToolRequestUserInputQuestion>,
|
||||
) -> 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!(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user