chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
230
third_party/pi/packages/coding-agent/src/experimental/client-runtime.ts
vendored
Normal file
230
third_party/pi/packages/coding-agent/src/experimental/client-runtime.ts
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
import { basename } from "node:path";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Client, ServerError } from "@earendil-works/pi-client";
|
||||
import { createUnixTransportFactory, discoverUnixServers, type UnixServerRoute } from "@earendil-works/pi-client/unix";
|
||||
import { isServerId, type ServerId } from "@earendil-works/pi-protocol";
|
||||
import type { ClientCommand } from "../cli/experimental/commands/client.ts";
|
||||
import { RadiusRelayAuthResolver } from "./radius-auth.ts";
|
||||
import { createRadiusClientTransportFactory, RadiusClientReconnect } from "./radius-relay.ts";
|
||||
import { activateServer, ENV_SERVER_ID, resolveServerDirectory, resolveSessionDirectory } from "./server.ts";
|
||||
import { AgentController } from "./services/agent-controller.ts";
|
||||
import {
|
||||
createServerServiceSource,
|
||||
createSessionServiceSource,
|
||||
type ServerServiceSource,
|
||||
type SessionServiceSource,
|
||||
} from "./services/connection.ts";
|
||||
import { Models } from "./services/models.ts";
|
||||
import { PresentationPlugins } from "./services/plugins.ts";
|
||||
import { SessionDirectory, SessionManagement } from "./services/sessions.ts";
|
||||
import { Transcript } from "./services/transcript.ts";
|
||||
|
||||
export type ClientRuntimeRoute =
|
||||
| ({ readonly transport: "unix" } & UnixServerRoute)
|
||||
| { readonly transport: "radius"; readonly serverId: ServerId };
|
||||
|
||||
export interface ClientRuntimeServer {
|
||||
readonly route: ClientRuntimeRoute;
|
||||
readonly client: Client;
|
||||
readonly server: ServerServiceSource;
|
||||
readonly session: SessionServiceSource;
|
||||
}
|
||||
|
||||
export interface ActivatedClientRuntimeServer extends ClientRuntimeServer {
|
||||
readonly directory: SessionDirectory;
|
||||
readonly management: SessionManagement;
|
||||
readonly plugins: PresentationPlugins;
|
||||
readonly models: Models;
|
||||
readonly agent: AgentController;
|
||||
readonly transcript: Transcript;
|
||||
}
|
||||
|
||||
export interface ClientRuntime {
|
||||
readonly servers: readonly ClientRuntimeServer[];
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface OpenClientRuntimeOptions {
|
||||
/** Directory searched when --connect is omitted. Defaults to PI_SERVER_DIR or ~/.pi/server. */
|
||||
readonly directory?: string;
|
||||
}
|
||||
|
||||
/** Open live server/session service namespaces for one experimental presentation. */
|
||||
export async function openClientRuntime(
|
||||
command: ClientCommand,
|
||||
options: OpenClientRuntimeOptions = {},
|
||||
): Promise<ClientRuntime> {
|
||||
if (command.auth !== undefined && command.connect?.transport !== "radius") {
|
||||
throw new Error("Authentication is only supported for experimental Radius connections");
|
||||
}
|
||||
if (command.provider !== undefined && command.model === undefined) {
|
||||
throw new Error("Server model provider requires a model");
|
||||
}
|
||||
if (command.connect && command.model !== undefined) {
|
||||
throw new Error("Model selection is only valid when automatically activating a new server");
|
||||
}
|
||||
if (command.connect?.transport === "radius" && command.pluginPackages !== undefined) {
|
||||
throw new Error("Plugin package paths can only be configured on a local Unix server");
|
||||
}
|
||||
const directory = resolveServerDirectory(options.directory);
|
||||
let routes: ClientRuntimeRoute[];
|
||||
let activatedClient: Client | undefined;
|
||||
if (command.connect) {
|
||||
routes = [
|
||||
command.connect.transport === "radius"
|
||||
? { transport: "radius", serverId: command.connect.serverId }
|
||||
: { transport: "unix", ...routeFromExplicitPath(command.connect.path) },
|
||||
];
|
||||
} else {
|
||||
routes = (await discoverUnixServers({ directory })).map((route) => ({ transport: "unix", ...route }));
|
||||
if (routes.length > 0 && command.model !== undefined) {
|
||||
throw new Error("Model selection is only valid when automatically activating a new server");
|
||||
}
|
||||
if (routes.length === 0) {
|
||||
const activated = await activateServer({
|
||||
directory,
|
||||
requestedServerId: process.env[ENV_SERVER_ID],
|
||||
sessionDir: resolveSessionDirectory(),
|
||||
provider: command.provider,
|
||||
model: command.model,
|
||||
});
|
||||
routes = [{ transport: "unix", ...activated.route }];
|
||||
activatedClient = activated.client;
|
||||
}
|
||||
}
|
||||
if (command.pluginPackages !== undefined && routes.length !== 1) {
|
||||
throw new Error("Plugin selection requires exactly one local server");
|
||||
}
|
||||
|
||||
const clients: Client[] = [];
|
||||
const reconnectors: RadiusClientReconnect[] = [];
|
||||
const serviceSources: Array<ServerServiceSource | SessionServiceSource> = [];
|
||||
const servers: ClientRuntimeServer[] = [];
|
||||
let disposed = false;
|
||||
const dispose = async (): Promise<void> => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
const reconnectResults = await Promise.allSettled(reconnectors.map((reconnector) => reconnector.dispose()));
|
||||
const sourceResults = await Promise.allSettled(
|
||||
serviceSources.map((source) => source.dispose(BACKGROUND_CONTEXT)),
|
||||
);
|
||||
const clientResults = await Promise.allSettled(clients.map((client) => client.dispose()));
|
||||
const errors = [...reconnectResults, ...sourceResults, ...clientResults].flatMap((result) =>
|
||||
result.status === "rejected" ? [result.reason] : [],
|
||||
);
|
||||
if (errors.length === 1) throw errors[0];
|
||||
if (errors.length > 1) throw new AggregateError(errors, "Failed to dispose experimental client runtime");
|
||||
};
|
||||
|
||||
try {
|
||||
for (const route of routes) {
|
||||
let client = activatedClient;
|
||||
if (client === undefined) {
|
||||
try {
|
||||
client = await Client.connect({
|
||||
serverId: route.serverId,
|
||||
transportFactory:
|
||||
route.transport === "unix"
|
||||
? createUnixTransportFactory({ path: route.path })
|
||||
: createRadiusClientTransportFactory({
|
||||
serverId: route.serverId,
|
||||
auth: new RadiusRelayAuthResolver(command.auth),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
command.connect !== undefined ||
|
||||
route.transport !== "unix" ||
|
||||
!(error instanceof ServerError) ||
|
||||
error.code !== "version"
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
client = (
|
||||
await activateServer({
|
||||
directory,
|
||||
requestedServerId: route.serverId,
|
||||
sessionDir: resolveSessionDirectory(),
|
||||
})
|
||||
).client;
|
||||
}
|
||||
}
|
||||
activatedClient = undefined;
|
||||
clients.push(client);
|
||||
const server = createServerServiceSource(client);
|
||||
serviceSources.push(server);
|
||||
if (route.transport === "radius") {
|
||||
const reconnectServices = server.open({
|
||||
services: [SessionManagement],
|
||||
assertAccess() {},
|
||||
onError() {},
|
||||
});
|
||||
const reconnectManagement = reconnectServices.use(SessionManagement);
|
||||
reconnectors.push(
|
||||
new RadiusClientReconnect(client, async (sessionId) => {
|
||||
await reconnectServices.ready(BACKGROUND_CONTEXT);
|
||||
await reconnectManagement.attach(sessionId, BACKGROUND_CONTEXT);
|
||||
}),
|
||||
);
|
||||
}
|
||||
const session = createSessionServiceSource(client);
|
||||
serviceSources.push(session);
|
||||
servers.push({ route, client, server, session });
|
||||
}
|
||||
return { servers, dispose };
|
||||
} catch (error) {
|
||||
try {
|
||||
await dispose();
|
||||
} catch (cleanupError) {
|
||||
throw new AggregateError([error, cleanupError], "Experimental client startup and cleanup failed");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Acquire and connect the built-in service facades used by the non-interactive client. */
|
||||
export async function activateBuiltinClientServices(
|
||||
server: ClientRuntimeServer,
|
||||
): Promise<ActivatedClientRuntimeServer> {
|
||||
const serverServices = server.server.open({
|
||||
services: [SessionDirectory, SessionManagement, PresentationPlugins],
|
||||
assertAccess() {},
|
||||
onError() {},
|
||||
});
|
||||
const sessionServices = server.session.open({
|
||||
services: [Models, AgentController, Transcript],
|
||||
assertAccess() {},
|
||||
onError() {},
|
||||
});
|
||||
const directory = serverServices.use(SessionDirectory);
|
||||
const remoteManagement = serverServices.use(SessionManagement);
|
||||
const management: SessionManagement = {
|
||||
create: (options, context) => remoteManagement.create(options, context),
|
||||
async remove(sessionId, context) {
|
||||
const removesCurrentAttachment = server.client.attachment?.sessionId === sessionId;
|
||||
await remoteManagement.remove(sessionId, context);
|
||||
if (removesCurrentAttachment) await server.session.whenDetached(context);
|
||||
},
|
||||
async attach(sessionId, context) {
|
||||
await remoteManagement.attach(sessionId, context);
|
||||
await server.session.whenAttached(sessionId, context);
|
||||
},
|
||||
async detach(context) {
|
||||
await remoteManagement.detach(context);
|
||||
await server.session.whenDetached(context);
|
||||
},
|
||||
};
|
||||
const plugins = serverServices.use(PresentationPlugins);
|
||||
const models = sessionServices.use(Models);
|
||||
const agent = sessionServices.use(AgentController);
|
||||
const transcript = sessionServices.use(Transcript);
|
||||
await Promise.all([serverServices.ready(BACKGROUND_CONTEXT), sessionServices.ready(BACKGROUND_CONTEXT)]);
|
||||
return { ...server, directory, management, plugins, models, agent, transcript };
|
||||
}
|
||||
|
||||
function routeFromExplicitPath(path: string): UnixServerRoute {
|
||||
const name = basename(path);
|
||||
const serverId = name.endsWith(".sock") ? name.slice(0, -".sock".length) : "";
|
||||
if (!isServerId(serverId)) throw new Error("--connect path must end with <uuidv4-server-id>.sock");
|
||||
return { serverId, path };
|
||||
}
|
||||
Reference in New Issue
Block a user