feat: initial public release (MAESTRO)

This commit is contained in:
oss-sync
2026-06-03 05:08:00 +00:00
commit f5c7666f6b
823 changed files with 184150 additions and 0 deletions
+310
View File
@@ -0,0 +1,310 @@
import { resolve, join } from 'path';
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
const BUILTIN_PIECES_DIR = resolve(process.cwd(), 'pieces');
const VALID_NAME = /^[a-z0-9-]+$/;
function findPiecePath(name: string, customDir: string | undefined): string | null {
if (customDir) {
const customPath = join(customDir, `${name}.yaml`);
if (existsSync(customPath)) return customPath;
}
const builtinPath = join(BUILTIN_PIECES_DIR, `${name}.yaml`);
if (existsSync(builtinPath)) return builtinPath;
return null;
}
/**
* A piece is "built-in" when it lives only under the bundled BUILTIN_PIECES_DIR
* (no override in customDir). Built-ins are git-tracked and shipped with the
* app — letting the LLM rewrite them in place corrupts the install (a real
* incident: the agent silently replaced game-tweet-generator with a version
* missing max_movements, making every subsequent run abort instantly). The
* LLM should use CreatePiece with a new name to derive a customized variant
* instead.
*/
function isBuiltinOnly(name: string, customDir: string | undefined): boolean {
if (customDir) {
const customPath = join(customDir, `${name}.yaml`);
if (existsSync(customPath)) return false;
}
const builtinPath = join(BUILTIN_PIECES_DIR, `${name}.yaml`);
return existsSync(builtinPath);
}
// --- Validation (same logic as pieces-api.ts) ---
function validatePiece(piece: any): string | null {
if (!piece.name || !VALID_NAME.test(piece.name)) return 'name must be lowercase alphanumeric with hyphens';
if (!piece.description) return 'description is required';
if (!Array.isArray(piece.movements) || piece.movements.length === 0) return 'movements must be non-empty array';
if (!piece.initial_movement) return 'initial_movement is required';
// Required so the runtime loop has a hard ceiling. Without this a
// forgotten/0/garbage value makes `while (steps < piece.max_movements)`
// false on the first iteration → the run aborts immediately with
// "Exceeded max movements (undefined)".
if (typeof piece.max_movements !== 'number' || !Number.isFinite(piece.max_movements) || piece.max_movements <= 0) {
return 'max_movements is required (positive integer, e.g. 50 for short tasks, 999 for open-ended ones)';
}
const names = new Set(piece.movements.map((m: any) => m.name));
if (!names.has(piece.initial_movement)) return 'initial_movement must reference an existing movement';
// Phase 6b: rules[].next only accepts existing movement names + WAIT_SUBTASKS.
// Terminal moves (COMPLETE/ABORT/ASK) go through the `complete` tool now.
// default_next is engine-internal and still accepts COMPLETE/ABORT/ASK.
const validRuleNexts = new Set([...names, 'WAIT_SUBTASKS']);
const validDefaultNexts = new Set([...names, 'COMPLETE', 'ABORT', 'ASK', 'WAIT_SUBTASKS']);
for (const m of piece.movements) {
if (!m.name) return 'each movement must have a name';
if (m.default_next && !validDefaultNexts.has(m.default_next)) {
return `movement "${m.name}": default_next "${m.default_next}" is invalid`;
}
if (Array.isArray(m.rules)) {
for (const r of m.rules) {
if (!validRuleNexts.has(r.next)) {
if (r.next === 'COMPLETE' || r.next === 'ABORT' || r.next === 'ASK') {
return `movement "${m.name}": rules[].next cannot be "${r.next}" (use the \`complete\` tool for terminal moves)`;
}
return `movement "${m.name}": rule next "${r.next}" is invalid`;
}
}
}
}
return null;
}
// --- Tool definitions ---
const LIST_PIECES_DEF: ToolDef = {
type: 'function',
function: {
name: 'ListPieces',
description: '全 Piece(実行テンプレート: ツール制限・movement フロー制御)の一覧を取得する。Skill(参照知識)の一覧は ListSkills を使うこと。新規作成前に必ず実行。詳細は ReadToolDoc({ name: "ListPieces" })。',
parameters: {
type: 'object',
properties: {},
required: [],
},
},
};
const GET_PIECE_DEF: ToolDef = {
type: 'function',
function: {
name: 'GetPiece',
description: '指定 Piece(実行テンプレート)の完全な YAML 定義を取得する。Skill の全文取得には ReadSkill を使うこと。詳細は ReadToolDoc({ name: "GetPiece" })。',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'Piece 名(例: chat, general, research' },
},
required: ['name'],
},
},
};
const CREATE_PIECE_DEF: ToolDef = {
type: 'function',
function: {
name: 'CreatePiece',
description: '新 Piece(実行テンプレート: movement + allowed_tools を定義)を YAML から作成する。Skill の追加には InstallSkill を使うこと。詳細は ReadToolDoc({ name: "CreatePiece" })。',
parameters: {
type: 'object',
properties: {
yaml_content: {
type: 'string',
description: 'Piece の完全な YAML 定義。name, description, initial_movement, movements を含むこと。',
},
},
required: ['yaml_content'],
},
},
};
const UPDATE_PIECE_DEF: ToolDef = {
type: 'function',
function: {
name: 'UpdatePiece',
description: '既存 Piece を完全な YAML で全体置換する(差分更新ではない)。詳細は ReadToolDoc({ name: "UpdatePiece" })。',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: '更新対象の Piece 名' },
yaml_content: {
type: 'string',
description: '更新後の完全な YAML 定義',
},
},
required: ['name', 'yaml_content'],
},
},
};
export const TOOL_DEFS: Record<string, ToolDef> = {
ListPieces: LIST_PIECES_DEF,
GetPiece: GET_PIECE_DEF,
CreatePiece: CREATE_PIECE_DEF,
UpdatePiece: UPDATE_PIECE_DEF,
};
// --- Tool execution ---
function executeListPieces(ctx: ToolContext): ToolResult {
try {
const seen = new Set<string>();
const pieces: Array<{ name: string; description: string; keywords: string[]; custom: boolean }> = [];
const dirs: Array<{ dir: string; custom: boolean }> = [];
if (ctx.customPiecesDir && existsSync(ctx.customPiecesDir)) dirs.push({ dir: ctx.customPiecesDir, custom: true });
dirs.push({ dir: BUILTIN_PIECES_DIR, custom: false });
for (const { dir, custom } of dirs) {
const files = readdirSync(dir).filter(f => f.endsWith('.yaml'));
for (const f of files) {
const name = f.replace('.yaml', '');
if (seen.has(name)) continue;
seen.add(name);
try {
const raw = readFileSync(join(dir, f), 'utf-8');
const p = parseYaml(raw);
pieces.push({
name: p.name ?? name,
description: (p.description ?? '').split('\n')[0].trim(),
keywords: p.triggers?.keywords ?? [],
custom,
});
} catch {
pieces.push({ name, description: '(parse error)', keywords: [], custom });
}
}
}
const lines = pieces.map(p => {
const kw = p.keywords.length > 0 ? ` [keywords: ${p.keywords.join(', ')}]` : '';
const tag = p.custom ? ' (custom)' : '';
return `- ${p.name}: ${p.description}${kw}${tag}`;
});
return { output: `登録済み Piece (${pieces.length}件):\n${lines.join('\n')}`, isError: false };
} catch (e) {
return { output: `Failed to list pieces: ${(e as Error).message}`, isError: true };
}
}
function executeGetPiece(input: Record<string, unknown>, ctx: ToolContext): ToolResult {
const name = input['name'] as string;
if (!name || !VALID_NAME.test(name)) {
return { output: 'Invalid piece name. Use lowercase alphanumeric with hyphens.', isError: true };
}
const filePath = findPiecePath(name, ctx.customPiecesDir);
if (!filePath) {
return { output: `Piece "${name}" not found.`, isError: true };
}
try {
const raw = readFileSync(filePath, 'utf-8');
return { output: raw, isError: false };
} catch (e) {
return { output: `Failed to read piece: ${(e as Error).message}`, isError: true };
}
}
function executeCreatePiece(input: Record<string, unknown>, ctx: ToolContext): ToolResult {
const yamlContent = input['yaml_content'] as string;
if (!yamlContent) {
return { output: 'yaml_content is required.', isError: true };
}
let piece: any;
try {
piece = parseYaml(yamlContent);
} catch (e) {
return { output: `YAML parse error: ${(e as Error).message}`, isError: true };
}
const error = validatePiece(piece);
if (error) {
return { output: `Validation error: ${error}`, isError: true };
}
// 両ディレクトリで名前衝突確認
if (findPiecePath(piece.name, ctx.customPiecesDir)) {
return { output: `Piece "${piece.name}" already exists. Use UpdatePiece to modify it.`, isError: true };
}
// カスタムディレクトリがあればそこに、なければ builtin に書き込み
const targetDir = ctx.customPiecesDir ?? BUILTIN_PIECES_DIR;
mkdirSync(targetDir, { recursive: true });
const filePath = join(targetDir, `${piece.name}.yaml`);
try {
writeFileSync(filePath, stringifyYaml(piece, { lineWidth: 120 }), 'utf-8');
return { output: `Piece "${piece.name}" を作成しました。`, isError: false };
} catch (e) {
return { output: `Failed to create piece: ${(e as Error).message}`, isError: true };
}
}
function executeUpdatePiece(input: Record<string, unknown>, ctx: ToolContext): ToolResult {
const name = input['name'] as string;
const yamlContent = input['yaml_content'] as string;
if (!name || !VALID_NAME.test(name)) {
return { output: 'Invalid piece name.', isError: true };
}
if (!yamlContent) {
return { output: 'yaml_content is required.', isError: true };
}
// Refuse to overwrite git-tracked built-in pieces. Force the LLM to use
// CreatePiece with a new name when it wants a customized variant.
if (isBuiltinOnly(name, ctx.customPiecesDir)) {
return {
output: `Piece "${name}" は組み込み (built-in) のため UpdatePiece では編集できません。カスタマイズが必要なら CreatePiece で別名 (例: "${name}-custom") として新規作成してください。`,
isError: true,
};
}
const filePath = findPiecePath(name, ctx.customPiecesDir);
if (!filePath) {
return { output: `Piece "${name}" not found. Use CreatePiece to create it.`, isError: true };
}
let piece: any;
try {
piece = parseYaml(yamlContent);
} catch (e) {
return { output: `YAML parse error: ${(e as Error).message}`, isError: true };
}
piece.name = name;
const error = validatePiece(piece);
if (error) {
return { output: `Validation error: ${error}`, isError: true };
}
try {
writeFileSync(filePath, stringifyYaml(piece, { lineWidth: 120 }), 'utf-8');
return { output: `Piece "${name}" を更新しました。`, isError: false };
} catch (e) {
return { output: `Failed to update piece: ${(e as Error).message}`, isError: true };
}
}
export async function executeTool(
name: string,
input: Record<string, unknown>,
ctx: ToolContext,
): Promise<ToolResult | null> {
switch (name) {
case 'ListPieces':
return executeListPieces(ctx);
case 'GetPiece':
return executeGetPiece(input, ctx);
case 'CreatePiece':
return executeCreatePiece(input, ctx);
case 'UpdatePiece':
return executeUpdatePiece(input, ctx);
default:
return null;
}
}