Skip to content

fix(core): actionable tool-failure feedback to break agent retry loops - #111

Open
Zi-Yi-Ming wants to merge 5 commits into
stepfun-ai:mainfrom
Zi-Yi-Ming:fix/tool-call-failure-feedback
Open

fix(core): actionable tool-failure feedback to break agent retry loops#111
Zi-Yi-Ming wants to merge 5 commits into
stepfun-ai:mainfrom
Zi-Yi-Ming:fix/tool-call-failure-feedback

Conversation

@Zi-Yi-Ming

@Zi-Yi-Ming Zi-Yi-Ming commented Aug 30, 2026

Copy link
Copy Markdown

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_TOOL errors give the model nothing to recover with

With 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-level read_file, the error was just Tool '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_CALL

After run_command failures the model misdiagnosed timeouts and escalated timeout_ms exponentially (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 repeatedToolCallLimit fires. 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: true with Script completed; failures were shown only as bare marks, and the recorded failure summary (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.failedToolCalls is added for observability.

Verification

  • 314 core tests green (23 new), full pnpm check clean (oxlint, dependency-cruiser guardrails, knip, tsc, prettier)
  • End-to-end re-run of the failing benchmark tasks: a locate-and-explain task that previously timed out now completes in 47s with 20 tool calls and zero wasted retries; a precise-edit task that previously hit the 30-call circuit breaker now converges in 9 calls, with two script errors correctly diagnosed from the new feedback (importrequire → nested tools) and no repeated calls

Honest 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 修复导致这三类失败的反馈缺口:

  1. UNKNOWN_TOOL 报错没有给模型任何恢复信息——Code Mode 下顶层只有 exec+wait,模型按 Claude Code 习惯调用顶层 read_file 后,原报错只说"未注册",实测最多 13+ 轮无效重试。现在报错会列出真实工具清单与全部嵌套绑定,名字相近时直接给出 tools.read_file(...) 的正确调用方式;嵌套作用域的报错列出真实嵌套工具并给出最近匹配建议。

  2. 数值微调逃逸重复调用检测——命令失败后模型误诊为超时,指数级抬高 timeout_ms(2e10 → 2.4e37 → 3.6e76),每次都生成新指纹绕过 REPEATED_TOOL_CALL 拦截,一次失控运行烧掉 600 秒 / 305 请求 / 约 1480 万 prompt tokens。现在指纹归一化把 |数值| > 1e9 的参数折叠为占位符——合法参数(timeout/offset/limit)远低于阈值不受影响,爆炸数值全部落进同一指纹被既有上限正常拦截;拦截文案同时明确"只改数值参数也会被抑制"。

  3. exec 沙盒反馈隐藏工具失败——脚本内嵌套工具失败仍返回 ok: true + Script completed,失败原因(exit code/stderr)被记录但没渲染进模型可见的输出。模型无法区分"脚本没返回值""工具失败""命令超时",这直接喂养了第 2 类误诊。现在 行带失败原因,summary 如实计数失败调用,无返回值 Diagnostic 在有失败时明确排除超时方向,并新增 data.failedToolCalls 结构化计数。

验证:core 314 个测试全绿(新增 23 个),pnpm check 全过;端到端重跑原失败任务——定位检索任务从 90s 超时变为 47s 完成且零无效重试,精确编辑任务从 30 次调用触发熔断变为 9 次收敛、两次脚本失败均凭新反馈正确归因、无重复调用。

诚实边界

这些修复让"踩雷后"的恢复变快,但不能阻止首次幻觉调用——那是模型指令遵循的固有限制,我们在 prompt 侧单独缓解。

基线实测 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 个单测
Copilot AI lite review requested due to automatic review settings August 30, 2026 18:16
@github-actions github-actions Bot added the area/core packages/core label Aug 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_TOOL messaging 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 exec cell rendering to include per-call failure reasons, failure counts in the summary, and structured failedToolCalls observability.

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.`,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core packages/core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] agent loop 三类失败回路:UNKNOWN_TOOL 无恢复信息、数值微调逃逸重复检测、exec 反馈隐藏工具失败

2 participants