feat: supervisor
This commit is contained in:
@@ -39,7 +39,9 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.19.0"
|
"node": ">=22.19.0"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"@earendil-works/pi-coding-agent": "0.79.6"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"shx": "0.4.0"
|
"shx": "0.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import type {
|
import type {
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
InstanceSummary,
|
InstanceSummary,
|
||||||
@@ -13,7 +12,7 @@ import type {
|
|||||||
StopRequest,
|
StopRequest,
|
||||||
StopResponse,
|
StopResponse,
|
||||||
} from "./ipc/protocol.ts";
|
} from "./ipc/protocol.ts";
|
||||||
import { getInstance, loadInstances, removeInstance, upsertInstance } from "./storage.ts";
|
import { supervisor } from "./supervisor.ts";
|
||||||
import type { InstanceRecord } from "./types.ts";
|
import type { InstanceRecord } from "./types.ts";
|
||||||
|
|
||||||
function toInstanceSummary(instance: InstanceRecord): InstanceSummary {
|
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> {
|
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse> {
|
||||||
switch (request.type) {
|
switch (request.type) {
|
||||||
case "spawn": {
|
case "spawn": {
|
||||||
const instance: InstanceRecord = {
|
const instance = await supervisor.spawnInstance({
|
||||||
id: randomUUID(),
|
|
||||||
status: "starting",
|
|
||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
lastSeenAt: new Date().toISOString(),
|
|
||||||
label: request.label,
|
label: request.label,
|
||||||
};
|
});
|
||||||
upsertInstance(instance);
|
|
||||||
return {
|
return {
|
||||||
type: "spawn_result",
|
type: "spawn_result",
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -63,12 +57,12 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
|
|||||||
return {
|
return {
|
||||||
type: "list_result",
|
type: "list_result",
|
||||||
ok: true,
|
ok: true,
|
||||||
instances: loadInstances().map(toInstanceSummary),
|
instances: supervisor.listInstances().map(toInstanceSummary),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "status": {
|
case "status": {
|
||||||
const instance = getInstance(request.instanceId);
|
const instance = supervisor.getInstance(request.instanceId);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return unknownInstanceError(request.instanceId);
|
return unknownInstanceError(request.instanceId);
|
||||||
}
|
}
|
||||||
@@ -81,12 +75,11 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "stop": {
|
case "stop": {
|
||||||
const instance = getInstance(request.instanceId);
|
const instance = await supervisor.stopInstance(request.instanceId);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return unknownInstanceError(request.instanceId);
|
return unknownInstanceError(request.instanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInstance(request.instanceId);
|
|
||||||
return {
|
return {
|
||||||
type: "stop_result",
|
type: "stop_result",
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export * from "./ipc/protocol.ts";
|
|||||||
export * from "./ipc/server.ts";
|
export * from "./ipc/server.ts";
|
||||||
export * from "./serve.ts";
|
export * from "./serve.ts";
|
||||||
export * from "./storage.ts";
|
export * from "./storage.ts";
|
||||||
|
export * from "./supervisor.ts";
|
||||||
export * from "./types.ts";
|
export * from "./types.ts";
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user