fix(agent-core-v2): restore UpdateGoal completion/blocked prompts and no-goal fallbacks

Align with v1: completing or blocking a goal now returns the dynamic
summary/blocked-reason prompt (buildGoalCompletionSummaryPrompt /
buildGoalBlockedReasonPrompt, already present in outcome-prompts.ts but
unused) instead of the static "Goal marked complete/blocked." text, and
all three statuses report the "no active/current goal" fallback when
there is nothing to transition.
This commit is contained in:
_Kerman 2026-07-10 14:39:27 +08:00
parent ff14e519c2
commit d9e411f5e7
2 changed files with 58 additions and 4 deletions

View file

@ -15,6 +15,10 @@ import type { BuiltinTool, ToolExecution } from '#/agent/tool/toolContract';
import { registerTool } from '#/agent/toolRegistry/toolContribution';
import { IAgentGoalService } from '#/agent/goal/goal';
import {
buildGoalBlockedReasonPrompt,
buildGoalCompletionSummaryPrompt,
} from './outcome-prompts';
import DESCRIPTION from './update-goal.md?raw';
export const UpdateGoalToolInputSchema = z
@ -54,16 +58,25 @@ export class UpdateGoalTool implements BuiltinTool<UpdateGoalToolInput> {
approvalRule: this.name,
execute: async () => {
if (status === 'active') {
if (currentGoal === null) {
return { output: 'Goal not resumed: no current goal.' };
}
await this.goal.resumeGoal({}, 'model');
return { output: 'Goal resumed.' };
}
if (status === 'complete') {
await this.goal.markComplete({}, 'model');
return { output: 'Goal marked complete.', stopTurn: true };
const completed = await this.goal.markComplete({}, 'model');
if (completed === null) {
return { output: 'Goal not completed: no active goal.' };
}
return { output: buildGoalCompletionSummaryPrompt(completed), stopTurn: true };
}
if (status === 'blocked') {
await this.goal.markBlocked({}, 'model');
return { output: 'Goal marked blocked.', stopTurn: true };
const blocked = await this.goal.markBlocked({}, 'model');
if (blocked === null) {
return { output: 'Goal not blocked: no active goal.' };
}
return { output: buildGoalBlockedReasonPrompt(blocked), stopTurn: true };
}
return {
isError: true,

View file

@ -117,6 +117,47 @@ describe('goal tools', () => {
expect(goals.getGoal().goal?.status).toBe('active');
});
it('UpdateGoal complete returns the completion summary prompt and stops the turn', async () => {
await goals.createGoal({ objective: 'ship it' });
const execution = updateGoalTool.resolveExecution({ status: 'complete' });
if (execution.isError === true) throw new Error('execution should not be an error');
const result = await execution.execute({ turnId: 0, toolCallId: 'call_c', signal });
expect(result.stopTurn).toBe(true);
expect(result.output).toContain('Goal completed successfully');
expect(result.output).toContain('Worked');
expect(result.output).toContain('Write a concise final message for the user');
});
it('UpdateGoal blocked returns the blocked-reason prompt and stops the turn', async () => {
await goals.createGoal({ objective: 'ship it' });
const execution = updateGoalTool.resolveExecution({ status: 'blocked' });
if (execution.isError === true) throw new Error('execution should not be an error');
const result = await execution.execute({ turnId: 0, toolCallId: 'call_b', signal });
expect(result.stopTurn).toBe(true);
expect(result.output).toContain('Goal blocked.');
expect(result.output).toContain('Worked');
expect(result.output).toContain('concrete blocker');
});
it('UpdateGoal reports no active goal when completing/blocking/resuming without one', async () => {
const done = updateGoalTool.resolveExecution({ status: 'complete' });
if (done.isError === true) throw new Error('execution should not be an error');
const doneResult = await done.execute({ turnId: 0, toolCallId: 'call_n1', signal });
expect(doneResult.output).toBe('Goal not completed: no active goal.');
const blocked = updateGoalTool.resolveExecution({ status: 'blocked' });
if (blocked.isError === true) throw new Error('execution should not be an error');
const blockedResult = await blocked.execute({ turnId: 0, toolCallId: 'call_n2', signal });
expect(blockedResult.output).toBe('Goal not blocked: no active goal.');
const resumed = updateGoalTool.resolveExecution({ status: 'active' });
if (resumed.isError === true) throw new Error('execution should not be an error');
const resumedResult = await resumed.execute({ turnId: 0, toolCallId: 'call_n3', signal });
expect(resumedResult.output).toBe('Goal not resumed: no current goal.');
});
async function countGoalTurn(turnId: number): Promise<void> {
const abortController = new AbortController();
eventBus.publish({ type: 'turn.started', turnId, origin: USER_PROMPT_ORIGIN });