fix: bridge missing rpc commands
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
|
import { createConnection } from "node:net";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { cwd } from "node:process";
|
import { cwd } from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { getSocketPath } from "./config.ts";
|
||||||
import { sendIpcRequest } from "./ipc/client.ts";
|
import { sendIpcRequest } from "./ipc/client.ts";
|
||||||
|
import { encodeMessage } from "./ipc/protocol.ts";
|
||||||
import { serve } from "./serve.ts";
|
import { serve } from "./serve.ts";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -14,7 +17,7 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"),
|
|||||||
|
|
||||||
function printHelp(): void {
|
function printHelp(): void {
|
||||||
console.log(
|
console.log(
|
||||||
`orchestrator v${packageJson.version}\n\nUsage:\n orchestrator serve\n orchestrator list\n orchestrator spawn [--cwd <path>] [--label <label>]\n orchestrator status <instance-id>\n orchestrator stop <instance-id>\n orchestrator rpc <instance-id> <json-command>\n orchestrator --help\n orchestrator --version`,
|
`orchestrator v${packageJson.version}\n\nUsage:\n orchestrator serve\n orchestrator list\n orchestrator spawn [--cwd <path>] [--label <label>]\n orchestrator status <instance-id>\n orchestrator stop <instance-id>\n orchestrator rpc <instance-id> <json-command>\n orchestrator attach <instance-id>\n orchestrator --help\n orchestrator --version`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +33,39 @@ function getFlagValue(args: string[], flag: string): string | undefined {
|
|||||||
return args[index + 1];
|
return args[index + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function attach(instanceId: string): Promise<void> {
|
||||||
|
const socket = createConnection(getSocketPath());
|
||||||
|
process.stdin.setEncoding("utf8");
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
socket.once("connect", () => {
|
||||||
|
socket.write(encodeMessage({ type: "attach", instanceId }));
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
socket.once("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("data", (chunk: Buffer | string) => {
|
||||||
|
process.stdout.write(chunk.toString());
|
||||||
|
});
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
socket.on("end", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
process.stdin.on("data", (chunk: string) => {
|
||||||
|
const lines = chunk
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
for (const line of lines) {
|
||||||
|
socket.write(encodeMessage({ type: "attach_rpc", command: JSON.parse(line) }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
@@ -97,6 +133,16 @@ async function main(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args[0] === "attach") {
|
||||||
|
const instanceId = args[1];
|
||||||
|
if (!instanceId) {
|
||||||
|
console.error("Usage: orchestrator attach <instance-id>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await attach(instanceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.error(`Unknown command: ${args[0]}`);
|
console.error(`Unknown command: ${args[0]}`);
|
||||||
printHelp();
|
printHelp();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import type { AgentSessionEvent, RpcCommand } from "@earendil-works/pi-coding-agent";
|
||||||
import type {
|
import type {
|
||||||
|
AttachReadyResponse,
|
||||||
|
AttachRequest,
|
||||||
|
AttachRpcResponse,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
InstanceSummary,
|
InstanceSummary,
|
||||||
ListRequest,
|
ListRequest,
|
||||||
@@ -43,6 +47,7 @@ export async function handleIpcRequest(request: ListRequest): Promise<ListRespon
|
|||||||
export async function handleIpcRequest(request: StopRequest): Promise<StopResponse | ErrorResponse>;
|
export async function handleIpcRequest(request: StopRequest): Promise<StopResponse | ErrorResponse>;
|
||||||
export async function handleIpcRequest(request: StatusRequest): Promise<StatusResponse | ErrorResponse>;
|
export async function handleIpcRequest(request: StatusRequest): Promise<StatusResponse | ErrorResponse>;
|
||||||
export async function handleIpcRequest(request: RpcRequest): Promise<RpcBridgeResponse | ErrorResponse>;
|
export async function handleIpcRequest(request: RpcRequest): Promise<RpcBridgeResponse | ErrorResponse>;
|
||||||
|
export async function handleIpcRequest(request: AttachRequest): Promise<AttachReadyResponse | ErrorResponse>;
|
||||||
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse>;
|
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse>;
|
||||||
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse> {
|
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse> {
|
||||||
switch (request.type) {
|
switch (request.type) {
|
||||||
@@ -104,5 +109,38 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
|
|||||||
response,
|
response,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "attach": {
|
||||||
|
const instance = supervisor.getInstance(request.instanceId);
|
||||||
|
if (!instance) {
|
||||||
|
return unknownInstanceError(request.instanceId);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "attach_ready",
|
||||||
|
ok: true,
|
||||||
|
instance: toInstanceSummary(instance),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function attachIpcInstance(
|
||||||
|
instanceId: string,
|
||||||
|
onResponse: (response: AttachRpcResponse) => void,
|
||||||
|
onSessionEvent: (event: AgentSessionEvent) => void,
|
||||||
|
): { handleRequest(request: { type: "attach_rpc"; command: RpcCommand }): Promise<void>; close(): void } | undefined {
|
||||||
|
const handle = supervisor.attachInstance(instanceId, onSessionEvent);
|
||||||
|
if (!handle) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async handleRequest(request): Promise<void> {
|
||||||
|
const response = await handle.handleRpc(request.command);
|
||||||
|
onResponse({ type: "attach_rpc_result", response });
|
||||||
|
},
|
||||||
|
close(): void {
|
||||||
|
handle.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { RpcCommand, RpcResponse } from "@earendil-works/pi-coding-agent";
|
import type { AgentSessionEvent, RpcCommand, RpcResponse } from "@earendil-works/pi-coding-agent";
|
||||||
import type { InstanceStatus } from "../types.ts";
|
import type { InstanceStatus } from "../types.ts";
|
||||||
|
|
||||||
export interface SpawnRequest {
|
export interface SpawnRequest {
|
||||||
@@ -29,12 +29,23 @@ export interface RpcRequest {
|
|||||||
command: RpcCommand;
|
command: RpcCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AttachRequest {
|
||||||
|
type: "attach";
|
||||||
|
instanceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachRpcRequest {
|
||||||
|
type: "attach_rpc";
|
||||||
|
command: RpcCommand;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RequestMap {
|
export interface RequestMap {
|
||||||
spawn: SpawnRequest;
|
spawn: SpawnRequest;
|
||||||
list: ListRequest;
|
list: ListRequest;
|
||||||
stop: StopRequest;
|
stop: StopRequest;
|
||||||
status: StatusRequest;
|
status: StatusRequest;
|
||||||
rpc: RpcRequest;
|
rpc: RpcRequest;
|
||||||
|
attach: AttachRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrchestratorRequest = RequestMap[keyof RequestMap];
|
export type OrchestratorRequest = RequestMap[keyof RequestMap];
|
||||||
@@ -79,6 +90,21 @@ export interface RpcBridgeResponse extends ResponseBase {
|
|||||||
response: RpcResponse;
|
response: RpcResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AttachReadyResponse extends ResponseBase {
|
||||||
|
type: "attach_ready";
|
||||||
|
instance?: InstanceSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachEventResponse {
|
||||||
|
type: "attach_event";
|
||||||
|
event: AgentSessionEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachRpcResponse {
|
||||||
|
type: "attach_rpc_result";
|
||||||
|
response: RpcResponse;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ErrorResponse extends ResponseBase {
|
export interface ErrorResponse extends ResponseBase {
|
||||||
type: "error";
|
type: "error";
|
||||||
ok: false;
|
ok: false;
|
||||||
@@ -91,9 +117,12 @@ export interface ResponseMap {
|
|||||||
stop: StopResponse;
|
stop: StopResponse;
|
||||||
status: StatusResponse;
|
status: StatusResponse;
|
||||||
rpc: RpcBridgeResponse;
|
rpc: RpcBridgeResponse;
|
||||||
|
attach: AttachReadyResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrchestratorResponse = ResponseMap[keyof ResponseMap] | ErrorResponse;
|
export type OrchestratorResponse = ResponseMap[keyof ResponseMap] | ErrorResponse;
|
||||||
|
export type AttachClientRequest = AttachRpcRequest;
|
||||||
|
export type AttachServerResponse = AttachReadyResponse | AttachEventResponse | AttachRpcResponse | ErrorResponse;
|
||||||
|
|
||||||
export type ResponseFor<T extends OrchestratorRequest> = T extends { type: infer K }
|
export type ResponseFor<T extends OrchestratorRequest> = T extends { type: infer K }
|
||||||
? K extends keyof ResponseMap
|
? K extends keyof ResponseMap
|
||||||
@@ -101,7 +130,7 @@ export type ResponseFor<T extends OrchestratorRequest> = T extends { type: infer
|
|||||||
: ErrorResponse
|
: ErrorResponse
|
||||||
: ErrorResponse;
|
: ErrorResponse;
|
||||||
|
|
||||||
export function encodeMessage(message: OrchestratorRequest | OrchestratorResponse): string {
|
export function encodeMessage(message: unknown): string {
|
||||||
return `${JSON.stringify(message)}\n`;
|
return `${JSON.stringify(message)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { existsSync, unlinkSync } from "node:fs";
|
|||||||
import { createConnection, createServer, type Server } from "node:net";
|
import { createConnection, createServer, type Server } from "node:net";
|
||||||
import { getSocketPath } from "../config.ts";
|
import { getSocketPath } from "../config.ts";
|
||||||
import {
|
import {
|
||||||
|
type AttachClientRequest,
|
||||||
|
type AttachReadyResponse,
|
||||||
|
type AttachRequest,
|
||||||
|
type AttachRpcResponse,
|
||||||
type ErrorResponse,
|
type ErrorResponse,
|
||||||
encodeMessage,
|
encodeMessage,
|
||||||
type ListRequest,
|
type ListRequest,
|
||||||
@@ -25,7 +29,18 @@ export interface IpcRequestHandler {
|
|||||||
(request: StopRequest): Promise<StopResponse | ErrorResponse> | StopResponse | ErrorResponse;
|
(request: StopRequest): Promise<StopResponse | ErrorResponse> | StopResponse | ErrorResponse;
|
||||||
(request: StatusRequest): Promise<StatusResponse | ErrorResponse> | StatusResponse | ErrorResponse;
|
(request: StatusRequest): Promise<StatusResponse | ErrorResponse> | StatusResponse | ErrorResponse;
|
||||||
(request: RpcRequest): Promise<RpcBridgeResponse | ErrorResponse> | RpcBridgeResponse | ErrorResponse;
|
(request: RpcRequest): Promise<RpcBridgeResponse | ErrorResponse> | RpcBridgeResponse | ErrorResponse;
|
||||||
|
(request: AttachRequest): Promise<AttachReadyResponse | ErrorResponse> | AttachReadyResponse | ErrorResponse;
|
||||||
(request: OrchestratorRequest): Promise<OrchestratorResponse> | OrchestratorResponse;
|
(request: OrchestratorRequest): Promise<OrchestratorResponse> | OrchestratorResponse;
|
||||||
|
attach(
|
||||||
|
instanceId: string,
|
||||||
|
onEvent: (response: AttachRpcResponse) => void,
|
||||||
|
onSessionEvent: (event: import("@earendil-works/pi-coding-agent").AgentSessionEvent) => void,
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
handleRequest(request: AttachClientRequest): Promise<void>;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startIpcServer(handler: IpcRequestHandler): Promise<Server> {
|
export async function startIpcServer(handler: IpcRequestHandler): Promise<Server> {
|
||||||
@@ -50,6 +65,63 @@ export async function startIpcServer(handler: IpcRequestHandler): Promise<Server
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const request = parseRequestLine(line);
|
const request = parseRequestLine(line);
|
||||||
|
if (request.type === "attach") {
|
||||||
|
const response = await handler(request);
|
||||||
|
if (!response.ok || !response.instance) {
|
||||||
|
socket.end(encodeMessage(response));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = handler.attach(
|
||||||
|
request.instanceId,
|
||||||
|
(response) => {
|
||||||
|
socket.write(encodeMessage(response));
|
||||||
|
},
|
||||||
|
(event) => {
|
||||||
|
socket.write(encodeMessage({ type: "attach_event", event }));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!attachment) {
|
||||||
|
socket.end(
|
||||||
|
encodeMessage({ type: "error", ok: false, error: `Unknown instance: ${request.instanceId}` }),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.write(encodeMessage(response));
|
||||||
|
socket.removeAllListeners("data");
|
||||||
|
socket.on("data", (attachChunk: Buffer | string) => {
|
||||||
|
buffer += attachChunk.toString();
|
||||||
|
for (;;) {
|
||||||
|
const attachNewlineIndex = buffer.indexOf("\n");
|
||||||
|
if (attachNewlineIndex === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const attachLine = buffer.slice(0, attachNewlineIndex).trim();
|
||||||
|
buffer = buffer.slice(attachNewlineIndex + 1);
|
||||||
|
if (!attachLine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const attachRequest = JSON.parse(attachLine) as AttachClientRequest;
|
||||||
|
await attachment.handleRequest(attachRequest);
|
||||||
|
} catch (attachError) {
|
||||||
|
socket.write(
|
||||||
|
encodeMessage({
|
||||||
|
type: "error",
|
||||||
|
ok: false,
|
||||||
|
error: attachError instanceof Error ? attachError.message : String(attachError),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.once("close", () => attachment.close());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await handler(request);
|
const response = await handler(request);
|
||||||
socket.end(encodeMessage(response));
|
socket.end(encodeMessage(response));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -11,22 +11,33 @@ function error(id: string | undefined, command: string, message: string): RpcRes
|
|||||||
return { id, type: "response", command, success: false, error: message };
|
return { id, type: "response", command, success: false, error: message };
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsupported(id: string | undefined, command: RpcCommand["type"]): RpcResponse {
|
|
||||||
return error(id, command, `Unsupported RPC command in orchestrator bridge: ${command}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleRpcCommand(runtime: AgentSessionRuntime, command: RpcCommand): Promise<RpcResponse> {
|
export async function handleRpcCommand(runtime: AgentSessionRuntime, command: RpcCommand): Promise<RpcResponse> {
|
||||||
const session = runtime.session;
|
const session = runtime.session;
|
||||||
const id = command.id;
|
const id = command.id;
|
||||||
|
|
||||||
switch (command.type) {
|
switch (command.type) {
|
||||||
case "prompt": {
|
case "prompt": {
|
||||||
await session.prompt(command.message, {
|
return await new Promise<RpcResponse>((resolve) => {
|
||||||
images: command.images,
|
let settled = false;
|
||||||
streamingBehavior: command.streamingBehavior,
|
void session
|
||||||
source: "rpc",
|
.prompt(command.message, {
|
||||||
|
images: command.images,
|
||||||
|
streamingBehavior: command.streamingBehavior,
|
||||||
|
source: "rpc",
|
||||||
|
preflightResult: (didSucceed) => {
|
||||||
|
if (!settled && didSucceed) {
|
||||||
|
settled = true;
|
||||||
|
resolve(success(id, "prompt"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((rpcError: unknown) => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
resolve(error(id, "prompt", rpcError instanceof Error ? rpcError.message : String(rpcError)));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return success(id, "prompt");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "steer": {
|
case "steer": {
|
||||||
@@ -44,11 +55,30 @@ export async function handleRpcCommand(runtime: AgentSessionRuntime, command: Rp
|
|||||||
return success(id, "abort");
|
return success(id, "abort");
|
||||||
}
|
}
|
||||||
|
|
||||||
case "new_session":
|
case "new_session": {
|
||||||
case "switch_session":
|
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
||||||
case "fork":
|
const result = await runtime.newSession(options);
|
||||||
case "clone":
|
return success(id, "new_session", result);
|
||||||
return unsupported(id, command.type);
|
}
|
||||||
|
|
||||||
|
case "switch_session": {
|
||||||
|
const result = await runtime.switchSession(command.sessionPath);
|
||||||
|
return success(id, "switch_session", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "fork": {
|
||||||
|
const result = await runtime.fork(command.entryId);
|
||||||
|
return success(id, "fork", { text: result.selectedText ?? "", cancelled: result.cancelled });
|
||||||
|
}
|
||||||
|
|
||||||
|
case "clone": {
|
||||||
|
const leafId = session.sessionManager.getLeafId();
|
||||||
|
if (!leafId) {
|
||||||
|
return error(id, "clone", "Cannot clone session: no current entry selected");
|
||||||
|
}
|
||||||
|
const result = await runtime.fork(leafId, { position: "at" });
|
||||||
|
return success(id, "clone", { cancelled: result.cancelled });
|
||||||
|
}
|
||||||
|
|
||||||
case "get_state": {
|
case "get_state": {
|
||||||
const state: RpcSessionState = {
|
const state: RpcSessionState = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import { getSocketPath } from "./config.ts";
|
import { getSocketPath } from "./config.ts";
|
||||||
import { handleIpcRequest } from "./handler.ts";
|
import { attachIpcInstance, handleIpcRequest } from "./handler.ts";
|
||||||
import { startIpcServer } from "./ipc/server.ts";
|
import { startIpcServer } from "./ipc/server.ts";
|
||||||
import { getRadiusOrchestratorBaseUrl, isRadiusEnabled, radiusPresence } from "./radius.ts";
|
import { getRadiusOrchestratorBaseUrl, isRadiusEnabled, radiusPresence } from "./radius.ts";
|
||||||
import { supervisor } from "./supervisor.ts";
|
import { supervisor } from "./supervisor.ts";
|
||||||
@@ -19,7 +19,11 @@ export async function serve(): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
console.log("radius integration disabled: set PI_RADIUS_API_KEY to enable");
|
console.log("radius integration disabled: set PI_RADIUS_API_KEY to enable");
|
||||||
}
|
}
|
||||||
const server = await startIpcServer(handleIpcRequest);
|
const server = await startIpcServer(
|
||||||
|
Object.assign(handleIpcRequest, {
|
||||||
|
attach: attachIpcInstance,
|
||||||
|
}),
|
||||||
|
);
|
||||||
console.log(`orchestrator listening on ${socketPath}`);
|
console.log(`orchestrator listening on ${socketPath}`);
|
||||||
|
|
||||||
let shutdownPromise: Promise<void> | undefined;
|
let shutdownPromise: Promise<void> | undefined;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
|
type AgentSessionEvent,
|
||||||
|
type AgentSessionEventListener,
|
||||||
type AgentSessionRuntime,
|
type AgentSessionRuntime,
|
||||||
type CreateAgentSessionRuntimeFactory,
|
type CreateAgentSessionRuntimeFactory,
|
||||||
createAgentSessionFromServices,
|
createAgentSessionFromServices,
|
||||||
@@ -18,6 +20,8 @@ import type { InstanceRecord } from "./types.ts";
|
|||||||
interface LiveInstance {
|
interface LiveInstance {
|
||||||
runtime: AgentSessionRuntime;
|
runtime: AgentSessionRuntime;
|
||||||
record: InstanceRecord;
|
record: InstanceRecord;
|
||||||
|
subscribers: Set<AgentSessionEventListener>;
|
||||||
|
unsubscribeSession?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneInstance(record: InstanceRecord): InstanceRecord {
|
function cloneInstance(record: InstanceRecord): InstanceRecord {
|
||||||
@@ -56,6 +60,29 @@ async function createRuntime(cwd: string): Promise<AgentSessionRuntime> {
|
|||||||
export class OrchestratorSupervisor {
|
export class OrchestratorSupervisor {
|
||||||
private readonly liveInstances = new Map<string, LiveInstance>();
|
private readonly liveInstances = new Map<string, LiveInstance>();
|
||||||
|
|
||||||
|
private syncInstanceRecord(live: LiveInstance): void {
|
||||||
|
live.record = {
|
||||||
|
...live.record,
|
||||||
|
sessionId: live.runtime.session.sessionId,
|
||||||
|
sessionFile: live.runtime.session.sessionFile,
|
||||||
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
upsertInstance(live.record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindLiveInstance(live: LiveInstance): void {
|
||||||
|
live.unsubscribeSession?.();
|
||||||
|
live.unsubscribeSession = live.runtime.session.subscribe((event) => {
|
||||||
|
for (const subscriber of live.subscribers) {
|
||||||
|
subscriber(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
live.runtime.setRebindSession(async () => {
|
||||||
|
this.syncInstanceRecord(live);
|
||||||
|
this.bindLiveInstance(live);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateInstance(instance: InstanceRecord): void {
|
updateInstance(instance: InstanceRecord): void {
|
||||||
const live = this.liveInstances.get(instance.id);
|
const live = this.liveInstances.get(instance.id);
|
||||||
if (live) {
|
if (live) {
|
||||||
@@ -64,6 +91,27 @@ export class OrchestratorSupervisor {
|
|||||||
upsertInstance(instance);
|
upsertInstance(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachInstance(
|
||||||
|
instanceId: string,
|
||||||
|
onEvent: (event: AgentSessionEvent) => void,
|
||||||
|
): { handleRpc(command: RpcCommand): Promise<RpcResponse>; close(): void } | undefined {
|
||||||
|
const live = this.liveInstances.get(instanceId);
|
||||||
|
if (!live) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
live.subscribers.add(onEvent);
|
||||||
|
return {
|
||||||
|
handleRpc: async (command) => {
|
||||||
|
const response = await handleRpcCommand(live.runtime, command);
|
||||||
|
this.syncInstanceRecord(live);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
live.subscribers.delete(onEvent);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getLiveInstance(instanceId: string): InstanceRecord | undefined {
|
getLiveInstance(instanceId: string): InstanceRecord | undefined {
|
||||||
const live = this.liveInstances.get(instanceId);
|
const live = this.liveInstances.get(instanceId);
|
||||||
return live ? cloneInstance(live.record) : undefined;
|
return live ? cloneInstance(live.record) : undefined;
|
||||||
@@ -114,7 +162,9 @@ export class OrchestratorSupervisor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const registeredRecord = await radiusPresence.registerPi(record);
|
const registeredRecord = await radiusPresence.registerPi(record);
|
||||||
this.liveInstances.set(registeredRecord.id, { runtime, record: registeredRecord });
|
const live: LiveInstance = { runtime, record: registeredRecord, subscribers: new Set() };
|
||||||
|
this.bindLiveInstance(live);
|
||||||
|
this.liveInstances.set(registeredRecord.id, live);
|
||||||
upsertInstance(registeredRecord);
|
upsertInstance(registeredRecord);
|
||||||
return cloneInstance(registeredRecord);
|
return cloneInstance(registeredRecord);
|
||||||
}
|
}
|
||||||
@@ -126,6 +176,8 @@ export class OrchestratorSupervisor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await radiusPresence.disconnectPi(live.record);
|
await radiusPresence.disconnectPi(live.record);
|
||||||
|
live.unsubscribeSession?.();
|
||||||
|
live.runtime.setRebindSession(undefined);
|
||||||
await live.runtime.dispose();
|
await live.runtime.dispose();
|
||||||
this.liveInstances.delete(instanceId);
|
this.liveInstances.delete(instanceId);
|
||||||
removeInstance(instanceId);
|
removeInstance(instanceId);
|
||||||
@@ -138,7 +190,9 @@ export class OrchestratorSupervisor {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleRpcCommand(live.runtime, command);
|
const response = await handleRpcCommand(live.runtime, command);
|
||||||
|
this.syncInstanceRecord(live);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user