From 2a3bf9a7cfc5b0968383c4d8de0c943e30565c2a Mon Sep 17 00:00:00 2001 From: chengluyu <2239547+chengluyu@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:32:13 +0800 Subject: [PATCH 1/4] fix(agent-core): continue goal pursuit when a goal turn hits the per-turn step limit --- .changeset/goal-max-steps-continue.md | 5 ++ .../src/agent/goal/goalService.ts | 51 +++++++++++-- .../test/agent/goal/goal.test.ts | 31 +++++++- packages/agent-core/src/agent/turn/index.ts | 34 ++++++++- .../test/harness/goal-session.test.ts | 74 +++++++++++++++++++ 5 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 .changeset/goal-max-steps-continue.md diff --git a/.changeset/goal-max-steps-continue.md b/.changeset/goal-max-steps-continue.md new file mode 100644 index 0000000000..ff33430f99 --- /dev/null +++ b/.changeset/goal-max-steps-continue.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix goal pursuit being interrupted when a goal turn reaches the per-turn step limit (`loop_control.max_steps_per_turn`); the limit now splits goal work into more continuation turns instead of pausing the goal. diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts index 0048bcb364..3ef9682359 100644 --- a/packages/agent-core-v2/src/agent/goal/goalService.ts +++ b/packages/agent-core-v2/src/agent/goal/goalService.ts @@ -59,6 +59,7 @@ import { type EnqueueReceipt, } from '#/agent/loop/loop'; import { LOOP_CONTROL_SECTION, type LoopControl } from '#/agent/loop/configSection'; +import { LoopErrors } from '#/agent/loop/errors'; import { ContinuationStepRequest, MessageStepRequest } from '#/agent/loop/stepRequest'; import { IAgentSystemReminderService } from '#/agent/systemReminder/systemReminder'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; @@ -176,6 +177,20 @@ const GOAL_CONTINUATION_PROMPT = [ 'leaving the goal active. Do not ask the user for input unless a real blocker prevents progress.', ].join(' '); +/** + * Variant of {@link GOAL_CONTINUATION_PROMPT} used when the previous goal turn + * ended by hitting the per-turn step limit (`loop_control.max_steps_per_turn`). + * The limit fragments goal work into more continuation turns instead of + * pausing the goal; the notice tells the model why, so it can size the next + * slice to fit the limit. + */ +const GOAL_STEP_CAP_CONTINUATION_PROMPT = [ + 'The previous goal turn reached the per-turn step limit before finishing its work,', + 'so a new turn was started for you. Pick up where that turn stopped and keep each', + 'slice of work small enough to fit the limit.', + GOAL_CONTINUATION_PROMPT, +].join(' '); + interface GoalForkNoticeState { readonly goalPresent: boolean; readonly reminderPending: boolean; @@ -840,10 +855,17 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { return; } if (goalId === undefined || lifecycleGoalId === undefined) return; + // A turn that failed only by reaching the per-turn step limit ended at a + // clean step boundary, so it is not a goal failure: skip the abnormal-turn + // settlement (which would pause the goal) and continue with a fresh + // continuation turn that tells the model why. Goal budgets below still + // bound the pursuit. + const stepCapped = isMaxStepsTurnFailure(result); if ( - result.reason === 'blocked' || - result.reason === 'cancelled' || - result.reason === 'failed' + !stepCapped && + (result.reason === 'blocked' || + result.reason === 'cancelled' || + result.reason === 'failed') ) { await this.settleAbnormalTurn(result, lifecycleGoalId); return; @@ -853,7 +875,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { const state = this.goalState; if (state === null || state.status !== 'active' || state.goalId !== lifecycleGoalId) return; if (this.blockIfBudgetReached(state) !== null) return; - this.launchContinuationTurn(lifecycleGoalId); + this.launchContinuationTurn(lifecycleGoalId, stepCapped); } private clearTurnTracking( @@ -913,12 +935,17 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { } catch {} } - private launchContinuationTurn(goalId: string): void { + private launchContinuationTurn(goalId: string, stepCapped = false): void { if (!this.isActiveGoal(goalId)) return; if (this.pendingContinuation !== undefined) return; const message: ContextMessage = { role: 'user', - content: [{ type: 'text', text: GOAL_CONTINUATION_PROMPT }], + content: [ + { + type: 'text', + text: stepCapped ? GOAL_STEP_CAP_CONTINUATION_PROMPT : GOAL_CONTINUATION_PROMPT, + }, + ], toolCalls: [], origin: GOAL_CONTINUATION_ORIGIN, }; @@ -1273,6 +1300,18 @@ function isTerminalUpdateGoalResult( return status === 'complete' || status === 'blocked'; } +/** + * True when a turn failed only by reaching the per-turn step limit + * (`loop_control.max_steps_per_turn`). Such a turn ended at a clean step + * boundary, so the goal driver continues the goal instead of pausing it. + */ +function isMaxStepsTurnFailure(result: Pick): boolean { + return ( + result.reason === 'failed' && + normalizeGoalErrorPayload(result.error).code === LoopErrors.codes.LOOP_MAX_STEPS_EXCEEDED + ); +} + function goalFailurePauseReason(error: unknown): string { const payload = normalizeGoalErrorPayload(error); switch (payload.code) { diff --git a/packages/agent-core-v2/test/agent/goal/goal.test.ts b/packages/agent-core-v2/test/agent/goal/goal.test.ts index 9827a91c7f..2b9380c7bb 100644 --- a/packages/agent-core-v2/test/agent/goal/goal.test.ts +++ b/packages/agent-core-v2/test/agent/goal/goal.test.ts @@ -16,7 +16,14 @@ import { IGoalDeadlineScheduler } from '#/agent/goal/goalDeadlineScheduler'; import { type AgentGoalService } from '#/agent/goal/goalService'; import { UpdateGoalToolInputSchema } from '#/agent/tools/goal/update-goal/update-goal'; import { UpdateGoalTool } from '#/agent/tools/goal/update-goal/updateGoalTool'; -import { IAgentLoopService, type AfterStepContext, type EnqueueReceipt, type Step, type Turn } from '#/agent/loop/loop'; +import { + createMaxStepsExceededError, + IAgentLoopService, + type AfterStepContext, + type EnqueueReceipt, + type Step, + type Turn, +} from '#/agent/loop/loop'; import { MessageStepRequest } from '#/agent/loop/stepRequest'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; import { IAgentSwarmService } from '#/agent/swarm/swarm'; @@ -1545,6 +1552,28 @@ describe('AgentGoalService core workflow hooks', () => { expect(loopService.launches).toEqual([]); }); + it('continues the goal when a goal turn hits the per-turn step limit', async () => { + await goals.createGoal({ objective: 'finish the task' }); + + const turn = makeTurn(4); + eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN }); + await runGoalStep(loopService, turn); + endTurn(eventBus, turn, { reason: 'failed', error: createMaxStepsExceededError(1) }); + + // The step cap must not pause the goal: a fresh continuation is launched + // and its prompt explains why a new turn was started. + expect(goals.getGoal().goal).toMatchObject({ status: 'active', turnsUsed: 1 }); + expect(loopService.launches).toHaveLength(1); + expect(loopService.drainNextBatch(context)).toBeDefined(); + expect(context.get().at(-1)?.origin).toEqual({ + kind: 'system_trigger', + name: 'goal_continuation', + }); + const prompt = JSON.stringify(context.get().at(-1)?.content); + expect(prompt).toContain('per-turn step limit'); + expect(prompt).toContain('Pick up where that turn stopped'); + }); + it('blocks active goals when the user prompt hook blocks the turn', async () => { await goals.createGoal({ objective: 'finish the task' }); diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 0e112594eb..94a45c1d86 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -121,6 +121,20 @@ const GOAL_CONTINUATION_PROMPT = [ 'leaving the goal active. Do not ask the user for input unless a real blocker prevents progress.', ].join(' '); +/** + * Variant of {@link GOAL_CONTINUATION_PROMPT} used when the previous goal turn + * ended by hitting the per-turn step limit (`loop_control.max_steps_per_turn`). + * The limit fragments goal work into more continuation turns instead of + * pausing the goal; the notice tells the model why, so it can size the next + * slice to fit the limit. + */ +const GOAL_STEP_CAP_CONTINUATION_PROMPT = [ + 'The previous goal turn reached the per-turn step limit before finishing its work,', + 'so a new turn was started for you. Pick up where that turn stopped and keep each', + 'slice of work small enough to fit the limit.', + GOAL_CONTINUATION_PROMPT, +].join(' '); + export class TurnFlow { private steerBuffer: BufferedSteer[] = []; private turnId = -1; @@ -438,7 +452,9 @@ export class TurnFlow { * full turn, then reads the goal status the model set via `UpdateGoal`: * `complete` (the record is cleared) / `blocked` stop the loop; `active` * (the model didn't decide) re-injects the goal reminder and runs the - * next continuation turn. Aborted or failed turns pause the goal. Goal-state + * next continuation turn. Aborted or failed turns pause the goal — except a + * turn that only failed by reaching the per-turn step limit, which just + * fragments goal work into more continuation turns. Goal-state * blockers, such as explicit `UpdateGoal('blocked')`, prompt-hook blocks, and * budget limits, block it (all resumable). Returns the final turn's result. */ @@ -470,7 +486,14 @@ export class TurnFlow { await this.agent.goal.pauseOnInterrupt({ reason: 'Paused after interruption' }); return end; } - if (end.event.reason === 'failed') { + // A turn that failed only by reaching the per-turn step limit ended at a + // clean step boundary, so it is not a goal failure: fall through to the + // normal continuation decision below and keep pursuing the goal. The + // `turn.ended` event still reports the failure (and the limit) to hosts. + const hitStepCap = + end.event.reason === 'failed' && + end.event.error?.code === ErrorCodes.LOOP_MAX_STEPS_EXCEEDED; + if (end.event.reason === 'failed' && !hitStepCap) { await this.agent.goal.pauseActiveGoal({ reason: goalFailurePauseReason(end.event.error) }); return end; } @@ -495,7 +518,12 @@ export class TurnFlow { } turnId = this.allocateTurnId(); - turnInput = [{ type: 'text', text: GOAL_CONTINUATION_PROMPT }]; + turnInput = [ + { + type: 'text', + text: hitStepCap ? GOAL_STEP_CAP_CONTINUATION_PROMPT : GOAL_CONTINUATION_PROMPT, + }, + ]; turnOrigin = GOAL_CONTINUATION_ORIGIN; } } diff --git a/packages/agent-core/test/harness/goal-session.test.ts b/packages/agent-core/test/harness/goal-session.test.ts index 5b27ea829e..85ac2bf2c6 100644 --- a/packages/agent-core/test/harness/goal-session.test.ts +++ b/packages/agent-core/test/harness/goal-session.test.ts @@ -475,6 +475,80 @@ describe('goal session end-to-end', () => { expect(agent.context.history.at(-1)?.role).toBe('tool'); }); + it('continues the goal when a goal turn hits maxStepsPerTurn', async () => { + const sessionDir = await makeTempDir(); + const events: Array> = []; + const { session, agent, scripted } = await setupSession( + sessionDir, + events, + ['GetGoal', 'UpdateGoal'], + undefined, + undefined, + { providers: {}, loopControl: { maxStepsPerTurn: 1 } }, + ); + const api = new SessionAPIImpl(session); + await api.createGoal({ agentId: 'main', objective: 'work' }); + + // Each of the first two turns spends its single allowed step on a tool + // call and is then cut by the step cap; the goal must keep going. The + // third turn completes the goal within the cap. + scripted.mockNextResponse({ + type: 'function', + id: 'check-1', + name: 'GetGoal', + arguments: '{}', + }); + scripted.mockNextResponse({ + type: 'function', + id: 'check-2', + name: 'GetGoal', + arguments: '{}', + }); + scripted.mockNextResponse({ + type: 'function', + id: 'complete', + name: 'UpdateGoal', + arguments: JSON.stringify({ status: 'complete' }), + }); + + agent.turn.prompt([{ type: 'text', text: 'work' }]); + await agent.turn.waitForCurrentTurn(); + + expect(scripted.calls).toHaveLength(3); + const maxStepEnds = events.filter( + (event) => + event['type'] === 'turn.ended' && + event['reason'] === 'failed' && + (event['error'] as Record | undefined)?.['code'] === + ErrorCodes.LOOP_MAX_STEPS_EXCEEDED, + ); + expect(maxStepEnds).toHaveLength(2); + // 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( + events.filter( + (event) => + event['type'] === 'goal.updated' && + ['paused', 'blocked'].includes( + String((event['snapshot'] as Record | null)?.['status']), + ), + ), + ).toEqual([]); + expect((await api.getGoal({ agentId: 'main' })).goal).toBeNull(); + const completion = events.find( + (event) => + event['type'] === 'goal.updated' && + (event['change'] as Record | undefined)?.['kind'] === 'completion', + ); + expect((completion?.['change'] as Record)?.['stats']).toMatchObject({ + turnsUsed: 3, + }); + // Capped continuation turns are told why a new turn was started. + const history = JSON.stringify(agent.context.history); + expect(history).toContain('per-turn step limit'); + expect(history).toContain('Pick up where that turn stopped'); + }); + it('pauses the goal on provider rate limits', async () => { const sessionDir = await makeTempDir(); const events: Array> = []; From 3c3f3cef31071c05b2e76fffd979b071f08eb489 Mon Sep 17 00:00:00 2001 From: chengluyu <2239547+chengluyu@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:37:44 +0800 Subject: [PATCH 2/4] chore(agent-core-v2): regenerate state manifest --- packages/agent-core-v2/docs/state-manifest.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent-core-v2/docs/state-manifest.d.ts b/packages/agent-core-v2/docs/state-manifest.d.ts index 128a9878d7..e26afd4492 100644 --- a/packages/agent-core-v2/docs/state-manifest.d.ts +++ b/packages/agent-core-v2/docs/state-manifest.d.ts @@ -1002,7 +1002,7 @@ export interface AgentStateSnapshot { 'llmRequester.lastConfigLogSignature': string | undefined; 'llmRequester.mediaDegradedTurns': Set; 'llmRequester.mediaStrippedTurns': Map; 'llmRequester.turnConfigs': Map Date: Mon, 27 Jul 2026 00:19:21 +0800 Subject: [PATCH 3/4] fix(agent-core): start goal pursuit when the goal-creating turn hits the step limit --- .../src/agent/goal/goalService.ts | 20 +----- packages/agent-core/src/agent/turn/index.ts | 31 ++++++++-- .../test/harness/goal-session.test.ts | 61 +++++++++++++++++++ 3 files changed, 88 insertions(+), 24 deletions(-) diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts index 3ef9682359..a21c568783 100644 --- a/packages/agent-core-v2/src/agent/goal/goalService.ts +++ b/packages/agent-core-v2/src/agent/goal/goalService.ts @@ -17,7 +17,8 @@ * Injects reminders through * `contextInjector`, drives continuation turns by enqueueing `newTurn` * `StepRequest`s onto `loop` (the continuation message materializes when the - * loop pops it), accounts live + * loop pops it; a goal turn cut by the per-turn step limit is followed by a + * step-cap variant of that message instead of pausing the goal), accounts live * turn usage through `usage`, observes terminal goal tool results through * `toolExecutor`, writes system reminders through `systemReminder`, reports * telemetry through `telemetry`, and checks main-agent eligibility through @@ -177,13 +178,6 @@ const GOAL_CONTINUATION_PROMPT = [ 'leaving the goal active. Do not ask the user for input unless a real blocker prevents progress.', ].join(' '); -/** - * Variant of {@link GOAL_CONTINUATION_PROMPT} used when the previous goal turn - * ended by hitting the per-turn step limit (`loop_control.max_steps_per_turn`). - * The limit fragments goal work into more continuation turns instead of - * pausing the goal; the notice tells the model why, so it can size the next - * slice to fit the limit. - */ const GOAL_STEP_CAP_CONTINUATION_PROMPT = [ 'The previous goal turn reached the per-turn step limit before finishing its work,', 'so a new turn was started for you. Pick up where that turn stopped and keep each', @@ -855,11 +849,6 @@ export class AgentGoalService extends Disposable implements IAgentGoalService { return; } if (goalId === undefined || lifecycleGoalId === undefined) return; - // A turn that failed only by reaching the per-turn step limit ended at a - // clean step boundary, so it is not a goal failure: skip the abnormal-turn - // settlement (which would pause the goal) and continue with a fresh - // continuation turn that tells the model why. Goal budgets below still - // bound the pursuit. const stepCapped = isMaxStepsTurnFailure(result); if ( !stepCapped && @@ -1300,11 +1289,6 @@ function isTerminalUpdateGoalResult( return status === 'complete' || status === 'blocked'; } -/** - * True when a turn failed only by reaching the per-turn step limit - * (`loop_control.max_steps_per_turn`). Such a turn ended at a clean step - * boundary, so the goal driver continues the goal instead of pausing it. - */ function isMaxStepsTurnFailure(result: Pick): boolean { return ( result.reason === 'failed' && diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 94a45c1d86..11e20b054b 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -418,11 +418,15 @@ export class TurnFlow { // instead of stopping after the turn that merely started it. (The // already-active case took the early return above.) const goalBecameActive = this.agent.goal.getGoal().goal?.status === 'active'; + // The same per-turn-step-limit exemption as the driver's continuation + // loop: a turn that failed only at the step cap does not block the + // handoff — pursuit starts with a fresh continuation turn (told why). + const hitStepCap = isMaxStepsTurnFailure(end); if ( goalBecameActive && end.event.reason !== 'cancelled' && - end.event.reason !== 'failed' && - end.event.reason !== 'blocked' + end.event.reason !== 'blocked' && + (end.event.reason !== 'failed' || hitStepCap) ) { // The ordinary turn created or resumed the goal, so it counts as the // first active goal turn before the continuation driver takes over. @@ -433,7 +437,12 @@ export class TurnFlow { } return await this.driveGoal( this.allocateTurnId(), - [{ type: 'text', text: GOAL_CONTINUATION_PROMPT }], + [ + { + type: 'text', + text: hitStepCap ? GOAL_STEP_CAP_CONTINUATION_PROMPT : GOAL_CONTINUATION_PROMPT, + }, + ], GOAL_CONTINUATION_ORIGIN, signal, ); @@ -490,9 +499,7 @@ export class TurnFlow { // clean step boundary, so it is not a goal failure: fall through to the // normal continuation decision below and keep pursuing the goal. The // `turn.ended` event still reports the failure (and the limit) to hosts. - const hitStepCap = - end.event.reason === 'failed' && - end.event.error?.code === ErrorCodes.LOOP_MAX_STEPS_EXCEEDED; + const hitStepCap = isMaxStepsTurnFailure(end); if (end.event.reason === 'failed' && !hitStepCap) { await this.agent.goal.pauseActiveGoal({ reason: goalFailurePauseReason(end.event.error) }); return end; @@ -1297,6 +1304,18 @@ function hasStepBudgetRemaining(maxSteps: number | undefined, currentStep: numbe return maxSteps === undefined || maxSteps <= 0 || currentStep < maxSteps; } +/** + * True when a turn ended `failed` only because it reached the per-turn step + * limit (`loop_control.max_steps_per_turn`). Such a turn stopped at a clean + * step boundary, so goal pursuit continues instead of pausing. + */ +function isMaxStepsTurnFailure(end: TurnEndResult): boolean { + return ( + end.event.reason === 'failed' && + end.event.error?.code === ErrorCodes.LOOP_MAX_STEPS_EXCEEDED + ); +} + function isTerminalUpdateGoalResult( toolName: string, args: unknown, diff --git a/packages/agent-core/test/harness/goal-session.test.ts b/packages/agent-core/test/harness/goal-session.test.ts index 85ac2bf2c6..68be35ec42 100644 --- a/packages/agent-core/test/harness/goal-session.test.ts +++ b/packages/agent-core/test/harness/goal-session.test.ts @@ -549,6 +549,67 @@ describe('goal session end-to-end', () => { expect(history).toContain('Pick up where that turn stopped'); }); + it('starts pursuing a goal created mid-turn even when that turn hits maxStepsPerTurn', async () => { + const sessionDir = await makeTempDir(); + const events: Array> = []; + const { session, agent, scripted } = await setupSession( + sessionDir, + events, + ['CreateGoal', 'UpdateGoal'], + undefined, + undefined, + { providers: {}, loopControl: { maxStepsPerTurn: 1 } }, + ); + const api = new SessionAPIImpl(session); + + // No goal at turn start: the model creates one on the only allowed step, + // the turn is then cut by the cap, and pursuit must still begin — with a + // continuation turn that explains the cap. + scripted.mockNextResponse({ + type: 'function', + id: 'create', + name: 'CreateGoal', + arguments: JSON.stringify({ objective: 'work' }), + }); + scripted.mockNextResponse({ + type: 'function', + id: 'complete', + name: 'UpdateGoal', + arguments: JSON.stringify({ status: 'complete' }), + }); + + agent.turn.prompt([{ type: 'text', text: 'work' }]); + await agent.turn.waitForCurrentTurn(); + + expect(scripted.calls).toHaveLength(2); + expect(events).toContainEqual( + expect.objectContaining({ + type: 'turn.ended', + reason: 'failed', + error: expect.objectContaining({ code: ErrorCodes.LOOP_MAX_STEPS_EXCEEDED }), + }), + ); + expect( + events.filter( + (event) => + event['type'] === 'goal.updated' && + ['paused', 'blocked'].includes( + String((event['snapshot'] as Record | null)?.['status']), + ), + ), + ).toEqual([]); + expect((await api.getGoal({ agentId: 'main' })).goal).toBeNull(); + const completion = events.find( + (event) => + event['type'] === 'goal.updated' && + (event['change'] as Record | undefined)?.['kind'] === 'completion', + ); + expect((completion?.['change'] as Record)?.['stats']).toMatchObject({ + turnsUsed: 2, + }); + expect(JSON.stringify(agent.context.history)).toContain('per-turn step limit'); + }); + it('pauses the goal on provider rate limits', async () => { const sessionDir = await makeTempDir(); const events: Array> = []; From 085b8e13a014072b99653a0618423b1e74d5cc16 Mon Sep 17 00:00:00 2001 From: chengluyu <2239547+chengluyu@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:25:27 +0800 Subject: [PATCH 4/4] refactor(agent-core-v2): drop step-cap narration from goal module and test --- packages/agent-core-v2/src/agent/goal/goalService.ts | 3 +-- packages/agent-core-v2/test/agent/goal/goal.test.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts index a21c568783..5c38e1b97f 100644 --- a/packages/agent-core-v2/src/agent/goal/goalService.ts +++ b/packages/agent-core-v2/src/agent/goal/goalService.ts @@ -17,8 +17,7 @@ * Injects reminders through * `contextInjector`, drives continuation turns by enqueueing `newTurn` * `StepRequest`s onto `loop` (the continuation message materializes when the - * loop pops it; a goal turn cut by the per-turn step limit is followed by a - * step-cap variant of that message instead of pausing the goal), accounts live + * loop pops it), accounts live * turn usage through `usage`, observes terminal goal tool results through * `toolExecutor`, writes system reminders through `systemReminder`, reports * telemetry through `telemetry`, and checks main-agent eligibility through diff --git a/packages/agent-core-v2/test/agent/goal/goal.test.ts b/packages/agent-core-v2/test/agent/goal/goal.test.ts index 2b9380c7bb..8c59fd7931 100644 --- a/packages/agent-core-v2/test/agent/goal/goal.test.ts +++ b/packages/agent-core-v2/test/agent/goal/goal.test.ts @@ -1560,8 +1560,6 @@ describe('AgentGoalService core workflow hooks', () => { await runGoalStep(loopService, turn); endTurn(eventBus, turn, { reason: 'failed', error: createMaxStepsExceededError(1) }); - // The step cap must not pause the goal: a fresh continuation is launched - // and its prompt explains why a new turn was started. expect(goals.getGoal().goal).toMatchObject({ status: 'active', turnsUsed: 1 }); expect(loopService.launches).toHaveLength(1); expect(loopService.drainNextBatch(context)).toBeDefined();