Refactor UI modules and harden run/API behavior

This commit is contained in:
2026-02-25 00:21:04 -05:00
parent 422e8fe5a5
commit 659f3edcee
39 changed files with 6392 additions and 995 deletions

View File

@@ -1,183 +0,0 @@
import { resolve } from "node:path";
import { loadConfig, type AppConfig } from "../config.js";
import { parseEnvFile, writeEnvFileUpdates } from "./env-store.js";
export type RuntimeNotificationSettings = {
webhookUrl: string;
minSeverity: "info" | "warning" | "critical";
alwaysNotifyTypes: string[];
};
export type SecurityPolicySettings = {
violationMode: "hard_abort" | "validation_fail";
allowedBinaries: string[];
commandTimeoutMs: number;
inheritedEnv: string[];
scrubbedEnv: string[];
};
export type LimitSettings = {
maxConcurrent: number;
maxSession: number;
maxRecursiveDepth: number;
topologyMaxDepth: number;
topologyMaxRetries: number;
relationshipMaxChildren: number;
portBase: number;
portBlockSize: number;
portBlockCount: number;
portPrimaryOffset: number;
};
export type UiConfigSnapshot = {
envFilePath: string;
runtimeEvents: RuntimeNotificationSettings;
security: SecurityPolicySettings;
limits: LimitSettings;
paths: {
stateRoot: string;
projectContextPath: string;
runtimeEventLogPath: string;
securityAuditLogPath: string;
};
};
function toCsv(values: readonly string[]): string {
return values.join(",");
}
function sanitizeCsv(values: readonly string[]): string[] {
const output: string[] = [];
const seen = new Set<string>();
for (const value of values) {
const normalized = value.trim();
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
output.push(normalized);
}
return output;
}
function toRuntimeEvents(config: Readonly<AppConfig>): RuntimeNotificationSettings {
return {
webhookUrl: config.runtimeEvents.discordWebhookUrl ?? "",
minSeverity: config.runtimeEvents.discordMinSeverity,
alwaysNotifyTypes: [...config.runtimeEvents.discordAlwaysNotifyTypes],
};
}
function toSecurity(config: Readonly<AppConfig>): SecurityPolicySettings {
return {
violationMode: config.security.violationHandling,
allowedBinaries: [...config.security.shellAllowedBinaries],
commandTimeoutMs: config.security.commandTimeoutMs,
inheritedEnv: [...config.security.inheritedEnvVars],
scrubbedEnv: [...config.security.scrubbedEnvVars],
};
}
function toLimits(config: Readonly<AppConfig>): LimitSettings {
return {
maxConcurrent: config.agentManager.maxConcurrentAgents,
maxSession: config.agentManager.maxSessionAgents,
maxRecursiveDepth: config.agentManager.maxRecursiveDepth,
topologyMaxDepth: config.orchestration.maxDepth,
topologyMaxRetries: config.orchestration.maxRetries,
relationshipMaxChildren: config.orchestration.maxChildren,
portBase: config.provisioning.portRange.basePort,
portBlockSize: config.provisioning.portRange.blockSize,
portBlockCount: config.provisioning.portRange.blockCount,
portPrimaryOffset: config.provisioning.portRange.primaryPortOffset,
};
}
function toSnapshot(config: Readonly<AppConfig>, envFilePath: string): UiConfigSnapshot {
return {
envFilePath,
runtimeEvents: toRuntimeEvents(config),
security: toSecurity(config),
limits: toLimits(config),
paths: {
stateRoot: config.orchestration.stateRoot,
projectContextPath: config.orchestration.projectContextPath,
runtimeEventLogPath: config.runtimeEvents.logPath,
securityAuditLogPath: config.security.auditLogPath,
},
};
}
function mergeEnv(fileValues: Record<string, string>): NodeJS.ProcessEnv {
return {
...process.env,
...fileValues,
};
}
export class UiConfigStore {
private readonly envFilePath: string;
constructor(input: { workspaceRoot: string; envFilePath?: string }) {
this.envFilePath = resolve(input.workspaceRoot, input.envFilePath ?? ".env");
}
getEnvFilePath(): string {
return this.envFilePath;
}
async readSnapshot(): Promise<UiConfigSnapshot> {
const parsed = await parseEnvFile(this.envFilePath);
const config = loadConfig(mergeEnv(parsed.values));
return toSnapshot(config, this.envFilePath);
}
async updateRuntimeEvents(input: RuntimeNotificationSettings): Promise<UiConfigSnapshot> {
const alwaysNotifyTypes = sanitizeCsv(input.alwaysNotifyTypes);
const updates: Record<string, string> = {
AGENT_RUNTIME_DISCORD_WEBHOOK_URL: input.webhookUrl.trim(),
AGENT_RUNTIME_DISCORD_MIN_SEVERITY: input.minSeverity,
AGENT_RUNTIME_DISCORD_ALWAYS_NOTIFY_TYPES: toCsv(alwaysNotifyTypes),
};
const parsed = await writeEnvFileUpdates(this.envFilePath, updates);
const config = loadConfig(mergeEnv(parsed.values));
return toSnapshot(config, this.envFilePath);
}
async updateSecurityPolicy(input: SecurityPolicySettings): Promise<UiConfigSnapshot> {
const updates: Record<string, string> = {
AGENT_SECURITY_VIOLATION_MODE: input.violationMode,
AGENT_SECURITY_ALLOWED_BINARIES: toCsv(sanitizeCsv(input.allowedBinaries)),
AGENT_SECURITY_COMMAND_TIMEOUT_MS: String(input.commandTimeoutMs),
AGENT_SECURITY_ENV_INHERIT: toCsv(sanitizeCsv(input.inheritedEnv)),
AGENT_SECURITY_ENV_SCRUB: toCsv(sanitizeCsv(input.scrubbedEnv)),
};
const parsed = await writeEnvFileUpdates(this.envFilePath, updates);
const config = loadConfig(mergeEnv(parsed.values));
return toSnapshot(config, this.envFilePath);
}
async updateLimits(input: LimitSettings): Promise<UiConfigSnapshot> {
const updates: Record<string, string> = {
AGENT_MAX_CONCURRENT: String(input.maxConcurrent),
AGENT_MAX_SESSION: String(input.maxSession),
AGENT_MAX_RECURSIVE_DEPTH: String(input.maxRecursiveDepth),
AGENT_TOPOLOGY_MAX_DEPTH: String(input.topologyMaxDepth),
AGENT_TOPOLOGY_MAX_RETRIES: String(input.topologyMaxRetries),
AGENT_RELATIONSHIP_MAX_CHILDREN: String(input.relationshipMaxChildren),
AGENT_PORT_BASE: String(input.portBase),
AGENT_PORT_BLOCK_SIZE: String(input.portBlockSize),
AGENT_PORT_BLOCK_COUNT: String(input.portBlockCount),
AGENT_PORT_PRIMARY_OFFSET: String(input.portPrimaryOffset),
};
const parsed = await writeEnvFileUpdates(this.envFilePath, updates);
const config = loadConfig(mergeEnv(parsed.values));
return toSnapshot(config, this.envFilePath);
}
}

View File

