mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-07-09 17:28:45 +00:00
fix: raw rpc
This commit is contained in:
parent
ac92f92b65
commit
a7b0138ef0
9 changed files with 315 additions and 642 deletions
|
|
@ -1,216 +0,0 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import type {
|
||||
AgentSessionRuntime,
|
||||
ExtensionUIContext,
|
||||
RpcExtensionUIRequest,
|
||||
RpcExtensionUIResponse,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
type DialogRequest =
|
||||
| Extract<RpcExtensionUIRequest, { method: "select" }>
|
||||
| Extract<RpcExtensionUIRequest, { method: "confirm" }>
|
||||
| Extract<RpcExtensionUIRequest, { method: "input" }>
|
||||
| Extract<RpcExtensionUIRequest, { method: "editor" }>;
|
||||
|
||||
type FireAndForgetRequest = Exclude<RpcExtensionUIRequest, DialogRequest>;
|
||||
|
||||
interface PendingExtensionRequest {
|
||||
resolve(response: RpcExtensionUIResponse): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class AttachUiBridge {
|
||||
private readonly pendingRequests = new Map<string, PendingExtensionRequest>();
|
||||
private onRequest?: (request: RpcExtensionUIRequest) => void;
|
||||
private themeOverride?: unknown;
|
||||
|
||||
attach(onRequest: (request: RpcExtensionUIRequest) => void): () => void {
|
||||
this.onRequest = onRequest;
|
||||
return () => {
|
||||
if (this.onRequest === onRequest) {
|
||||
this.onRequest = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setThemeOverride(theme: unknown): void {
|
||||
// This comes from attach host context over JSONL IPC. Pi's Theme is a runtime class,
|
||||
// not a wire-safe protocol type, so this remains opaque until we define a proper
|
||||
// serializable theme snapshot/DTO for the attach protocol.
|
||||
this.themeOverride = theme;
|
||||
}
|
||||
|
||||
handleResponse(response: RpcExtensionUIResponse): void {
|
||||
const pending = this.pendingRequests.get(response.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
this.pendingRequests.delete(response.id);
|
||||
pending.resolve(response);
|
||||
}
|
||||
|
||||
createUiContext(): ExtensionUIContext {
|
||||
const uiBridge = this;
|
||||
const requestDialog = <T>(
|
||||
request: DialogRequest,
|
||||
fallbackValue: T,
|
||||
parseResponse: (response: RpcExtensionUIResponse) => T,
|
||||
): Promise<T> => {
|
||||
if (!this.onRequest) {
|
||||
return Promise.resolve(fallbackValue);
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve) => {
|
||||
this.pendingRequests.set(request.id, {
|
||||
resolve: (response) => resolve(parseResponse(response)),
|
||||
cancel: () => resolve(fallbackValue),
|
||||
});
|
||||
this.onRequest?.(request);
|
||||
});
|
||||
};
|
||||
|
||||
const emit = (request: FireAndForgetRequest): void => {
|
||||
if (!this.onRequest) {
|
||||
return;
|
||||
}
|
||||
this.onRequest(request);
|
||||
};
|
||||
|
||||
return {
|
||||
select: async (title, options, opts) =>
|
||||
requestDialog(
|
||||
{
|
||||
type: "extension_ui_request",
|
||||
id: randomUUID(),
|
||||
method: "select",
|
||||
title,
|
||||
options,
|
||||
timeout: opts?.timeout,
|
||||
},
|
||||
undefined,
|
||||
(response) => ("value" in response ? response.value : undefined),
|
||||
),
|
||||
confirm: async (title, message, opts) =>
|
||||
requestDialog(
|
||||
{
|
||||
type: "extension_ui_request",
|
||||
id: randomUUID(),
|
||||
method: "confirm",
|
||||
title,
|
||||
message,
|
||||
timeout: opts?.timeout,
|
||||
},
|
||||
false,
|
||||
(response) => ("confirmed" in response ? response.confirmed : false),
|
||||
),
|
||||
input: async (title, placeholder, opts) =>
|
||||
requestDialog(
|
||||
{
|
||||
type: "extension_ui_request",
|
||||
id: randomUUID(),
|
||||
method: "input",
|
||||
title,
|
||||
placeholder,
|
||||
timeout: opts?.timeout,
|
||||
},
|
||||
undefined,
|
||||
(response) => ("value" in response ? response.value : undefined),
|
||||
),
|
||||
notify: (message, notifyType) => {
|
||||
emit({ type: "extension_ui_request", id: randomUUID(), method: "notify", message, notifyType });
|
||||
},
|
||||
onTerminalInput: () => () => {},
|
||||
setStatus: (statusKey, statusText) => {
|
||||
emit({ type: "extension_ui_request", id: randomUUID(), method: "setStatus", statusKey, statusText });
|
||||
},
|
||||
setWorkingMessage: () => {},
|
||||
setWorkingVisible: () => {},
|
||||
setWorkingIndicator: () => {},
|
||||
setHiddenThinkingLabel: () => {},
|
||||
setWidget: (widgetKey, widgetLines, options) => {
|
||||
if (widgetLines === undefined || Array.isArray(widgetLines)) {
|
||||
emit({
|
||||
type: "extension_ui_request",
|
||||
id: randomUUID(),
|
||||
method: "setWidget",
|
||||
widgetKey,
|
||||
widgetLines,
|
||||
widgetPlacement: options?.placement,
|
||||
});
|
||||
}
|
||||
},
|
||||
setFooter: () => {},
|
||||
setHeader: () => {},
|
||||
setTitle: (title) => {
|
||||
emit({ type: "extension_ui_request", id: randomUUID(), method: "setTitle", title });
|
||||
},
|
||||
custom: async () => Promise.reject(new Error("Custom UI not supported in orchestrator attach mode")),
|
||||
pasteToEditor: (text) => {
|
||||
emit({ type: "extension_ui_request", id: randomUUID(), method: "set_editor_text", text });
|
||||
},
|
||||
setEditorText: (text) => {
|
||||
emit({ type: "extension_ui_request", id: randomUUID(), method: "set_editor_text", text });
|
||||
},
|
||||
getEditorText: () => "",
|
||||
editor: async (title, prefill) =>
|
||||
requestDialog(
|
||||
{ type: "extension_ui_request", id: randomUUID(), method: "editor", title, prefill },
|
||||
undefined,
|
||||
(response) => ("value" in response ? response.value : undefined),
|
||||
),
|
||||
addAutocompleteProvider: () => {},
|
||||
setEditorComponent: () => {},
|
||||
getEditorComponent: () => undefined,
|
||||
get theme(): ExtensionUIContext["theme"] {
|
||||
// If the attach host provides a theme object, forward it. Otherwise attach mode
|
||||
// still has no real TUI/theme runtime, so fall back to an empty placeholder.
|
||||
return (uiBridge.themeOverride ?? {}) as ExtensionUIContext["theme"];
|
||||
},
|
||||
getAllThemes: () => [],
|
||||
getTheme: () => undefined,
|
||||
setTheme: () => ({ success: false, error: "Theme switching not supported in orchestrator attach mode" }),
|
||||
getToolsExpanded: () => false,
|
||||
setToolsExpanded: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
cancelPendingRequests(): void {
|
||||
for (const [id, pending] of this.pendingRequests) {
|
||||
this.pendingRequests.delete(id);
|
||||
pending.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function bindAttachExtensions(runtime: AgentSessionRuntime, uiBridge: AttachUiBridge): Promise<void> {
|
||||
const session = runtime.session;
|
||||
await session.bindExtensions({
|
||||
uiContext: uiBridge.createUiContext(),
|
||||
mode: "rpc",
|
||||
commandContextActions: {
|
||||
waitForIdle: () => session.agent.waitForIdle(),
|
||||
newSession: async (options) => runtime.newSession(options),
|
||||
fork: async (entryId, forkOptions) => {
|
||||
const result = await runtime.fork(entryId, forkOptions);
|
||||
return { cancelled: result.cancelled };
|
||||
},
|
||||
navigateTree: async (targetId, options) => {
|
||||
const result = await session.navigateTree(targetId, {
|
||||
summarize: options?.summarize,
|
||||
customInstructions: options?.customInstructions,
|
||||
replaceInstructions: options?.replaceInstructions,
|
||||
label: options?.label,
|
||||
});
|
||||
return { cancelled: result.cancelled };
|
||||
},
|
||||
switchSession: async (sessionPath, options) => runtime.switchSession(sessionPath, options),
|
||||
reload: async () => {
|
||||
await session.reload();
|
||||
},
|
||||
},
|
||||
shutdownHandler: () => {},
|
||||
onError: (error) => {
|
||||
console.error("Extension error in orchestrator attach mode", error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"),
|
|||
|
||||
function printHelp(): void {
|
||||
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 attach <instance-id>\n orchestrator --help\n orchestrator --version\n\nAttach stdin expects JSONL RpcCommand or extension_ui_response messages.`,
|
||||
`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 rpc-stream <instance-id>\n orchestrator --help\n orchestrator --version\n\nRPC stream stdin expects JSONL RpcCommand or extension_ui_response messages.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -34,14 +34,14 @@ function getFlagValue(args: string[], flag: string): string | undefined {
|
|||
return args[index + 1];
|
||||
}
|
||||
|
||||
async function attach(instanceId: string): Promise<void> {
|
||||
async function rpcStream(instanceId: string): Promise<void> {
|
||||
const socket = createConnection(getSocketPath());
|
||||
let stdinBuffer = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once("connect", () => {
|
||||
socket.write(encodeMessage({ type: "attach", instanceId }));
|
||||
socket.write(encodeMessage({ type: "rpc", instanceId }));
|
||||
resolve();
|
||||
});
|
||||
socket.once("error", reject);
|
||||
|
|
@ -50,7 +50,7 @@ async function attach(instanceId: string): Promise<void> {
|
|||
socket.on("data", (chunk: Buffer | string) => {
|
||||
process.stdout.write(chunk.toString());
|
||||
});
|
||||
console.error(`attached to ${instanceId}; send JSONL RpcCommand or extension_ui_response on stdin`);
|
||||
console.error(`connected to rpc stream ${instanceId}; send JSONL RpcCommand or extension_ui_response on stdin`);
|
||||
socket.on("error", (error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
|
|
@ -71,11 +71,7 @@ async function attach(instanceId: string): Promise<void> {
|
|||
continue;
|
||||
}
|
||||
const parsed = JSON.parse(line) as RpcCommand | RpcExtensionUIResponse;
|
||||
if (parsed.type === "extension_ui_response") {
|
||||
socket.write(encodeMessage(parsed));
|
||||
continue;
|
||||
}
|
||||
socket.write(encodeMessage({ type: "attach_rpc", command: parsed }));
|
||||
socket.write(encodeMessage(parsed));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -147,13 +143,13 @@ async function main(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (args[0] === "attach") {
|
||||
if (args[0] === "rpc-stream") {
|
||||
const instanceId = args[1];
|
||||
if (!instanceId) {
|
||||
console.error("Usage: orchestrator attach <instance-id>");
|
||||
console.error("Usage: orchestrator rpc-stream <instance-id>");
|
||||
process.exit(1);
|
||||
}
|
||||
await attach(instanceId);
|
||||
await rpcStream(instanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@ import type {
|
|||
RpcCommand,
|
||||
RpcExtensionUIRequest,
|
||||
RpcExtensionUIResponse,
|
||||
RpcResponse,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import type {
|
||||
AttachHostContextRequest,
|
||||
AttachReadyResponse,
|
||||
AttachRequest,
|
||||
AttachRpcResponse,
|
||||
ErrorResponse,
|
||||
InstanceSummary,
|
||||
ListRequest,
|
||||
|
|
@ -16,6 +13,7 @@ import type {
|
|||
OrchestratorRequest,
|
||||
OrchestratorResponse,
|
||||
RpcBridgeResponse,
|
||||
RpcReadyResponse,
|
||||
RpcRequest,
|
||||
SpawnRequest,
|
||||
SpawnResponse,
|
||||
|
|
@ -52,8 +50,9 @@ export async function handleIpcRequest(request: SpawnRequest): Promise<SpawnResp
|
|||
export async function handleIpcRequest(request: ListRequest): Promise<ListResponse | ErrorResponse>;
|
||||
export async function handleIpcRequest(request: StopRequest): Promise<StopResponse | ErrorResponse>;
|
||||
export async function handleIpcRequest(request: StatusRequest): Promise<StatusResponse | ErrorResponse>;
|
||||
export async function handleIpcRequest(request: RpcRequest): Promise<RpcBridgeResponse | ErrorResponse>;
|
||||
export async function handleIpcRequest(request: AttachRequest): Promise<AttachReadyResponse | ErrorResponse>;
|
||||
export async function handleIpcRequest(
|
||||
request: RpcRequest,
|
||||
): Promise<RpcBridgeResponse | RpcReadyResponse | ErrorResponse>;
|
||||
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse>;
|
||||
export async function handleIpcRequest(request: OrchestratorRequest): Promise<OrchestratorResponse> {
|
||||
switch (request.type) {
|
||||
|
|
@ -104,6 +103,17 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
|
|||
}
|
||||
|
||||
case "rpc": {
|
||||
if (!request.command) {
|
||||
const instance = supervisor.getInstance(request.instanceId);
|
||||
if (!instance) {
|
||||
return unknownInstanceError(request.instanceId);
|
||||
}
|
||||
return {
|
||||
type: "rpc_ready",
|
||||
ok: true,
|
||||
instance: toInstanceSummary(instance),
|
||||
};
|
||||
}
|
||||
const response = await supervisor.handleRpc(request.instanceId, request.command);
|
||||
if (!response) {
|
||||
return unknownInstanceError(request.instanceId);
|
||||
|
|
@ -115,31 +125,17 @@ export async function handleIpcRequest(request: OrchestratorRequest): Promise<Or
|
|||
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,
|
||||
onResponse: (response: RpcResponse) => void,
|
||||
onSessionEvent: (event: AgentSessionEvent) => void,
|
||||
onUiRequest: (request: RpcExtensionUIRequest) => void,
|
||||
):
|
||||
| {
|
||||
handleRequest(
|
||||
request: { type: "attach_rpc"; command: RpcCommand } | AttachHostContextRequest | RpcExtensionUIResponse,
|
||||
): Promise<void>;
|
||||
handleRequest(request: RpcCommand | RpcExtensionUIResponse): Promise<void>;
|
||||
close(): void;
|
||||
}
|
||||
| undefined {
|
||||
|
|
@ -150,16 +146,12 @@ export function attachIpcInstance(
|
|||
|
||||
return {
|
||||
async handleRequest(request): Promise<void> {
|
||||
if (request.type === "attach_rpc") {
|
||||
const response = await handle.handleRpc(request.command);
|
||||
onResponse({ type: "attach_rpc_result", response });
|
||||
if (request.type === "extension_ui_response") {
|
||||
handle.handleUiResponse(request);
|
||||
return;
|
||||
}
|
||||
if (request.type === "attach_host_context") {
|
||||
handle.setHostTheme(request.theme);
|
||||
return;
|
||||
}
|
||||
handle.handleUiResponse(request);
|
||||
const response = await handle.handleRpc(request);
|
||||
onResponse(response);
|
||||
},
|
||||
close(): void {
|
||||
handle.close();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export * from "./handler.ts";
|
|||
export * from "./ipc/client.ts";
|
||||
export * from "./ipc/protocol.ts";
|
||||
export * from "./ipc/server.ts";
|
||||
export * from "./rpc-process.ts";
|
||||
export * from "./serve.ts";
|
||||
export * from "./storage.ts";
|
||||
export * from "./supervisor.ts";
|
||||
|
|
|
|||
|
|
@ -32,22 +32,7 @@ export interface StatusRequest {
|
|||
export interface RpcRequest {
|
||||
type: "rpc";
|
||||
instanceId: string;
|
||||
command: RpcCommand;
|
||||
}
|
||||
|
||||
export interface AttachRequest {
|
||||
type: "attach";
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
export interface AttachRpcRequest {
|
||||
type: "attach_rpc";
|
||||
command: RpcCommand;
|
||||
}
|
||||
|
||||
export interface AttachHostContextRequest {
|
||||
type: "attach_host_context";
|
||||
theme?: unknown;
|
||||
command?: RpcCommand;
|
||||
}
|
||||
|
||||
export interface RequestMap {
|
||||
|
|
@ -56,7 +41,6 @@ export interface RequestMap {
|
|||
stop: StopRequest;
|
||||
status: StatusRequest;
|
||||
rpc: RpcRequest;
|
||||
attach: AttachRequest;
|
||||
}
|
||||
|
||||
export type OrchestratorRequest = RequestMap[keyof RequestMap];
|
||||
|
|
@ -101,21 +85,11 @@ export interface RpcBridgeResponse extends ResponseBase {
|
|||
response: RpcResponse;
|
||||
}
|
||||
|
||||
export interface AttachReadyResponse extends ResponseBase {
|
||||
type: "attach_ready";
|
||||
export interface RpcReadyResponse extends ResponseBase {
|
||||
type: "rpc_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 {
|
||||
type: "error";
|
||||
ok: false;
|
||||
|
|
@ -127,19 +101,18 @@ export interface ResponseMap {
|
|||
list: ListResponse;
|
||||
stop: StopResponse;
|
||||
status: StatusResponse;
|
||||
rpc: RpcBridgeResponse;
|
||||
attach: AttachReadyResponse;
|
||||
rpc: RpcBridgeResponse | RpcReadyResponse;
|
||||
}
|
||||
|
||||
export type OrchestratorResponse = ResponseMap[keyof ResponseMap] | ErrorResponse;
|
||||
export type AttachClientRequest = AttachRpcRequest | AttachHostContextRequest | RpcExtensionUIResponse;
|
||||
export type AttachServerResponse =
|
||||
| AttachReadyResponse
|
||||
| AttachEventResponse
|
||||
| AttachRpcResponse
|
||||
export type RpcClientMessage = RpcCommand | RpcExtensionUIResponse;
|
||||
export type RpcServerMessage =
|
||||
| RpcReadyResponse
|
||||
| RpcResponse
|
||||
| AgentSessionEvent
|
||||
| RpcExtensionUIRequest
|
||||
| ErrorResponse;
|
||||
export type ProtocolMessage = OrchestratorRequest | OrchestratorResponse | AttachClientRequest | AttachServerResponse;
|
||||
export type ProtocolMessage = OrchestratorRequest | OrchestratorResponse | RpcClientMessage | RpcServerMessage;
|
||||
|
||||
export type ResponseFor<T extends OrchestratorRequest> = T extends { type: infer K }
|
||||
? K extends keyof ResponseMap
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ import { existsSync, unlinkSync } from "node:fs";
|
|||
import { createConnection, createServer, type Server } from "node:net";
|
||||
import { getSocketPath } from "../config.ts";
|
||||
import {
|
||||
type AttachClientRequest,
|
||||
type AttachReadyResponse,
|
||||
type AttachRequest,
|
||||
type AttachRpcResponse,
|
||||
type ErrorResponse,
|
||||
encodeMessage,
|
||||
type ListRequest,
|
||||
|
|
@ -14,6 +10,8 @@ import {
|
|||
type OrchestratorResponse,
|
||||
parseRequestLine,
|
||||
type RpcBridgeResponse,
|
||||
type RpcClientMessage,
|
||||
type RpcReadyResponse,
|
||||
type RpcRequest,
|
||||
type SpawnRequest,
|
||||
type SpawnResponse,
|
||||
|
|
@ -28,17 +26,22 @@ export interface IpcRequestHandler {
|
|||
(request: ListRequest): Promise<ListResponse | ErrorResponse> | ListResponse | ErrorResponse;
|
||||
(request: StopRequest): Promise<StopResponse | ErrorResponse> | StopResponse | ErrorResponse;
|
||||
(request: StatusRequest): Promise<StatusResponse | ErrorResponse> | StatusResponse | ErrorResponse;
|
||||
(request: RpcRequest): Promise<RpcBridgeResponse | ErrorResponse> | RpcBridgeResponse | ErrorResponse;
|
||||
(request: AttachRequest): Promise<AttachReadyResponse | ErrorResponse> | AttachReadyResponse | ErrorResponse;
|
||||
(
|
||||
request: RpcRequest,
|
||||
):
|
||||
| Promise<RpcBridgeResponse | RpcReadyResponse | ErrorResponse>
|
||||
| RpcBridgeResponse
|
||||
| RpcReadyResponse
|
||||
| ErrorResponse;
|
||||
(request: OrchestratorRequest): Promise<OrchestratorResponse> | OrchestratorResponse;
|
||||
attach(
|
||||
instanceId: string,
|
||||
onEvent: (response: AttachRpcResponse) => void,
|
||||
onResponse: (response: import("@earendil-works/pi-coding-agent").RpcResponse) => void,
|
||||
onSessionEvent: (event: import("@earendil-works/pi-coding-agent").AgentSessionEvent) => void,
|
||||
onUiRequest: (request: import("@earendil-works/pi-coding-agent").RpcExtensionUIRequest) => void,
|
||||
):
|
||||
| {
|
||||
handleRequest(request: AttachClientRequest): Promise<void>;
|
||||
handleRequest(request: RpcClientMessage): Promise<void>;
|
||||
close(): void;
|
||||
}
|
||||
| undefined;
|
||||
|
|
@ -66,9 +69,9 @@ export async function startIpcServer(handler: IpcRequestHandler): Promise<Server
|
|||
|
||||
try {
|
||||
const request = parseRequestLine(line);
|
||||
if (request.type === "attach") {
|
||||
if (request.type === "rpc" && request.command === undefined) {
|
||||
const response = await handler(request);
|
||||
if (!response.ok || !response.instance) {
|
||||
if (!response.ok || response.type !== "rpc_ready" || !response.instance) {
|
||||
socket.end(encodeMessage(response));
|
||||
return;
|
||||
}
|
||||
|
|
@ -79,7 +82,7 @@ export async function startIpcServer(handler: IpcRequestHandler): Promise<Server
|
|||
socket.write(encodeMessage(response));
|
||||
},
|
||||
(event) => {
|
||||
socket.write(encodeMessage({ type: "attach_event", event }));
|
||||
socket.write(encodeMessage(event));
|
||||
},
|
||||
(request) => {
|
||||
socket.write(encodeMessage(request));
|
||||
|
|
@ -94,28 +97,28 @@ export async function startIpcServer(handler: IpcRequestHandler): Promise<Server
|
|||
|
||||
socket.write(encodeMessage(response));
|
||||
socket.removeAllListeners("data");
|
||||
socket.on("data", (attachChunk: Buffer | string) => {
|
||||
buffer += attachChunk.toString();
|
||||
socket.on("data", (rpcChunk: Buffer | string) => {
|
||||
buffer += rpcChunk.toString();
|
||||
for (;;) {
|
||||
const attachNewlineIndex = buffer.indexOf("\n");
|
||||
if (attachNewlineIndex === -1) {
|
||||
const rpcNewlineIndex = buffer.indexOf("\n");
|
||||
if (rpcNewlineIndex === -1) {
|
||||
break;
|
||||
}
|
||||
const attachLine = buffer.slice(0, attachNewlineIndex).trim();
|
||||
buffer = buffer.slice(attachNewlineIndex + 1);
|
||||
if (!attachLine) {
|
||||
const rpcLine = buffer.slice(0, rpcNewlineIndex).trim();
|
||||
buffer = buffer.slice(rpcNewlineIndex + 1);
|
||||
if (!rpcLine) {
|
||||
continue;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const attachRequest = JSON.parse(attachLine) as AttachClientRequest;
|
||||
await attachment.handleRequest(attachRequest);
|
||||
} catch (attachError) {
|
||||
const rpcRequest = JSON.parse(rpcLine) as RpcClientMessage;
|
||||
await attachment.handleRequest(rpcRequest);
|
||||
} catch (rpcError) {
|
||||
socket.write(
|
||||
encodeMessage({
|
||||
type: "error",
|
||||
ok: false,
|
||||
error: attachError instanceof Error ? attachError.message : String(attachError),
|
||||
error: rpcError instanceof Error ? rpcError.message : String(rpcError),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,246 +0,0 @@
|
|||
import type { AgentSessionRuntime, RpcCommand, RpcResponse, RpcSessionState } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
function success<T extends RpcCommand["type"]>(id: string | undefined, command: T, data?: object | null): RpcResponse {
|
||||
if (data === undefined) {
|
||||
return { id, type: "response", command, success: true } as RpcResponse;
|
||||
}
|
||||
return { id, type: "response", command, success: true, data } as RpcResponse;
|
||||
}
|
||||
|
||||
function error(id: string | undefined, command: string, message: string): RpcResponse {
|
||||
return { id, type: "response", command, success: false, error: message };
|
||||
}
|
||||
|
||||
function assertNeverRpcCommand(command: never): never {
|
||||
throw new Error(`Unknown command: ${(command as { type: string }).type}`);
|
||||
}
|
||||
|
||||
export async function handleRpcCommand(runtime: AgentSessionRuntime, command: RpcCommand): Promise<RpcResponse> {
|
||||
const session = runtime.session;
|
||||
const id = command.id;
|
||||
|
||||
switch (command.type) {
|
||||
case "prompt": {
|
||||
return await new Promise<RpcResponse>((resolve) => {
|
||||
let settled = false;
|
||||
void session
|
||||
.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)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
case "steer": {
|
||||
await session.steer(command.message, command.images);
|
||||
return success(id, "steer");
|
||||
}
|
||||
|
||||
case "follow_up": {
|
||||
await session.followUp(command.message, command.images);
|
||||
return success(id, "follow_up");
|
||||
}
|
||||
|
||||
case "abort": {
|
||||
await session.abort();
|
||||
return success(id, "abort");
|
||||
}
|
||||
|
||||
case "new_session": {
|
||||
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
||||
const result = await runtime.newSession(options);
|
||||
return success(id, "new_session", result);
|
||||
}
|
||||
|
||||
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": {
|
||||
const state: RpcSessionState = {
|
||||
model: session.model,
|
||||
thinkingLevel: session.thinkingLevel,
|
||||
isStreaming: session.isStreaming,
|
||||
isCompacting: session.isCompacting,
|
||||
steeringMode: session.steeringMode,
|
||||
followUpMode: session.followUpMode,
|
||||
sessionFile: session.sessionFile,
|
||||
sessionId: session.sessionId,
|
||||
sessionName: session.sessionName,
|
||||
autoCompactionEnabled: session.autoCompactionEnabled,
|
||||
messageCount: session.messages.length,
|
||||
pendingMessageCount: session.pendingMessageCount,
|
||||
};
|
||||
return success(id, "get_state", state);
|
||||
}
|
||||
|
||||
case "set_model": {
|
||||
const models = await session.modelRegistry.getAvailable();
|
||||
const model = models.find((m) => m.provider === command.provider && m.id === command.modelId);
|
||||
if (!model) {
|
||||
return error(id, "set_model", `Model not found: ${command.provider}/${command.modelId}`);
|
||||
}
|
||||
await session.setModel(model);
|
||||
return success(id, "set_model", model);
|
||||
}
|
||||
|
||||
case "cycle_model": {
|
||||
const result = await session.cycleModel();
|
||||
return success(id, "cycle_model", result ?? null);
|
||||
}
|
||||
|
||||
case "get_available_models": {
|
||||
const models = await session.modelRegistry.getAvailable();
|
||||
return success(id, "get_available_models", { models });
|
||||
}
|
||||
|
||||
case "set_thinking_level": {
|
||||
session.setThinkingLevel(command.level);
|
||||
return success(id, "set_thinking_level");
|
||||
}
|
||||
|
||||
case "cycle_thinking_level": {
|
||||
const level = session.cycleThinkingLevel();
|
||||
return success(id, "cycle_thinking_level", level ? { level } : null);
|
||||
}
|
||||
|
||||
case "set_steering_mode": {
|
||||
session.setSteeringMode(command.mode);
|
||||
return success(id, "set_steering_mode");
|
||||
}
|
||||
|
||||
case "set_follow_up_mode": {
|
||||
session.setFollowUpMode(command.mode);
|
||||
return success(id, "set_follow_up_mode");
|
||||
}
|
||||
|
||||
case "compact": {
|
||||
const result = await session.compact(command.customInstructions);
|
||||
return success(id, "compact", result);
|
||||
}
|
||||
|
||||
case "set_auto_compaction": {
|
||||
session.setAutoCompactionEnabled(command.enabled);
|
||||
return success(id, "set_auto_compaction");
|
||||
}
|
||||
|
||||
case "set_auto_retry": {
|
||||
session.setAutoRetryEnabled(command.enabled);
|
||||
return success(id, "set_auto_retry");
|
||||
}
|
||||
|
||||
case "abort_retry": {
|
||||
session.abortRetry();
|
||||
return success(id, "abort_retry");
|
||||
}
|
||||
|
||||
case "bash": {
|
||||
const result = await session.executeBash(command.command, undefined, {
|
||||
excludeFromContext: command.excludeFromContext,
|
||||
});
|
||||
return success(id, "bash", result);
|
||||
}
|
||||
|
||||
case "abort_bash": {
|
||||
session.abortBash();
|
||||
return success(id, "abort_bash");
|
||||
}
|
||||
|
||||
case "get_session_stats": {
|
||||
const stats = session.getSessionStats();
|
||||
return success(id, "get_session_stats", stats);
|
||||
}
|
||||
|
||||
case "export_html": {
|
||||
const path = await session.exportToHtml(command.outputPath);
|
||||
return success(id, "export_html", { path });
|
||||
}
|
||||
|
||||
case "get_fork_messages": {
|
||||
const messages = session.getUserMessagesForForking();
|
||||
return success(id, "get_fork_messages", { messages });
|
||||
}
|
||||
|
||||
case "get_last_assistant_text": {
|
||||
const text = session.getLastAssistantText();
|
||||
return success(id, "get_last_assistant_text", { text });
|
||||
}
|
||||
|
||||
case "set_session_name": {
|
||||
const name = command.name.trim();
|
||||
if (!name) {
|
||||
return error(id, "set_session_name", "Session name cannot be empty");
|
||||
}
|
||||
session.setSessionName(name);
|
||||
return success(id, "set_session_name");
|
||||
}
|
||||
|
||||
case "get_messages": {
|
||||
return success(id, "get_messages", { messages: session.messages });
|
||||
}
|
||||
|
||||
case "get_commands": {
|
||||
const commands = [];
|
||||
|
||||
for (const registeredCommand of session.extensionRunner.getRegisteredCommands()) {
|
||||
commands.push({
|
||||
name: registeredCommand.invocationName,
|
||||
description: registeredCommand.description,
|
||||
source: "extension",
|
||||
sourceInfo: registeredCommand.sourceInfo,
|
||||
});
|
||||
}
|
||||
|
||||
for (const template of session.promptTemplates) {
|
||||
commands.push({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
source: "prompt",
|
||||
sourceInfo: template.sourceInfo,
|
||||
});
|
||||
}
|
||||
|
||||
for (const skill of session.resourceLoader.getSkills().skills) {
|
||||
commands.push({
|
||||
name: `skill:${skill.name}`,
|
||||
description: skill.description,
|
||||
source: "skill",
|
||||
sourceInfo: skill.sourceInfo,
|
||||
});
|
||||
}
|
||||
|
||||
return success(id, "get_commands", { commands });
|
||||
}
|
||||
|
||||
default:
|
||||
return assertNeverRpcCommand(command);
|
||||
}
|
||||
}
|
||||
183
packages/orchestrator/src/rpc-process.ts
Normal file
183
packages/orchestrator/src/rpc-process.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createRequire } from "node:module";
|
||||
import type {
|
||||
AgentSessionEvent,
|
||||
RpcCommand,
|
||||
RpcExtensionUIRequest,
|
||||
RpcExtensionUIResponse,
|
||||
RpcResponse,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
interface PendingRequest {
|
||||
resolve(response: RpcResponse): void;
|
||||
reject(error: Error): void;
|
||||
}
|
||||
|
||||
export interface RpcProcessInstance {
|
||||
process: ChildProcess;
|
||||
send(command: RpcCommand): Promise<RpcResponse>;
|
||||
handleUiResponse(response: RpcExtensionUIResponse): void;
|
||||
setUiRequestHandler(handler?: (request: RpcExtensionUIRequest) => void): void;
|
||||
onEvent(listener: (event: AgentSessionEvent) => void): () => void;
|
||||
onExit(listener: (error?: Error) => void): () => void;
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
function resolveCodingAgentCli(): string {
|
||||
const require = createRequire(import.meta.url);
|
||||
return require.resolve("@earendil-works/pi-coding-agent/dist/cli.js");
|
||||
}
|
||||
|
||||
function toError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
|
||||
export function createRpcProcessInstance(options: { cwd: string }): RpcProcessInstance {
|
||||
const cliPath = resolveCodingAgentCli();
|
||||
const child = spawn(process.execPath, [cliPath, "--mode", "rpc"], {
|
||||
cwd: options.cwd,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
if (!child.stdin || !child.stdout) {
|
||||
throw new Error("Failed to create RPC process stdio");
|
||||
}
|
||||
|
||||
let exited = false;
|
||||
let nextRequestId = 0;
|
||||
let stdoutBuffer = "";
|
||||
let stderrBuffer = "";
|
||||
const pendingRequests = new Map<string, PendingRequest>();
|
||||
const eventListeners = new Set<(event: AgentSessionEvent) => void>();
|
||||
const exitListeners = new Set<(error?: Error) => void>();
|
||||
|
||||
const rejectAllPending = (error: Error) => {
|
||||
for (const [id, pending] of pendingRequests) {
|
||||
pendingRequests.delete(id);
|
||||
pending.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const notifyExit = (error?: Error) => {
|
||||
for (const listener of exitListeners) {
|
||||
listener(error);
|
||||
}
|
||||
};
|
||||
|
||||
let uiRequestHandler: ((request: RpcExtensionUIRequest) => void) | undefined;
|
||||
|
||||
const handleLine = (line: string) => {
|
||||
const parsed = JSON.parse(line) as { type?: string; id?: string };
|
||||
if (parsed.type === "response") {
|
||||
if (parsed.id) {
|
||||
const pending = pendingRequests.get(parsed.id);
|
||||
if (pending) {
|
||||
pendingRequests.delete(parsed.id);
|
||||
pending.resolve(parsed as RpcResponse);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.type === "extension_ui_request") {
|
||||
uiRequestHandler?.(parsed as RpcExtensionUIRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of eventListeners) {
|
||||
listener(parsed as AgentSessionEvent);
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk: string) => {
|
||||
stdoutBuffer += chunk;
|
||||
while (true) {
|
||||
const newlineIndex = stdoutBuffer.indexOf("\n");
|
||||
if (newlineIndex === -1) {
|
||||
break;
|
||||
}
|
||||
const line = stdoutBuffer.slice(0, newlineIndex).trim();
|
||||
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
handleLine(line);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr?.setEncoding("utf8");
|
||||
child.stderr?.on("data", (chunk: string) => {
|
||||
stderrBuffer += chunk;
|
||||
});
|
||||
|
||||
child.once("error", (error) => {
|
||||
exited = true;
|
||||
const wrapped = new Error(`RPC process error: ${error.message}. Stderr: ${stderrBuffer}`);
|
||||
rejectAllPending(wrapped);
|
||||
notifyExit(wrapped);
|
||||
});
|
||||
|
||||
child.once("exit", (code, signal) => {
|
||||
exited = true;
|
||||
const error = new Error(`RPC process exited (code=${code} signal=${signal}). Stderr: ${stderrBuffer}`);
|
||||
rejectAllPending(error);
|
||||
notifyExit(error);
|
||||
});
|
||||
|
||||
const send = async (command: RpcCommand): Promise<RpcResponse> => {
|
||||
if (exited) {
|
||||
throw new Error(`RPC process is not running. Stderr: ${stderrBuffer}`);
|
||||
}
|
||||
const id = command.id ?? `orchestrator_${++nextRequestId}_${randomUUID()}`;
|
||||
const fullCommand = { ...command, id };
|
||||
return new Promise<RpcResponse>((resolve, reject) => {
|
||||
pendingRequests.set(id, { resolve, reject });
|
||||
child.stdin.write(`${JSON.stringify(fullCommand)}\n`, (error) => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
pendingRequests.delete(id);
|
||||
reject(toError(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
process: child,
|
||||
send,
|
||||
handleUiResponse(response) {
|
||||
if (exited) {
|
||||
return;
|
||||
}
|
||||
child.stdin.write(`${JSON.stringify(response)}\n`);
|
||||
},
|
||||
setUiRequestHandler(handler) {
|
||||
uiRequestHandler = handler;
|
||||
},
|
||||
onEvent(listener) {
|
||||
eventListeners.add(listener);
|
||||
return () => {
|
||||
eventListeners.delete(listener);
|
||||
};
|
||||
},
|
||||
onExit(listener) {
|
||||
exitListeners.add(listener);
|
||||
return () => {
|
||||
exitListeners.delete(listener);
|
||||
};
|
||||
},
|
||||
async dispose() {
|
||||
uiRequestHandler = undefined;
|
||||
rejectAllPending(new Error("RPC process disposed"));
|
||||
if (exited) {
|
||||
return;
|
||||
}
|
||||
child.kill("SIGTERM");
|
||||
await new Promise<void>((resolve) => {
|
||||
child.once("exit", () => resolve());
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,90 +1,80 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
type AgentSessionEvent,
|
||||
type AgentSessionEventListener,
|
||||
type AgentSessionRuntime,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionRuntime,
|
||||
createAgentSessionServices,
|
||||
getAgentDir,
|
||||
type RpcCommand,
|
||||
type RpcExtensionUIRequest,
|
||||
type RpcExtensionUIResponse,
|
||||
type RpcResponse,
|
||||
SessionManager,
|
||||
import type {
|
||||
AgentSessionEvent,
|
||||
AgentSessionEventListener,
|
||||
RpcCommand,
|
||||
RpcExtensionUIRequest,
|
||||
RpcExtensionUIResponse,
|
||||
RpcResponse,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { AttachUiBridge, bindAttachExtensions } from "./attach-ui.ts";
|
||||
import { radiusPresence } from "./radius.ts";
|
||||
import { handleRpcCommand } from "./rpc-bridge.ts";
|
||||
import { createRpcProcessInstance, type RpcProcessInstance } from "./rpc-process.ts";
|
||||
import { getInstance, loadInstances, removeInstance, saveInstances, upsertInstance } from "./storage.ts";
|
||||
import type { InstanceRecord } from "./types.ts";
|
||||
|
||||
interface LiveInstance {
|
||||
runtime: AgentSessionRuntime;
|
||||
rpc: RpcProcessInstance;
|
||||
record: InstanceRecord;
|
||||
subscribers: Set<AgentSessionEventListener>;
|
||||
uiBridge: AttachUiBridge;
|
||||
unsubscribeSession?: () => void;
|
||||
onUiRequest?: (request: RpcExtensionUIRequest) => void;
|
||||
unsubscribeEvents?: () => void;
|
||||
unsubscribeExit?: () => void;
|
||||
}
|
||||
|
||||
function cloneInstance(record: InstanceRecord): InstanceRecord {
|
||||
return { ...record };
|
||||
}
|
||||
|
||||
async function createRuntime(cwd: string): Promise<AgentSessionRuntime> {
|
||||
const agentDir = getAgentDir();
|
||||
const sessionManager = SessionManager.create(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,
|
||||
});
|
||||
function isGetStateSuccess(
|
||||
response: RpcResponse,
|
||||
): response is Extract<
|
||||
RpcResponse,
|
||||
{ success: true; command: "get_state"; data: { sessionId: string; sessionFile?: string } }
|
||||
> {
|
||||
return response.success === true && response.command === "get_state" && "data" in response;
|
||||
}
|
||||
|
||||
export class OrchestratorSupervisor {
|
||||
private readonly liveInstances = new Map<string, LiveInstance>();
|
||||
|
||||
private syncInstanceRecord(live: LiveInstance): void {
|
||||
private async syncInstanceRecord(live: LiveInstance): Promise<void> {
|
||||
const response = await live.rpc.send({ type: "get_state" });
|
||||
if (!isGetStateSuccess(response)) {
|
||||
live.record = {
|
||||
...live.record,
|
||||
lastSeenAt: new Date().toISOString(),
|
||||
};
|
||||
upsertInstance(live.record);
|
||||
return;
|
||||
}
|
||||
live.record = {
|
||||
...live.record,
|
||||
sessionId: live.runtime.session.sessionId,
|
||||
sessionFile: live.runtime.session.sessionFile,
|
||||
sessionId: response.data.sessionId,
|
||||
sessionFile: response.data.sessionFile,
|
||||
lastSeenAt: new Date().toISOString(),
|
||||
};
|
||||
upsertInstance(live.record);
|
||||
}
|
||||
|
||||
private async bindLiveInstance(live: LiveInstance): Promise<void> {
|
||||
await bindAttachExtensions(live.runtime, live.uiBridge);
|
||||
live.unsubscribeSession?.();
|
||||
live.unsubscribeSession = live.runtime.session.subscribe((event) => {
|
||||
private bindLiveInstance(live: LiveInstance): void {
|
||||
live.unsubscribeEvents?.();
|
||||
live.unsubscribeExit?.();
|
||||
live.unsubscribeEvents = live.rpc.onEvent((event) => {
|
||||
for (const subscriber of live.subscribers) {
|
||||
subscriber(event);
|
||||
}
|
||||
});
|
||||
live.runtime.setRebindSession(async () => {
|
||||
this.syncInstanceRecord(live);
|
||||
await this.bindLiveInstance(live);
|
||||
live.unsubscribeExit = live.rpc.onExit(() => {
|
||||
live.record = {
|
||||
...live.record,
|
||||
status: "stopped",
|
||||
lastSeenAt: new Date().toISOString(),
|
||||
};
|
||||
upsertInstance(live.record);
|
||||
this.liveInstances.delete(live.record.id);
|
||||
});
|
||||
live.rpc.setUiRequestHandler((request) => {
|
||||
live.onUiRequest?.(request);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -113,23 +103,22 @@ export class OrchestratorSupervisor {
|
|||
return undefined;
|
||||
}
|
||||
live.subscribers.add(onEvent);
|
||||
const detachUi = live.uiBridge.attach(onUiRequest);
|
||||
live.onUiRequest = onUiRequest;
|
||||
return {
|
||||
handleRpc: async (command) => {
|
||||
const response = await handleRpcCommand(live.runtime, command);
|
||||
this.syncInstanceRecord(live);
|
||||
const response = await live.rpc.send(command);
|
||||
await this.syncInstanceRecord(live);
|
||||
return response;
|
||||
},
|
||||
handleUiResponse: (response) => {
|
||||
live.uiBridge.handleResponse(response);
|
||||
},
|
||||
setHostTheme: (theme) => {
|
||||
live.uiBridge.setThemeOverride(theme);
|
||||
live.rpc.handleUiResponse(response);
|
||||
},
|
||||
setHostTheme: (_theme) => {},
|
||||
close: () => {
|
||||
detachUi();
|
||||
if (live.onUiRequest === onUiRequest) {
|
||||
live.onUiRequest = undefined;
|
||||
}
|
||||
live.subscribers.delete(onEvent);
|
||||
live.uiBridge.cancelPendingRequests();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -170,7 +159,7 @@ export class OrchestratorSupervisor {
|
|||
}
|
||||
|
||||
async spawnInstance(options: { cwd: string; label?: string }): Promise<InstanceRecord> {
|
||||
const runtime = await createRuntime(options.cwd);
|
||||
const rpc = createRpcProcessInstance({ cwd: options.cwd });
|
||||
const now = new Date().toISOString();
|
||||
const record: InstanceRecord = {
|
||||
id: randomUUID(),
|
||||
|
|
@ -179,21 +168,19 @@ export class OrchestratorSupervisor {
|
|||
createdAt: now,
|
||||
lastSeenAt: now,
|
||||
label: options.label,
|
||||
sessionId: runtime.session.sessionId,
|
||||
sessionFile: runtime.session.sessionFile,
|
||||
};
|
||||
|
||||
const registeredRecord = await radiusPresence.registerPi(record);
|
||||
const live: LiveInstance = {
|
||||
runtime,
|
||||
rpc,
|
||||
record: registeredRecord,
|
||||
subscribers: new Set(),
|
||||
uiBridge: new AttachUiBridge(),
|
||||
};
|
||||
await this.bindLiveInstance(live);
|
||||
this.bindLiveInstance(live);
|
||||
this.liveInstances.set(registeredRecord.id, live);
|
||||
upsertInstance(registeredRecord);
|
||||
return cloneInstance(registeredRecord);
|
||||
await this.syncInstanceRecord(live);
|
||||
upsertInstance(live.record);
|
||||
return cloneInstance(live.record);
|
||||
}
|
||||
|
||||
async stopInstance(instanceId: string): Promise<InstanceRecord | undefined> {
|
||||
|
|
@ -203,10 +190,10 @@ export class OrchestratorSupervisor {
|
|||
}
|
||||
|
||||
await radiusPresence.disconnectPi(live.record);
|
||||
live.unsubscribeSession?.();
|
||||
live.uiBridge.cancelPendingRequests();
|
||||
live.runtime.setRebindSession(undefined);
|
||||
await live.runtime.dispose();
|
||||
live.unsubscribeEvents?.();
|
||||
live.unsubscribeExit?.();
|
||||
live.onUiRequest = undefined;
|
||||
await live.rpc.dispose();
|
||||
this.liveInstances.delete(instanceId);
|
||||
removeInstance(instanceId);
|
||||
return cloneInstance(live.record);
|
||||
|
|
@ -218,8 +205,8 @@ export class OrchestratorSupervisor {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const response = await handleRpcCommand(live.runtime, command);
|
||||
this.syncInstanceRecord(live);
|
||||
const response = await live.rpc.send(command);
|
||||
await this.syncInstanceRecord(live);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue