feat: add Pi subagent scheduler and child runtime
This commit is contained in:
@@ -6,8 +6,14 @@ import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host';
|
||||
import { PiSubagentScheduler } from '../../electron/coding-runtime/pi/subagent';
|
||||
import { PiProcessBudget } from '../../electron/coding-runtime/pi/worker-pool';
|
||||
|
||||
type ExtensionHandler = (...arguments_: unknown[]) => Promise<unknown> | unknown;
|
||||
type ExtensionTool = {
|
||||
name: string;
|
||||
execute?: (...arguments_: unknown[]) => Promise<unknown>;
|
||||
};
|
||||
|
||||
const roots: string[] = [];
|
||||
const hosts: PiManagedExtensionHost[] = [];
|
||||
@@ -22,6 +28,15 @@ describe('Makelore Pi extension bundle', () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-extension-bundle-'));
|
||||
roots.push(root);
|
||||
const host = new PiManagedExtensionHost();
|
||||
const scheduler = new PiSubagentScheduler({
|
||||
processBudget: new PiProcessBudget(8),
|
||||
openChild: async (input) => ({
|
||||
id: input.taskId,
|
||||
async run() { return { summary: `done ${input.agentId}` }; },
|
||||
async stop() {},
|
||||
}),
|
||||
});
|
||||
host.configureSubagents({ scheduler });
|
||||
hosts.push(host);
|
||||
const extensionWorker = await host.registerWorker({
|
||||
conversationId: 'conversation-a1', generation: 1, projectId: 'project-a', extensionsDir: root,
|
||||
@@ -38,22 +53,44 @@ describe('Makelore Pi extension bundle', () => {
|
||||
bridge: process.env.MAKELORE_PI_BRIDGE_URL,
|
||||
token: process.env.MAKELORE_PI_WORKER_TOKEN,
|
||||
context: process.env.MAKELORE_PI_CONTEXT_FILE,
|
||||
role: process.env.MAKELORE_PI_WORKER_ROLE,
|
||||
};
|
||||
Object.assign(process.env, extensionWorker.env);
|
||||
try {
|
||||
const module = await import(/* @vite-ignore */ pathToFileURL(extensionWorker.extensionPath).href) as {
|
||||
default(factory: {
|
||||
registerTool(tool: { name: string }): void;
|
||||
registerTool(tool: ExtensionTool): void;
|
||||
on(event: string, handler: ExtensionHandler): void;
|
||||
}): void;
|
||||
};
|
||||
const handlers = new Map<string, ExtensionHandler>();
|
||||
const tools: string[] = [];
|
||||
const tools = new Map<string, ExtensionTool>();
|
||||
module.default({
|
||||
registerTool: (tool) => tools.push(tool.name),
|
||||
registerTool: (tool) => tools.set(tool.name, tool),
|
||||
on: (event, handler) => handlers.set(event, handler),
|
||||
});
|
||||
expect(tools).toEqual(['ask_user']);
|
||||
expect([...tools.keys()]).toEqual(['ask_user', 'subagent']);
|
||||
|
||||
const updates: unknown[] = [];
|
||||
const subagentResult = await tools.get('subagent')?.execute?.(
|
||||
'subagent-1',
|
||||
{
|
||||
mode: 'single',
|
||||
tasks: [{ agentId: 'agent-a', task: 'Inspect', toolProfile: 'read-only' }],
|
||||
},
|
||||
new AbortController().signal,
|
||||
(update: unknown) => updates.push(update),
|
||||
);
|
||||
expect(subagentResult).toMatchObject({
|
||||
details: {
|
||||
schema: 'subagent.v1', mode: 'single',
|
||||
tasks: [{ agentId: 'agent-a', status: 'complete', summary: 'done agent-a' }],
|
||||
},
|
||||
});
|
||||
expect(updates.length).toBeGreaterThan(0);
|
||||
expect(updates.every((update) => (
|
||||
(update as { details?: { schema?: string } }).details?.schema === 'subagent.v1'
|
||||
))).toBe(true);
|
||||
|
||||
const statuses: Array<string | undefined> = [];
|
||||
const context = {
|
||||
@@ -84,12 +121,55 @@ describe('Makelore Pi extension bundle', () => {
|
||||
await handlers.get('tool_result')?.({ toolCallId: 'write-1' });
|
||||
expect((await waiting).status).toBe(200);
|
||||
} finally {
|
||||
await scheduler.close();
|
||||
if (previousEnvironment.bridge === undefined) delete process.env.MAKELORE_PI_BRIDGE_URL;
|
||||
else process.env.MAKELORE_PI_BRIDGE_URL = previousEnvironment.bridge;
|
||||
if (previousEnvironment.token === undefined) delete process.env.MAKELORE_PI_WORKER_TOKEN;
|
||||
else process.env.MAKELORE_PI_WORKER_TOKEN = previousEnvironment.token;
|
||||
if (previousEnvironment.context === undefined) delete process.env.MAKELORE_PI_CONTEXT_FILE;
|
||||
else process.env.MAKELORE_PI_CONTEXT_FILE = previousEnvironment.context;
|
||||
if (previousEnvironment.role === undefined) delete process.env.MAKELORE_PI_WORKER_ROLE;
|
||||
else process.env.MAKELORE_PI_WORKER_ROLE = previousEnvironment.role;
|
||||
}
|
||||
});
|
||||
|
||||
it('does not expose parent-only tools from a child process', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-extension-child-bundle-'));
|
||||
roots.push(root);
|
||||
const host = new PiManagedExtensionHost();
|
||||
hosts.push(host);
|
||||
const child = await host.registerWorker({
|
||||
conversationId: 'conversation-child', generation: 1, projectId: 'project-a',
|
||||
extensionsDir: root, role: 'child', runId: 'run-parent',
|
||||
});
|
||||
const previous = {
|
||||
bridge: process.env.MAKELORE_PI_BRIDGE_URL,
|
||||
token: process.env.MAKELORE_PI_WORKER_TOKEN,
|
||||
context: process.env.MAKELORE_PI_CONTEXT_FILE,
|
||||
role: process.env.MAKELORE_PI_WORKER_ROLE,
|
||||
};
|
||||
Object.assign(process.env, child.env);
|
||||
try {
|
||||
const module = await import(
|
||||
/* @vite-ignore */ `${pathToFileURL(child.extensionPath).href}?child=${Date.now()}`
|
||||
) as {
|
||||
default(factory: {
|
||||
registerTool(tool: ExtensionTool): void;
|
||||
on(event: string, handler: ExtensionHandler): void;
|
||||
}): void;
|
||||
};
|
||||
const tools: string[] = [];
|
||||
module.default({ registerTool: (tool) => tools.push(tool.name), on: () => undefined });
|
||||
expect(tools).toEqual([]);
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
const environmentKey = key === 'bridge' ? 'MAKELORE_PI_BRIDGE_URL'
|
||||
: key === 'token' ? 'MAKELORE_PI_WORKER_TOKEN'
|
||||
: key === 'context' ? 'MAKELORE_PI_CONTEXT_FILE'
|
||||
: 'MAKELORE_PI_WORKER_ROLE';
|
||||
if (value === undefined) delete process.env[environmentKey];
|
||||
else process.env[environmentKey] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user