From 5df791101364ab0a41e5b22d5e0d6796f6617c27 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Thu, 9 Jul 2026 18:12:02 +0530 Subject: [PATCH] fix(agent_orchestration): Unbounded timeout for delegation tools (#4734) ArchetypeDelegationTool synthesizes delegate_tools_agent (tools_agent) and run_code (code_executor). It defaulted to ToolTimeout::Inherit = the global 120s per-tool cap, so any delegated sub-agent run exceeding two minutes was hard-killed and truncated (Sentry TAURI-RUST-K29: 1236 events / 71 users; TAURI-RUST-8HB: 755 / 51). The child bounds its own lifetime via max_iterations, the run cancellation token, and each inner tool's timeout, so override timeout_policy -> Unbounded, matching spawn_parallel_agents (#4686) and the long-running scripting tools. Adds a unit test asserting the override. --- .../tools/archetype_delegation.rs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/openhuman/agent_orchestration/tools/archetype_delegation.rs b/src/openhuman/agent_orchestration/tools/archetype_delegation.rs index d888375a2b..d051f40074 100644 --- a/src/openhuman/agent_orchestration/tools/archetype_delegation.rs +++ b/src/openhuman/agent_orchestration/tools/archetype_delegation.rs @@ -3,7 +3,7 @@ use serde_json::json; use serde_json::Value; use crate::openhuman::tools::traits::{ - PermissionLevel, Tool, ToolCallOptions, ToolCategory, ToolResult, + PermissionLevel, Tool, ToolCallOptions, ToolCategory, ToolResult, ToolTimeout, }; use tinyagents::harness::tool::ToolExecutionContext; @@ -76,6 +76,22 @@ impl Tool for ArchetypeDelegationTool { ToolCategory::System } + /// Run **without** the global per-tool wall-clock deadline. This tool is a + /// delegation primitive: it hands a task to a bounded sub-agent + /// (`tools_agent` → `delegate_tools_agent`, `code_executor` → `run_code`, + /// …) and awaits that agent's full run. Under the default `Inherit` policy + /// the whole delegation is hard-killed at the single-tool timeout (120s) — + /// so any sub-agent run that legitimately exceeds two minutes is truncated + /// mid-flight (Sentry TAURI-RUST-K29 `delegate_tools_agent` and + /// TAURI-RUST-8HB `run_code`: thousands of 120.000s truncations). The + /// child's lifetime is already bounded internally — by its `max_iterations`, + /// the run cancellation token, and each inner tool's own timeout — so it + /// governs its own duration, exactly like the sibling `spawn_parallel_agents` + /// fan-out and 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 @@ -200,6 +216,21 @@ mod tests { assert_eq!(tool.category(), ToolCategory::System); } + #[test] + fn delegation_opts_out_of_the_global_tool_timeout() { + // A delegated sub-agent run (delegate_tools_agent / run_code / …) can + // legitimately outlast the single-tool wall-clock default (120s): under + // `Inherit` every such run is hard-killed and truncated (Sentry + // TAURI-RUST-K29 / TAURI-RUST-8HB). The child bounds its own lifetime + // via its max_iterations, the run cancellation token, and each inner + // tool's own timeout — so this primitive must be Unbounded, like + // spawn_parallel_agents and the long-running scripting tools. + assert_eq!( + sample_tool().timeout_policy(&json!({})), + ToolTimeout::Unbounded, + ); + } + #[test] fn parameters_schema_requires_prompt_only() { let tool = sample_tool();