feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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<string, ToolDef> = {
|
||||
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<string, unknown>,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult | null> {
|
||||
if (name !== 'MissionUpdate') return null;
|
||||
|
||||
const io = ctx.missionBrief;
|
||||
if (!io) {
|
||||
return {
|
||||
output: 'MissionUpdate はこのコンテキストでは利用できません (subtask など local_task と紐付かない実行)。',
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const patch: Record<string, string | undefined> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user