From aefbc7f727c76f9a8d452be9187d0af03482f00a Mon Sep 17 00:00:00 2001 From: chengluyu <2239547+chengluyu@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:38:19 +0800 Subject: [PATCH] fix(agent-core): suppress the spurious error event for step-capped goal turns --- .changeset/goal-step-cap-error-banner.md | 5 +++++ packages/agent-core/src/agent/turn/index.ts | 9 ++++++++- packages/agent-core/test/agent/turn.test.ts | 9 +++++++++ packages/agent-core/test/harness/goal-session.test.ts | 3 +++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/goal-step-cap-error-banner.md diff --git a/.changeset/goal-step-cap-error-banner.md b/.changeset/goal-step-cap-error-banner.md new file mode 100644 index 0000000000..a591f0e80d --- /dev/null +++ b/.changeset/goal-step-cap-error-banner.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Stop showing a spurious error banner when a goal turn reaches the per-turn step limit (`loop_control.max_steps_per_turn`) during autonomous goal pursuit; the limit is expected control flow there and still surfaces for ordinary turns. diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 11e20b054b..c204941235 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -633,7 +633,14 @@ export class TurnFlow { inputData: { errorType: summary.name, errorMessage: summary.message }, }); ended = { type: 'turn.ended', turnId, reason: 'failed', error: summary, durationMs: Date.now() - startedAt }; - errorEvent = { type: 'error', ...summary }; + // A goal-driven turn cut by the per-turn step limit is expected + // control flow — the goal driver immediately continues with a fresh + // continuation turn — so it must not raise the standalone error event + // that hosts render as an actionable failure. `turn.ended` and the + // `turn.interrupted` loop event still report the cap. + if (!(isMaxStepsExceededError(error) && this.agent.goal.getActiveGoal() !== null)) { + errorEvent = { type: 'error', ...summary }; + } if (this.shouldTrackApiError(turnId)) { const classification = classifyApiError(error, summary); const properties: Record = { diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index 6e4c46de0a..d4be501d3c 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -1875,6 +1875,15 @@ describe('Agent turn flow', () => { }), }), ); + // Without an active goal the cap is a user-facing failure: the standalone + // error event (suppressed for goal-driven turns) is emitted right after + // turn.ended. + expect(ctx.newEvents()).toContainEqual( + expect.objectContaining({ + event: 'error', + args: expect.objectContaining({ code: 'loop.max_steps_exceeded' }), + }), + ); }); describe('loop control env overrides', () => { diff --git a/packages/agent-core/test/harness/goal-session.test.ts b/packages/agent-core/test/harness/goal-session.test.ts index 68be35ec42..5073adda8a 100644 --- a/packages/agent-core/test/harness/goal-session.test.ts +++ b/packages/agent-core/test/harness/goal-session.test.ts @@ -523,6 +523,9 @@ describe('goal session end-to-end', () => { ErrorCodes.LOOP_MAX_STEPS_EXCEEDED, ); expect(maxStepEnds).toHaveLength(2); + // Capped goal turns are expected control flow: no standalone error event + // is emitted for hosts to render as an actionable failure. + expect(events.filter((event) => event['type'] === 'error')).toEqual([]); // The cap never pauses or blocks the goal: no stopped status is ever // emitted, and the goal completes (record cleared) in the third turn. expect(