feat(ui): add operator UI server, stores, and insights
This commit is contained in:
@@ -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),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user