agentscope-ai / agentscope-ai/agentscope-java
[Bug]: ToolExecutor异常逃逸与PendingToolRecoveryHook失效问题
- 主要语言
- Java
- 星标
- 5.6k
- 派生
- 1.3k
- 平均合并
- 4 天 12 小时
- 30 天内合并 PR
- 77
描述
一、问题现象
用户在多轮对话中,某次 SubAgent 工具调用超时后,后续所有请求均报错,会话永久不可用:
// 第 N 轮:工具调用超时
WARN ToolExecutor : Tool call failed: search_agent
java.lang.RuntimeException: Tool execution timeout after PT30M
// 第 N+1 轮及之后:每次新消息必报
ERROR AiAgent : Flux 流异常
java.lang.IllegalStateException: Pending tool calls exist without results.
Pending IDs: [call_pp47mdun8f7kkz4aeka9zoe4]
at io.agentscope.core.ReActAgent.doCall(ReActAgent.java:393)
---
二、根本原因
这是两个框架 Bug 的复合故障,缺一不会触发该现象。
---
Bug 1 — ToolExecutor.executeAll() 超时异常逃逸 onErrorResume
文件:io.agentscope.core.tool.ToolExecutor
预期行为:executeWithInfrastructure() 内部的 onErrorResume 负责将所有工具执行异常(含超时)转为 ToolResultBlock.error(...),确保 Supervisor memory 始终有完整的工具结果。
实际行为:超时异常穿透 onErrorResume,在 executeAll() 的 .toList() 处以原始 Java 异常的形式抛出,导致 Supervisor acting 阶段整体失败,memory 中留下一个没有对应 Result 的孤立 ToolUseBlock。
关键代码(ToolExecutor.java:296-302):
// executeAll() 里用 Stream.toList() 组装 Mono 列表
List> monos =
toolCalls.stream()
.map(toolCall -> executeWithInfrastructure( // ← 若此处同步抛出
toolCall, executionConfig, agent, agentContext))
.toList(); // ← 异常在这里作为原始 Java 异常逃逸,绕过下游 onErrorResume
applyTimeout 在特定 Scheduler 上下文下可能在 Mono 组装期(而非订阅期)同步抛出异常,此时 Reactive 链尚未激活,onErrorResume 完全无效。
后果:Supervisor memory 中产生孤立的 ToolUseBlock(有调用、无结果),会话陷入不一致状态。
---
Bug 2 — PendingToolRecoveryHook 误判跳过修复
文件:io.agentscope.core.hook.PendingToolRecoveryHook
这是 Bug 1 的兜底机制,本应在下一轮请求时检测到孤立 ToolUseBlock 并自动修复。但由于自身存在判断逻辑缺陷,修复从未生效。
问题代码(PendingToolRecoveryHook.java:110-113):
// ❌ BUG:inputMessages = memory 历史快照 + 用户新输入的混合视图
// Supervisor 历史里必然有大量历史 ToolResultBlock,导致 userProvidedResults 永远为 true
boolean userProvidedResults =
inputMessages.stream().anyMatch(m -> m.hasContentBlocks(ToolResultBlock.class));
if (userProvidedResults) {
return Mono.just(event); // ← 永远走这里,patchPendingToolCalls() 从未被调用
}
原因:AgentBase.notifyPreCall() 在构建 PreCallEvent 时,将完整的 memory 历史快照 + 用户新消息合并为一个列表传入:
// AgentBase.java:672-678
List fullInput = new ArrayList<>(snapshot); // 包含所有历史消息(含历史 ToolResultBlock)
if (callArgs != null) {
fullInput.addAll(callArgs);
}
PreCallEvent event = new PreCallEvent(this, fullInput); // 传给所有 Hook
Hook 的本意是判断当前用户输入(callArgs)是否携带了 HITL 手动补充的 tool result。但实际扫描的是混合列表,只要历史里有任何一条 ToolResultBlock(多轮 Supervisor 对话必然存在),userProvidedResults 就永远为 true,修复逻辑被永久短路。
后果:enablePendingToolRecovery(true) 在多轮会话场景下完全失效,ReActAgent.doCall() 必然抛出 IllegalStateException,会话永久不可用。
---
三、故障链路
用户发起请求
↓
Supervisor 调用 SubAgent 工具(search_agent)
↓
【Bug 1】工具执行超时(PT30M)
→ 超时异常逃逸 onErrorResume
→ ToolResultBlock.error() 未写入 memory
→ memory 中留下孤立 ToolUseBlock(无对应 Result)
↓
用户发起下一轮请求
↓
AgentBase.notifyPreCall() 触发 PendingToolRecoveryHook
↓
【Bug 2】Hook 扫描混合 inputMessages(含历史 ToolResultBlock)
→ userProvidedResults 误判为 true
→ patchPendingToolCalls() 跳过,孤立 ToolUseBlock 未修复
↓
ReActAgent.doCall() 检测到 pendingIds 非空
↓
抛出 IllegalStateException
会话永久不可用(直到手动清除 session)
---
四、修复建议
Bug 1 修复
在 executeAll() 的 Mono 组装阶段捕获同步异常,保证任何情况下都能返回合法的 ToolResultBlock:
// ToolExecutor.java:296-302 — 修改后
List> monos =
toolCalls.stream()
.map(toolCall -> {
try {
return executeWithInfrastructure(toolCall, executionConfig, agent, agentContext);
} catch (Exception e) {
logger.warn("Tool call assembly failed: {}", toolCall.getName(), e);
return Mono.just(
ToolResultBlock.error("Tool execution failed: " + e.getMessage())
.withIdAndName(toolCall.getId(), toolCall.getName()));
}
})
.toList();
同时建议排查 applyTimeout 是否存在在 Mono 组装期同步抛出的路径,确保超时错误始终在 Reactive 链内传播。
Bug 2 修复
只检查用户新输入(callArgs 部分),排除 memory 历史快照中的 ASSISTANT/TOOL 消息:
// PendingToolRecoveryHook.java:110-113 — 修改后
// 只有当前轮次用户新输入中携带 ToolResultBlock(HITL 场景),才跳过自动修复
// ASSISTANT / TOOL 角色消息来自 memory 历史,不属于当前轮次用户输入
boolean userProvidedResults = inputMessages.stream()
.filter(m -> m.getRole() != MsgRole.ASSISTANT && m.getRole() != MsgRole.TOOL)
.anyMatch(m -> m.hasContentBlocks(ToolResultBlock.class));
更优雅的方案是在 PreCallEvent 上暴露 snapshotSize 或 getCallArgs() 方法,让 Hook 能精确获取 callArgs 边界,避免依赖角色过滤这一间接手段。
---
贡献指南
评估
这个 Issue 还没有评估数据。