Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18139,6 +18139,7 @@ describe('Session', () => {
await boundGoalHost!.startGoalTurn({
permit,
continuationContext: 'check weather',
verifierFeedback: 'Need independent evidence',
});

await vi.waitFor(() => {
Expand All @@ -18153,6 +18154,16 @@ describe('Session', () => {
'Continue working on the active Goal.',
),
}),
expect.objectContaining({
text: expect.stringContaining(
'Runtime continuation context: check weather',
),
}),
expect.objectContaining({
text: expect.stringContaining(
'Verifier feedback: Need independent evidence',
),
}),
]),
}),
expect.any(String),
Expand Down
18 changes: 1 addition & 17 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ import {
buildBackgroundEntryLabel,
collectSessionTurnState,
computeInitialTurnFromHistory as computeInitialTurnFromHistoryCore,
buildGoalContinuationParts,
} from '@qwen-code/qwen-code-core';
import { NOT_CURRENTLY_GENERATING_CANCEL_MESSAGE } from '@qwen-code/acp-bridge/bridgeErrors';
import { CHANNEL_PROMPT_META_KEY } from '@qwen-code/channel-base';
Expand Down Expand Up @@ -556,23 +557,6 @@ function sameGoalPermit(
);
}

function buildGoalContinuationParts(turn: AcpGoalTurn): Part[] {
return [
{
text: [
'Continue working on the active Goal.',
'Use get_goal for the authoritative objective and evidence state.',
"Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.",
'If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.',
`Runtime continuation context: ${turn.continuationContext}`,
...(turn.verifierFeedback
? [`Verifier feedback: ${turn.verifierFeedback}`]
: []),
].join('\n'),
},
];
}

