import path from 'node:path'; import { atomicWriteText } from '../../../coding-projects/atomic-json'; export const MAKELORE_PI_EXTENSION_VERSION = 3; export const MAKELORE_PI_EXTENSION_FILENAME = `makelore-runtime-v${MAKELORE_PI_EXTENSION_VERSION}.mjs`; const BUNDLE_SOURCE = String.raw` import { readFile } from 'node:fs/promises'; import path from 'node:path'; const MUTATION_TOOLS = new Set([ 'bash', 'edit', 'write', 'game_asset_browser', 'game_asset_review', ]); const WORKER_ROLE = process.env.MAKELORE_PI_WORKER_ROLE || 'parent'; const leases = new Map(); const touchedPaths = new Map(); async function runtimeContext() { const value = JSON.parse(await readFile(process.env.MAKELORE_PI_CONTEXT_FILE, 'utf8')); if (!value.runId) throw new Error('Makelore run context is unavailable'); return value; } async function bridge(action, body, signal) { const context = await runtimeContext(); const response = await fetch(process.env.MAKELORE_PI_BRIDGE_URL, { method: 'POST', headers: { authorization: 'Bearer ' + process.env.MAKELORE_PI_WORKER_TOKEN, 'content-type': 'application/json', }, body: JSON.stringify({ ...context, action, ...body }), signal, }); const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.error || 'Makelore runtime bridge rejected the request'); return result; } async function bridgeStream(action, body, signal, onUpdate) { const context = await runtimeContext(); const response = await fetch(process.env.MAKELORE_PI_BRIDGE_URL, { method: 'POST', headers: { authorization: 'Bearer ' + process.env.MAKELORE_PI_WORKER_TOKEN, 'content-type': 'application/json', }, body: JSON.stringify({ ...context, action, ...body }), signal, }); if (!response.ok) { const result = await response.json().catch(() => ({})); throw new Error(result.error || 'Makelore runtime bridge rejected the request'); } if (!response.body) throw new Error('Makelore runtime bridge returned no stream'); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffered = ''; let details; while (true) { const { value, done } = await reader.read(); buffered += decoder.decode(value || new Uint8Array(), { stream: !done }); let newline = buffered.indexOf('\n'); while (newline >= 0) { const line = buffered.slice(0, newline); buffered = buffered.slice(newline + 1); if (line) { const item = JSON.parse(line); if (item.details?.schema === 'subagent.v1') { details = item.details; onUpdate?.({ content: [], details }); } } newline = buffered.indexOf('\n'); } if (done) break; } if (!details) throw new Error('Makelore subagent stream returned no details'); return details; } async function releaseLease(toolCallId) { const leaseId = leases.get(toolCallId); if (!leaseId) return; leases.delete(toolCallId); await bridge('lease.release', { leaseId, resourceId: toolCallId }).catch(() => undefined); } async function releaseAll() { await Promise.all([...leases.keys()].map(releaseLease)); touchedPaths.clear(); } function projectRelativePath(value) { if (typeof value !== 'string' || !value.trim()) return undefined; const projectPath = path.resolve(process.cwd()); const targetPath = path.resolve(projectPath, value); const relative = path.relative(projectPath, targetPath); if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return undefined; return relative.split(path.sep).join('/'); } async function invokeProduct(toolCallId, toolName, input, signal) { const response = await bridge('product.invoke', { resourceId: toolCallId, toolName, input, }, signal); return response.result; } function registerProductTool(pi, name, label, description, parameters) { pi.registerTool({ name, label, description, parameters, async execute(toolCallId, params, signal) { return await invokeProduct(toolCallId, name, params, signal); }, }); } export default function makeloreRuntime(pi) { if (WORKER_ROLE === 'parent') pi.registerTool({ name: 'ask_user', label: 'Ask user', description: 'Ask the user for a selection, confirmation, short input, or editor text.', parameters: { type: 'object', additionalProperties: false, required: ['kind', 'title'], properties: { kind: { type: 'string', enum: ['select', 'confirm', 'input', 'editor'] }, title: { type: 'string' }, message: { type: 'string' }, options: { type: 'array', items: { type: 'string' } }, }, }, async execute(_toolCallId, params, signal, _onUpdate, ctx) { let value; if (params.kind === 'select') { value = await ctx.ui.select(params.title, Array.isArray(params.options) ? params.options : [], { signal }); } else if (params.kind === 'confirm') { value = await ctx.ui.confirm(params.title, params.message || '', { signal }); } else if (params.kind === 'editor') { value = await ctx.ui.editor(params.title, params.message || '', { signal }); } else { value = await ctx.ui.input(params.title, params.message || '', { signal }); } return { content: [{ type: 'text', text: value === undefined ? 'User cancelled' : String(value) }], details: { kind: params.kind, cancelled: value === undefined }, }; }, }); if (WORKER_ROLE === 'parent') pi.registerTool({ name: 'subagent', label: 'Subagent', description: 'Dispatch one or more managed project Agents in single, parallel, or chain mode.', parameters: { type: 'object', additionalProperties: false, required: ['mode', 'tasks'], properties: { mode: { type: 'string', enum: ['single', 'parallel', 'chain'] }, tasks: { type: 'array', minItems: 1, maxItems: 8, items: { type: 'object', additionalProperties: false, required: ['agentId', 'task', 'toolProfile'], properties: { agentId: { type: 'string' }, task: { type: 'string' }, toolProfile: { type: 'string', enum: ['read-only', 'coding'] }, }, }, }, }, }, async execute(toolCallId, params, signal, onUpdate) { const details = await bridgeStream( 'subagent.dispatch', { resourceId: toolCallId, request: params }, signal, onUpdate, ); const complete = details.tasks.filter((task) => task.status === 'complete').length; const failed = details.tasks.length - complete; return { content: [{ type: 'text', text: failed === 0 ? complete + ' subagent task(s) completed' : complete + ' completed; ' + failed + ' did not complete', }], details, }; }, }); if (WORKER_ROLE === 'parent') registerProductTool( pi, 'agent_browser', 'Agent browser', 'Open, navigate, inspect, or close the Main-owned Makelore development browser.', { type: 'object', additionalProperties: true, required: ['action'], properties: { action: { type: 'string', enum: ['open', 'status', 'close', 'reset_profile', 'navigate', 'send_cdp', 'read_events', 'read_payload'] }, url: { type: 'string' }, navigation: { type: 'string', enum: ['url', 'back', 'forward', 'reload'] }, method: { type: 'string' }, params: { type: 'object' }, sessionRef: { type: 'string' }, timeoutMs: { type: 'number' }, after: { type: 'number' }, methods: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, waitMs: { type: 'number' }, handle: { type: 'string' }, offset: { type: 'number' }, maxBytes: { type: 'number' }, }, }, ); if (WORKER_ROLE === 'parent') registerProductTool( pi, 'game_asset_browser', 'Game assets', 'Load product-owned game asset candidates and their current review state.', { type: 'object', additionalProperties: false, properties: { invocationId: { type: 'string' } } }, ); if (WORKER_ROLE === 'parent') registerProductTool( pi, 'game_asset_review', 'Game asset review', 'Load one versioned game asset review interaction without encoding decisions in message text.', { type: 'object', additionalProperties: false, properties: { invocationId: { type: 'string' }, candidateIds: { type: 'array', maxItems: 200, items: { type: 'string' } }, }, }, ); if (WORKER_ROLE === 'parent') registerProductTool( pi, 'task_state', 'Task state', 'Publish versioned task steps and progress to the Makelore product timeline.', { type: 'object', additionalProperties: false, required: ['tasks'], properties: { tasks: { type: 'array', minItems: 1, maxItems: 100, items: { type: 'object', additionalProperties: false, required: ['id', 'title', 'status'], properties: { id: { type: 'string' }, title: { type: 'string' }, status: { type: 'string', enum: ['pending', 'running', 'complete', 'error'] }, }, }, }, }, }, ); if (WORKER_ROLE === 'parent') registerProductTool( pi, 'changed_file', 'Changed file', 'Report project-relative paths touched by managed tools and refresh Conversation changes.', { type: 'object', additionalProperties: false, properties: { path: { type: 'string' }, paths: { type: 'array', maxItems: 200, items: { type: 'string' } }, refresh: { type: 'boolean' }, }, }, ); if (WORKER_ROLE === 'parent') registerProductTool( pi, 'runtime_context', 'Runtime context', 'Read the safe selected-skill and command catalog for this managed worker.', { type: 'object', additionalProperties: false, properties: {} }, ); pi.on('tool_call', async (event, ctx) => { if (!MUTATION_TOOLS.has(event.toolName)) return; const input = event.input || event.arguments || event.args || {}; const touchedPath = (event.toolName === 'write' || event.toolName === 'edit') ? projectRelativePath(input.path) : undefined; if (touchedPath) touchedPaths.set(event.toolCallId, [touchedPath]); if (event.toolName === 'bash') { await bridge('changes.bash', { resourceId: event.toolCallId }, ctx.signal).catch(() => undefined); } ctx.ui.setStatus('makelore.write-lease', '等待项目写入'); try { const result = await bridge('lease.acquire', { resourceId: event.toolCallId }, ctx.signal); leases.set(event.toolCallId, result.leaseId); } finally { ctx.ui.setStatus('makelore.write-lease', undefined); } }); pi.on('tool_result', async (event) => { await releaseLease(event.toolCallId); const paths = touchedPaths.get(event.toolCallId); touchedPaths.delete(event.toolCallId); if (paths) { await bridge('changes.touched', { resourceId: event.toolCallId, paths, }).catch(() => undefined); } }); pi.on('agent_end', releaseAll); pi.on('session_shutdown', releaseAll); } `; export async function materializeMakelorePiExtension(extensionsDir: string): Promise { const extensionPath = path.join(extensionsDir, MAKELORE_PI_EXTENSION_FILENAME); await atomicWriteText(extensionPath, BUNDLE_SOURCE.trimStart()); return extensionPath; }