@@ -1,130 +0,0 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
type AssignmentLine = {
kind: "assignment";
key: string;
value: string;
leading: string;
};
type RawLine = {
kind: "raw";
raw: string;
};
type EnvLine = AssignmentLine | RawLine;
export type ParsedEnvFile = {
filePath: string;
lines: EnvLine[];
values: Record<string, string>;
};
const ASSIGNMENT_PATTERN = /^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
function unquoteValue(raw: string): string {
const trimmed = raw.trim();
if (trimmed.length >= 2) {
const first = trimmed[0];
const last = trimmed[trimmed.length - 1];
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
return trimmed.slice(1, -1);
}
}
return trimmed;
}
function formatValue(value: string): string {
if (value.length === 0) {
return "";
}
if (/\s|#|"|'/.test(value)) {
const escaped = value
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"');
return `"${escaped}"`;
}
return value;
}
export async function parseEnvFile(filePath: string): Promise<ParsedEnvFile> {
const resolvedPath = resolve(filePath);
let content = "";
try {
content = await readFile(resolvedPath, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
}
const lines = content.length > 0 ? content.split(/\r?\n/) : [];
const parsedLines: EnvLine[] = [];
const values: Record<string, string> = {};
for (const line of lines) {
const match = line.match(ASSIGNMENT_PATTERN);
if (!match) {
parsedLines.push({ kind: "raw", raw: line });
continue;
}
const leading = match[1] ?? "";
const key = match[2] ?? "";
const rawValue = match[3] ?? "";
const value = unquoteValue(rawValue);
parsedLines.push({
kind: "assignment",
key,
value,
leading,
});
values[key] = value;
}
return {
filePath: resolvedPath,
lines: parsedLines,
values,
};
}
export async function writeEnvFileUpdates(
filePath: string,
updates: Record<string, string>,
): Promise<ParsedEnvFile> {
const parsed = await parseEnvFile(filePath);
const keysToApply = new Set(Object.keys(updates));
const renderedLines = parsed.lines.map((line) => {
if (line.kind !== "assignment") {
return line.raw;
}
if (!keysToApply.has(line.key)) {
return `${line.leading}${line.key}=${formatValue(line.value)}`;
}
keysToApply.delete(line.key);
const nextValue = updates[line.key] ?? "";
return `${line.leading}${line.key}=${formatValue(nextValue)}`;
});
for (const key of keysToApply) {
const nextValue = updates[key] ?? "";
renderedLines.push(`${key}=${formatValue(nextValue)}`);
}
const output = `${renderedLines.join("\n").replace(/\n+$/u, "")}\n`;
const resolvedPath = resolve(filePath);
await mkdir(dirname(resolvedPath), { recursive: true });
await writeFile(resolvedPath, output, "utf8");
return parseEnvFile(resolvedPath);
}

View File

@@ -1,90 +0,0 @@
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { extname, resolve } from "node:path";
import type { IncomingMessage, ServerResponse } from "node:http";
const CONTENT_TYPES: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
};
export function sendJson(response: ServerResponse, statusCode: number, body: unknown): void {
const payload = JSON.stringify(body);
response.statusCode = statusCode;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.end(payload);
}
export function sendText(response: ServerResponse, statusCode: number, body: string): void {
response.statusCode = statusCode;
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.end(body);
}
export async function parseJsonBody<T>(request: IncomingMessage): Promise<T> {
const chunks: Buffer[] = [];
await new Promise<void>((resolveBody, rejectBody) => {
request.on("data", (chunk: Buffer) => {
chunks.push(chunk);
});
request.on("end", () => resolveBody());
request.on("error", rejectBody);
});
const body = Buffer.concat(chunks).toString("utf8").trim();
if (!body) {
throw new Error("Request body is required.");
}
return JSON.parse(body) as T;
}
export function methodNotAllowed(response: ServerResponse): void {
sendJson(response, 405, {
ok: false,
error: "Method not allowed.",
});
}
export function notFound(response: ServerResponse): void {
sendJson(response, 404, {
ok: false,
error: "Not found.",
});
}
export async function serveStaticFile(input: {
response: ServerResponse;
filePath: string;
}): Promise<boolean> {
try {
const absolutePath = resolve(input.filePath);
const fileStats = await stat(absolutePath);
if (!fileStats.isFile()) {
return false;
}
const extension = extname(absolutePath).toLowerCase();
const contentType = CONTENT_TYPES[extension] ?? "application/octet-stream";
input.response.statusCode = 200;
input.response.setHeader("Content-Type", contentType);
await new Promise<void>((resolveStream, rejectStream) => {
const stream = createReadStream(absolutePath);
stream.on("error", rejectStream);
stream.on("end", () => resolveStream());
stream.pipe(input.response);
});
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
}
throw error;
}
}

View File

