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

View File

@@ -3,7 +3,7 @@ import {
type DomainEvent,
type DomainEventType,
} from "./domain-events.js";
import type { PipelineNode } from "./manifest.js";
import type { NodeTopologyKind, PipelineNode } from "./manifest.js";
import { type ProjectContextPatch, type FileSystemProjectContextStore } from "./project-context.js";
import { PersonaRegistry } from "./persona-registry.js";
import {
@@ -23,6 +23,10 @@ export type PipelineNodeAttemptObservedEvent = {
attempt: number;
result: ActorExecutionResult;
domainEvents: DomainEvent[];
executionContext: JsonObject;
fromNodeId?: string;
retrySpawned: boolean;
topologyKind: NodeTopologyKind;
};
function toBehaviorEvent(status: ActorResultStatus): "onTaskComplete" | "onValidationFail" | undefined {
@@ -149,6 +153,41 @@ function extractUsageMetrics(result: ActorExecutionResult): RuntimeEventUsage |
return undefined;
}
function extractSubtasks(result: ActorExecutionResult): string[] {
const candidates = [
result.payload?.subtasks,
result.stateMetadata?.subtasks,
];
for (const candidate of candidates) {
if (!Array.isArray(candidate)) {
continue;
}
const subtasks: string[] = [];
for (const item of candidate) {
if (typeof item !== "string") {
continue;
}
const normalized = item.trim();
if (!normalized) {
continue;
}
subtasks.push(normalized);
}
if (subtasks.length > 0) {
return subtasks;
}
}
return [];
}
function hasSecurityViolation(result: ActorExecutionResult): boolean {
return isRecord(result.payload) && result.payload.security_violation === true;
}
export interface PipelineLifecycleObserver {
onNodeAttempt(event: PipelineNodeAttemptObservedEvent): Promise<void>;
}
@@ -213,6 +252,12 @@ export class PersistenceLifecycleObserver implements PipelineLifecycleObserver {
status: event.result.status,
...(event.result.failureKind ? { failureKind: event.result.failureKind } : {}),
...(event.result.failureCode ? { failureCode: event.result.failureCode } : {}),
executionContext: event.executionContext,
topologyKind: event.topologyKind,
retrySpawned: event.retrySpawned,
...(event.fromNodeId ? { fromNodeId: event.fromNodeId } : {}),
subtasks: extractSubtasks(event.result),
securityViolation: hasSecurityViolation(event.result),
},
});

View File

@@ -14,7 +14,13 @@ import {
PersistenceLifecycleObserver,
type PipelineLifecycleObserver,
} from "./lifecycle-observer.js";
import type { AgentManifest, PipelineEdge, PipelineNode, RouteCondition } from "./manifest.js";
import type {
AgentManifest,
NodeTopologyKind,
PipelineEdge,
PipelineNode,
RouteCondition,
} from "./manifest.js";
import type { AgentManager, RecursiveChildIntent } from "./manager.js";
import type {
CodexConfigObject,
@@ -183,6 +189,10 @@ type NodeAttemptResult = {
payloadForNext: JsonObject;
domainEvents: DomainEvent[];
hardFailure: boolean;
executionContext: ResolvedExecutionContext;
fromNodeId?: string;
retrySpawned: boolean;
topologyKind: NodeTopologyKind;
};
type NodeExecutionOutcome = {
@@ -854,6 +864,12 @@ export class PipelineExecutor {
attempt,
},
});
const toolClearance = this.personaRegistry.getToolClearance(node.personaId);
const executionContext = this.resolveExecutionContext({
node,
toolClearance,
prompt,
});
const result = await this.invokeActorExecutor({
sessionId,
@@ -861,6 +877,7 @@ export class PipelineExecutor {
prompt,
context,
signal: recursiveSignal,
executionContext,
executor,
});
@@ -871,6 +888,12 @@ export class PipelineExecutor {
status: result.status,
customEvents: result.events,
});
const topologyKind: NodeTopologyKind = node.topology?.kind ?? "sequential";
const payloadForNext = result.payload ?? context.handoff.payload;
const shouldRetry =
result.status === "validation_fail" &&
this.shouldRetryValidation(node) &&
attempt <= maxRetriesForNode;
await this.lifecycleObserver.onNodeAttempt({
sessionId,
@@ -878,6 +901,10 @@ export class PipelineExecutor {
attempt,
result,
domainEvents,
executionContext,
fromNodeId: context.handoff.fromNodeId,
retrySpawned: shouldRetry,
topologyKind,
});
const emittedEventTypes = domainEvents.map((event) => event.type);
@@ -893,12 +920,6 @@ export class PipelineExecutor {
const hardFailure = this.failurePolicy.isHardFailure(result);
hardFailureAttempts.push(hardFailure);
const payloadForNext = result.payload ?? context.handoff.payload;
const shouldRetry =
result.status === "validation_fail" &&
this.shouldRetryValidation(node) &&
attempt <= maxRetriesForNode;
if (!shouldRetry) {
return {
type: "complete" as const,
@@ -909,6 +930,10 @@ export class PipelineExecutor {
payloadForNext,
domainEvents,
hardFailure,
executionContext,
fromNodeId: context.handoff.fromNodeId,
retrySpawned: false,
topologyKind,
},
};
}
@@ -935,7 +960,10 @@ export class PipelineExecutor {
if (!first) {
throw new Error(`Retry aggregation for node "${node.id}" did not receive child output.`);
}
return first.output;
return {
...first.output,
retrySpawned: true,
};
},
};
},
@@ -965,16 +993,11 @@ export class PipelineExecutor {
prompt: string;
context: NodeExecutionContext;
signal: AbortSignal;
executionContext: ResolvedExecutionContext;
executor: ActorExecutor;
}): Promise<ActorExecutionResult> {
try {
throwIfAborted(input.signal);
const toolClearance = this.personaRegistry.getToolClearance(input.node.personaId);
const executionContext = this.resolveExecutionContext({
node: input.node,
toolClearance,
prompt: input.prompt,
});
return await input.executor({
sessionId: input.sessionId,
@@ -982,8 +1005,8 @@ export class PipelineExecutor {
prompt: input.prompt,
context: input.context,
signal: input.signal,
executionContext,
mcp: this.buildActorMcpContext(executionContext, input.prompt),
executionContext: input.executionContext,
mcp: this.buildActorMcpContext(input.executionContext, input.prompt),
security: this.securityContext,
});
} catch (error) {