Skip to content

Commit 152d84f

Browse files
0xslineclaude
andcommitted
fix(agent): keep the model reply when a server run ends with a failed tool
When a tool failed and the model then answered, the server executor still marked the run failed and threw the tracker's English "I couldn't complete the requested operation" template, which landed under a Chinese reply. The browser runtimes dropped that template in 66af696 for exactly this reason: the model saw the failed result and replied to it, and the failed call stays visible on its tool card. It also made every documented fallback impossible to complete — probe_media failing and finalize_uploaded_asset proceeding with ingest defaults — because only a successful retry of the same tool resolves a failure. turnDisposition no longer takes a failure flag; the tracker still records outcomes but never decides the terminal status. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 0e175a8 commit 152d84f

5 files changed

Lines changed: 32 additions & 23 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
2929
- **`probe_media` now runs the ffprobe bundled with the app instead of the e2b cloud sandbox** — on a machine without an E2B key the tool used to fail with "e2b sandbox is not configured" even though import, previews and export QA were already probing locally. Uploads and bundled assets are read in place; a public URL is fetched through the SSRF-safe transport into a temp file first.
3030
**`probe_media` 改用应用自带的 ffprobe,不再依赖 e2b 云沙箱**——没有配置 E2B key 的机器上,这个工具之前会报「e2b sandbox is not configured」,而导入、预览、导出质检其实早就在本地跑 ffprobe 了。上传文件和内置素材直接就地读取;公开 URL 先经过防 SSRF 的抓取落到临时文件再探测。
3131

32+
- **A server-side agent run no longer ends with "I couldn't complete the requested operation" under the model's own reply** — when a tool failed and the model then answered, the run was still marked failed and an English template was appended beneath a Chinese answer. The reply is the outcome; the failed call stays visible on its tool card, matching the in-browser runtime. This also lets the documented fallbacks (probe fails → finalize with ingest defaults) complete.
33+
**服务端 Agent 运行不再在模型回复下方追加「I couldn't complete the requested operation」**——工具失败后模型已经作答,整轮却仍被判为失败,并在中文回复下面贴一段英文模板。现在以模型的回复为准,失败的调用仍在其工具卡片上可见,与浏览器端运行时一致;也让文档约定的降级路径(探测失败 → 用导入默认值 finalize)能正常完成。
34+
3235
- **Media-pool ratio badges snap to the ratio people actually name** — a 427×240 trailer read "427:240" because the badge reduced the exact pixel fraction; encoders round to codec-friendly sizes, so a frame within 2% of 16:9, 4:3, 1:1, 3:2, 5:4 or 21:9 (and their portrait forms) now shows that name, a small exact fraction such as 7:5 stays, and anything else shows a proportion like 2.40:1. Canvas sizes are exact and unchanged.
3336
**素材池比例角标按人们常说的比例显示**——427×240 的预告片之前显示「427:240」,因为角标直接约分了像素分数;编码器会把尺寸凑成编码友好的数值,所以现在与 16:9、4:3、1:1、3:2、5:4、21:9(及其竖版)偏差在 2% 内的都显示该名称,7:5 这类小分数保留,其余显示为 2.40:1 这样的比例。画布尺寸精确,不受影响。
3437

‎server/agent-runs/acceptance-loop.ts‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,19 @@ export function acceptanceInstructions(enabled: boolean): string {
8686
: '';
8787
}
8888

