Skip to content

Commit 2878d46

Browse files
committed
feat: add per-tool timeout in batch execution
1 parent a22a368 commit 2878d46

2 files changed

Lines changed: 30 additions & 11 deletions

File tree

src/cortex-engine/src/tools/handlers/batch.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use std::sync::Arc;
77
use std::time::{Duration, Instant};
88

99
use async_trait::async_trait;
10-
use cortex_common::DEFAULT_BATCH_TIMEOUT_SECS;
1110
use futures::future::join_all;
1211
use serde::{Deserialize, Serialize};
1312
use serde_json::{Value, json};
@@ -21,6 +20,10 @@ use crate::tools::spec::{ToolDefinition, ToolHandler, ToolResult};
2120
/// Maximum number of tools that can be executed in a batch.
2221
pub const MAX_BATCH_SIZE: usize = 10;
2322

23+
/// Default timeout for individual tool execution in seconds.
24+
/// This prevents a single tool from consuming the entire batch timeout.
25+
pub const DEFAULT_TOOL_TIMEOUT_SECS: u64 = 60;
26+
2427
/// Tools that cannot be called within a batch (prevent recursion).
2528
/// Note: Task is now allowed to enable parallel task execution.
2629
pub const DISALLOWED_TOOLS: &[&str] = &["Batch", "batch", "Agent", "agent"];
@@ -43,6 +46,10 @@ pub struct BatchToolArgs {
4346
/// Optional timeout in seconds for the entire batch (default: 300s).
4447
#[serde(default)]
4548
pub timeout_secs: Option<u64>,
49+
/// Optional timeout in seconds for individual tools (default: 60s).
50+
/// This prevents a single tool from consuming the entire batch timeout.
51+
#[serde(default)]
52+
pub tool_timeout_secs: Option<u64>,
4653
}
4754

4855
/// Result of a single tool call within the batch.
@@ -156,7 +163,7 @@ impl BatchToolHandler {
156163
&self,
157164
calls: Vec<BatchToolCall>,
158165
context: &ToolContext,
159-
timeout_duration: Duration,
166+
tool_timeout: Duration,
160167
) -> BatchResult {
161168
let start_time = Instant::now();
162169
let results = Arc::new(Mutex::new(Vec::with_capacity(calls.len())));
@@ -174,9 +181,9 @@ impl BatchToolHandler {
174181
async move {
175182
let call_start = Instant::now();
176183

177-
// Execute with per-call timeout (use batch timeout for each call)
184+
// Execute with per-tool timeout to prevent single tools from blocking others
178185
let execution_result = timeout(
179-
timeout_duration,
186+
tool_timeout,
180187
executor.execute_tool(&call.tool, call.arguments, &ctx),
181188
)
182189
.await;
@@ -200,19 +207,23 @@ impl BatchToolHandler {
200207
duration_ms,
201208
},
202209
Ok(Err(e)) => BatchCallResult {
203-
tool: tool_name,
210+
tool: tool_name.clone(),
204211
index,
205212
success: false,
206213
result: None,
207214
error: Some(format!("Execution error: {}", e)),
208215
duration_ms,
209216
},
210217
Err(_) => BatchCallResult {
211-
tool: tool_name,
218+
tool: tool_name.clone(),
212219
index,
213220
success: false,
214221
result: None,
215-
error: Some(format!("Timeout after {}s", timeout_duration.as_secs())),
222+
error: Some(format!(
223+
"Tool '{}' timed out after {}s",
224+
tool_name,
225+
tool_timeout.as_secs()
226+
)),
216227
duration_ms,
217228
},
218229
};
@@ -326,13 +337,13 @@ impl ToolHandler for BatchToolHandler {
326337
// Validate calls
327338
self.validate_calls(&args.calls)?;
328339

329-
// Determine timeout
330-
let timeout_secs = args.timeout_secs.unwrap_or(DEFAULT_BATCH_TIMEOUT_SECS);
331-
let timeout_duration = Duration::from_secs(timeout_secs);
340+
// Determine per-tool timeout (prevents single tool from blocking others)
341+
let tool_timeout_secs = args.tool_timeout_secs.unwrap_or(DEFAULT_TOOL_TIMEOUT_SECS);
342+
let tool_timeout = Duration::from_secs(tool_timeout_secs);
332343

333344
// Execute all tools in parallel
334345
let batch_result = self
335-
.execute_parallel(args.calls, context, timeout_duration)
346+
.execute_parallel(args.calls, context, tool_timeout)
336347
.await;
337348

338349
// Format output
@@ -382,6 +393,12 @@ pub fn batch_tool_definition() -> ToolDefinition {
382393
"description": "Optional timeout in seconds for the entire batch execution (default: 300)",
383394
"minimum": 1,
384395
"maximum": 600
396+
},
397+
"tool_timeout_secs": {
398+
"type": "integer",
399+
"description": "Optional timeout in seconds for individual tool execution (default: 60). Prevents a single tool from consuming the entire batch timeout.",
400+
"minimum": 1,
401+
"maximum": 300
385402
}
386403
}
387404
}),
@@ -407,6 +424,7 @@ pub async fn execute_batch(
407424
})
408425
.collect(),
409426
timeout_secs: None,
427+
tool_timeout_secs: None,
410428
};
411429

412430
let arguments = serde_json::to_value(args)

src/cortex-engine/src/tools/unified_executor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ impl UnifiedToolExecutor {
466466
Ok(BatchToolArgs {
467467
calls,
468468
timeout_secs: arguments.get("timeout_secs").and_then(|v| v.as_u64()),
469+
tool_timeout_secs: arguments.get("tool_timeout_secs").and_then(|v| v.as_u64()),
469470
})
470471
} else {
471472
Err(CortexError::InvalidInput(

0 commit comments

Comments
 (0)