blob: ee092a8329d29cc69f31be74a48e4035c8867a6d [file]
import { randomUUID } from 'node:crypto';
import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/core/runtime-policy';
import { SessionActivityRegistry, type InvocableSkillEntry } from '@maka/runtime';
import { readRuntimeHostSkillCatalog, type RuntimeHostConnection } from '@maka/runtime-host/client';
import { connectRuntimeHostCli, resolveRuntimeHostCliTarget } from './runtime-host-cli-context.js';
import type {
MakaPiTuiGoalLifecycle,
ModelChoice,
SessionRecapGenerator,
} from './pi-tui-contracts.js';
import {
createRuntimeHostMakaSessionDriver,
type RuntimeHostMakaSessionDriverInput,
} from './runtime-host-session-driver.js';
export interface RuntimeHostTuiContext {
readonly connection: RuntimeHostConnection;
readonly driver: ReturnType<typeof createRuntimeHostMakaSessionDriver>;
readonly cwd: string;
readonly connectionSlug: string;
readonly connectionName: string;
readonly providerType: ConnectionCatalogEntry['providerType'];
readonly model: string;
readonly modelContextWindow?: number;
readonly modelChoices: readonly ModelChoice[];
readonly goalLifecycle: MakaPiTuiGoalLifecycle;
readonly listSkills: (cwd: string) => Promise<readonly InvocableSkillEntry[]>;
readonly recap: SessionRecapGenerator;
close(): Promise<void>;
}
export interface CreateRuntimeHostTuiContextInput {
readonly rootPath: string;
readonly cwd: string;
readonly resumeSessionId?: string;
}
export async function createRuntimeHostTuiContext(
input: CreateRuntimeHostTuiContextInput,
): Promise<RuntimeHostTuiContext> {
const connected = await connectRuntimeHostCli({
rootPath: input.rootPath,
surface: 'tui',
});
const connection = connected.connection;
try {
const catalog = connected.catalog;
const target = input.resumeSessionId
? await resolveResumeTarget(connection, catalog, input.resumeSessionId)
: resolveTarget(catalog);
const modelChoices = projectModelChoices(catalog);
const driverInput: RuntimeHostMakaSessionDriverInput = {
connection,
cwd: input.cwd,
llmConnectionSlug: target.connection.slug,
model: target.model,
permissionMode: 'ask',
};
const driver = createRuntimeHostMakaSessionDriver(driverInput);
return {
connection,
driver,
cwd: input.cwd,
connectionSlug: target.connection.slug,
connectionName: target.connection.name,
providerType: target.connection.providerType,
model: target.model,
modelContextWindow: target.connection.models.find((model) => model.id === target.model)
?.contextWindow,
modelChoices,
goalLifecycle: createHostOwnedGoalLifecycle(),
listSkills: (cwd) => listStablePresentedSkills(connection, cwd),
recap: createRuntimeHostRecapGenerator(connection),
close: () => connected.close(),
};
} catch (error) {
await connection.close().catch(() => undefined);
throw error;
}
}
function createRuntimeHostRecapGenerator(connection: RuntimeHostConnection): SessionRecapGenerator {
return {
generate: async (sessionId, reason) => {
try {
const result = await connection.request('session.recap.generate', {
sessionId,
effectId: randomUUID(),
reason,
});
return result.kind === 'generated'
? { ok: true, text: result.text, raw: result.raw }
: { ok: false, error: result.errorClass };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
},
};
}
async function listStablePresentedSkills(
connection: RuntimeHostConnection,
projectRoot: string,
): Promise<InvocableSkillEntry[]> {
const catalog = await readRuntimeHostSkillCatalog(connection, { projectRoot }, 'governance');
return catalog.items.flatMap((item) =>
item.kind === 'skill' &&
item.enabled &&
item.runtimeStatus === 'enabled' &&
(item.contextStatus === 'advertised' || item.contextStatus === 'unknown')
? [{ ref: item.ref, id: item.id, name: item.name, description: item.description }]
: [],
);
}
function resolveTarget(catalog: ConnectionCatalogSnapshot): {
connection: ConnectionCatalogEntry;
model: string;
} {
return resolveRuntimeHostCliTarget(catalog);
}
async function resolveResumeTarget(
connection: RuntimeHostConnection,
catalog: ConnectionCatalogSnapshot,
sessionId: string,
): Promise<{ connection: ConnectionCatalogEntry; model: string }> {
const result = await connection.request('session.catalog.query', { kind: 'get', sessionId });
const session = result.kind === 'session' ? result.session : null;
if (session && !('kind' in session)) {
const sessionConnection = catalog.connections.find(
(candidate) => candidate.slug === session.llmConnectionSlug && candidate.enabled,
);
if (sessionConnection) return { connection: sessionConnection, model: session.model };
}
return resolveTarget(catalog);
}
function projectModelChoices(catalog: ConnectionCatalogSnapshot): ModelChoice[] {
const choices: ModelChoice[] = [];
for (const connection of catalog.connections) {
if (!connection.enabled) continue;
const modelsById = new Map(connection.models.map((model) => [model.id, model]));
const ids = new Set(connection.enabledModelIds);
if (catalog.defaultTarget?.connectionId === connection.connectionId) {
ids.add(catalog.defaultTarget.modelId);
}
for (const model of ids) {
choices.push({
connectionSlug: connection.slug,
connectionName: connection.name,
providerType: connection.providerType,
model,
isDefaultConnection: catalog.defaultTarget?.connectionId === connection.connectionId,
contextWindow: modelsById.get(model)?.contextWindow,
});
}
}
return choices;
}
function createHostOwnedGoalLifecycle(): MakaPiTuiGoalLifecycle {
return {
activities: new SessionActivityRegistry(),
beginObservedTurn: () => ({ kind: 'registered', settle: async () => {} }),
// Goal continuation is a Runtime Host responsibility. Binding a local
// admission callback would create a second scheduler authority.
bindHost: () => () => {},
};
}