mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Clamp short wait_agent timeouts to the configured minimum (#37357)
## What changed - Clamp `wait_agent` timeout requests below `min_wait_timeout_ms` instead of rejecting them. - Include the timeout adjustment in the wait result message and document it in the tool output schema. ## Testing - Update the handler test to verify the configured minimum is honored and the adjustment is reported. - Update the schema test for the expanded result message. GitOrigin-RevId: 2b4a770ed6d1e9c45b351b479f5ae50c9a3d5388
This commit is contained in:
committed by
copyberry
parent
b94343ab9f
commit
4d7e3e90d9
@@ -517,7 +517,7 @@ fn wait_output_schema_v2() -> Value {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Brief wait summary without the agent's final content."
|
||||
"description": "Brief wait summary without the agent's final content, including any timeout adjustment."
|
||||
},
|
||||
"timed_out": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -426,7 +426,9 @@ fn wait_agent_tool_v2_uses_timeout_only_summary_output() {
|
||||
assert_eq!(parameters.required.as_ref(), None);
|
||||
assert_eq!(
|
||||
output_schema.expect("wait output schema")["properties"]["message"]["description"],
|
||||
json!("Brief wait summary without the agent's final content.")
|
||||
json!(
|
||||
"Brief wait summary without the agent's final content, including any timeout adjustment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2989,7 +2989,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_v2_wait_agent_rejects_timeout_below_configured_min() {
|
||||
async fn multi_agent_v2_wait_agent_clamps_timeout_below_configured_min() {
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
let mut config = (*turn.config).clone();
|
||||
config
|
||||
@@ -3001,7 +3001,9 @@ async fn multi_agent_v2_wait_agent_rejects_timeout_below_configured_min() {
|
||||
config.multi_agent_v2.default_wait_timeout_ms = 50;
|
||||
set_turn_config(&mut turn, config);
|
||||
|
||||
let Err(err) = WaitAgentHandlerV2::default()
|
||||
tokio::time::pause();
|
||||
let started_at = tokio::time::Instant::now();
|
||||
let output = WaitAgentHandlerV2::default()
|
||||
.handle(invocation(
|
||||
Arc::new(session),
|
||||
Arc::new(turn),
|
||||
@@ -3009,13 +3011,28 @@ async fn multi_agent_v2_wait_agent_rejects_timeout_below_configured_min() {
|
||||
function_payload(json!({"timeout_ms": 1})),
|
||||
))
|
||||
.await
|
||||
else {
|
||||
panic!("timeout below configured minimum should be rejected");
|
||||
};
|
||||
assert_eq!(
|
||||
err,
|
||||
FunctionCallError::RespondToModel("timeout_ms must be at least 50".to_string())
|
||||
.expect("wait_agent should succeed");
|
||||
let elapsed = started_at.elapsed();
|
||||
tokio::time::resume();
|
||||
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(/*millis*/ 50)
|
||||
&& elapsed <= Duration::from_millis(/*millis*/ 51),
|
||||
"wait_agent should time out at the configured minimum: {elapsed:?}"
|
||||
);
|
||||
let (content, success) = expect_text_output(output);
|
||||
let result: crate::tools::handlers::multi_agents_v2::wait::WaitAgentResult =
|
||||
serde_json::from_str(&content).expect("wait_agent result should be json");
|
||||
assert_eq!(
|
||||
result,
|
||||
crate::tools::handlers::multi_agents_v2::wait::WaitAgentResult {
|
||||
message:
|
||||
"Wait timed out.\n\nRequested timeout of 1ms was clamped to the minimum of 50ms."
|
||||
.to_string(),
|
||||
timed_out: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(success, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -50,18 +50,14 @@ impl Handler {
|
||||
let min_timeout_ms = turn.config.multi_agent_v2.min_wait_timeout_ms;
|
||||
let max_timeout_ms = turn.config.multi_agent_v2.max_wait_timeout_ms;
|
||||
let default_timeout_ms = turn.config.multi_agent_v2.default_wait_timeout_ms;
|
||||
let timeout_ms = match args.timeout_ms {
|
||||
Some(ms) if ms < min_timeout_ms => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"timeout_ms must be at least {min_timeout_ms}"
|
||||
)));
|
||||
}
|
||||
let requested_timeout_ms = args.timeout_ms;
|
||||
let timeout_ms = match requested_timeout_ms {
|
||||
Some(ms) if ms > max_timeout_ms => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"timeout_ms must be at most {max_timeout_ms}"
|
||||
)));
|
||||
}
|
||||
Some(ms) => ms,
|
||||
Some(ms) => ms.max(min_timeout_ms),
|
||||
None => default_timeout_ms,
|
||||
};
|
||||
|
||||
@@ -94,7 +90,7 @@ impl Handler {
|
||||
|
||||
let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
|
||||
let outcome = wait_for_activity(&mut activity_rx, pending_activity, deadline).await;
|
||||
let result = WaitAgentResult::from_outcome(outcome);
|
||||
let result = WaitAgentResult::from_outcome(outcome, requested_timeout_ms, timeout_ms);
|
||||
|
||||
session
|
||||
.emit_turn_item_completed(
|
||||
@@ -137,14 +133,24 @@ pub(crate) struct WaitAgentResult {
|
||||
}
|
||||
|
||||
impl WaitAgentResult {
|
||||
fn from_outcome(outcome: WaitOutcome) -> Self {
|
||||
fn from_outcome(
|
||||
outcome: WaitOutcome,
|
||||
requested_timeout_ms: Option<i64>,
|
||||
timeout_ms: i64,
|
||||
) -> Self {
|
||||
let message = match outcome {
|
||||
WaitOutcome::MailboxActivity => "Wait completed.",
|
||||
WaitOutcome::Steered => "Wait interrupted by new input.",
|
||||
WaitOutcome::TimedOut => "Wait timed out.",
|
||||
};
|
||||
let message = match requested_timeout_ms {
|
||||
Some(requested_timeout_ms) if requested_timeout_ms < timeout_ms => format!(
|
||||
"{message}\n\nRequested timeout of {requested_timeout_ms}ms was clamped to the minimum of {timeout_ms}ms."
|
||||
),
|
||||
Some(_) | None => message.to_string(),
|
||||
};
|
||||
Self {
|
||||
message: message.to_string(),
|
||||
message,
|
||||
timed_out: outcome == WaitOutcome::TimedOut,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user