From 150206a6f7027879df954e26736b4baa5d336235 Mon Sep 17 00:00:00 2001 From: Luyu Cheng <2239547+chengluyu@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:51:29 +0800 Subject: [PATCH] fix: count goal creation turn (#1477) --- .changeset/count-created-goal-turn.md | 5 +++ packages/agent-core/src/agent/turn/index.ts | 7 +++ .../test/harness/goal-session.test.ts | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 .changeset/count-created-goal-turn.md diff --git a/.changeset/count-created-goal-turn.md b/.changeset/count-created-goal-turn.md new file mode 100644 index 000000000..8e9ba8ea2 --- /dev/null +++ b/.changeset/count-created-goal-turn.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Count the turn that starts an autonomous goal toward its goal turn usage. diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 95dbb68fa..d77d81029 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -370,6 +370,13 @@ export class TurnFlow { end.event.reason !== 'failed' && end.event.reason !== 'filtered' ) { + // The ordinary turn created or resumed the goal, so it counts as the + // first active goal turn before the continuation driver takes over. + const countedGoal = await this.agent.goal.incrementTurn(); + if (countedGoal?.budget.overBudget === true) { + await this.agent.goal.markBlocked({ reason: 'A configured budget was reached' }); + return end; + } return await this.driveGoal( this.allocateTurnId(), [{ type: 'text', text: GOAL_CONTINUATION_PROMPT }], diff --git a/packages/agent-core/test/harness/goal-session.test.ts b/packages/agent-core/test/harness/goal-session.test.ts index d2d5c0d14..4db52cf07 100644 --- a/packages/agent-core/test/harness/goal-session.test.ts +++ b/packages/agent-core/test/harness/goal-session.test.ts @@ -291,9 +291,53 @@ describe('goal session end-to-end', () => { ); const turnStarts = events.filter((e) => e['type'] === 'turn.started').length; expect(turnStarts).toBeGreaterThanOrEqual(2); + const completionEvent = events.find((event) => { + const change = event['change'] as Record | undefined; + return event['type'] === 'goal.updated' && change?.['kind'] === 'completion'; + }); + expect(completionEvent?.['change']).toMatchObject({ + kind: 'completion', + stats: expect.objectContaining({ turnsUsed: 2 }), + }); expect((await api.getGoal({ agentId: 'main' })).goal).toBeNull(); }); + it('does not start a synthetic continuation when creating the goal exhausts its turn budget', async () => { + const sessionDir = await makeTempDir(); + const events: Array> = []; + const { session, agent, scripted } = await setupSession(sessionDir, events, [ + 'CreateGoal', + 'SetGoalBudget', + ]); + const api = new SessionAPIImpl(session); + + scripted.mockNextResponse({ + type: 'function', + id: 'create', + name: 'CreateGoal', + arguments: JSON.stringify({ objective: 'work' }), + }); + scripted.mockNextResponse({ + type: 'function', + id: 'budget', + name: 'SetGoalBudget', + arguments: JSON.stringify({ value: 1, unit: 'turns' }), + }); + scripted.mockNextResponse({ type: 'text', text: 'Goal created with a one-turn budget.' }); + + agent.turn.prompt([{ type: 'text', text: 'Please start a one-turn goal' }]); + await agent.turn.waitForCurrentTurn(); + + const goal = (await api.getGoal({ agentId: 'main' })).goal; + expect(goal?.status).toBe('blocked'); + expect(goal?.turnsUsed).toBe(1); + expect(scripted.calls).toHaveLength(3); + expect(events.filter((e) => e['type'] === 'turn.started')).toHaveLength(1); + expect(JSON.stringify(agent.context.history)).not.toContain( + 'Continue working toward the active goal', + ); + }); + it('keeps the active turn alive (cancelable) while driving a goal created mid-turn', async () => { const sessionDir = await makeTempDir(); const events: Array> = [];