Wire pipeline DAG execution to manager with events and project context

This commit is contained in:
2026-02-23 13:14:20 -05:00
parent 53af0d44cd
commit 889087daa1
13 changed files with 1668 additions and 380 deletions

View File

@@ -1,4 +1,5 @@
import { isRecord } from "./types.js";
import { isDomainEventType, type DomainEventType } from "./domain-events.js";
export type ToolClearancePolicy = {
allowlist: string[];
@@ -45,23 +46,32 @@ export type PipelineConstraint = {
maxRetries?: number;
};
export type NodeTopologyKind = "sequential" | "parallel" | "hierarchical" | "retry-unrolled";
export type PipelineNodeTopology = {
kind: NodeTopologyKind;
blockId?: string;
};
export type PipelineNode = {
id: string;
actorId: string;
personaId: string;
constraints?: PipelineConstraint;
topology?: PipelineNodeTopology;
};
export type PipelineEdge = {
from: string;
to: string;
on:
on?:
| "success"
| "validation_fail"
| "failure"
| "always"
| "onTaskComplete"
| "onValidationFail";
event?: DomainEventType;
when?: RouteCondition[];
};
@@ -71,7 +81,7 @@ export type PipelineGraph = {
edges: PipelineEdge[];
};
export type TopologyKind = "hierarchical" | "retry-unrolled" | "sequential";
export type TopologyKind = "hierarchical" | "parallel" | "retry-unrolled" | "sequential";
export type TopologyConstraint = {
maxDepth: number;
@@ -216,6 +226,34 @@ function parsePipelineNode(value: unknown): PipelineNode {
throw new Error("Pipeline node must be an object.");
}
const topology = value.topology;
let parsedTopology: PipelineNodeTopology | undefined;
if (topology !== undefined) {
if (!isRecord(topology)) {
throw new Error("Pipeline node topology must be an object when provided.");
}
const kind = readString(topology, "kind");
if (
kind !== "sequential" &&
kind !== "parallel" &&
kind !== "hierarchical" &&
kind !== "retry-unrolled"
) {
throw new Error(`Pipeline node topology kind "${kind}" is not supported.`);
}
const blockIdRaw = topology.blockId;
if (blockIdRaw !== undefined && (typeof blockIdRaw !== "string" || blockIdRaw.trim().length === 0)) {
throw new Error("Pipeline node topology blockId must be a non-empty string when provided.");
}
parsedTopology = {
kind,
...(typeof blockIdRaw === "string" ? { blockId: blockIdRaw.trim() } : {}),
};
}
const constraints = isRecord(value.constraints)
? {
maxRetries: readOptionalInteger(value.constraints, "maxRetries", { min: 0 }),
@@ -227,6 +265,7 @@ function parsePipelineNode(value: unknown): PipelineNode {
actorId: readString(value, "actorId"),
personaId: readString(value, "personaId"),
constraints,
...(parsedTopology ? { topology: parsedTopology } : {}),
};
}
@@ -235,8 +274,7 @@ function parsePipelineEdge(value: unknown): PipelineEdge {
throw new Error("Pipeline edge must be an object.");
}
const on = readString(value, "on");
const validEvents: PipelineEdge["on"][] = [
const validEvents: NonNullable<PipelineEdge["on"]>[] = [
"success",
"validation_fail",
"failure",
@@ -245,8 +283,29 @@ function parsePipelineEdge(value: unknown): PipelineEdge {
"onValidationFail",
];
if (!validEvents.includes(on as PipelineEdge["on"])) {
throw new Error(`Pipeline edge field \"on\" has unsupported event \"${on}\".`);
const rawOn = value.on;
let on: PipelineEdge["on"];
if (rawOn !== undefined) {
if (typeof rawOn !== "string" || !validEvents.includes(rawOn as NonNullable<PipelineEdge["on"]>)) {
throw new Error(`Pipeline edge field "on" has unsupported event "${String(rawOn)}".`);
}
on = rawOn as NonNullable<PipelineEdge["on"]>;
}
const rawDomainEvent = value.event;
let event: DomainEventType | undefined;
if (rawDomainEvent !== undefined) {
if (typeof rawDomainEvent !== "string" || !isDomainEventType(rawDomainEvent)) {
throw new Error(`Pipeline edge field "event" has unsupported domain event "${String(rawDomainEvent)}".`);
}
event = rawDomainEvent;
}
if (!on && !event) {
throw new Error('Pipeline edge must provide either an "on" trigger or an "event" trigger.');
}
if (on && event) {
throw new Error('Pipeline edge cannot define both "on" and "event" triggers simultaneously.');
}
const rawWhen = value.when;
@@ -263,7 +322,8 @@ function parsePipelineEdge(value: unknown): PipelineEdge {
return {
from: readString(value, "from"),
to: readString(value, "to"),
on: on as PipelineEdge["on"],
...(on ? { on } : {}),
...(event ? { event } : {}),
...(when.length > 0 ? { when } : {}),
};
}
@@ -298,7 +358,7 @@ function parseTopologies(value: unknown): TopologyKind[] {
throw new Error("Manifest topologies must be a non-empty array.");
}
const valid = new Set<TopologyKind>(["hierarchical", "retry-unrolled", "sequential"]);
const valid = new Set<TopologyKind>(["hierarchical", "parallel", "retry-unrolled", "sequential"]);
const result: TopologyKind[] = [];
for (const item of value) {
@@ -498,6 +558,12 @@ export function parseAgentManifest(input: unknown): AgentManifest {
if (!personaIds.has(node.personaId)) {
throw new Error(`Pipeline node \"${node.id}\" references unknown persona \"${node.personaId}\".`);
}
if (node.topology && !manifest.topologies.includes(node.topology.kind as TopologyKind)) {
throw new Error(
`Pipeline node "${node.id}" topology "${node.topology.kind}" is not listed in manifest topologies.`,
);
}
}
assertPipelineDag(manifest.pipeline);