diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index 09817de..e323738 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -429,10 +429,17 @@ impl AgentLoop { continue; } middleware::ToolCallDecision::ReplaceInput(new_input) => { - // Execute with replaced input - let tool_result = match self.execute_tool(&name, new_input, &tool_context).await { - Ok(result) => result, - Err(e) => serde_json::json!({ "error": e.to_string() }), + // Execute with replaced input (with timeout) + let tool_result = match tokio::time::timeout( + std::time::Duration::from_secs(30), + self.execute_tool(&name, new_input, &tool_context), + ).await { + Ok(Ok(result)) => result, + Ok(Err(e)) => serde_json::json!({ "error": e.to_string() }), + Err(_) => { + tracing::warn!("[AgentLoop] Tool '{}' (replaced input) timed out after 30s", name); + serde_json::json!({ "error": format!("工具 '{}' 执行超时(30秒),请重试", name) }) + } }; messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), tool_result, false)); continue; @@ -471,9 +478,16 @@ impl AgentLoop { } } - let tool_result = match self.execute_tool(&name, input, &tool_context).await { - Ok(result) => result, - Err(e) => serde_json::json!({ "error": e.to_string() }), + let tool_result = match tokio::time::timeout( + std::time::Duration::from_secs(30), + self.execute_tool(&name, input, &tool_context), + ).await { + Ok(Ok(result)) => result, + Ok(Err(e)) => serde_json::json!({ "error": e.to_string() }), + Err(_) => { + tracing::warn!("[AgentLoop] Tool '{}' timed out after 30s", name); + serde_json::json!({ "error": format!("工具 '{}' 执行超时(30秒),请重试", name) }) + } }; // Check if this is a clarification response — terminate loop immediately