feat(coding-runtime): add managed Pi extension host
This commit is contained in:
100
electron/coding-runtime/pi/extensions/makelore-runtime.ts
Normal file
100
electron/coding-runtime/pi/extensions/makelore-runtime.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import path from 'node:path';
|
||||
import { atomicWriteText } from '../../../coding-projects/atomic-json';
|
||||
|
||||
export const MAKELORE_PI_EXTENSION_VERSION = 1;
|
||||
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 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 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) {
|
||||
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 },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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<string> {
|
||||
const extensionPath = path.join(extensionsDir, MAKELORE_PI_EXTENSION_FILENAME);
|
||||
await atomicWriteText(extensionPath, BUNDLE_SOURCE.trimStart());
|
||||
return extensionPath;
|
||||
}
|
||||
Reference in New Issue
Block a user