feat(ui): add operator UI server, stores, and insights

This commit is contained in:
2026-02-23 18:49:53 -05:00
parent 8100f4d1c6
commit cf386e1aaa
18 changed files with 3252 additions and 17 deletions

587
src/ui/server.ts Normal file
View File

@@ -0,0 +1,587 @@
import "dotenv/config";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { buildSessionGraphInsight, buildSessionSummaries } from "./session-insights.js";
import { UiConfigStore, type LimitSettings, type RuntimeNotificationSettings, type SecurityPolicySettings } from "./config-store.js";
import { ManifestStore } from "./manifest-store.js";
import { filterRuntimeEvents, readRuntimeEvents } from "./runtime-events-store.js";
import { parseJsonBody, sendJson, methodNotAllowed, notFound, serveStaticFile } from "./http-utils.js";
import { readRunMetaBySession, UiRunService, type RunExecutionMode } from "./run-service.js";
import type { RunProvider } from "./provider-executor.js";
type StartRunRequest = {
prompt: string;
manifestPath?: string;
manifest?: unknown;
sessionId?: string;
topologyHint?: string;
initialFlags?: Record<string, boolean>;
runtimeContextOverrides?: Record<string, string | number | boolean>;
simulateValidationNodeIds?: string[];
executionMode?: RunExecutionMode;
provider?: RunProvider;
};
function parsePort(value: string | undefined): number {
const parsed = Number(value ?? "4317");
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
throw new Error("UI server port must be an integer between 1 and 65535.");
}
return parsed;
}
function parseLimit(value: string | null, fallback: number): number {
if (!value) {
return fallback;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1) {
return fallback;
}
return parsed;
}
function toRelativePathFromApi(urlPath: string): string {
return decodeURIComponent(urlPath);
}
function ensureBooleanRecord(value: unknown): Record<string, boolean> {
if (!value || typeof value !== "object") {
return {};
}
const output: Record<string, boolean> = {};
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
if (typeof raw === "boolean") {
output[key] = raw;
}
}
return output;
}
function ensureStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
const output: string[] = [];
for (const item of value) {
if (typeof item !== "string") {
continue;
}
const normalized = item.trim();
if (!normalized) {
continue;
}
output.push(normalized);
}
return output;
}
function ensureRuntimeContext(value: unknown): Record<string, string | number | boolean> {
if (!value || typeof value !== "object") {
return {};
}
const output: Record<string, string | number | boolean> = {};
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
output[key] = raw;
}
}
return output;
}
function ensureExecutionMode(value: unknown): RunExecutionMode {
return value === "provider" ? "provider" : "mock";
}
function ensureProvider(value: unknown): RunProvider {
return value === "claude" ? "claude" : "codex";
}
async function readRuntimePaths(configStore: UiConfigStore, workspaceRoot: string): Promise<{
stateRoot: string;
runtimeEventLogPath: string;
}> {
const snapshot = await configStore.readSnapshot();
return {
stateRoot: resolve(workspaceRoot, snapshot.paths.stateRoot),
runtimeEventLogPath: resolve(workspaceRoot, snapshot.paths.runtimeEventLogPath),
};
}
async function handleApiRequest(input: {
request: IncomingMessage;
response: ServerResponse;
workspaceRoot: string;
configStore: UiConfigStore;
manifestStore: ManifestStore;
runService: UiRunService;
}): Promise<boolean> {
const { request, response, workspaceRoot, configStore, manifestStore, runService } = input;
const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
const { pathname } = requestUrl;
const method = request.method ?? "GET";
if (!pathname.startsWith("/api/")) {
return false;
}
try {
if (pathname === "/api/health") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
sendJson(response, 200, {
ok: true,
now: new Date().toISOString(),
});
return true;
}
if (pathname === "/api/config") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const snapshot = await configStore.readSnapshot();
sendJson(response, 200, {
ok: true,
config: snapshot,
});
return true;
}
if (pathname === "/api/config/runtime-events") {
if (method !== "PUT") {
methodNotAllowed(response);
return true;
}
const body = await parseJsonBody<RuntimeNotificationSettings>(request);
const snapshot = await configStore.updateRuntimeEvents(body);
sendJson(response, 200, {
ok: true,
config: snapshot,
});
return true;
}
if (pathname === "/api/config/security") {
if (method !== "PUT") {
methodNotAllowed(response);
return true;
}
const body = await parseJsonBody<SecurityPolicySettings>(request);
const snapshot = await configStore.updateSecurityPolicy(body);
sendJson(response, 200, {
ok: true,
config: snapshot,
});
return true;
}
if (pathname === "/api/config/limits") {
if (method !== "PUT") {
methodNotAllowed(response);
return true;
}
const body = await parseJsonBody<LimitSettings>(request);
const snapshot = await configStore.updateLimits(body);
sendJson(response, 200, {
ok: true,
config: snapshot,
});
return true;
}
if (pathname === "/api/manifests") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const listing = await manifestStore.list();
sendJson(response, 200, {
ok: true,
manifests: listing.paths,
});
return true;
}
if (pathname === "/api/manifests/read") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const manifestPath = requestUrl.searchParams.get("path");
if (!manifestPath) {
sendJson(response, 400, {
ok: false,
error: 'Query parameter "path" is required.',
});
return true;
}
const record = await manifestStore.read(manifestPath);
sendJson(response, 200, {
ok: true,
manifest: record,
});
return true;
}
if (pathname === "/api/manifests/validate") {
if (method !== "POST") {
methodNotAllowed(response);
return true;
}
const body = await parseJsonBody<{ manifest: unknown }>(request);
const manifest = await manifestStore.validate(body.manifest);
sendJson(response, 200, {
ok: true,
manifest,
});
return true;
}
if (pathname === "/api/manifests/save") {
if (method !== "PUT") {
methodNotAllowed(response);
return true;
}
const body = await parseJsonBody<{ path: string; manifest: unknown }>(request);
if (!body.path || typeof body.path !== "string") {
sendJson(response, 400, {
ok: false,
error: 'Field "path" is required.',
});
return true;
}
const record = await manifestStore.save(body.path, body.manifest);
sendJson(response, 200, {
ok: true,
manifest: record,
});
return true;
}
if (pathname === "/api/runtime-events") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const { runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const limit = parseLimit(requestUrl.searchParams.get("limit"), 200);
const sessionId = requestUrl.searchParams.get("sessionId") ?? undefined;
const events = filterRuntimeEvents(await readRuntimeEvents(runtimeEventLogPath), {
...(sessionId ? { sessionId } : {}),
limit,
});
sendJson(response, 200, {
ok: true,
events,
});
return true;
}
if (pathname === "/api/sessions") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const { stateRoot, runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const sessions = await buildSessionSummaries({
stateRoot,
runtimeEventLogPath,
});
sendJson(response, 200, {
ok: true,
sessions,
runs: runService.listRuns(),
});
return true;
}
if (pathname === "/api/sessions/graph") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const sessionId = requestUrl.searchParams.get("sessionId") ?? "";
if (!sessionId) {
sendJson(response, 400, {
ok: false,
error: 'Query parameter "sessionId" is required.',
});
return true;
}
const { stateRoot, runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const explicitManifestPath = requestUrl.searchParams.get("manifestPath");
const runMeta = await readRunMetaBySession({ stateRoot, sessionId });
const manifestPath = explicitManifestPath ?? runMeta?.manifestPath;
if (!manifestPath) {
sendJson(response, 400, {
ok: false,
error: "No manifestPath available for this session. Provide one in query string.",
});
return true;
}
const manifestRecord = await manifestStore.read(manifestPath);
const graph = await buildSessionGraphInsight({
stateRoot,
runtimeEventLogPath,
sessionId,
manifest: manifestRecord.manifest,
});
sendJson(response, 200, {
ok: true,
graph,
manifestPath: manifestRecord.path,
});
return true;
}
if (pathname === "/api/runs") {
if (method === "GET") {
sendJson(response, 200, {
ok: true,
runs: runService.listRuns(),
});
return true;
}
if (method === "POST") {
const body = await parseJsonBody<StartRunRequest>(request);
if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) {
sendJson(response, 400, {
ok: false,
error: 'Field "prompt" is required.',
});
return true;
}
const manifestSource = (() => {
if (body.manifest !== undefined) {
return body.manifest;
}
if (typeof body.manifestPath === "string" && body.manifestPath.trim().length > 0) {
return undefined;
}
return undefined;
})();
const resolvedManifest = manifestSource ?? (() => {
if (!body.manifestPath) {
return undefined;
}
return body.manifestPath;
})();
let manifest: unknown;
if (typeof resolvedManifest === "string") {
manifest = (await manifestStore.read(resolvedManifest)).source;
} else if (resolvedManifest !== undefined) {
manifest = resolvedManifest;
}
if (!manifest) {
sendJson(response, 400, {
ok: false,
error: "A manifest or manifestPath is required to start a run.",
});
return true;
}
const record = await runService.startRun({
prompt: body.prompt,
manifest,
manifestPath: body.manifestPath,
sessionId: body.sessionId,
topologyHint: body.topologyHint,
initialFlags: ensureBooleanRecord(body.initialFlags),
runtimeContextOverrides: ensureRuntimeContext(body.runtimeContextOverrides),
simulateValidationNodeIds: ensureStringArray(body.simulateValidationNodeIds),
executionMode: ensureExecutionMode(body.executionMode),
provider: ensureProvider(body.provider),
});
sendJson(response, 202, {
ok: true,
run: record,
});
return true;
}
methodNotAllowed(response);
return true;
}
if (pathname.startsWith("/api/runs/") && pathname.endsWith("/cancel")) {
if (method !== "POST") {
methodNotAllowed(response);
return true;
}
const runId = toRelativePathFromApi(pathname.slice("/api/runs/".length, -"/cancel".length));
const run = await runService.cancelRun(runId);
sendJson(response, 200, {
ok: true,
run,
});
return true;
}
if (pathname.startsWith("/api/runs/")) {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const runId = toRelativePathFromApi(pathname.slice("/api/runs/".length));
const run = runService.getRun(runId);
if (!run) {
sendJson(response, 404, {
ok: false,
error: `Run \"${runId}\" was not found.`,
});
return true;
}
sendJson(response, 200, {
ok: true,
run,
});
return true;
}
notFound(response);
return true;
} catch (error) {
sendJson(response, 400, {
ok: false,
error: error instanceof Error ? error.message : String(error),
});
return true;
}
}
export async function startUiServer(input: {
workspaceRoot: string;
port?: number;
host?: string;
}): Promise<{
close: () => Promise<void>;
}> {
const workspaceRoot = resolve(input.workspaceRoot);
const staticRoot = resolve(workspaceRoot, "src/ui/public");
const configStore = new UiConfigStore({ workspaceRoot });
const manifestStore = new ManifestStore({ workspaceRoot });
const runService = new UiRunService({ workspaceRoot });
const server = createServer(async (request, response) => {
const handledApi = await handleApiRequest({
request,
response,
workspaceRoot,
configStore,
manifestStore,
runService,
});
if (handledApi) {
return;
}
const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
const pathname = requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname;
const cleanPath = pathname.replace(/^\//, "");
if (cleanPath.includes("..")) {
notFound(response);
return;
}
const staticPath = resolve(staticRoot, cleanPath);
const served = await serveStaticFile({
response,
filePath: staticPath,
});
if (served) {
return;
}
const fallbackServed = await serveStaticFile({
response,
filePath: resolve(staticRoot, "index.html"),
});
if (!fallbackServed) {
notFound(response);
}
});
const host = input.host ?? "127.0.0.1";
const port = input.port ?? parsePort(process.env.AGENT_UI_PORT);
await new Promise<void>((resolveReady, rejectReady) => {
server.once("error", rejectReady);
server.listen(port, host, () => {
server.off("error", rejectReady);
resolveReady();
});
});
console.log(`AI Ops UI listening at http://${host}:${String(port)}`);
return {
close: async () => {
await new Promise<void>((resolveClose, rejectClose) => {
server.close((error) => {
if (error) {
rejectClose(error);
return;
}
resolveClose();
});
});
},
};
}
async function main(): Promise<void> {
const port = parsePort(process.env.AGENT_UI_PORT);
await startUiServer({
workspaceRoot: process.cwd(),
port,
host: process.env.AGENT_UI_HOST ?? "127.0.0.1",
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
}