fix: keep paired review prompts output-only

This commit is contained in:
ejclaw
2026-05-25 18:34:01 +09:00
parent 9219cf9c9d
commit 0ad33b62dd
2 changed files with 137 additions and 28 deletions

View File

@@ -1,12 +1,13 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
buildArbiterPromptForTask,
buildFinalizePendingPrompt, buildFinalizePendingPrompt,
buildOwnerPendingPrompt, buildOwnerPendingPrompt,
buildPairedTurnPrompt, buildPairedTurnPrompt,
buildReviewerPendingPrompt, buildReviewerPendingPrompt,
} from './message-runtime-prompts.js'; } from './message-runtime-prompts.js';
import type { NewMessage, PairedTurnOutput } from './types.js'; import type { NewMessage, PairedTask, PairedTurnOutput } from './types.js';
const CARRY_FORWARD_MARKER = const CARRY_FORWARD_MARKER =
'[Carried forward context from the previous task: latest owner final]'; '[Carried forward context from the previous task: latest owner final]';
@@ -27,6 +28,7 @@ function makeHumanMessage(content: string): NewMessage {
function makeTurnOutput( function makeTurnOutput(
outputText: string, outputText: string,
role: PairedTurnOutput['role'] = 'owner', role: PairedTurnOutput['role'] = 'owner',
overrides: Partial<PairedTurnOutput> = {},
): PairedTurnOutput { ): PairedTurnOutput {
return { return {
id: 1, id: 1,
@@ -35,6 +37,37 @@ function makeTurnOutput(
role, role,
output_text: outputText, output_text: outputText,
created_at: '2026-04-20T00:59:00.000Z', created_at: '2026-04-20T00:59:00.000Z',
...overrides,
};
}
function makeTask(overrides: Partial<PairedTask> = {}): PairedTask {
return {
id: 'task-1',
chat_jid: 'group@test',
group_folder: 'group',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
owner_agent_type: 'codex',
reviewer_agent_type: 'claude-code',
arbiter_agent_type: 'codex',
title: null,
source_ref: null,
plan_notes: null,
review_requested_at: null,
round_trip_count: 1,
owner_failure_count: 0,
owner_step_done_streak: 0,
finalize_step_done_count: 0,
task_done_then_user_reopen_count: 0,
empty_step_done_streak: 0,
status: 'review_ready',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: '2026-04-20T00:58:00.000Z',
updated_at: '2026-04-20T00:59:00.000Z',
...overrides,
}; };
} }
@@ -79,6 +112,26 @@ describe('message-runtime-prompts carry-forward guidance', () => {
); );
}); });
it('keeps reviewer pending prompts output-only when current task outputs exist', () => {
const prompt = buildReviewerPendingPrompt({
chatJid: 'group@test',
timezone: 'UTC',
turnOutputs: [
makeTurnOutput('TASK_DONE\n현재 owner 결과', 'owner', {
turn_number: 1,
created_at: '2026-04-20T02:00:00.000Z',
}),
],
recentHumanMessages: [
makeHumanMessage('과거 요청을 다시 기준으로 보면 안 됨'),
],
lastHumanMessage: '과거 요청을 다시 기준으로 보면 안 됨',
});
expect(prompt).toContain('현재 owner 결과');
expect(prompt).not.toContain('과거 요청을 다시 기준으로 보면 안 됨');
});
it('prepends a carry-forward warning to owner pending prompts', () => { it('prepends a carry-forward warning to owner pending prompts', () => {
const prompt = buildOwnerPendingPrompt({ const prompt = buildOwnerPendingPrompt({
chatJid: 'group@test', chatJid: 'group@test',
@@ -98,6 +151,75 @@ describe('message-runtime-prompts carry-forward guidance', () => {
); );
}); });
it('keeps owner pending prompts output-only when reviewer feedback exists', () => {
const prompt = buildOwnerPendingPrompt({
chatJid: 'group@test',
timezone: 'UTC',
turnOutputs: [
makeTurnOutput('TASK_DONE\n현재 owner 결과', 'owner', {
turn_number: 1,
}),
makeTurnOutput('DONE_WITH_CONCERNS\n현재 reviewer 피드백', 'reviewer', {
id: 2,
turn_number: 2,
}),
],
recentHumanMessages: [makeHumanMessage('이전 작업의 사용자 메시지')],
lastHumanMessage: '이전 작업의 사용자 메시지',
});
expect(prompt).toContain('현재 owner 결과');
expect(prompt).toContain('현재 reviewer 피드백');
expect(prompt).not.toContain('이전 작업의 사용자 메시지');
});
it('keeps arbiter prompts output-only when turn outputs exist', () => {
const prompt = buildArbiterPromptForTask({
task: makeTask({ status: 'arbiter_requested' }),
chatJid: 'group@test',
timezone: 'UTC',
turnOutputs: [
makeTurnOutput('TASK_DONE\nowner 주장', 'owner', {
turn_number: 1,
}),
makeTurnOutput('DONE_WITH_CONCERNS\nreviewer 반박', 'reviewer', {
id: 2,
turn_number: 2,
}),
],
recentMessages: [makeHumanMessage('예전 유저 지시')],
labeledRecentMessages: [makeHumanMessage('예전 유저 지시')],
});
expect(prompt).toContain('owner 주장');
expect(prompt).toContain('reviewer 반박');
expect(prompt).not.toContain('예전 유저 지시');
});
it('preserves turn output order instead of timestamp interleaving', () => {
const prompt = buildReviewerPendingPrompt({
chatJid: 'group@test',
timezone: 'UTC',
turnOutputs: [
makeTurnOutput('owner output first', 'owner', {
turn_number: 1,
created_at: '2026-04-20T02:00:00.000Z',
}),
makeTurnOutput('reviewer output second', 'reviewer', {
id: 2,
turn_number: 2,
created_at: '2026-04-20T01:00:00.000Z',
}),
],
recentHumanMessages: [],
lastHumanMessage: null,
});
expect(prompt.indexOf('owner output first')).toBeLessThan(
prompt.indexOf('reviewer output second'),
);
});
it('does not prepend the warning when there is no carried-forward turn output', () => { it('does not prepend the warning when there is no carried-forward turn output', () => {
const prompt = buildPairedTurnPrompt({ const prompt = buildPairedTurnPrompt({
taskId: 'task-1', taskId: 'task-1',

View File

@@ -79,6 +79,17 @@ export function buildPairedTurnPrompt(args: {
); );
} }
function turnOutputsOnlyPrompt(
chatJid: string,
timezone: string,
turnOutputs: PairedTurnOutput[],
): string {
return prependCarriedForwardGuidance(
formatMessages(turnOutputsToMessages(turnOutputs, chatJid), timezone),
turnOutputs,
);
}
export function buildReviewerPendingPrompt(args: { export function buildReviewerPendingPrompt(args: {
chatJid: string; chatJid: string;
timezone: string; timezone: string;
@@ -87,17 +98,7 @@ export function buildReviewerPendingPrompt(args: {
lastHumanMessage: string | null | undefined; lastHumanMessage: string | null | undefined;
}): string { }): string {
if (args.turnOutputs.length > 0) { if (args.turnOutputs.length > 0) {
return prependCarriedForwardGuidance( return turnOutputsOnlyPrompt(args.chatJid, args.timezone, args.turnOutputs);
formatMessages(
mergeHumanAndTurnOutputMessages(
args.chatJid,
args.recentHumanMessages,
args.turnOutputs,
),
args.timezone,
),
args.turnOutputs,
);
} }
if (!args.lastHumanMessage) { if (!args.lastHumanMessage) {
@@ -115,17 +116,7 @@ export function buildOwnerPendingPrompt(args: {
lastHumanMessage: string | null | undefined; lastHumanMessage: string | null | undefined;
}): string { }): string {
if (args.turnOutputs.length > 0) { if (args.turnOutputs.length > 0) {
return prependCarriedForwardGuidance( return turnOutputsOnlyPrompt(args.chatJid, args.timezone, args.turnOutputs);
formatMessages(
mergeHumanAndTurnOutputMessages(
args.chatJid,
args.recentHumanMessages,
args.turnOutputs,
),
args.timezone,
),
args.turnOutputs,
);
} }
if (!args.lastHumanMessage) { if (!args.lastHumanMessage) {
@@ -145,11 +136,7 @@ export function buildArbiterPromptForTask(args: {
}): string { }): string {
const messages = const messages =
args.turnOutputs.length > 0 args.turnOutputs.length > 0
? mergeHumanAndTurnOutputMessages( ? turnOutputsToMessages(args.turnOutputs, args.chatJid)
args.chatJid,
args.recentMessages.filter((message) => !message.is_bot_message),
args.turnOutputs,
)
: args.labeledRecentMessages; : args.labeledRecentMessages;
return buildArbiterContextPrompt({ return buildArbiterContextPrompt({