@@ -1,116 +0,0 @@
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
import { parseAgentManifest, type AgentManifest } from "../agents/manifest.js";
export type ManifestRecord = {
path: string;
manifest: AgentManifest;
source: unknown;
};
export type ManifestListing = {
paths: string[];
};
async function walkJsonFiles(root: string): Promise<string[]> {
const output: string[] = [];
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
const fullPath = resolve(root, entry.name);
if (entry.isDirectory()) {
output.push(...(await walkJsonFiles(fullPath)));
continue;
}
if (entry.isFile() && extname(entry.name).toLowerCase() === ".json") {
output.push(fullPath);
}
}
return output;
}
function assertWorkspacePath(workspaceRoot: string, inputPath: string): string {
const resolved = isAbsolute(inputPath)
? resolve(inputPath)
: resolve(workspaceRoot, inputPath);
const rel = relative(workspaceRoot, resolved);
if (rel === ".." || rel.startsWith(`..${sep}`)) {
throw new Error("Path is outside workspace root.");
}
return resolved;
}
function toRelativePath(workspaceRoot: string, absolutePath: string): string {
return relative(workspaceRoot, absolutePath) || ".";
}
export class ManifestStore {
private readonly workspaceRoot: string;
private readonly manifestDirectory: string;
constructor(input: { workspaceRoot: string; manifestDirectory?: string }) {
this.workspaceRoot = resolve(input.workspaceRoot);
this.manifestDirectory = assertWorkspacePath(
this.workspaceRoot,
input.manifestDirectory ?? ".ai_ops/manifests",
);
}
getManifestDirectory(): string {
return this.manifestDirectory;
}
async list(): Promise<ManifestListing> {
try {
const files = await walkJsonFiles(this.manifestDirectory);
const relPaths = files
.map((filePath) => toRelativePath(this.workspaceRoot, filePath))
.sort((left, right) => left.localeCompare(right));
return {
paths: relPaths,
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return {
paths: [],
};
}
throw error;
}
}
async read(pathInput: string): Promise<ManifestRecord> {
const absolutePath = assertWorkspacePath(this.workspaceRoot, pathInput);
const sourceText = await readFile(absolutePath, "utf8");
const source = JSON.parse(sourceText) as unknown;
const manifest = parseAgentManifest(source);
return {
path: toRelativePath(this.workspaceRoot, absolutePath),
manifest,
source,
};
}
async validate(source: unknown): Promise<AgentManifest> {
return parseAgentManifest(source);
}
async save(pathInput: string, source: unknown): Promise<ManifestRecord> {
const manifest = parseAgentManifest(source);
const absolutePath = assertWorkspacePath(this.workspaceRoot, pathInput);
await mkdir(dirname(absolutePath), { recursive: true });
await writeFile(absolutePath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
return {
path: toRelativePath(this.workspaceRoot, absolutePath),
manifest,
source: manifest,
};
}
}

View File

@@ -1,597 +0,0 @@
import { Codex } from "@openai/codex-sdk";
import { query, type Options, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import {
buildClaudeAuthEnv,
resolveAnthropicToken,
resolveOpenAiApiKey,
type AppConfig,
} from "../config.js";
import { isDomainEventType, type DomainEventEmission } from "../agents/domain-events.js";
import type { ActorExecutionInput, ActorExecutionResult, ActorExecutor } from "../agents/pipeline.js";
import { isRecord, type JsonObject, type JsonValue } from "../agents/types.js";
import { createSessionContext, type SessionContext } from "../examples/session-context.js";
export type RunProvider = "codex" | "claude";
export type ProviderRunRuntime = {
provider: RunProvider;
config: Readonly<AppConfig>;
sessionContext: SessionContext;
close: () => Promise<void>;
};
type ProviderUsage = {
tokenInput?: number;
tokenOutput?: number;
tokenTotal?: number;
durationMs?: number;
costUsd?: number;
};
const ACTOR_RESPONSE_SCHEMA = {
type: "object",
additionalProperties: true,
properties: {
status: {
type: "string",
enum: ["success", "validation_fail", "failure"],
},
payload: {
type: "object",
},
stateFlags: {
type: "object",
additionalProperties: {
type: "boolean",
},
},
stateMetadata: {
type: "object",
},
events: {
type: "array",
items: {
type: "object",
additionalProperties: true,
},
},
failureKind: {
type: "string",
enum: ["soft", "hard"],
},
failureCode: {
type: "string",
},
},
required: ["status"],
};
const CLAUDE_OUTPUT_FORMAT = {
type: "json_schema",
name: "actor_execution_result",
schema: ACTOR_RESPONSE_SCHEMA,
} as const;
const CLAUDE_PROVIDER_MAX_TURNS = 2;
function toErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function toJsonValue(value: unknown): JsonValue {
return JSON.parse(JSON.stringify(value)) as JsonValue;
}
function toJsonObject(value: unknown): JsonObject | undefined {
if (!isRecord(value)) {
return undefined;
}
try {
const cloned = toJsonValue(value);
if (!isRecord(cloned)) {
return undefined;
}
return cloned as JsonObject;
} catch {
return undefined;
}
}
function toBooleanRecord(value: unknown): Record<string, boolean> | undefined {
if (!isRecord(value)) {
return undefined;
}
const output: Record<string, boolean> = {};
for (const [key, candidate] of Object.entries(value)) {
if (typeof candidate === "boolean") {
output[key] = candidate;
}
}
return Object.keys(output).length > 0 ? output : undefined;
}
function toEventEmissions(value: unknown): DomainEventEmission[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const output: DomainEventEmission[] = [];
for (const item of value) {
if (!isRecord(item)) {
continue;
}
const type = item.type;
if (typeof type !== "string" || !isDomainEventType(type)) {
continue;
}
const payload = toJsonObject(item.payload);
output.push({
type,
...(payload ? { payload } : {}),
});
}
return output.length > 0 ? output : undefined;
}
function extractJsonFromFencedBlock(text: string): unknown {
const matches = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
if (!matches || !matches[1]) {
return undefined;
}
try {
return JSON.parse(matches[1]);
} catch {
return undefined;
}
}
function extractFirstBalancedJsonObject(text: string): unknown {
const start = text.indexOf("{");
if (start < 0) {
return undefined;
}
let depth = 0;
let inString = false;
let escaped = false;
for (let index = start; index < text.length; index += 1) {
const character = text[index];
if (!character) {
continue;
}
if (inString) {
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === '"') {
inString = false;
}
continue;
}
if (character === '"') {
inString = true;
continue;
}
if (character === "{") {
depth += 1;
continue;
}
if (character === "}") {
depth -= 1;
if (depth === 0) {
const candidate = text.slice(start, index + 1);
try {
return JSON.parse(candidate);
} catch {
return undefined;
}
}
}
}
return undefined;
}
function tryParseResponseObject(rawText: string, structuredOutput?: unknown): unknown {
if (structuredOutput !== undefined) {
return structuredOutput;
}
const trimmed = rawText.trim();
if (!trimmed) {
return undefined;
}
try {
return JSON.parse(trimmed);
} catch {
// fall through
}
const fenced = extractJsonFromFencedBlock(trimmed);
if (fenced !== undefined) {
return fenced;
}
return extractFirstBalancedJsonObject(trimmed);
}
function ensureUsageMetadata(input: {
result: ActorExecutionResult;
providerUsage: ProviderUsage;
}): ActorExecutionResult {
const stateMetadata = toJsonObject(input.result.stateMetadata) ?? {};
const existingUsage = toJsonObject(stateMetadata.usage) ?? {};
const usage: JsonObject = {
...existingUsage,
...(typeof input.providerUsage.tokenInput === "number"
? { tokenInput: input.providerUsage.tokenInput }
: {}),
...(typeof input.providerUsage.tokenOutput === "number"
? { tokenOutput: input.providerUsage.tokenOutput }
: {}),
...(typeof input.providerUsage.tokenTotal === "number"
? { tokenTotal: input.providerUsage.tokenTotal }
: {}),
...(typeof input.providerUsage.durationMs === "number"
? { durationMs: input.providerUsage.durationMs }
: {}),
...(typeof input.providerUsage.costUsd === "number"
? { costUsd: input.providerUsage.costUsd }
: {}),
};
return {
...input.result,
stateMetadata: {
...stateMetadata,
usage,
},
};
}
export function parseActorExecutionResultFromModelOutput(input: {
rawText: string;
structuredOutput?: unknown;
}): ActorExecutionResult {
const parsed = tryParseResponseObject(input.rawText, input.structuredOutput);
if (!isRecord(parsed)) {
return {
status: "success",
payload: {
assistantResponse: input.rawText.trim(),
},
};
}
const status = parsed.status;
if (status !== "success" && status !== "validation_fail" && status !== "failure") {
return {
status: "success",
payload: {
assistantResponse: input.rawText.trim(),
},
};
}
const payload = toJsonObject(parsed.payload) ?? {
assistantResponse: input.rawText.trim(),
};
const stateMetadata = toJsonObject(parsed.stateMetadata);
const stateFlags = toBooleanRecord(parsed.stateFlags);
const events = toEventEmissions(parsed.events);
const failureKind = parsed.failureKind === "soft" || parsed.failureKind === "hard"
? parsed.failureKind
: undefined;
const failureCode = typeof parsed.failureCode === "string"
? parsed.failureCode
: undefined;
return {
status,
payload,
...(stateFlags ? { stateFlags } : {}),
...(stateMetadata ? { stateMetadata } : {}),
...(events ? { events } : {}),
...(failureKind ? { failureKind } : {}),
...(failureCode ? { failureCode } : {}),
};
}
function buildActorPrompt(input: ActorExecutionInput): string {
const recentHistory = input.context.state.history.slice(-15);
return [
"You are executing one orchestration node in a schema-driven DAG runtime.",
"Return ONLY JSON with this object shape:",
JSON.stringify(
{
status: "success | validation_fail | failure",
payload: {},
stateFlags: {
optional_boolean_flag: true,
},
stateMetadata: {
optional_metadata: "value",
},
events: [
{
type: "requirements_defined | tasks_planned | code_committed | task_blocked | validation_passed | validation_failed | branch_merged",
payload: {
summary: "optional",
details: {},
errorCode: "optional",
artifactPointer: "optional",
},
},
],
failureKind: "soft | hard",
failureCode: "optional",
},
null,
2,
),
"Do not include markdown or extra explanation outside JSON.",
`Node Prompt:\n${input.prompt}`,
`Execution Context:\n${JSON.stringify(input.executionContext, null, 2)}`,
`Current Handoff Payload:\n${JSON.stringify(input.context.handoff.payload, null, 2)}`,
`Session Flags:\n${JSON.stringify(input.context.state.flags, null, 2)}`,
`Recent Domain History:\n${JSON.stringify(recentHistory, null, 2)}`,
].join("\n\n");
}
async function runCodexActor(input: {
runtime: ProviderRunRuntime;
actorInput: ActorExecutionInput;
}): Promise<ActorExecutionResult> {
const { runtime, actorInput } = input;
const prompt = buildActorPrompt(actorInput);
const startedAt = Date.now();
const apiKey = resolveOpenAiApiKey(runtime.config.provider);
const codex = new Codex({
...(apiKey ? { apiKey } : {}),
...(runtime.config.provider.openAiBaseUrl
? { baseUrl: runtime.config.provider.openAiBaseUrl }
: {}),
...(actorInput.mcp.resolvedConfig.codexConfig
? { config: actorInput.mcp.resolvedConfig.codexConfig }
: {}),
env: runtime.sessionContext.runtimeInjection.env,
});
const thread = codex.startThread({
workingDirectory: runtime.sessionContext.runtimeInjection.workingDirectory,
skipGitRepoCheck: runtime.config.provider.codexSkipGitCheck,
});
const turn = await runtime.sessionContext.runInSession(() =>
thread.run(prompt, {
signal: actorInput.signal,
outputSchema: ACTOR_RESPONSE_SCHEMA,
}),
);
const usage: ProviderUsage = {
...(turn.usage
? {
tokenInput: turn.usage.input_tokens + turn.usage.cached_input_tokens,
tokenOutput: turn.usage.output_tokens,
tokenTotal: turn.usage.input_tokens + turn.usage.cached_input_tokens + turn.usage.output_tokens,
}
: {}),
durationMs: Date.now() - startedAt,
};
const parsed = parseActorExecutionResultFromModelOutput({
rawText: turn.finalResponse,
});
return ensureUsageMetadata({
result: parsed,
providerUsage: usage,
});
}
type ClaudeTurnResult = {
text: string;
structuredOutput?: unknown;
usage: ProviderUsage;
};
function buildClaudeOptions(input: {
runtime: ProviderRunRuntime;
actorInput: ActorExecutionInput;
}): Options {
const { runtime, actorInput } = input;
const authOptionOverrides = runtime.config.provider.anthropicOauthToken
? { authToken: runtime.config.provider.anthropicOauthToken }
: (() => {
const token = resolveAnthropicToken(runtime.config.provider);
return token ? { apiKey: token } : {};
})();
const runtimeEnv = {
...runtime.sessionContext.runtimeInjection.env,
...buildClaudeAuthEnv(runtime.config.provider),
};
return {
maxTurns: CLAUDE_PROVIDER_MAX_TURNS,
...(runtime.config.provider.claudeModel
? { model: runtime.config.provider.claudeModel }
: {}),
...(runtime.config.provider.claudeCodePath
? { pathToClaudeCodeExecutable: runtime.config.provider.claudeCodePath }
: {}),
...authOptionOverrides,
...(actorInput.mcp.resolvedConfig.claudeMcpServers
? { mcpServers: actorInput.mcp.resolvedConfig.claudeMcpServers as Options["mcpServers"] }
: {}),
canUseTool: actorInput.mcp.createClaudeCanUseTool(),
cwd: runtime.sessionContext.runtimeInjection.workingDirectory,
env: runtimeEnv,
outputFormat: CLAUDE_OUTPUT_FORMAT,
};
}
async function runClaudeTurn(input: {
runtime: ProviderRunRuntime;
actorInput: ActorExecutionInput;
prompt: string;
}): Promise<ClaudeTurnResult> {
const options = buildClaudeOptions({
runtime: input.runtime,
actorInput: input.actorInput,
});
const startedAt = Date.now();
const stream = query({
prompt: input.prompt,
options,
});
let resultText = "";
let structuredOutput: unknown;
let usage: ProviderUsage = {};
const onAbort = (): void => {
stream.close();
};
input.actorInput.signal.addEventListener("abort", onAbort, { once: true });
try {
for await (const message of stream as AsyncIterable<SDKMessage>) {
if (message.type !== "result") {
continue;
}
if (message.subtype !== "success") {
const detail = message.errors.join("; ");
throw new Error(
`Claude query failed (${message.subtype})${detail ? `: ${detail}` : ""}`,
);
}
resultText = message.result.trim();
structuredOutput = message.structured_output;
usage = {
tokenInput: message.usage.input_tokens,
tokenOutput: message.usage.output_tokens,
tokenTotal: message.usage.input_tokens + message.usage.output_tokens,
durationMs: message.duration_ms,
costUsd: message.total_cost_usd,
};
}
} finally {
input.actorInput.signal.removeEventListener("abort", onAbort);
stream.close();
}
if (!resultText && structuredOutput !== undefined) {
resultText = JSON.stringify(structuredOutput);
}
if (!resultText) {
throw new Error("Claude run completed without a final result.");
}
return {
text: resultText,
structuredOutput,
usage: {
...usage,
durationMs: usage.durationMs ?? Date.now() - startedAt,
},
};
}
async function runClaudeActor(input: {
runtime: ProviderRunRuntime;
actorInput: ActorExecutionInput;
}): Promise<ActorExecutionResult> {
const prompt = buildActorPrompt(input.actorInput);
const turn = await input.runtime.sessionContext.runInSession(() =>
runClaudeTurn({
runtime: input.runtime,
actorInput: input.actorInput,
prompt,
}),
);
const parsed = parseActorExecutionResultFromModelOutput({
rawText: turn.text,
structuredOutput: turn.structuredOutput,
});
return ensureUsageMetadata({
result: parsed,
providerUsage: turn.usage,
});
}
export async function createProviderRunRuntime(input: {
provider: RunProvider;
initialPrompt: string;
config: Readonly<AppConfig>;
}): Promise<ProviderRunRuntime> {
const sessionContext = await createSessionContext(input.provider, {
prompt: input.initialPrompt,
config: input.config,
});
return {
provider: input.provider,
config: input.config,
sessionContext,
close: async () => {
await sessionContext.close();
},
};
}
export function createProviderActorExecutor(runtime: ProviderRunRuntime): ActorExecutor {
return async (actorInput) => {
try {
if (runtime.provider === "codex") {
return await runCodexActor({
runtime,
actorInput,
});
}
return await runClaudeActor({
runtime,
actorInput,
});
} catch (error) {
return {
status: "failure",
payload: {
error: toErrorMessage(error),
},
failureKind: "hard",
failureCode: `provider_${runtime.provider}_execution_error`,
};
}
};
}

View File

@@ -1,483 +0,0 @@
import { randomUUID } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { SchemaDrivenExecutionEngine } from "../agents/orchestration.js";
import { parseAgentManifest, type AgentManifest } from "../agents/manifest.js";
import type {
ActorExecutionResult,
ActorExecutor,
PipelineAggregateStatus,
} from "../agents/pipeline.js";
import { loadConfig, type AppConfig } from "../config.js";
import { parseEnvFile } from "./env-store.js";
import {
createProviderActorExecutor,
createProviderRunRuntime,
type RunProvider,
} from "./provider-executor.js";
const RUN_META_FILE_NAME = "ui-run-meta.json";
export type RunStatus = "running" | "success" | "failure" | "cancelled";
export type RunExecutionMode = "mock" | "provider";
export type StartRunInput = {
prompt: string;
manifest: unknown;
sessionId?: string;
manifestPath?: string;
topologyHint?: string;
initialFlags?: Record<string, boolean>;
runtimeContextOverrides?: Record<string, string | number | boolean>;
simulateValidationNodeIds?: string[];
executionMode?: RunExecutionMode;
provider?: RunProvider;
};
export type RunRecord = {
runId: string;
sessionId: string;
status: RunStatus;
startedAt: string;
endedAt?: string;
manifestPath?: string;
topologyHint?: string;
executionMode: RunExecutionMode;
provider: RunProvider;
error?: string;
};
function toRunStatus(status: PipelineAggregateStatus): Extract<RunStatus, "success" | "failure"> {
return status === "success" ? "success" : "failure";
}
type ActiveRun = {
controller: AbortController;
record: RunRecord;
promise: Promise<void>;
};
function toSessionId(): string {
return `ui-session-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
}
function dedupeStrings(values: readonly string[]): string[] {
const output: string[] = [];
const seen = new Set<string>();
for (const value of values) {
const normalized = value.trim();
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
output.push(normalized);
}
return output;
}
async function waitWithSignal(ms: number, signal: AbortSignal): Promise<void> {
if (signal.aborted) {
throw signal.reason instanceof Error
? signal.reason
: new Error(String(signal.reason ?? "Run aborted."));
}
await new Promise<void>((resolveWait, rejectWait) => {
const timeout = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolveWait();
}, ms);
function onAbort(): void {
clearTimeout(timeout);
signal.removeEventListener("abort", onAbort);
rejectWait(
signal.reason instanceof Error
? signal.reason
: new Error(String(signal.reason ?? "Run aborted.")),
);
}
signal.addEventListener("abort", onAbort, { once: true });
});
}
function estimateUsage(prompt: string, toolCount: number): {
tokenInput: number;
tokenOutput: number;
durationMs: number;
costUsd: number;
} {
const tokenInput = Math.max(1, Math.ceil(prompt.length / 4));
const tokenOutput = Math.max(16, Math.ceil(tokenInput * 0.7));
const durationMs = 220 + (tokenInput % 11) * 35 + toolCount * 20;
const tokenTotal = tokenInput + tokenOutput;
const costUsd = Number((tokenTotal * 0.000002).toFixed(6));
return {
tokenInput,
tokenOutput,
durationMs,
costUsd,
};
}
function extractSubtasks(prompt: string): string[] {
const sentences = prompt
.split(/[.!?\n]/)
.map((part) => part.trim())
.filter((part) => part.length > 0)
.slice(0, 3);
if (sentences.length > 0) {
return sentences;
}
const words = prompt.split(/\s+/).filter((word) => word.length > 0).slice(0, 3);
return words.length > 0 ? [words.join(" ")] : [];
}
function createMockActorExecutors(
manifest: AgentManifest,
input: {
prompt: string;
topologyHint?: string;
simulateValidationNodeIds: Set<string>;
},
): Record<string, ActorExecutor> {
const attemptsByNode = new Map<string, number>();
const uniqueActorIds = dedupeStrings(manifest.pipeline.nodes.map((node) => node.actorId));
const execute: ActorExecutor = async (actorInput) => {
const attempt = (attemptsByNode.get(actorInput.node.id) ?? 0) + 1;
attemptsByNode.set(actorInput.node.id, attempt);
const shouldValidationFail =
attempt === 1 && input.simulateValidationNodeIds.has(actorInput.node.id);
const usage = estimateUsage(actorInput.prompt, actorInput.executionContext.allowedTools.length);
await waitWithSignal(Math.min(usage.durationMs, 900), actorInput.signal);
if (shouldValidationFail) {
const failure: ActorExecutionResult = {
status: "validation_fail",
payload: {
summary: `Node ${actorInput.node.id} requires remediation on first pass.`,
subtasks: extractSubtasks(input.prompt),
security_violation: false,
},
stateMetadata: {
usage: {
...usage,
tokenTotal: usage.tokenInput + usage.tokenOutput,
toolCalls: actorInput.executionContext.allowedTools.length,
},
topologyHint: input.topologyHint ?? "manifest-default",
},
failureKind: "soft",
failureCode: "ui_mock_validation_required",
};
return failure;
}
return {
status: "success",
payload: {
summary: `Node ${actorInput.node.id} completed in mock mode.`,
prompt: input.prompt,
subtasks: extractSubtasks(input.prompt),
},
stateMetadata: {
usage: {
...usage,
tokenTotal: usage.tokenInput + usage.tokenOutput,
toolCalls: actorInput.executionContext.allowedTools.length,
},
topologyHint: input.topologyHint ?? "manifest-default",
},
stateFlags: {
[`${actorInput.node.id}_completed`]: true,
},
};
};
const executors: Record<string, ActorExecutor> = {};
for (const actorId of uniqueActorIds) {
executors[actorId] = execute;
}
return executors;
}
function createSingleExecutorMap(manifest: AgentManifest, executor: ActorExecutor): Record<string, ActorExecutor> {
const uniqueActorIds = dedupeStrings(manifest.pipeline.nodes.map((node) => node.actorId));
const executors: Record<string, ActorExecutor> = {};
for (const actorId of uniqueActorIds) {
executors[actorId] = executor;
}
return executors;
}
function toAbortErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function isAbort(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
return error.name === "AbortError" || error.message.toLowerCase().includes("abort");
}
async function loadRuntimeConfig(envPath: string): Promise<Readonly<AppConfig>> {
const parsed = await parseEnvFile(envPath);
return loadConfig({
...process.env,
...parsed.values,
});
}
async function writeRunMeta(input: {
stateRoot: string;
sessionId: string;
run: RunRecord;
}): Promise<void> {
const sessionDirectory = resolve(input.stateRoot, input.sessionId);
await mkdir(sessionDirectory, { recursive: true });
const path = resolve(sessionDirectory, RUN_META_FILE_NAME);
await writeFile(path, `${JSON.stringify(input.run, null, 2)}\n`, "utf8");
}
export async function readRunMetaBySession(input: {
stateRoot: string;
sessionId: string;
}): Promise<RunRecord | undefined> {
const path = resolve(input.stateRoot, input.sessionId, RUN_META_FILE_NAME);
try {
const content = await readFile(path, "utf8");
const parsed = JSON.parse(content) as unknown;
if (!parsed || typeof parsed !== "object") {
return undefined;
}
const record = parsed as Partial<RunRecord>;
if (
typeof record.runId !== "string" ||
typeof record.sessionId !== "string" ||
typeof record.status !== "string" ||
typeof record.startedAt !== "string"
) {
return undefined;
}
const normalized: RunRecord = {
runId: record.runId,
sessionId: record.sessionId,
status:
record.status === "running" ||
record.status === "success" ||
record.status === "failure" ||
record.status === "cancelled"
? record.status
: "failure",
startedAt: record.startedAt,
executionMode:
record.executionMode === "provider" || record.executionMode === "mock"
? record.executionMode
: "mock",
provider: record.provider === "claude" || record.provider === "codex" ? record.provider : "codex",
...(typeof record.endedAt === "string" ? { endedAt: record.endedAt } : {}),
...(typeof record.manifestPath === "string" ? { manifestPath: record.manifestPath } : {}),
...(typeof record.topologyHint === "string" ? { topologyHint: record.topologyHint } : {}),
...(typeof record.error === "string" ? { error: record.error } : {}),
};
return normalized;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return undefined;
}
throw error;
}
}
export class UiRunService {
private readonly workspaceRoot: string;
private readonly envFilePath: string;
private readonly activeRuns = new Map<string, ActiveRun>();
private readonly runHistory = new Map<string, RunRecord>();
constructor(input: {
workspaceRoot: string;
envFilePath?: string;
}) {
this.workspaceRoot = resolve(input.workspaceRoot);
this.envFilePath = resolve(this.workspaceRoot, input.envFilePath ?? ".env");
}
listRuns(): RunRecord[] {
const output = [...this.runHistory.values()].sort((left, right) => {
return right.startedAt.localeCompare(left.startedAt);
});
return output;
}
getRun(runId: string): RunRecord | undefined {
return this.runHistory.get(runId);
}
async startRun(input: StartRunInput): Promise<RunRecord> {
const config = await loadRuntimeConfig(this.envFilePath);
const manifest = parseAgentManifest(input.manifest);
const executionMode = input.executionMode ?? "mock";
const provider = input.provider ?? "codex";
const sessionId = input.sessionId?.trim() || toSessionId();
const runId = randomUUID();
const controller = new AbortController();
const record: RunRecord = {
runId,
sessionId,
status: "running",
startedAt: new Date().toISOString(),
executionMode,
provider,
...(input.manifestPath ? { manifestPath: input.manifestPath } : {}),
...(input.topologyHint ? { topologyHint: input.topologyHint } : {}),
};
this.runHistory.set(runId, record);
const runPromise = (async () => {
let providerRuntime: Awaited<ReturnType<typeof createProviderRunRuntime>> | undefined;
try {
if (executionMode === "provider") {
providerRuntime = await createProviderRunRuntime({
provider,
initialPrompt: input.prompt,
config,
});
}
const actorExecutors =
executionMode === "provider" && providerRuntime
? createSingleExecutorMap(manifest, createProviderActorExecutor(providerRuntime))
: createMockActorExecutors(manifest, {
prompt: input.prompt,
topologyHint: input.topologyHint,
simulateValidationNodeIds: new Set(input.simulateValidationNodeIds ?? []),
});
const engine = new SchemaDrivenExecutionEngine({
manifest,
actorExecutors,
settings: {
workspaceRoot: this.workspaceRoot,
stateRoot: config.orchestration.stateRoot,
projectContextPath: config.orchestration.projectContextPath,
runtimeContext: {
ui_mode: executionMode,
run_provider: provider,
...(input.runtimeContextOverrides ?? {}),
},
},
config,
});
await writeRunMeta({
stateRoot: config.orchestration.stateRoot,
sessionId,
run: record,
});
const summary = await engine.runSession({
sessionId,
initialPayload: {
prompt: input.prompt,
},
initialState: {
flags: {
...(input.initialFlags ?? {}),
},
},
signal: controller.signal,
});
const completedRecord = this.runHistory.get(runId);
if (!completedRecord) {
return;
}
const next: RunRecord = {
...completedRecord,
status: toRunStatus(summary.status),
endedAt: new Date().toISOString(),
};
this.runHistory.set(runId, next);
await writeRunMeta({
stateRoot: config.orchestration.stateRoot,
sessionId,
run: next,
});
} catch (error) {
const current = this.runHistory.get(runId);
if (!current) {
return;
}
const cancelled = controller.signal.aborted || isAbort(error);
const next: RunRecord = {
...current,
status: cancelled ? "cancelled" : "failure",
endedAt: new Date().toISOString(),
error: toAbortErrorMessage(error),
};
this.runHistory.set(runId, next);
await writeRunMeta({
stateRoot: config.orchestration.stateRoot,
sessionId,
run: next,
});
} finally {
if (providerRuntime) {
await providerRuntime.close();
}
this.activeRuns.delete(runId);
}
})();
this.activeRuns.set(runId, {
controller,
record,
promise: runPromise,
});
return record;
}
async cancelRun(runId: string): Promise<RunRecord> {
const active = this.activeRuns.get(runId);
if (!active) {
const existing = this.runHistory.get(runId);
if (!existing) {
throw new Error(`Run \"${runId}\" does not exist.`);
}
return existing;
}
active.controller.abort(new Error("Cancelled by operator from UI kill switch."));
await active.promise;
const finalRecord = this.runHistory.get(runId);
if (!finalRecord) {
throw new Error(`Run \"${runId}\" cancellation did not produce a final record.`);
}
return finalRecord;
}
}