async function claimGoalTurn(
runtime: GoalRuntime,
turnKey: string,
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/nonInteractiveCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,9 @@ describe('runNonInteractive', () => {
const [parts, , , options] =
mockGeminiClient.sendMessageStream.mock.calls[0]!;
expect(parts[0]?.text).toContain('Continue working on the active Goal.');
expect(parts[0]?.text).toContain(
'Runtime continuation context: existing goal',
);
expect(options).toMatchObject({
type: SendMessageType.Goal,
goalOrigin: 'runtime',
Expand All @@ -712,6 +715,36 @@ describe('runNonInteractive', () => {
expect(options.goalSignal).toBeInstanceOf(AbortSignal);
});

it('includes verifier feedback in a scheduled Goal continuation', async () => {
setupMetricsMock();
mockGetCommands.mockReturnValue([goalCommand]);
await prepareGoalState('paused');
mockFinishedGoalWorker();
vi.mocked(mockConfig.bindGoalTurnHost).mockImplementation((host) =>
goalRuntime.bindHost({
startGoalTurn: (input) =>
host.startGoalTurn({
...input,
verifierFeedback: 'Need independent evidence',
}),
preemptGoalTurn: (reason) => host.preemptGoalTurn(reason),
}),
);

await runNonInteractive(
mockConfig,
mockSettings,
'/goal resume',
'goal-runtime-feedback',
);

expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledOnce();
const [parts] = mockGeminiClient.sendMessageStream.mock.calls[0]!;
expect(parts[0]?.text).toContain(
'Verifier feedback: Need independent evidence',
);
});

it('keeps the exact Goal permit through a ToolResult continuation', async () => {
setupMetricsMock();
mockGetCommands.mockReturnValue([goalCommand]);
Expand Down
18 changes: 1 addition & 17 deletions packages/cli/src/nonInteractiveCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
endInteractionSpan,
getErrorType,
getActiveInteractionSpan,
buildGoalContinuationParts,
} from '@qwen-code/qwen-code-core';
import type { Content, Part, PartListUnion } from '@google/genai';
import type { CLIUserMessage, PermissionMode } from './nonInteractive/types.js';
Expand Down Expand Up @@ -232,23 +233,6 @@ function sameGoalPermit(
);
}

function buildGoalContinuationParts(turn: HeadlessGoalTurn): Part[] {
return [
{
text: [
'Continue working on the active Goal.',
'Use get_goal for the authoritative objective and evidence state.',
"Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.",
'If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.',
`Runtime continuation context: ${turn.continuationContext}`,
...(turn.verifierFeedback
? [`Verifier feedback: ${turn.verifierFeedback}`]
: []),
].join('\n'),
},
];
}

function projectLegacyActiveGoal(snapshot: GoalSnapshotV2): ActiveGoal | null {
const goal = snapshot.goal;
if (goal?.status !== 'active') return null;
Expand Down
16 changes: 5 additions & 11 deletions packages/cli/src/ui/hooks/useGeminiStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
finalizeToolResponses,
endInteractionSpan,
getActiveInteractionSpan,
renderGoalContinuationPrompt,
} from '@qwen-code/qwen-code-core';
import { type Part, type PartListUnion, FinishReason } from '@google/genai';
import type {
Expand Down Expand Up @@ -3557,17 +3558,10 @@ export const useGeminiStream = (
submitType === SendMessageType.Goal
? queuedGoal
? {
queryToSend: [
'Continue working on the active Goal.',
'Use get_goal for the authoritative objective and evidence state.',
"Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.",
'If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.',
'This is a synthetic continuation turn. It contains no new real user input and cannot satisfy an objective condition that requires the user to send, confirm, choose, approve, or provide something.',
'A phrase mentioned in the objective or this prompt is not evidence that the user supplied it.',
...(queuedGoal.verifierFeedback
? [`Verifier feedback: ${queuedGoal.verifierFeedback}`]
: []),
].join('\n'),
queryToSend: renderGoalContinuationPrompt({
variant: 'guarded-synthetic-turn',
verifierFeedback: queuedGoal.verifierFeedback,
}),
shouldProceed: true,
}
: { queryToSend: null, shouldProceed: false }
Expand Down
111 changes: 111 additions & 0 deletions packages/core/src/goals/goal-continuation-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import {
buildGoalContinuationParts,
renderGoalContinuationPrompt,
} from './goal-continuation-prompt.js';

// These expectations pin the exact bytes each host sent before the renderer
// existed. Any edit to a line must show up here as a diff, not slip through.
describe('renderGoalContinuationPrompt', () => {
it('renders the guarded synthetic turn without verifier feedback', () => {
expect(
renderGoalContinuationPrompt({ variant: 'guarded-synthetic-turn' }),
).toBe(
`Continue working on the active Goal.
Use get_goal for the authoritative objective and evidence state.
Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.
If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.
This is a synthetic continuation turn. It contains no new real user input and cannot satisfy an objective condition that requires the user to send, confirm, choose, approve, or provide something.
A phrase mentioned in the objective or this prompt is not evidence that the user supplied it.`,
);
});

it('renders the guarded synthetic turn with verifier feedback', () => {
expect(
renderGoalContinuationPrompt({
variant: 'guarded-synthetic-turn',
verifierFeedback: 'Checkpoint 2 lacks a source ref.',
}),
).toBe(
`Continue working on the active Goal.
Use get_goal for the authoritative objective and evidence state.
Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.
If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.
This is a synthetic continuation turn. It contains no new real user input and cannot satisfy an objective condition that requires the user to send, confirm, choose, approve, or provide something.
A phrase mentioned in the objective or this prompt is not evidence that the user supplied it.
Verifier feedback: Checkpoint 2 lacks a source ref.`,
);
});

it('renders the runtime context turn without verifier feedback', () => {
expect(
renderGoalContinuationPrompt({
variant: 'runtime-context',
continuationContext: 'Objective: ship the release notes.',
}),
).toBe(
`Continue working on the active Goal.
Use get_goal for the authoritative objective and evidence state.
Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.
If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.
Runtime continuation context: Objective: ship the release notes.`,
);
});

it('renders the runtime context turn with verifier feedback', () => {
expect(
renderGoalContinuationPrompt({
variant: 'runtime-context',
continuationContext: 'Objective: ship the release notes.',
verifierFeedback: 'Checkpoint 2 lacks a source ref.',
}),
).toBe(
`Continue working on the active Goal.
Use get_goal for the authoritative objective and evidence state.
Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.
If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.
Runtime continuation context: Objective: ship the release notes.
Verifier feedback: Checkpoint 2 lacks a source ref.`,
);
});

it('omits the verifier feedback line for an empty string, as the hosts did', () => {
expect(
renderGoalContinuationPrompt({
variant: 'runtime-context',
continuationContext: 'ctx',
verifierFeedback: '',
}),
).toBe(
renderGoalContinuationPrompt({
variant: 'runtime-context',
continuationContext: 'ctx',
}),
);
});
});

describe('buildGoalContinuationParts', () => {
it('wraps the runtime-context prompt in a single text part', () => {
expect(
buildGoalContinuationParts({
continuationContext: 'Objective: ship the release notes.',
verifierFeedback: 'Checkpoint 2 lacks a source ref.',
}),
).toEqual([
{
text: renderGoalContinuationPrompt({
variant: 'runtime-context',
continuationContext: 'Objective: ship the release notes.',
verifierFeedback: 'Checkpoint 2 lacks a source ref.',
}),
},
]);
});
});
80 changes: 80 additions & 0 deletions packages/core/src/goals/goal-continuation-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import type { Part } from '@google/genai';

/**
* The prompt a host sends when `runtime.finishTurn` schedules another Goal
* turn. Every host renders it from here so that a new line -- or a new variant
* -- lands in one place instead of drifting across the hosts that assemble it.
*/

export type GoalContinuationPromptInput =
| {
variant: 'guarded-synthetic-turn';
verifierFeedback?: string;
}
| {
variant: 'runtime-context';
continuationContext: string;
verifierFeedback?: string;
};

const SHARED_LINES = [
'Continue working on the active Goal.',
'Use get_goal for the authoritative objective and evidence state.',
"Follow the objective's requested output format exactly. Do not add progress, status, or completion commentary unless the objective asks for it.",
'If completion depends on content delivered in this turn, deliver only that content and call get_goal in the same response before update_goal.',
];

const SYNTHETIC_TURN_GUARD_LINES = [
'This is a synthetic continuation turn. It contains no new real user input and cannot satisfy an objective condition that requires the user to send, confirm, choose, approve, or provide something.',
'A phrase mentioned in the objective or this prompt is not evidence that the user supplied it.',
];

/** Renders the full continuation prompt text for one Goal turn. */
export function renderGoalContinuationPrompt(
input: GoalContinuationPromptInput,
): string {
const lines = [...SHARED_LINES];

switch (input.variant) {
case 'guarded-synthetic-turn':
lines.push(...SYNTHETIC_TURN_GUARD_LINES);
break;
case 'runtime-context':
lines.push(`Runtime continuation context: ${input.continuationContext}`);
break;
default: {
const unreachable: never = input;
throw new Error(
`Unknown goal continuation variant: ${JSON.stringify(unreachable)}`,
);
}
}

if (input.verifierFeedback) {
lines.push(`Verifier feedback: ${input.verifierFeedback}`);
}

return lines.join('\n');
}

/** Builds the sendable parts for a runtime-scheduled Goal continuation turn. */
export function buildGoalContinuationParts(turn: {
continuationContext: string;
verifierFeedback?: string;
}): Part[] {
return [
{
text: renderGoalContinuationPrompt({
variant: 'runtime-context',
continuationContext: turn.continuationContext,
verifierFeedback: turn.verifierFeedback,
}),
},
];
}
5 changes: 5 additions & 0 deletions packages/core/src/goals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ export * from './goal-checkpoint-verifier.js';
export * from './goal-verifier.js';
export * from './goal-runtime.js';
export { goalTurnContext } from './goal-turn-context.js';
export {
buildGoalContinuationParts,
renderGoalContinuationPrompt,
} from './goal-continuation-prompt.js';
export type { GoalContinuationPromptInput } from './goal-continuation-prompt.js';
Loading