diff --git a/src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs b/src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs index 8aace755b9..b99c853b42 100644 --- a/src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs +++ b/src/openhuman/agent_orchestration/tools/spawn_parallel_agents.rs @@ -14,7 +14,9 @@ use crate::openhuman::agent_orchestration::spawn_parallel_graph::{ SpawnParallelGraphOutcome, SpawnParallelTaskValidationError, }; use crate::openhuman::tinyagents::current_run_cancellation; -use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCallOptions, ToolResult}; +use crate::openhuman::tools::traits::{ + PermissionLevel, Tool, ToolCallOptions, ToolResult, ToolTimeout, +}; use async_trait::async_trait; use serde_json::json; use tinyagents::harness::tool::ToolExecutionContext; @@ -95,6 +97,20 @@ impl Tool for SpawnParallelAgentsTool { PermissionLevel::Execute } + /// Run **without** the global per-tool wall-clock deadline. This is a + /// fan-out primitive: it spawns N independent sub-agents and awaits them + /// concurrently via `spawn_parallel_graph`'s bounded `map_reduce`. Under the + /// default `Inherit` policy the whole fan-out is hard-killed at the + /// single-tool timeout (120s by default) — so a group of long workers is + /// truncated at one worker's budget instead of running to ~the slowest, and + /// each worker's research is cut short. The fan-out is already bounded + /// internally — by `max_concurrency`, the run cancellation token, and each + /// sub-agent's own iteration/turn caps — so it governs its own lifetime, + /// like the long-running scripting tools (`shell`, `node_exec`). + fn timeout_policy(&self, _args: &serde_json::Value) -> ToolTimeout { + ToolTimeout::Unbounded + } + async fn execute(&self, args: serde_json::Value) -> anyhow::Result { self.execute_with_context(args, ToolCallOptions::default(), None) .await diff --git a/src/openhuman/agent_orchestration/tools/spawn_parallel_agents_tests.rs b/src/openhuman/agent_orchestration/tools/spawn_parallel_agents_tests.rs index fe01aeb1fa..bb2d85389c 100644 --- a/src/openhuman/agent_orchestration/tools/spawn_parallel_agents_tests.rs +++ b/src/openhuman/agent_orchestration/tools/spawn_parallel_agents_tests.rs @@ -16,6 +16,7 @@ use crate::openhuman::inference::provider::{ ChatRequest, ChatResponse, ConversationMessage, Provider, ToolCall, }; use crate::openhuman::memory::{Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts}; +use crate::openhuman::tools::traits::ToolTimeout; use crate::openhuman::tools::{PermissionLevel, Tool, ToolResult}; use async_trait::async_trait; use parking_lot::Mutex; @@ -899,3 +900,15 @@ async fn agent_turn_runs_long_parallel_subagent_flow_with_many_nested_tool_calls "each subagent should run three tool calls plus a final completion iteration" ); } + +#[test] +fn spawn_parallel_agents_opts_out_of_the_global_tool_timeout() { + // A fan-out of N long sub-agents must not be hard-killed at the single-tool + // wall-clock default (120s): that truncates every worker and bounds the whole + // group at one worker's budget. The tool governs its own lifetime via its + // internal max_concurrency, cancellation token, and per-sub-agent caps. + assert_eq!( + SpawnParallelAgentsTool::new().timeout_policy(&json!({})), + ToolTimeout::Unbounded, + ); +}