feat: supervisor

This commit is contained in:
Cristina Poncela Cubeiro 2026-06-18 13:25:50 +02:00
parent 0cbdc283a3
commit 0d02df760f
4 changed files with 109 additions and 14 deletions

View file

@ -39,7 +39,9 @@
"engines": {
"node": ">=22.19.0"
},
"dependencies": {},
"dependencies": {
"@earendil-works/pi-coding-agent": "0.79.6"
},
"devDependencies": {
"shx": "0.4.0"
}

View file

@ -1,4 +1,3 @@
import { randomUUID } from "node:crypto";
import type {
ErrorResponse,
InstanceSummary,
@ -13,7 +12,7 @@ import type {
StopRequest,
StopResponse,
} from "./ipc/protocol.ts";
import { getInstance, loadInstances, removeInstance, upsertInstance } from "./storage.ts";
import { supervisor } from "./supervisor.ts";
import type { InstanceRecord } from "./types.ts";
function toInstanceSummary(instance: InstanceRecord): InstanceSummary {
@ -43,15 +42,10 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse> {
switch (request.type) {
case "spawn": {
const instance: InstanceRecord = {
id: randomUUID(),
status: "starting",
const instance = await supervisor.spawnInstance({
cwd: request.cwd,
createdAt: new Date().toISOString(),
lastSeenAt: new Date().toISOString(),
label: request.label,
};
upsertInstance(instance);
});
return {
type: "spawn_result",
ok: true,
@ -63,12 +57,12 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
return {
type: "list_result",
ok: true,
instances: loadInstances().map(toInstanceSummary),
instances: supervisor.listInstances().map(toInstanceSummary),
};
}
case "status": {
const instance = getInstance(request.instanceId);
const instance = supervisor.getInstance(request.instanceId);
if (!instance) {
return unknownInstanceError(request.instanceId);
}
@ -81,12 +75,11 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
}
case "stop": {
const instance = getInstance(request.instanceId);
const instance = await supervisor.stopInstance(request.instanceId);
if (!instance) {
return unknownInstanceError(request.instanceId);
}
removeInstance(request.instanceId);
return {
type: "stop_result",
ok: true,

View file

@ -5,4 +5,5 @@ export * from "./ipc/protocol.ts";
export * from "./ipc/server.ts";
export * from "./serve.ts";
export * from "./storage.ts";
export * from "./supervisor.ts";
export * from "./types.ts";

View file

@ -0,0 +1,99 @@
import { randomUUID } from "node:crypto";
import {
type AgentSessionRuntime,
type CreateAgentSessionRuntimeFactory,
createAgentSessionFromServices,
createAgentSessionRuntime,
createAgentSessionServices,
getAgentDir,
SessionManager,
} from "@earendil-works/pi-coding-agent";
import { getInstance, loadInstances, removeInstance, upsertInstance } from "./storage.ts";
import type { InstanceRecord } from "./types.ts";
interface LiveInstance {
runtime: AgentSessionRuntime;
record: InstanceRecord;
}
function cloneInstance(record: InstanceRecord): InstanceRecord {
return { ...record };
}
async function createRuntime(cwd: string): Promise<AgentSessionRuntime> {
const agentDir = getAgentDir();
const sessionManager = SessionManager.inMemory(cwd);
const runtimeFactory: CreateAgentSessionRuntimeFactory = async ({
cwd,
agentDir,
sessionManager,
sessionStartEvent,
}) => {
const services = await createAgentSessionServices({ cwd, agentDir });
const created = await createAgentSessionFromServices({
services,
sessionManager,
sessionStartEvent,
});
return {
...created,
services,
diagnostics: services.diagnostics,
};
};
return createAgentSessionRuntime(runtimeFactory, {
cwd,
agentDir,
sessionManager,
});
}
export class OrchestratorSupervisor {
private readonly liveInstances = new Map<string, LiveInstance>();
listInstances(): InstanceRecord[] {
return loadInstances().map(cloneInstance);
}
getInstance(instanceId: string): InstanceRecord | undefined {
const live = this.liveInstances.get(instanceId);
if (live) {
return cloneInstance(live.record);
}
const stored = getInstance(instanceId);
return stored ? cloneInstance(stored) : undefined;
}
async spawnInstance(options: { cwd: string; label?: string }): Promise<InstanceRecord> {
const runtime = await createRuntime(options.cwd);
const now = new Date().toISOString();
const record: InstanceRecord = {
id: randomUUID(),
status: "online",
cwd: options.cwd,
createdAt: now,
lastSeenAt: now,
label: options.label,
sessionId: runtime.session.sessionId,
};
this.liveInstances.set(record.id, { runtime, record });
upsertInstance(record);
return cloneInstance(record);
}
async stopInstance(instanceId: string): Promise<InstanceRecord | undefined> {
const live = this.liveInstances.get(instanceId);
if (!live) {
return undefined;
}
await live.runtime.dispose();
this.liveInstances.delete(instanceId);
removeInstance(instanceId);
return cloneInstance(live.record);
}
}
export const supervisor = new OrchestratorSupervisor();