fix(core): actionable tool-failure feedback to break agent retry loops - #111
Open
Zi-Yi-Ming wants to merge 5 commits into
Open
fix(core): actionable tool-failure feedback to break agent retry loops#111Zi-Yi-Ming wants to merge 5 commits into
Zi-Yi-Ming wants to merge 5 commits into
Conversation
基线实测 FM1:模型幻觉顶层工具名(如 read_file)后,原报错只说 'not registered',不给可用工具信息,导致 7+ 轮无效重试循环。 - 顶层调用失败时列出真实可见工具 + Code Mode 嵌套绑定,命中相近 名字时直接给出 tools.<id>(...) 的正确调用方式 - Levenshtein<=2 + fuzzy>=60 双通道最近匹配,覆盖换位/漏分隔符拼写 - nested 作用域单独措辞,避免在沙箱内误导为顶层调用 验证:core 298 测试全绿;T4a 陷阱任务待重测
验证顶层幻觉调用返回 exec 内 tools.<id>(...) 路由指引、嵌套 幻觉调用返回真实嵌套工具清单与 Did-you-mean 建议
基线实测 FM2:模型失败后误诊为超时问题,指数级抬高 timeout_ms (2e10 → 2.4e37 → 3.6e76),每次只变一个数字就生成新指纹,完美 绕过 REPEATED_TOOL_CALL 拦截,600s 烧掉 1480 万 tokens。 - 指纹归一化时把 |数值| > 1e9 的参数折叠为占位符:合法参数 (timeout/offset/limit)远低于阈值不受影响,爆炸数值全部落进 同一指纹,被既有 repeatedToolCallLimit 正常拦截 - 拦截报错明确告知'只改数值参数也会被抑制',直接矫正误诊回路 - 指纹逻辑迁移至 agent/tool-fingerprint.ts 并补 8 个单测
基线实测 FM3:脚本内工具失败只显示 ✗ 标记(丢弃 summary 里的 失败原因),且整体 ok:true + 模糊 Diagnostic,导致模型把命令 失败误诊为超时(FM2 灾难的直接诱因)。 - ✗ 行追加失败调用的真实 summary(错误码/stderr 摘要) - completed summary 如实计数失败调用(1 failed tool call) - 无返回值 Diagnostic 在有失败时明确排除超时误诊方向 - data 增加结构化 failedToolCalls 计数 - renderCellResult 导出并补 6 个单测
There was a problem hiding this comment.
Pull request overview
This PR improves the agent’s recovery behavior around tool failures by making unknown-tool errors actionable (especially in Code Mode), making repeated-call suppression resilient to runaway numeric argument escalation, and surfacing nested tool-call failures from the exec sandbox more clearly for the model.
Changes:
- Add actionable
UNKNOWN_TOOLmessaging that lists available tools and provides Code Mode nested-call guidance + did-you-mean hints. - Normalize tool-call fingerprints to collapse excessively large numeric arguments so exponential “timeout_ms escalation” triggers
REPEATED_TOOL_CALL. - Improve Code Mode
execcell rendering to include per-call failure reasons, failure counts in the summary, and structuredfailedToolCallsobservability.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/tools/runtime.ts | Adds richer unknown-tool error formatting with tool lists and Code Mode nested-binding guidance. |
| packages/core/src/tools/runtime-unknown-tool.test.ts | New unit tests for unknown-tool messaging and formatting behavior. |
| packages/core/src/tools/code-mode/service.ts | Enhances exec cell rendering to surface nested tool failures and adds failedToolCalls to result data. |
| packages/core/src/tools/code-mode/service.test.ts | New tests validating the updated exec cell rendering and diagnostics. |
| packages/core/src/agent/tool-fingerprint.ts | New fingerprint/normalization helper to collapse huge numeric args and stabilize JSON ordering. |
| packages/core/src/agent/tool-fingerprint.test.ts | New tests verifying huge-number collapsing and stable normalization. |
| packages/core/src/agent/agent-loop.ts | Switches repeated-call detection to the new fingerprint helper and updates the block message. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+29
to
+33
| if (typeof value === "number") { | ||
| return Number.isFinite(value) && Math.abs(value) > HUGE_NUMBER_THRESHOLD | ||
| ? HUGE_NUMBER_PLACEHOLDER | ||
| : value; | ||
| } |
| error: { | ||
| code: "REPEATED_TOOL_CALL", | ||
| message: `Attempt ${attempts} exceeded repeatedToolCallLimit=${limit}. Adjust arguments or produce a final response.`, | ||
| message: `Attempt ${attempts} exceeded repeatedToolCallLimit=${limit}. This call keeps being suppressed even when only numeric arguments change (e.g. a larger timeout). Diagnose why the identical call keeps failing, switch to a different approach, or produce a final response.`, |
…tes the updated PR body
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #112
Motivation
We ran a 5-task baseline benchmark against
step-3.7-flash(392 chat requests / 15.9M prompt tokens, recorded through a local logging proxy) and found three failure loops in the agent's error-recovery path. All failures came from multi-step tool orchestration; single-step tasks were fine. This PR fixes the three feedback gaps that caused them.1.
UNKNOWN_TOOLerrors give the model nothing to recover withWith Code Mode active, the top-level tools are only
exec+wait, but the model's prior is Claude-Code-style top-level tools. When it called a hallucinated top-levelread_file, the error was justTool 'read_file' is not registered— the model retried hallucinated names for 13+ rounds, and one run never recovered within its 90s budget.Fix: the error now lists the real visible tools and all Code Mode nested bindings; when the requested name is close (Levenshtein ≤ 2 or fuzzy score ≥ 60), it states the exact call shape —
call exec and use tools.read_file(...) inside it. Nested-scope errors list the real nested tools with a did-you-mean hint.2. Numeric-argument escalation evades
REPEATED_TOOL_CALLAfter
run_commandfailures the model misdiagnosed timeouts and escalatedtimeout_msexponentially (2e10 → 2.4e37 → 3.6e76). Each call produced a fresh fingerprint (arguments hashed verbatim), so the repeated-call guard never fired — one runaway run consumed 600s / 305 requests / ~14.8M prompt tokens before being killed.Fix: fingerprint normalization collapses numeric arguments with |v| > 1e9 into a single placeholder. Legitimate values (timeouts, offsets, limits) sit far below the threshold, while escalated values all collapse into one fingerprint and the existing
repeatedToolCallLimitfires. The block message also states explicitly that numeric-only changes keep being suppressed.3. exec sandbox feedback hides tool failures
A script whose nested tool calls failed still returned
ok: truewithScript completed; failures were shown only as bare✗marks, and the recorded failuresummary(exit codes, stderr) was dropped from the rendered output. The model could not distinguish "script returned nothing" from "tool failed" from "timed out", which directly fed failure mode 2.Fix:
✗lines now carry the failure reason; the summary counts failures (Script completed · 2 tool calls, 1 failed tool call); the no-result diagnostic explicitly rules out timeouts when tool calls failed;data.failedToolCallsis added for observability.Verification
pnpm checkclean (oxlint, dependency-cruiser guardrails, knip, tsc, prettier)import→require→ nested tools) and no repeated callsHonest boundary
These fixes make recovery fast; they do not prevent the first hallucinated call. That is a model instruction-following limit which we mitigate separately on the prompt side.
中文说明
对
step-3.7-flash做了 5 类任务的基线实测(392 次请求 / 约 1590 万 prompt tokens,经本地记录代理),发现 agent 纠错回路有三类失败模式——失败全部集中在多步工具编排场景,单步任务正常。本 PR 修复导致这三类失败的反馈缺口:UNKNOWN_TOOL报错没有给模型任何恢复信息——Code Mode 下顶层只有exec+wait,模型按 Claude Code 习惯调用顶层read_file后,原报错只说"未注册",实测最多 13+ 轮无效重试。现在报错会列出真实工具清单与全部嵌套绑定,名字相近时直接给出tools.read_file(...)的正确调用方式;嵌套作用域的报错列出真实嵌套工具并给出最近匹配建议。数值微调逃逸重复调用检测——命令失败后模型误诊为超时,指数级抬高
timeout_ms(2e10 → 2.4e37 → 3.6e76),每次都生成新指纹绕过REPEATED_TOOL_CALL拦截,一次失控运行烧掉 600 秒 / 305 请求 / 约 1480 万 prompt tokens。现在指纹归一化把 |数值| > 1e9 的参数折叠为占位符——合法参数(timeout/offset/limit)远低于阈值不受影响,爆炸数值全部落进同一指纹被既有上限正常拦截;拦截文案同时明确"只改数值参数也会被抑制"。exec 沙盒反馈隐藏工具失败——脚本内嵌套工具失败仍返回
ok: true+Script completed,失败原因(exit code/stderr)被记录但没渲染进模型可见的输出。模型无法区分"脚本没返回值""工具失败""命令超时",这直接喂养了第 2 类误诊。现在✗行带失败原因,summary 如实计数失败调用,无返回值 Diagnostic 在有失败时明确排除超时方向,并新增data.failedToolCalls结构化计数。验证:core 314 个测试全绿(新增 23 个),
pnpm check全过;端到端重跑原失败任务——定位检索任务从 90s 超时变为 47s 完成且零无效重试,精确编辑任务从 30 次调用触发熔断变为 9 次收敛、两次脚本失败均凭新反馈正确归因、无重复调用。诚实边界
这些修复让"踩雷后"的恢复变快,但不能阻止首次幻觉调用——那是模型指令遵循的固有限制,我们在 prompt 侧单独缓解。