89-
export type TurnDisposition = 'continue' | 'completed' | 'failed' | 'max-tokens';
90-
export function turnDisposition(
91-
hitMaxTokens: boolean,
92-
continued: boolean,
93-
hasUnresolvedFailure = false,
94-
): TurnDisposition {
95-
if (hitMaxTokens) return hasUnresolvedFailure ? 'failed' : 'max-tokens';
96-
if (continued) return 'continue';
97-
return hasUnresolvedFailure ? 'failed' : 'completed';
89+
export type TurnDisposition = 'continue' | 'completed' | 'max-tokens';
90+
/**
91+
* A turn that ends without tool calls is complete even when a tool failed earlier in
92+
* the run. The model saw the failed result in its own context and replied to it, so
93+
* its reply — in the interface language — is the outcome; the failed call stays
94+
* visible on its own tool card. This used to return 'failed' and the executor then
95+
* threw the tracker's English "I couldn't complete the requested operation" template
96+
* underneath the model's reply, which is exactly what the browser runtimes stopped
97+
* doing (66af696b). It also broke every documented fallback: probe_media failing and
98+
* finalize_uploaded_asset proceeding with ingest defaults could never complete, since
99+
* only a successful retry of the same tool resolves a failure.
100+
*/
101+
export function turnDisposition(hitMaxTokens: boolean, continued: boolean): TurnDisposition {
102+
if (hitMaxTokens) return 'max-tokens';
103+
return continued ? 'continue' : 'completed';
98104
}

‎server/agent-runs/browser-tool.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ActivationState {
2222
current: ToolActivation;
2323
tail: Promise<void>;
2424
followupText: string | null;
25+
/** Records tool outcomes for the run; it never decides the terminal status (see turnDisposition). */
2526
toolFailures: ToolFailureTracker;
2627
acceptance: AcceptanceLoopState;
2728
repeatGuardNote?: string;

‎server/agent-runs/executor.ts‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -348,18 +348,11 @@ async function executeRunTurns(
348348
return;
349349
}
350350
messages = outcome.messages;
351-
const disposition = turnDisposition(
352-
outcome.hitMaxTokens,
353-
outcome.continued,
354-
plan.activation.toolFailures.hasUnresolved,
355-
);
351+
const disposition = turnDisposition(outcome.hitMaxTokens, outcome.continued);
356352
if (disposition === 'continue') continue;
357353
if (disposition === 'max-tokens') {
358354
pushRunEvent(run, 'max-tokens', { turn: turn + 1 });
359355
}
360-
if (disposition === 'failed') {
361-
throw new Error(plan.activation.toolFailures.report());
362-
}
363356
if (disposition === 'completed') {
364357
const acceptance = decideAcceptanceAfterTurn(plan.activation.acceptance);
365358
plan.activation.acceptance = acceptance.state;

‎server/agent-runs/executor.verify.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from 'node:assert/strict';
2+
import { readFileSync } from 'node:fs';
23
import { APICallError } from 'ai';
34
import { ASK_MODE_TOOL_SCHEMAS } from '../../src/agent/ask-mode-tools';
45
import { TOOL_SCHEMAS } from '../../src/agent/tools';
@@ -326,14 +327,19 @@ console.log('server agent executor message verification passed');
326327
// instead of feeding truncated text back into the next turn.
327328
assert.equal(turnDisposition(false, true), 'continue');
328329
assert.equal(turnDisposition(false, false), 'completed');
329-
assert.equal(turnDisposition(false, true, true), 'continue',
330-
'an unresolved tool failure may continue only while the model is retrying');
331-
assert.equal(turnDisposition(false, false, true), 'failed',
332-
'completion is rejected while a tool failure remains unresolved');
333-
assert.equal(turnDisposition(true, true, true), 'failed',
334-
'a token cutoff cannot turn an unresolved tool failure into completion');
335330
assert.equal(turnDisposition(true, true), 'max-tokens', 'output cutoff wins over pending tool calls');
336331
assert.equal(turnDisposition(true, false), 'max-tokens');
332+
// An unresolved tool failure is not a disposition input: the model already saw the
333+
// failed result and replied to it, and that reply is the run's outcome. Failing the run
334+
// here put the tracker's English template under a Chinese reply (a probe_media that
335+
// could not run, then a complete answer, then "I couldn't complete the requested
336+
// operation"), and made the documented probe→finalize fallback impossible to complete.
337+
assert.equal(turnDisposition.length, 2, 'the disposition takes no failure flag');
338+
assert.doesNotMatch(
339+
readFileSync(new URL('./executor.ts', import.meta.url), 'utf8'),
340+
/toolFailures\.report\(\)/,
341+
'the executor never surfaces the failure-report template',
342+
);
337343

338344
console.log('server executor turn-disposition checks passed');
339345

0 commit comments

Comments
 (0)