blob: e38752cb3603eda3edf9ae2bf2e77047adcef182 [file]
import { NO_REAL_CONNECTION_CODE } from '@maka/core';
import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/core';
import {
connectOrSpawnRuntimeHost,
readRuntimeHostConnectionCatalog,
type RuntimeHostConnection,
} from '@maka/runtime-host/client';
import { RUNTIME_HOST_PROTOCOL_VERSION, type ClientSurface } from '@maka/runtime-host/protocol';
export interface RuntimeHostCliConnectionContext {
readonly connection: RuntimeHostConnection;
readonly catalog: ConnectionCatalogSnapshot;
close(): Promise<void>;
}
export interface RuntimeHostCliTarget {
readonly connection: ConnectionCatalogEntry;
readonly model: string;
}
interface RuntimeHostCliContextDeps {
readonly connectOrSpawn: typeof connectOrSpawnRuntimeHost;
readonly readConnectionCatalog: typeof readRuntimeHostConnectionCatalog;
readonly executionCandidateEntrypoint: URL;
}
export async function connectRuntimeHostCli(
input: {
readonly rootPath: string;
readonly surface: ClientSurface;
readonly legacyConfigurationRoot?: string;
},
overrides: Partial<RuntimeHostCliContextDeps> = {},
): Promise<RuntimeHostCliConnectionContext> {
const deps: RuntimeHostCliContextDeps = {
connectOrSpawn: connectOrSpawnRuntimeHost,
readConnectionCatalog: readRuntimeHostConnectionCatalog,
executionCandidateEntrypoint: new URL(
import.meta.resolve('@maka/runtime-host/execution-candidate-main'),
),
...overrides,
};
const connected = await deps.connectOrSpawn({
rootPath: input.rootPath,
surface: input.surface,
protocol: { min: RUNTIME_HOST_PROTOCOL_VERSION, max: RUNTIME_HOST_PROTOCOL_VERSION },
candidateEntrypoint: deps.executionCandidateEntrypoint,
...(input.legacyConfigurationRoot
? { legacyConfigurationRoot: input.legacyConfigurationRoot }
: {}),
});
if (connected.kind === 'incompatible') {
throw new Error(
`Runtime Host protocol is incompatible (Host ${connected.handshake.protocolMin}-${connected.handshake.protocolMax}, CLI ${RUNTIME_HOST_PROTOCOL_VERSION})`,
);
}
if (connected.kind === 'failed') {
throw new Error(`Runtime Host startup failed: ${connected.reason}`);
}
const connection = connected.connection;
try {
await waitForReady(connection);
return {
connection,
catalog: await deps.readConnectionCatalog(connection),
close: () => connection.close(),
};
} catch (error) {
await connection.close().catch(() => undefined);
throw error;
}
}
export function resolveRuntimeHostCliTarget(
catalog: ConnectionCatalogSnapshot,
input: { readonly connectionSlug?: string; readonly model?: string } = {},
): RuntimeHostCliTarget {
const defaultTarget = catalog.defaultTarget;
const connection = input.connectionSlug
? catalog.connections.find((candidate) => candidate.slug === input.connectionSlug)
: catalog.connections.find(
(candidate) => candidate.connectionId === defaultTarget?.connectionId,
);
if (!connection || !connection.enabled) {
throw new Error(
input.connectionSlug
? `Runtime Host model connection is unavailable: ${input.connectionSlug}`
: `${NO_REAL_CONNECTION_CODE}:missing_default_connection: Runtime Host has no default model connection`,
);
}
const model =
input.model ??
(connection.connectionId === defaultTarget?.connectionId
? defaultTarget.modelId
: connection.enabledModelIds[0]);
if (!model || !connection.enabledModelIds.includes(model)) {
throw new Error(`Runtime Host model is unavailable for ${connection.slug}: ${model ?? ''}`);
}
return { connection, model };
}
async function waitForReady(connection: RuntimeHostConnection): Promise<void> {
const deadline = Date.now() + 45_000;
while (true) {
const status = await connection.status(Math.max(1, deadline - Date.now()));
if (status.state === 'ready') return;
if (status.state === 'draining') throw new Error('Runtime Host drained before becoming ready');
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error('Runtime Host did not become ready before the deadline');
await new Promise((resolve) => setTimeout(resolve, Math.min(25, remaining)));
}
}