/** * Mission Brief tool — per-task pinned memo of goal / done / open / * clarifications, always rendered at the top of every movement's system * prompt and editable by both the LLM (via this tool) and the user * (via the Overview tab). * * Design notes: * - Partial replace semantics: only fields explicitly provided in the * call are written. Undefined fields leave existing values intact. * - The brief is per-LocalTask (not per-job, not per-movement) so it * survives across iterations, ASK rounds, and follow-up messages * within the same task conversation. * - Storage is a single JSON column on local_tasks; see * src/db/repository.ts.MissionBrief / updateMissionBrief. * - Plumbing: piece-runner constructs a MissionBriefIO from the * localTaskId + repo and threads it through ToolContext. * Subtask contexts that aren't bound to a local_task simply leave * the IO unset; the tool then degrades to a no-op with a clear * error so the LLM doesn't get confused. */ import { ToolDef } from '../../llm/openai-compat.js'; import type { ToolContext, ToolResult } from './core.js'; const MISSION_UPDATE_DEF: ToolDef = { type: 'function', function: { name: 'MissionUpdate', description: 'タスクの Mission Brief (goal / done / open / clarifications) を更新する。常時利用可能で META_TOOL 扱い。**新規タスクの最初のツール呼び出しで goal を必ず set すること** ── ユーザー要件を verbatim に固定し、会話が長くなった後でも参照点として残す。以降は節目で done / open を更新。指定したフィールドだけ置き換わり、未指定は変更なし。詳細は ReadToolDoc({ name: "MissionUpdate" })。', parameters: { type: 'object', properties: { goal: { type: 'string', description: 'このタスク全体のゴール (ユーザーが最初に依頼した本質的な要件)。Markdown 可。', }, done: { type: 'string', description: 'これまでに完了した主要マイルストーン。Markdown 箇条書き推奨。重複作業を避けるための参照。', }, open: { type: 'string', description: '残っている作業 / 未解決のブロッカー。Markdown 箇条書き推奨。', }, clarifications: { type: 'string', description: 'ユーザーから途中で追加された補足・制約。「これは壊さないで」など。Markdown 可。', }, }, }, }, }; export const TOOL_DEFS: Record = { MissionUpdate: MISSION_UPDATE_DEF, }; const FIELD_MAX_CHARS = 2000; function clamp(value: unknown): string | undefined { if (value === undefined) return undefined; if (typeof value !== 'string') return undefined; if (value.length <= FIELD_MAX_CHARS) return value; return `${value.slice(0, FIELD_MAX_CHARS)}\n…[truncated, ${value.length} chars]`; } export async function executeTool( name: string, input: Record, ctx: ToolContext, ): Promise { if (name !== 'MissionUpdate') return null; const io = ctx.missionBrief; if (!io) { return { output: 'MissionUpdate はこのコンテキストでは利用できません (subtask など local_task と紐付かない実行)。', isError: true, }; } const patch: Record = { goal: clamp(input['goal']), done: clamp(input['done']), open: clamp(input['open']), clarifications: clamp(input['clarifications']), }; // Strip undefined so the IO layer treats them as "not provided" rather // than "set to empty string". const filtered: Partial<{ goal: string; done: string; open: string; clarifications: string }> = {}; for (const key of ['goal', 'done', 'open', 'clarifications'] as const) { if (patch[key] !== undefined) filtered[key] = patch[key]!; } if (Object.keys(filtered).length === 0) { return { output: '更新するフィールドが1つも指定されませんでした。goal / done / open / clarifications のいずれかを指定してください。', isError: true, }; } try { const merged = io.update(filtered); if (!merged) { return { output: 'Mission Brief をクリアしました (全フィールドが空)。', isError: false, }; } const summary = ['Mission Brief を更新しました:']; for (const key of ['goal', 'done', 'open', 'clarifications'] as const) { const value = merged[key]; if (value) summary.push(`- ${key} (${value.length} chars)`); } return { output: summary.join('\n'), isError: false }; } catch (err) { return { output: `Mission Brief の更新に失敗しました: ${err instanceof Error ? err.message : String(err)}`, isError: true, }; } }