Paired: GitHub #356 ↔ GitCode #188
来自 agent-solution 仓 smartresearch-alpha(agent-core-java 0.1.16 消费者,真 LLM e2e 实测背景)。
涉及分支:930(0.1.16 血统)。
标题:[Feature] ReActAgent 运行级 deadline:墙钟总预算、每次调用向下夹逼、soft→hard 两段收尾
1. 问题
即使有了 per-call timeout(Issue A),maxIterations × per-call 仍无总上界:以 smartresearch-alpha 为例,三相位 ReAct 各 ≤120/60 迭代 × 单调用上界 1200s ⇒ 名义运行上界 40+ 小时。真 LLM 实测中单次任务 10–90 分钟,目前只能靠部署外层的墙钟 timeout 进程级 kill 兜底——无法优雅收尾,任务已写的中间工件状态不可控,消费者也无法在框架内回答「这次调用还剩多少预算」。
具体三个缺口:
- 总预算:引擎没有「这次 run 最多跑多久」的概念;
- 向下传播:每次模型调用与工具执行拿不到「剩余预算」,无法做
min(per-call, remaining) 夹逼;
- 优雅收尾:预算耗尽只有硬中断一种结局——模型没有「知道快到点了、把手头结论写完」的机会。
2. 建议设计(三层,默认关闭零破坏)
① 总预算入口(Builder/引擎配置)
ReActAgent agent = new ReActAgent(card);
// 墙钟总预算(二选一语义均可,建议 Duration 为主、Instant 便于对齐外部期限)
agent.configureDeadline(java.time.Duration.ofMinutes(90));
// 可选:软线比例,默认 0.2
agent.configureSoftDeadlineRatio(0.2);
② 向下夹逼传播(ReAct 主循环内,每次迭代重算)
循环头:remaining = deadline − now
callModel 传给 client 的 callTimeout = min(配置的 per-call, remaining) // 依赖 Issue A 的配置口
remaining ≤ 0 → 不再派发新迭代,进入 hard 收尾
③ soft → hard 两段收尾
soft(一次性):remaining < ratio × budget 且未通知过 → 向对话注入一条 system/tool 消息
(建议常量:"Time budget nearly exhausted; finalize with what you have.")
——给模型最后一轮收敛机会(写终稿/摘要),此后照常迭代直至自然结束或 hard
hard:remaining ≤ 0 → 抛 AgentDeadlineExceededException
(建议继承 java.util.concurrent.TimeoutException,语义现成)
——fail-fast:已产生的对话与外部工件保持可读,重试/部分交付由调用方决定
④(可选第二步)工具侧可见性:ToolCallInputs(或工具执行上下文)增加 deadlineRemaining()——长退避类工具(如限流检索的 30s×N 退避 sleep)可自行夹逼 min(backoff, remaining),避免预算耗尽前「睡死」。
3. 兼容性
- 不配置 deadline = 现行为逐字节不变(默认关闭;这是本提案的硬约束);
- soft 通知是消息注入——对无状态工具无影响;消息文本收在常量里,便于消费者禁用/替换/本地化;
- hard 异常是新类型,不与既有异常混淆;建议 hard 后再 invoke 抛
IllegalStateException(与一次性任务的 fail-fast 模式一致);
- Issue A 的 per-call 配置是②夹逼公式的前置,但本 Issue 可独立先行(per-call 暂用全局配置值参与 min)。
4. 测试建议(消费者视角的最小验证集)
- 无 deadline 对照:stub 模型 N 轮迭代,行为/消息序列与现版本零差异;
- soft 恰一次:极短预算下软通知只注入一次(第二次迭代起不再注入);
- 夹逼断言:per-call=10s、remaining=1s 时传到 client 的 timeout=1s(Recording 断言参数);
- hard 语义:超限抛新异常、已写工件可读、再次 invoke 抛 IllegalStateException。
5. 开源先例
- langgraph(MIT):图级
recursion_limit 是迭代数总量上界;TimeoutPolicy 提供 run_timeout(总墙钟)+ idle_timeout(进度停滞)双闸——「总量上界 + 停滞检测」的公开组合设计;
- OpenAI codex(codex-rs,Apache-2.0):重试睡眠与握手预算夹逼(
timeout(remaining, sleep(delay)) 形态——退避永远不越过剩余预算);会话级超时旋钮原生配置;
- openclaw:turn 级超时在任务预算内夹逼计算(
min(perTurnMs, deadline − started))——与本提案②的公式同形。
6. 不含(明确出界)
工具实现内部的退避策略本身(各工具自治);分布式/跨进程 deadline 传播(gRPC-style context cancellation 是更重的方案,若本 Issue 落地后再评估)。
(关联:per-call timeout 配置化 Issue A 随后提交,为本文夹逼公式的前置,可独立评审。)
Paired: GitHub #356 ↔ GitCode #188
标题:
[Feature] ReActAgent 运行级 deadline:墙钟总预算、每次调用向下夹逼、soft→hard 两段收尾1. 问题
即使有了 per-call timeout(Issue A),
maxIterations × per-call仍无总上界:以 smartresearch-alpha 为例,三相位 ReAct 各 ≤120/60 迭代 × 单调用上界 1200s ⇒ 名义运行上界 40+ 小时。真 LLM 实测中单次任务 10–90 分钟,目前只能靠部署外层的墙钟 timeout 进程级 kill 兜底——无法优雅收尾,任务已写的中间工件状态不可控,消费者也无法在框架内回答「这次调用还剩多少预算」。具体三个缺口:
min(per-call, remaining)夹逼;2. 建议设计(三层,默认关闭零破坏)
① 总预算入口(Builder/引擎配置)
② 向下夹逼传播(ReAct 主循环内,每次迭代重算)
③ soft → hard 两段收尾
④(可选第二步)工具侧可见性:
ToolCallInputs(或工具执行上下文)增加deadlineRemaining()——长退避类工具(如限流检索的 30s×N 退避 sleep)可自行夹逼min(backoff, remaining),避免预算耗尽前「睡死」。3. 兼容性
IllegalStateException(与一次性任务的 fail-fast 模式一致);4. 测试建议(消费者视角的最小验证集)
5. 开源先例
recursion_limit是迭代数总量上界;TimeoutPolicy 提供run_timeout(总墙钟)+idle_timeout(进度停滞)双闸——「总量上界 + 停滞检测」的公开组合设计;timeout(remaining, sleep(delay))形态——退避永远不越过剩余预算);会话级超时旋钮原生配置;min(perTurnMs, deadline − started))——与本提案②的公式同形。6. 不含(明确出界)
工具实现内部的退避策略本身(各工具自治);分布式/跨进程 deadline 传播(gRPC-style context cancellation 是更重的方案,若本 Issue 落地后再评估)。
(关联:per-call timeout 配置化 Issue A 随后提交,为本文夹逼公式的前置,可独立评审。)