220 lines
6.7 KiB
TypeScript
220 lines
6.7 KiB
TypeScript
import { buildArbiterContextPrompt } from './arbiter-context.js';
|
|
import { formatMessages } from './router.js';
|
|
import type { NewMessage, PairedTask, PairedTurnOutput } from './types.js';
|
|
|
|
const CARRIED_FORWARD_OWNER_FINAL_MARKER =
|
|
'[Carried forward context from the previous task: latest owner final]';
|
|
|
|
const CARRIED_FORWARD_OWNER_FINAL_GUIDANCE = `System note:
|
|
If you see a message beginning with "${CARRIED_FORWARD_OWNER_FINAL_MARKER}", treat it as background only. Do not repeat, continue, or answer that carried-forward final directly. Respond only to the latest human request and the current task.`;
|
|
|
|
export interface PriorTaskPromptContext {
|
|
ownerFinal?: string | null;
|
|
reviewerFinal?: string | null;
|
|
}
|
|
|
|
function turnOutputsToMessages(
|
|
outputs: PairedTurnOutput[],
|
|
chatJid: string,
|
|
): NewMessage[] {
|
|
return outputs.map((turnOutput) => ({
|
|
id: `turn-${turnOutput.task_id}-${turnOutput.turn_number}`,
|
|
chat_jid: chatJid,
|
|
sender: turnOutput.role,
|
|
sender_name: turnOutput.role,
|
|
content: turnOutput.output_text,
|
|
timestamp: turnOutput.created_at,
|
|
is_bot_message: true as const,
|
|
is_from_me: false as const,
|
|
}));
|
|
}
|
|
|
|
function mergeHumanAndTurnOutputMessages(
|
|
chatJid: string,
|
|
humanMessages: NewMessage[],
|
|
turnOutputs: PairedTurnOutput[],
|
|
): NewMessage[] {
|
|
return [
|
|
...humanMessages,
|
|
...turnOutputsToMessages(turnOutputs, chatJid),
|
|
].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
}
|
|
|
|
function hasCarriedForwardOwnerFinal(outputs: PairedTurnOutput[]): boolean {
|
|
return outputs.some((output) =>
|
|
output.output_text.startsWith(CARRIED_FORWARD_OWNER_FINAL_MARKER),
|
|
);
|
|
}
|
|
|
|
function prependCarriedForwardGuidance(
|
|
prompt: string,
|
|
turnOutputs: PairedTurnOutput[],
|
|
): string {
|
|
if (!hasCarriedForwardOwnerFinal(turnOutputs)) {
|
|
return prompt;
|
|
}
|
|
return `${CARRIED_FORWARD_OWNER_FINAL_GUIDANCE}\n\n${prompt}`;
|
|
}
|
|
|
|
function truncatePriorTaskFinal(text: string, maxChars = 1200): string {
|
|
const normalized = text.trim();
|
|
if (normalized.length <= maxChars) {
|
|
return normalized;
|
|
}
|
|
return `${normalized.slice(0, maxChars)}...`;
|
|
}
|
|
|
|
function formatPriorTaskPromptContext(
|
|
priorTaskContext?: PriorTaskPromptContext | null,
|
|
): string {
|
|
if (!priorTaskContext) {
|
|
return '';
|
|
}
|
|
|
|
const sections: string[] = [];
|
|
if (priorTaskContext.ownerFinal?.trim()) {
|
|
sections.push(
|
|
`Previous task owner final:\n---\n${truncatePriorTaskFinal(priorTaskContext.ownerFinal)}\n---`,
|
|
);
|
|
}
|
|
if (priorTaskContext.reviewerFinal?.trim()) {
|
|
sections.push(
|
|
`Previous task reviewer final:\n---\n${truncatePriorTaskFinal(priorTaskContext.reviewerFinal)}\n---`,
|
|
);
|
|
}
|
|
|
|
if (sections.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
return `Background from the previous completed paired task:\n${sections.join('\n\n')}\n\n`;
|
|
}
|
|
|
|
export function buildPairedTurnPrompt(args: {
|
|
taskId: string;
|
|
chatJid: string;
|
|
timezone: string;
|
|
missedMessages: NewMessage[];
|
|
labeledFallbackMessages: NewMessage[];
|
|
turnOutputs: PairedTurnOutput[];
|
|
}): string {
|
|
if (args.turnOutputs.length === 0) {
|
|
return formatMessages(args.labeledFallbackMessages, args.timezone);
|
|
}
|
|
|
|
const humanMessages = args.missedMessages.filter(
|
|
(message) => !message.is_bot_message,
|
|
);
|
|
return prependCarriedForwardGuidance(
|
|
formatMessages(
|
|
mergeHumanAndTurnOutputMessages(
|
|
args.chatJid,
|
|
humanMessages,
|
|
args.turnOutputs,
|
|
),
|
|
args.timezone,
|
|
),
|
|
args.turnOutputs,
|
|
);
|
|
}
|
|
|
|
export function buildReviewerPendingPrompt(args: {
|
|
chatJid: string;
|
|
timezone: string;
|
|
turnOutputs: PairedTurnOutput[];
|
|
recentHumanMessages: NewMessage[];
|
|
lastHumanMessage: string | null | undefined;
|
|
priorTaskContext?: PriorTaskPromptContext | null;
|
|
}): string {
|
|
if (args.turnOutputs.length > 0) {
|
|
return prependCarriedForwardGuidance(
|
|
formatMessages(
|
|
mergeHumanAndTurnOutputMessages(
|
|
args.chatJid,
|
|
args.recentHumanMessages,
|
|
args.turnOutputs,
|
|
),
|
|
args.timezone,
|
|
),
|
|
args.turnOutputs,
|
|
);
|
|
}
|
|
|
|
if (!args.lastHumanMessage) {
|
|
return `${formatPriorTaskPromptContext(args.priorTaskContext)}Review the latest owner changes in the workspace.`;
|
|
}
|
|
|
|
return `${formatPriorTaskPromptContext(args.priorTaskContext)}User request:\n---\n${args.lastHumanMessage}\n---\n\nReview the latest owner changes in the workspace.`;
|
|
}
|
|
|
|
export function buildOwnerPendingPrompt(args: {
|
|
chatJid: string;
|
|
timezone: string;
|
|
turnOutputs: PairedTurnOutput[];
|
|
recentHumanMessages: NewMessage[];
|
|
lastHumanMessage: string | null | undefined;
|
|
priorTaskContext?: PriorTaskPromptContext | null;
|
|
}): string {
|
|
if (args.turnOutputs.length > 0) {
|
|
return prependCarriedForwardGuidance(
|
|
formatMessages(
|
|
mergeHumanAndTurnOutputMessages(
|
|
args.chatJid,
|
|
args.recentHumanMessages,
|
|
args.turnOutputs,
|
|
),
|
|
args.timezone,
|
|
),
|
|
args.turnOutputs,
|
|
);
|
|
}
|
|
|
|
if (!args.lastHumanMessage) {
|
|
return `${formatPriorTaskPromptContext(args.priorTaskContext)}Continue the owner turn using the latest reviewer or arbiter feedback.`;
|
|
}
|
|
|
|
return `${formatPriorTaskPromptContext(args.priorTaskContext)}User request:\n---\n${args.lastHumanMessage}\n---\n\nContinue the owner turn using the latest reviewer or arbiter feedback.`;
|
|
}
|
|
|
|
export function buildArbiterPromptForTask(args: {
|
|
task: PairedTask;
|
|
chatJid: string;
|
|
timezone: string;
|
|
turnOutputs: PairedTurnOutput[];
|
|
recentMessages: NewMessage[];
|
|
labeledRecentMessages: NewMessage[];
|
|
}): string {
|
|
const messages =
|
|
args.turnOutputs.length > 0
|
|
? mergeHumanAndTurnOutputMessages(
|
|
args.chatJid,
|
|
args.recentMessages.filter((message) => !message.is_bot_message),
|
|
args.turnOutputs,
|
|
)
|
|
: args.labeledRecentMessages;
|
|
|
|
return buildArbiterContextPrompt({
|
|
chatJid: args.chatJid,
|
|
taskId: args.task.id,
|
|
roundTripCount: args.task.round_trip_count,
|
|
timezone: args.timezone,
|
|
messages,
|
|
});
|
|
}
|
|
|
|
export function buildFinalizePendingPrompt(args: {
|
|
turnOutputs: PairedTurnOutput[];
|
|
}): string {
|
|
const lastReviewerOutput = [...args.turnOutputs]
|
|
.reverse()
|
|
.find((output) => output.role === 'reviewer');
|
|
const reviewerSummary = lastReviewerOutput?.output_text
|
|
? `\n\nReviewer's final assessment:\n${lastReviewerOutput.output_text.slice(0, 2000)}`
|
|
: '';
|
|
|
|
return `The reviewer approved the current task scope (TASK_DONE / legacy DONE). Finalize and report the result.
|
|
If you intend to close this paired turn now, your first line must be TASK_DONE.
|
|
If the original request still has remaining work and the owner flow should continue, your first line may be STEP_DONE.
|
|
If your first line is DONE_WITH_CONCERNS, the system will reopen review instead of finishing.${reviewerSummary}`;
|
|
}
|