From 43c9f9843de7049f1e5269d8c554934c2e1b42cc Mon Sep 17 00:00:00 2001 From: luowanghaoyun Date: Wed, 20 May 2026 21:57:34 +0800 Subject: [PATCH] fix(harness): align subagent task output timeout Ensure HarnessAgent tool execution timeout exceeds task_output blocking waits and include tool identifiers in timeout errors. --- .../io/agentscope/core/tool/ToolExecutor.java | 13 ++++++-- .../harness/agent/HarnessAgent.java | 31 ++++++++++++++++--- .../harness/agent/tool/TaskTool.java | 6 +++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java index 5ba4af8f1..699fd069a 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolExecutor.java @@ -358,9 +358,16 @@ private Mono applyTimeout( Duration timeout = config.getTimeout(); logger.debug("Applied timeout: {} for tool: {}", timeout, toolCall.getName()); - return execution.timeout( - timeout, - Mono.error(new RuntimeException("Tool execution timeout after " + timeout))); + return execution.timeout(timeout, Mono.error(toolExecutionTimeoutError(timeout, toolCall))); + } + + private static RuntimeException toolExecutionTimeoutError( + Duration timeout, ToolUseBlock toolCall) { + String toolName = toolCall.getName() != null ? toolCall.getName() : "unknown"; + String toolId = toolCall.getId() != null ? toolCall.getId() : "unknown"; + return new RuntimeException( + String.format( + "Tool '%s' (id=%s) execution timeout after %s", toolName, toolId, timeout)); } private Mono applyRetry( diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index 867ab46ed..d05a59875 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -93,10 +93,12 @@ import io.agentscope.harness.agent.tool.MemorySearchTool; import io.agentscope.harness.agent.tool.SessionSearchTool; import io.agentscope.harness.agent.tool.ShellExecuteTool; +import io.agentscope.harness.agent.tool.TaskTool; import io.agentscope.harness.agent.workspace.WorkspaceManager; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -146,6 +148,9 @@ public class HarnessAgent implements Agent, StateModule { private static final Logger log = LoggerFactory.getLogger(HarnessAgent.class); + private static final Duration SUBAGENT_TOOL_EXECUTION_TIMEOUT = + TaskTool.MAX_BLOCK_WAIT.plusMinutes(1); + private final ReActAgent delegate; private final WorkspaceManager workspaceManager; private final CompactionHook compactionHook; @@ -1380,7 +1385,11 @@ public HarnessAgent build() { allHooks.add(new SessionPersistenceHook()); } - if (!leafSubagent && !disableSubagents && model != null) { + boolean subagentsEnabled = !leafSubagent && !disableSubagents && model != null; + ExecutionConfig effectiveToolExecutionConfig = toolExecutionConfig; + if (subagentsEnabled) { + effectiveToolExecutionConfig = + withSubagentToolExecutionTimeout(toolExecutionConfig); SubagentsHook subagentsHook = buildSubagentsHook( wsManager, resolvedWorkspace, capturedSandboxFs, userIdRef); @@ -1425,8 +1434,8 @@ public HarnessAgent build() { if (modelExecutionConfig != null) { reactBuilder.modelExecutionConfig(modelExecutionConfig); } - if (toolExecutionConfig != null) { - reactBuilder.toolExecutionConfig(toolExecutionConfig); + if (effectiveToolExecutionConfig != null) { + reactBuilder.toolExecutionConfig(effectiveToolExecutionConfig); } if (generateOptions != null) { reactBuilder.generateOptions(generateOptions); @@ -1470,7 +1479,7 @@ public HarnessAgent build() { name, resolvedWorkspace, filesystem.getClass().getSimpleName(), - !leafSubagent && !disableSubagents && model != null); + subagentsEnabled); return new HarnessAgent( delegate, @@ -1596,6 +1605,20 @@ private static NamespaceFactory buildDynamicNamespaceFactory( }; } + private static ExecutionConfig withSubagentToolExecutionTimeout( + ExecutionConfig configured) { + ExecutionConfig timeoutOverride = + ExecutionConfig.builder().timeout(SUBAGENT_TOOL_EXECUTION_TIMEOUT).build(); + if (configured == null) { + return timeoutOverride; + } + Duration timeout = configured.getTimeout(); + if (timeout == null || timeout.compareTo(TaskTool.MAX_BLOCK_WAIT) <= 0) { + return ExecutionConfig.mergeConfigs(timeoutOverride, configured); + } + return configured; + } + // ----------------------------------------------------------------- // Subagents // ----------------------------------------------------------------- diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/TaskTool.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/TaskTool.java index aee6c7972..dd8bceba8 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/TaskTool.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/TaskTool.java @@ -21,6 +21,7 @@ import io.agentscope.harness.agent.subagent.task.BackgroundTask; import io.agentscope.harness.agent.subagent.task.TaskRepository; import io.agentscope.harness.agent.subagent.task.TaskStatus; +import java.time.Duration; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Collection; @@ -41,6 +42,9 @@ */ public class TaskTool { + public static final long MAX_BLOCK_WAIT_MILLIS = 600_000L; + public static final Duration MAX_BLOCK_WAIT = Duration.ofMillis(MAX_BLOCK_WAIT_MILLIS); + private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); @@ -96,7 +100,7 @@ public String taskOutput( bgTask.updateLastCheckedAt(); boolean shouldBlock = block == null || block; - long timeoutMs = timeout != null ? Math.min(timeout, 600_000) : 30_000; + long timeoutMs = timeout != null ? Math.min(timeout, MAX_BLOCK_WAIT_MILLIS) : 30_000; if (shouldBlock && !bgTask.isCompleted()) { // If the task has no local future (cross-node or post-restart), degrade gracefully