import path from 'node:path'; import { atomicWriteText } from '../../../coding-projects/atomic-json'; export const MAKELORE_PI_EXTENSION_VERSION = 2; 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'; const MUTATION_TOOLS = new Set(['bash', 'edit', 'write']); const WORKER_ROLE = process.env.MAKELORE_PI_WORKER_ROLE || 'parent'; const leases = 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)); } 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, }; }, }); pi.on('tool_call', async (event, ctx) => { if (!MUTATION_TOOLS.has(event.toolName)) return; 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) => releaseLease(event.toolCallId)); 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; }