View File

@@ -1,94 +0,0 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import type { RuntimeEvent } from "../telemetry/runtime-events.js";
type RuntimeEventFilter = {
sessionId?: string;
types?: string[];
severities?: Array<RuntimeEvent["severity"]>;
limit?: number;
};
function safeParseLine(line: string): RuntimeEvent | undefined {
const trimmed = line.trim();
if (!trimmed) {
return undefined;
}
try {
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object") {
return undefined;
}
const record = parsed as Partial<RuntimeEvent>;
if (
typeof record.id !== "string" ||
typeof record.timestamp !== "string" ||
typeof record.type !== "string" ||
typeof record.severity !== "string" ||
typeof record.message !== "string"
) {
return undefined;
}
return record as RuntimeEvent;
} catch {
return undefined;
}
}
export async function readRuntimeEvents(logPath: string): Promise<RuntimeEvent[]> {
const absolutePath = resolve(logPath);
let content = "";
try {
content = await readFile(absolutePath, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw error;
}
const parsed: RuntimeEvent[] = [];
const lines = content.split(/\r?\n/);
for (const line of lines) {
const event = safeParseLine(line);
if (event) {
parsed.push(event);
}
}
parsed.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
return parsed;
}
export function filterRuntimeEvents(
events: readonly RuntimeEvent[],
filter: RuntimeEventFilter,
): RuntimeEvent[] {
const filtered: RuntimeEvent[] = [];
const types = filter.types ? new Set(filter.types) : undefined;
const severities = filter.severities ? new Set(filter.severities) : undefined;
for (const event of events) {
if (filter.sessionId && event.sessionId !== filter.sessionId) {
continue;
}
if (types && !types.has(event.type)) {
continue;
}
if (severities && !severities.has(event.severity)) {
continue;
}
filtered.push(event);
}
if (!filter.limit || filter.limit < 1 || filtered.length <= filter.limit) {
return filtered;
}
return filtered.slice(-filter.limit);
}

View File

@@ -1,27 +1,14 @@
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;
};
import express from "express";
import cors from "cors";
import { z } from "zod";
import { buildSessionGraphInsight, buildSessionSummaries } from "../telemetry/session-insights.js";
import { UiConfigStore, type LimitSettings, type RuntimeNotificationSettings, type SecurityPolicySettings } from "../store/config-store.js";
import { ManifestStore } from "../agents/manifest-store.js";
import { filterRuntimeEvents, readRuntimeEvents } from "../telemetry/runtime-events-store.js";
import { readRunMetaBySession, UiRunService } from "../runs/run-service.js";
function parsePort(value: string | undefined): number {
const parsed = Number(value ?? "4317");
@@ -31,7 +18,7 @@ function parsePort(value: string | undefined): number {
return parsed;
}
function parseLimit(value: string | null, fallback: number): number {
function parseLimit(value: string | null | undefined, fallback: number): number {
if (!value) {
return fallback;
}
@@ -42,66 +29,6 @@ function parseLimit(value: string | null, fallback: number): number {
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;
@@ -113,237 +40,144 @@ async function readRuntimePaths(configStore: UiConfigStore, workspaceRoot: strin
};
}
async function handleApiRequest(input: {
request: IncomingMessage;
response: ServerResponse;
const StartRunSchema = z.object({
prompt: z.string().min(1, 'Field "prompt" is required.'),
manifestPath: z.string().optional(),
manifest: z.unknown().optional(),
sessionId: z.string().optional(),
topologyHint: z.string().optional(),
initialFlags: z.record(z.string(), z.boolean()).optional(),
runtimeContextOverrides: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
simulateValidationNodeIds: z.array(z.string()).optional(),
executionMode: z.enum(["mock", "provider"]).optional(),
provider: z.enum(["claude", "codex"]).optional(),
});
export async function startUiServer(input: {
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";
port?: number;
host?: string;
}): Promise<{
close: () => Promise<void>;
}> {
const workspaceRoot = resolve(input.workspaceRoot);
const staticRoot = resolve(workspaceRoot, "ui/dist");
if (!pathname.startsWith("/api/")) {
return false;
}
const configStore = new UiConfigStore({ workspaceRoot });
const manifestStore = new ManifestStore({ workspaceRoot });
const runService = new UiRunService({ workspaceRoot });
try {
if (pathname === "/api/health") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
sendJson(response, 200, {
ok: true,
now: new Date().toISOString(),
});
return true;
}
const app = express();
app.use(cors());
app.use(express.json({ limit: "50mb" }));
if (pathname === "/api/config") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
app.get("/api/health", (req, res) => {
res.json({ ok: true, now: new Date().toISOString() });
});
app.get("/api/config", async (req, res, next) => {
try {
const snapshot = await configStore.readSnapshot();
sendJson(response, 200, {
ok: true,
config: snapshot,
});
return true;
}
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
});
if (pathname === "/api/config/runtime-events") {
if (method !== "PUT") {
methodNotAllowed(response);
return true;
}
app.put("/api/config/runtime-events", async (req, res, next) => {
try {
const snapshot = await configStore.updateRuntimeEvents(req.body as RuntimeNotificationSettings);
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
});
const body = await parseJsonBody<RuntimeNotificationSettings>(request);
const snapshot = await configStore.updateRuntimeEvents(body);
sendJson(response, 200, {
ok: true,
config: snapshot,
});
return true;
}
app.put("/api/config/security", async (req, res, next) => {
try {
const snapshot = await configStore.updateSecurityPolicy(req.body as SecurityPolicySettings);
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
});
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;
}
app.put("/api/config/limits", async (req, res, next) => {
try {
const snapshot = await configStore.updateLimits(req.body as LimitSettings);
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
});
app.get("/api/manifests", async (req, res, next) => {
try {
const listing = await manifestStore.list();
sendJson(response, 200, {
ok: true,
manifests: listing.paths,
});
return true;
}
res.json({ ok: true, manifests: listing.paths });
} catch (error) { next(error); }
});
if (pathname === "/api/manifests/read") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
const manifestPath = requestUrl.searchParams.get("path");
app.get("/api/manifests/read", async (req, res, next) => {
try {
const manifestPath = req.query.path as string;
if (!manifestPath) {
sendJson(response, 400, {
ok: false,
error: 'Query parameter "path" is required.',
});
return true;
res.status(400).json({ ok: false, error: 'Query parameter "path" is required.' });
return;
}
const record = await manifestStore.read(manifestPath);
sendJson(response, 200, {
ok: true,
manifest: record,
});
return true;
}
res.json({ ok: true, manifest: record });
} catch (error) { next(error); }
});
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;
app.post("/api/manifests/validate", async (req, res, next) => {
try {
const manifest = await manifestStore.validate(req.body.manifest);
res.json({ ok: true, manifest });
} catch (error) { next(error); }
});
app.put("/api/manifests/save", async (req, res, next) => {
try {
const { path, manifest } = req.body;
if (!path || typeof path !== "string") {
res.status(400).json({ ok: false, error: 'Field "path" is required.' });
return;
}
const record = await manifestStore.save(path, manifest);
res.json({ ok: true, manifest: record });
} catch (error) { next(error); }
});
app.get("/api/runtime-events", async (req, res, next) => {
try {
const { runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const limit = parseLimit(requestUrl.searchParams.get("limit"), 200);
const sessionId = requestUrl.searchParams.get("sessionId") ?? undefined;
const limit = parseLimit(req.query.limit as string | undefined, 200);
const sessionId = (req.query.sessionId as string) || undefined;
const events = filterRuntimeEvents(await readRuntimeEvents(runtimeEventLogPath), {
...(sessionId ? { sessionId } : {}),
limit,
});
res.json({ ok: true, events });
} catch (error) { next(error); }
});
sendJson(response, 200, {
ok: true,
events,
});
return true;
}
if (pathname === "/api/sessions") {
if (method !== "GET") {
methodNotAllowed(response);
return true;
}
app.get("/api/sessions", async (req, res, next) => {
try {
const { stateRoot, runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const sessions = await buildSessionSummaries({
stateRoot,
runtimeEventLogPath,
});
const sessions = await buildSessionSummaries({ stateRoot, runtimeEventLogPath });
res.json({ ok: true, sessions, runs: runService.listRuns() });
} catch (error) { next(error); }
});
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") ?? "";
app.get("/api/sessions/graph", async (req, res, next) => {
try {
const sessionId = (req.query.sessionId as string) ?? "";
if (!sessionId) {
sendJson(response, 400, {
ok: false,
error: 'Query parameter "sessionId" is required.',
});
return true;
res.status(400).json({ ok: false, error: 'Query parameter "sessionId" is required.' });
return;
}
const { stateRoot, runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const explicitManifestPath = requestUrl.searchParams.get("manifestPath");
const explicitManifestPath = req.query.manifestPath as string | undefined;
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;
res.status(400).json({ ok: false, error: "No manifestPath available for this session. Provide one in query string." });
return;
}
const manifestRecord = await manifestStore.read(manifestPath);
@@ -354,212 +188,126 @@ async function handleApiRequest(input: {
manifest: manifestRecord.manifest,
});
sendJson(response, 200, {
ok: true,
graph,
manifestPath: manifestRecord.path,
res.json({ ok: true, graph, manifestPath: manifestRecord.path });
} catch (error) { next(error); }
});
app.get("/api/runs", (req, res) => {
res.json({ ok: true, runs: runService.listRuns() });
});
app.post("/api/runs", async (req, res, next) => {
try {
const parseResult = StartRunSchema.safeParse(req.body);
if (!parseResult.success) {
res.status(400).json({ ok: false, error: parseResult.error.issues[0]?.message || 'Invalid body' });
return;
}
const body = parseResult.data;
let manifest: unknown;
if (body.manifest !== undefined) {
manifest = body.manifest;
} else if (body.manifestPath) {
manifest = (await manifestStore.read(body.manifestPath)).source;
}
if (!manifest) {
res.status(400).json({ ok: false, error: "A manifest or manifestPath is required to start a run." });
return;
}
const record = await runService.startRun({
prompt: body.prompt,
manifest,
manifestPath: body.manifestPath,
sessionId: body.sessionId,
topologyHint: body.topologyHint,
initialFlags: body.initialFlags ?? {},
runtimeContextOverrides: body.runtimeContextOverrides ?? {},
simulateValidationNodeIds: body.simulateValidationNodeIds ?? [],
executionMode: body.executionMode ?? "mock",
provider: body.provider ?? "codex",
});
return true;
}
if (pathname === "/api/runs") {
if (method === "GET") {
sendJson(response, 200, {
ok: true,
runs: runService.listRuns(),
});
return true;
res.status(202).json({ ok: true, run: record });
} catch (error) { next(error); }
});
app.post("/api/runs/:runId/cancel", async (req, res, next) => {
try {
const runId = req.params.runId;
if (!runId) {
res.status(400).json({ ok: false, error: "runId required" });
return;
}
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;
}
res.json({ ok: true, run });
} catch (error) { next(error); }
});
if (pathname.startsWith("/api/runs/")) {
if (method !== "GET") {
methodNotAllowed(response);
return true;
app.get("/api/runs/:runId", async (req, res, next) => {
try {
const runId = req.params.runId;
if (!runId) {
res.status(400).json({ ok: false, error: "runId required" });
return;
}
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;
res.status(404).json({ ok: false, error: `Run "${runId}" was not found.` });
return;
}
res.json({ ok: true, run });
} catch (error) { next(error); }
});
sendJson(response, 200, {
ok: true,
run,
});
return true;
}
notFound(response);
return true;
} catch (error) {
sendJson(response, 400, {
app.use("/api", (req, res) => {
res.status(404).json({
ok: false,
error: error instanceof Error ? error.message : String(error),
error: `API route "${req.method} ${req.originalUrl}" was not found.`,
});
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,
// Default error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
res.status(400).json({
ok: false,
error: err instanceof Error ? err.message : String(err),
});
});
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);
}
// Serve static files
app.use(express.static(staticRoot));
app.get(/(.*)/, (req, res) => {
res.sendFile(resolve(staticRoot, "index.html"));
});
const host = input.host ?? "127.0.0.1";
const port = input.port ?? parsePort(process.env.AGENT_UI_PORT);
const server = app.listen(port, host);
await new Promise<void>((resolveReady, rejectReady) => {
server.once("error", rejectReady);
server.listen(port, host, () => {
server.once("listening", () => {
server.off("error", rejectReady);
console.log(`AI Ops UI listening at http://${host}:${String(port)}`);
resolveReady();
});
});
console.log(`AI Ops UI listening at http://${host}:${String(port)}`);
return {
close: async () => {
if (!server.listening) {
return;
}
await new Promise<void>((resolveClose, rejectClose) => {
server.close((error) => {
if (error) {
if ((error as NodeJS.ErrnoException).code === "ERR_SERVER_NOT_RUNNING") {
resolveClose();
return;
}
rejectClose(error);
return;
}

View File

@@ -1,537 +0,0 @@
import { readdir, readFile, stat } from "node:fs/promises";
import { resolve } from "node:path";
import type { PipelineEdge, PipelineNode, RouteCondition } from "../agents/manifest.js";
import type { AgentManifest } from "../agents/manifest.js";
import { isRecord, type JsonObject } from "../agents/types.js";
import type { RuntimeEvent, RuntimeEventUsage } from "../telemetry/runtime-events.js";
import { filterRuntimeEvents, readRuntimeEvents } from "./runtime-events-store.js";
export type SessionStatus = "success" | "failure" | "running" | "unknown";
export type SessionSummary = {
sessionId: string;
status: SessionStatus;
startedAt?: string;
endedAt?: string;
nodeAttemptCount: number;
distinctNodeCount: number;
costUsd: number;
durationMs: number;
criticalEventCount: number;
message?: string;
aborted: boolean;
};
export type NodeAttemptInsight = {
timestamp: string;
attempt: number;
status: "success" | "validation_fail" | "failure";
severity: RuntimeEvent["severity"];
message: string;
usage: RuntimeEventUsage;
metadata: JsonObject;
executionContext?: JsonObject;
retrySpawned: boolean;
securityViolation: boolean;
subtasks: string[];
};
export type NodeInsight = {
nodeId: string;
actorId: string;
personaId: string;
topology: string;
fromNodeId?: string;
attemptCount: number;
lastStatus?: "success" | "validation_fail" | "failure";
usage: RuntimeEventUsage;
subtaskCount: number;
securityViolationCount: number;
attempts: NodeAttemptInsight[];
domainEvents: RuntimeEvent[];
sandboxPayload?: JsonObject;
};
export type EdgeInsight = {
from: string;
to: string;
trigger: string;
conditionLabels: string[];
visited: boolean;
critical: boolean;
};
export type SessionGraphInsight = {
sessionId: string;
status: SessionStatus;
aborted: boolean;
abortMessage?: string;
nodes: NodeInsight[];
edges: EdgeInsight[];
runtimeEvents: RuntimeEvent[];
criticalPathNodeIds: string[];
};
type BuildSessionSummaryInput = {
stateRoot: string;
runtimeEventLogPath: string;
};
type BuildSessionGraphInput = {
stateRoot: string;
runtimeEventLogPath: string;
sessionId: string;
manifest: AgentManifest;
};
function toUsage(value: RuntimeEventUsage | undefined): RuntimeEventUsage {
return {
...(typeof value?.tokenInput === "number" ? { tokenInput: value.tokenInput } : {}),
...(typeof value?.tokenOutput === "number" ? { tokenOutput: value.tokenOutput } : {}),
...(typeof value?.tokenTotal === "number" ? { tokenTotal: value.tokenTotal } : {}),
...(typeof value?.toolCalls === "number" ? { toolCalls: value.toolCalls } : {}),
...(typeof value?.durationMs === "number" ? { durationMs: value.durationMs } : {}),
...(typeof value?.costUsd === "number" ? { costUsd: value.costUsd } : {}),
};
}
function addUsage(target: RuntimeEventUsage, source: RuntimeEventUsage): RuntimeEventUsage {
return {
tokenInput: (target.tokenInput ?? 0) + (source.tokenInput ?? 0),
tokenOutput: (target.tokenOutput ?? 0) + (source.tokenOutput ?? 0),
tokenTotal: (target.tokenTotal ?? 0) + (source.tokenTotal ?? 0),
toolCalls: (target.toolCalls ?? 0) + (source.toolCalls ?? 0),
durationMs: (target.durationMs ?? 0) + (source.durationMs ?? 0),
costUsd: (target.costUsd ?? 0) + (source.costUsd ?? 0),
};
}
function toConditionLabel(condition: RouteCondition): string {
if (condition.type === "always") {
return "always";
}
if (condition.type === "state_flag") {
return `state_flag:${condition.key}=${String(condition.equals)}`;
}
if (condition.type === "history_has_event") {
return `history_has_event:${condition.event}`;
}
return `file_exists:${condition.path}`;
}
function toEdgeTrigger(edge: PipelineEdge): string {
if (edge.event) {
return `event:${edge.event}`;
}
return `on:${edge.on ?? "unknown"}`;
}
function toStatusFromAttemptEvent(event: RuntimeEvent): "success" | "validation_fail" | "failure" {
const metadata = event.metadata;
if (isRecord(metadata)) {
const status = metadata.status;
if (status === "success" || status === "validation_fail" || status === "failure") {
return status;
}
}
if (event.severity === "critical") {
return "failure";
}
if (event.severity === "warning") {
return "validation_fail";
}
return "success";
}
function toSubtasks(metadata: JsonObject): string[] {
const raw = metadata.subtasks;
if (!Array.isArray(raw)) {
return [];
}
const subtasks: string[] = [];
for (const item of raw) {
if (typeof item !== "string") {
continue;
}
const normalized = item.trim();
if (!normalized) {
continue;
}
subtasks.push(normalized);
}
return subtasks;
}
function readBoolean(record: JsonObject, key: string): boolean {
return record[key] === true;
}
function readExecutionContext(metadata: JsonObject): JsonObject | undefined {
const raw = metadata.executionContext;
return isRecord(raw) ? (raw as JsonObject) : undefined;
}
function summarizeStatusForSession(events: readonly RuntimeEvent[]): {
status: SessionStatus;
startedAt?: string;
endedAt?: string;
message?: string;
aborted: boolean;
} {
const started = events.find((event) => event.type === "session.started");
const completed = [...events].reverse().find((event) => event.type === "session.completed");
const failed = [...events].reverse().find((event) => event.type === "session.failed");
if (failed) {
const message = failed.message;
const lower = message.toLowerCase();
return {
status: "failure",
startedAt: started?.timestamp,
endedAt: failed.timestamp,
message,
aborted: lower.includes("abort"),
};
}
if (completed) {
const metadata = isRecord(completed.metadata) ? completed.metadata : undefined;
const completedStatus = metadata?.status;
const status = completedStatus === "success" ? "success" : "failure";
return {
status,
startedAt: started?.timestamp,
endedAt: completed.timestamp,
message: completed.message,
aborted: false,
};
}
if (started) {
return {
status: "running",
startedAt: started.timestamp,
message: started.message,
aborted: false,
};
}
return {
status: "unknown",
aborted: false,
};
}
async function listSessionDirectories(stateRoot: string): Promise<string[]> {
try {
const entries = await readdir(resolve(stateRoot), { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort((left, right) => left.localeCompare(right));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw error;
}
}
export async function buildSessionSummaries(
input: BuildSessionSummaryInput,
): Promise<SessionSummary[]> {
const [sessionDirectories, allEvents] = await Promise.all([
listSessionDirectories(input.stateRoot),
readRuntimeEvents(input.runtimeEventLogPath),
]);
const sessionIds = new Set<string>(sessionDirectories);
for (const event of allEvents) {
if (event.sessionId) {
sessionIds.add(event.sessionId);
}
}
const summaries: SessionSummary[] = [];
for (const sessionId of sessionIds) {
const sessionEvents = filterRuntimeEvents(allEvents, {
sessionId,
});
const attempts = sessionEvents.filter((event) => event.type === "node.attempt.completed");
const distinctNodeIds = new Set(attempts.map((event) => event.nodeId).filter(Boolean));
const totalUsage = attempts.reduce<RuntimeEventUsage>(
(aggregate, event) => addUsage(aggregate, toUsage(event.usage)),
{},
);
const criticalEventCount = sessionEvents.filter((event) => event.severity === "critical").length;
const statusInfo = summarizeStatusForSession(sessionEvents);
summaries.push({
sessionId,
status: statusInfo.status,
startedAt: statusInfo.startedAt,
endedAt: statusInfo.endedAt,
nodeAttemptCount: attempts.length,
distinctNodeCount: distinctNodeIds.size,
costUsd: totalUsage.costUsd ?? 0,
durationMs: totalUsage.durationMs ?? 0,
criticalEventCount,
message: statusInfo.message,
aborted: statusInfo.aborted,
});
}
summaries.sort((left, right) => {
const leftTime = left.startedAt ?? left.endedAt ?? "";
const rightTime = right.startedAt ?? right.endedAt ?? "";
return rightTime.localeCompare(leftTime);
});
return summaries;
}
async function readHandoffParentByNode(
stateRoot: string,
sessionId: string,
): Promise<Record<string, string | undefined>> {
const handoffDirectory = resolve(stateRoot, sessionId, "handoffs");
const output: Record<string, string | undefined> = {};
try {
const entries = await readdir(handoffDirectory, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".json")) {
continue;
}
const path = resolve(handoffDirectory, entry.name);
const content = await readFile(path, "utf8");
const parsed = JSON.parse(content) as unknown;
if (!isRecord(parsed)) {
continue;
}
const nodeId = parsed.nodeId;
if (typeof nodeId !== "string" || !nodeId) {
continue;
}
const fromNodeId = parsed.fromNodeId;
output[nodeId] = typeof fromNodeId === "string" && fromNodeId
? fromNodeId
: undefined;
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return output;
}
throw error;
}
return output;
}
function buildCriticalPath(input: {
manifest: AgentManifest;
nodes: readonly NodeInsight[];
fromNodeByNode: Record<string, string | undefined>;
status: SessionStatus;
}): string[] {
const nodeById = new Map(input.nodes.map((node) => [node.nodeId, node]));
const failed = [...input.nodes]
.reverse()
.find((node) => node.lastStatus === "failure" || node.lastStatus === "validation_fail");
const targetNodeId = failed?.nodeId ?? (() => {
if (input.status !== "success") {
return undefined;
}
const executedNodeIds = new Set(input.nodes.filter((node) => node.attemptCount > 0).map((node) => node.nodeId));
const terminalNode = [...executedNodeIds].find((nodeId) => {
const outgoing = input.manifest.pipeline.edges.filter((edge) => edge.from === nodeId);
return !outgoing.some((edge) => executedNodeIds.has(edge.to));
});
return terminalNode;
})();
if (!targetNodeId || !nodeById.has(targetNodeId)) {
return [];
}
const path: string[] = [];
const visited = new Set<string>();
let current: string | undefined = targetNodeId;
while (current && !visited.has(current)) {
visited.add(current);
path.push(current);
current = input.fromNodeByNode[current];
}
return path.reverse();
}
function toCriticalEdgeSet(pathNodeIds: readonly string[]): Set<string> {
const output = new Set<string>();
for (let index = 1; index < pathNodeIds.length; index += 1) {
const from = pathNodeIds[index - 1];
const to = pathNodeIds[index];
if (from && to) {
output.add(`${from}->${to}`);
}
}
return output;
}
function toAttemptInsight(event: RuntimeEvent): NodeAttemptInsight {
const metadata = isRecord(event.metadata) ? (event.metadata as JsonObject) : {};
return {
timestamp: event.timestamp,
attempt: typeof event.attempt === "number" ? event.attempt : 1,
status: toStatusFromAttemptEvent(event),
severity: event.severity,
message: event.message,
usage: toUsage(event.usage),
metadata,
executionContext: readExecutionContext(metadata),
retrySpawned: readBoolean(metadata, "retrySpawned"),
securityViolation: readBoolean(metadata, "securityViolation"),
subtasks: toSubtasks(metadata),
};
}
function inferTopology(node: PipelineNode): string {
return node.topology?.kind ?? "sequential";
}
export async function buildSessionGraphInsight(
input: BuildSessionGraphInput,
): Promise<SessionGraphInsight> {
const [allEvents, handoffParentByNode] = await Promise.all([
readRuntimeEvents(input.runtimeEventLogPath),
readHandoffParentByNode(input.stateRoot, input.sessionId),
]);
const sessionEvents = filterRuntimeEvents(allEvents, {
sessionId: input.sessionId,
});
const statusInfo = summarizeStatusForSession(sessionEvents);
const attemptsByNode = new Map<string, NodeAttemptInsight[]>();
const domainEventsByNode = new Map<string, RuntimeEvent[]>();
for (const event of sessionEvents) {
if (event.type === "node.attempt.completed" && event.nodeId) {
const attempt = toAttemptInsight(event);
const list = attemptsByNode.get(event.nodeId);
if (list) {
list.push(attempt);
} else {
attemptsByNode.set(event.nodeId, [attempt]);
}
continue;
}
if (event.type.startsWith("domain.") && event.nodeId) {
const list = domainEventsByNode.get(event.nodeId);
if (list) {
list.push(event);
} else {
domainEventsByNode.set(event.nodeId, [event]);
}
}
}
const nodes: NodeInsight[] = input.manifest.pipeline.nodes.map((node) => {
const attempts = [...(attemptsByNode.get(node.id) ?? [])].sort((left, right) => {
if (left.attempt !== right.attempt) {
return left.attempt - right.attempt;
}
return left.timestamp.localeCompare(right.timestamp);
});
const usage = attempts.reduce<RuntimeEventUsage>((aggregate, attempt) => {
return addUsage(aggregate, attempt.usage);
}, {});
const last = attempts[attempts.length - 1];
const sandboxPayload = [...attempts]
.reverse()
.map((attempt) => attempt.executionContext)
.find((payload) => Boolean(payload));
const subtasks = attempts.flatMap((attempt) => attempt.subtasks);
return {
nodeId: node.id,
actorId: node.actorId,
personaId: node.personaId,
topology: inferTopology(node),
fromNodeId: handoffParentByNode[node.id],
attemptCount: attempts.length,
...(last ? { lastStatus: last.status } : {}),
usage,
subtaskCount: subtasks.length,
securityViolationCount: attempts.filter((attempt) => attempt.securityViolation).length,
attempts,
domainEvents: [...(domainEventsByNode.get(node.id) ?? [])],
...(sandboxPayload ? { sandboxPayload } : {}),
};
});
const criticalPathNodeIds = buildCriticalPath({
manifest: input.manifest,
nodes,
fromNodeByNode: handoffParentByNode,
status: statusInfo.status,
});
const criticalEdgeSet = toCriticalEdgeSet(criticalPathNodeIds);
const edges: EdgeInsight[] = input.manifest.pipeline.edges.map((edge) => {
const visited = handoffParentByNode[edge.to] === edge.from;
return {
from: edge.from,
to: edge.to,
trigger: toEdgeTrigger(edge),
conditionLabels: (edge.when ?? []).map(toConditionLabel),
visited,
critical: criticalEdgeSet.has(`${edge.from}->${edge.to}`),
};
});
return {
sessionId: input.sessionId,
status: statusInfo.status,
aborted: statusInfo.aborted,
...(statusInfo.message ? { abortMessage: statusInfo.message } : {}),
nodes,
edges,
runtimeEvents: sessionEvents,
criticalPathNodeIds,
};
}
export async function readSessionUpdatedAt(stateRoot: string, sessionId: string): Promise<string | undefined> {
const sessionPath = resolve(stateRoot, sessionId);
try {
const metadata = await stat(sessionPath);
return metadata.mtime.toISOString();
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return undefined;
}
throw error;
}
}