86 lines
4.0 KiB
TypeScript
86 lines
4.0 KiB
TypeScript
import type { LocalTaskComment } from '../db/repository.js';
|
|
|
|
/** Comment kinds that represent a direct user instruction (vs. agent progress/result/ask rows). */
|
|
export const USER_INSTRUCTION_KINDS = ['comment', 'request', 'interjection'] as const;
|
|
|
|
export interface LocalContextInput {
|
|
/** All comments for the task, oldest-first (as returned by listLocalTaskComments). */
|
|
comments: LocalTaskComment[];
|
|
/** The job's original instruction (task body). */
|
|
jobInstruction: string;
|
|
/** Filenames under input/ (display only). */
|
|
inputFiles: string[];
|
|
/** Filenames under output/ (display only). */
|
|
outputFiles: string[];
|
|
/** When true (continuation jobs that replay the full transcript), skip the
|
|
* "## これまでのやり取り" recap — the replayed real turns supersede it. */
|
|
omitRecentConversation?: boolean;
|
|
}
|
|
|
|
function isUserInstruction(c: LocalTaskComment): boolean {
|
|
return c.author === 'user' && (USER_INSTRUCTION_KINDS as readonly string[]).includes(c.kind);
|
|
}
|
|
|
|
/**
|
|
* Build the conversation-context block injected into a (re)started local-task job.
|
|
*
|
|
* Two behaviors matter when a task is cancelled mid-loop and re-run:
|
|
* - Interjections (kind='interjection', sent while the task was running) count as
|
|
* user instructions, so the LATEST one becomes the "current instruction" — not just
|
|
* the original task body. Without this, a resumed agent loses what the user actually
|
|
* asked for during the loop.
|
|
* - The "recent conversation" window is the last 10 comments, but a busy loop floods the
|
|
* task with agent `progress` rows that would otherwise push the user's message out of
|
|
* that window. We therefore guarantee the last few USER messages are always shown.
|
|
*
|
|
* Returns the context body (without the time-context prefix, which the caller prepends).
|
|
*/
|
|
export function buildLocalConversationContext(input: LocalContextInput): string {
|
|
const { comments, jobInstruction, inputFiles, outputFiles } = input;
|
|
const contextParts: string[] = [];
|
|
|
|
// The active instruction is the LATEST user message (comment/request/interjection),
|
|
// skipping agent progress/result/ask rows. The original task body stays as reference.
|
|
const latestUserComment = [...comments].reverse().find(isUserInstruction);
|
|
const currentInstructionBody =
|
|
latestUserComment && latestUserComment.body.trim() !== jobInstruction.trim()
|
|
? latestUserComment.body
|
|
: jobInstruction;
|
|
const hasFollowUp = currentInstructionBody !== jobInstruction;
|
|
|
|
// Recent conversation: last 10 overall, but never drop the last 5 user messages —
|
|
// otherwise a flood of agent progress rows hides what the user said.
|
|
const recentOverall = comments.slice(-10);
|
|
const recentUserMsgs = comments.filter(isUserInstruction).slice(-5);
|
|
const displayMap = new Map<number, LocalTaskComment>();
|
|
for (const c of [...recentOverall, ...recentUserMsgs]) displayMap.set(c.id, c);
|
|
const display = [...displayMap.values()].sort((a, b) => a.id - b.id);
|
|
|
|
if (!input.omitRecentConversation && display.length > 0) {
|
|
contextParts.push('## これまでのやり取り');
|
|
for (const comment of display) {
|
|
const truncated = comment.body.length > 500 ? comment.body.slice(0, 500) + '...' : comment.body;
|
|
contextParts.push(`[${comment.author}/${comment.kind}] ${truncated}`);
|
|
}
|
|
contextParts.push('');
|
|
}
|
|
|
|
contextParts.push('## ワークスペース状況');
|
|
if (inputFiles.length > 0) contextParts.push(`input/: ${inputFiles.join(', ')}`);
|
|
if (outputFiles.length > 0) contextParts.push(`output/: ${outputFiles.join(', ')}`);
|
|
contextParts.push('');
|
|
|
|
if (hasFollowUp) {
|
|
contextParts.push('## オリジナルタスク (参考、対応済みの可能性あり)');
|
|
contextParts.push(jobInstruction);
|
|
contextParts.push('');
|
|
contextParts.push('## 現在のユーザー指示 (これに対応する)');
|
|
contextParts.push(currentInstructionBody);
|
|
} else {
|
|
contextParts.push('## タスク');
|
|
contextParts.push(jobInstruction);
|
|
}
|
|
|
|
return contextParts.join('\n');
|
|
}
|