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,398 +1,113 @@
import { isRecord } from "./types.js";
import { z } from "zod";
import { isDomainEventType, type DomainEventType } from "./domain-events.js";
import {
parseToolClearancePolicy,
toolClearancePolicySchema,
type ToolClearancePolicy as SecurityToolClearancePolicy,
} from "../security/schemas.js";
export type ToolClearancePolicy = SecurityToolClearancePolicy;
export type ManifestPersona = {
id: string;
displayName: string;
systemPromptTemplate: string;
modelConstraint?: string;
toolClearance: ToolClearancePolicy;
};
export const ManifestPersonaSchema = z.object({
id: z.string().trim().min(1, 'Manifest field "id" must be a non-empty string.'),
displayName: z.string().trim().min(1, 'Manifest field "displayName" must be a non-empty string.'),
systemPromptTemplate: z.string().trim().min(1, 'Manifest field "systemPromptTemplate" must be a non-empty string.'),
modelConstraint: z.string().trim().min(1, 'Manifest persona field "modelConstraint" must be a non-empty string when provided.').optional(),
toolClearance: toolClearancePolicySchema,
});
export type ManifestPersona = z.infer<typeof ManifestPersonaSchema>;
export type RelationshipConstraint = {
maxDepth?: number;
maxChildren?: number;
};
export const RelationshipConstraintSchema = z.object({
maxDepth: z.number().int().min(1, 'Manifest field "maxDepth" must be an integer >= 1.').optional(),
maxChildren: z.number().int().min(1, 'Manifest field "maxChildren" must be an integer >= 1.').optional(),
});
export type RelationshipConstraint = z.infer<typeof RelationshipConstraintSchema>;
export type RelationshipEdge = {
parentPersonaId: string;
childPersonaId: string;
constraints?: RelationshipConstraint;
};
export const RelationshipEdgeSchema = z.object({
parentPersonaId: z.string().trim().min(1, 'Manifest field "parentPersonaId" must be a non-empty string.'),
childPersonaId: z.string().trim().min(1, 'Manifest field "childPersonaId" must be a non-empty string.'),
constraints: RelationshipConstraintSchema.optional(),
});
export type RelationshipEdge = z.infer<typeof RelationshipEdgeSchema>;
export type RouteCondition =
| {
type: "always";
}
| {
type: "state_flag";
key: string;
equals: boolean;
}
| {
type: "history_has_event";
event: string;
}
| {
type: "file_exists";
path: string;
};
export const RouteConditionSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("always") }),
z.object({ type: z.literal("state_flag"), key: z.string().trim().min(1), equals: z.boolean() }),
z.object({ type: z.literal("history_has_event"), event: z.string().trim().min(1) }),
z.object({ type: z.literal("file_exists"), path: z.string().trim().min(1) }),
]);
export type RouteCondition = z.infer<typeof RouteConditionSchema>;
export type PipelineConstraint = {
maxRetries?: number;
};
export const PipelineConstraintSchema = z.object({
maxRetries: z.number().int().min(0, 'Manifest field "maxRetries" must be an integer >= 0.').optional(),
});
export type PipelineConstraint = z.infer<typeof PipelineConstraintSchema>;
export type NodeTopologyKind = "sequential" | "parallel" | "hierarchical" | "retry-unrolled";
export const NodeTopologyKindSchema = z.enum(["sequential", "parallel", "hierarchical", "retry-unrolled"]);
export type NodeTopologyKind = z.infer<typeof NodeTopologyKindSchema>;
export type PipelineNodeTopology = {
kind: NodeTopologyKind;
blockId?: string;
};
export const PipelineNodeTopologySchema = z.object({
kind: NodeTopologyKindSchema,
blockId: z.string().trim().min(1, 'Pipeline node topology blockId must be a non-empty string when provided.').optional(),
});
export type PipelineNodeTopology = z.infer<typeof PipelineNodeTopologySchema>;
export type PipelineNode = {
id: string;
actorId: string;
personaId: string;
constraints?: PipelineConstraint;
topology?: PipelineNodeTopology;
};
export const PipelineNodeSchema = z.object({
id: z.string().trim().min(1),
actorId: z.string().trim().min(1),
personaId: z.string().trim().min(1),
constraints: PipelineConstraintSchema.optional(),
topology: PipelineNodeTopologySchema.optional(),
});
export type PipelineNode = z.infer<typeof PipelineNodeSchema>;
export type PipelineEdge = {
from: string;
to: string;
on?: "success" | "validation_fail" | "failure" | "always";
event?: DomainEventType;
when?: RouteCondition[];
};
export const PipelineEdgeSchema = z.object({
from: z.string().trim().min(1),
to: z.string().trim().min(1),
on: z.enum(["success", "validation_fail", "failure", "always"]).optional(),
event: z.string().refine((val): val is DomainEventType => isDomainEventType(val), {
message: "Pipeline edge field 'event' has unsupported domain event.",
}).optional(),
when: z.array(RouteConditionSchema).optional(),
}).refine((data) => {
if (!data.on && !data.event) return false;
if (data.on && data.event) return false;
return true;
}, {
message: 'Pipeline edge must provide either an "on" trigger or an "event" trigger, but not both.',
});
export type PipelineEdge = z.infer<typeof PipelineEdgeSchema>;
export type PipelineGraph = {
entryNodeId: string;
nodes: PipelineNode[];
edges: PipelineEdge[];
};
export const PipelineGraphSchema = z.object({
entryNodeId: z.string().trim().min(1),
nodes: z.array(PipelineNodeSchema).min(1, "Manifest pipeline.nodes must be a non-empty array."),
edges: z.array(PipelineEdgeSchema),
});
export type PipelineGraph = z.infer<typeof PipelineGraphSchema>;
export type TopologyKind = "hierarchical" | "parallel" | "retry-unrolled" | "sequential";
export const TopologyKindSchema = z.enum(["hierarchical", "parallel", "retry-unrolled", "sequential"]);
export type TopologyKind = z.infer<typeof TopologyKindSchema>;
export type TopologyConstraint = {
maxDepth: number;
maxRetries: number;
};
export const TopologyConstraintSchema = z.object({
maxDepth: z.number().int().min(1).default(4),
maxRetries: z.number().int().min(0).default(2),
});
export type TopologyConstraint = z.infer<typeof TopologyConstraintSchema>;
export type AgentManifest = {
schemaVersion: "1";
topologies: TopologyKind[];
personas: ManifestPersona[];
relationships: RelationshipEdge[];
pipeline: PipelineGraph;
topologyConstraints: TopologyConstraint;
};
function readString(record: Record<string, unknown>, key: string): string {
const value = record[key];
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`Manifest field \"${key}\" must be a non-empty string.`);
}
return value.trim();
}
function readOptionalInteger(
record: Record<string, unknown>,
key: string,
input: {
min: number;
},
): number | undefined {
const value = record[key];
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isInteger(value) || value < input.min) {
throw new Error(`Manifest field \"${key}\" must be an integer >= ${String(input.min)}.`);
}
return value;
}
function readStringArray(record: Record<string, unknown>, key: string): string[] {
const value = record[key];
if (!Array.isArray(value)) {
throw new Error(`Manifest field \"${key}\" must be an array.`);
}
const output: string[] = [];
for (const item of value) {
if (typeof item !== "string" || item.trim().length === 0) {
throw new Error(`Manifest field \"${key}\" contains an invalid string.`);
}
output.push(item.trim());
}
return output;
}
function parseToolClearance(value: unknown): ToolClearancePolicy {
try {
return parseToolClearancePolicy(value);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`Manifest persona toolClearance is invalid: ${detail}`);
}
}
function parsePersona(value: unknown): ManifestPersona {
if (!isRecord(value)) {
throw new Error("Manifest persona entry must be an object.");
}
const modelConstraintRaw = value.modelConstraint;
if (
modelConstraintRaw !== undefined &&
(typeof modelConstraintRaw !== "string" || modelConstraintRaw.trim().length === 0)
) {
throw new Error('Manifest persona field "modelConstraint" must be a non-empty string when provided.');
}
return {
id: readString(value, "id"),
displayName: readString(value, "displayName"),
systemPromptTemplate: readString(value, "systemPromptTemplate"),
...(typeof modelConstraintRaw === "string"
? { modelConstraint: modelConstraintRaw.trim() }
: {}),
toolClearance: parseToolClearance(value.toolClearance),
};
}
function parseRelationship(value: unknown): RelationshipEdge {
if (!isRecord(value)) {
throw new Error("Manifest relationship entry must be an object.");
}
const constraints = isRecord(value.constraints)
? {
maxDepth: readOptionalInteger(value.constraints, "maxDepth", { min: 1 }),
maxChildren: readOptionalInteger(value.constraints, "maxChildren", { min: 1 }),
}
: undefined;
return {
parentPersonaId: readString(value, "parentPersonaId"),
childPersonaId: readString(value, "childPersonaId"),
constraints,
};
}
function parseCondition(value: unknown): RouteCondition {
if (!isRecord(value)) {
throw new Error("Route condition must be an object.");
}
const type = readString(value, "type");
if (type === "always") {
return { type };
}
if (type === "state_flag") {
const key = readString(value, "key");
const equals = value.equals;
if (typeof equals !== "boolean") {
throw new Error('Route condition field "equals" must be a boolean.');
}
return {
type,
key,
equals,
};
}
if (type === "history_has_event") {
return {
type,
event: readString(value, "event"),
};
}
if (type === "file_exists") {
return {
type,
path: readString(value, "path"),
};
}
throw new Error(`Unsupported route condition type \"${type}\".`);
}
function parsePipelineNode(value: unknown): PipelineNode {
if (!isRecord(value)) {
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 }),
}
: undefined;
return {
id: readString(value, "id"),
actorId: readString(value, "actorId"),
personaId: readString(value, "personaId"),
constraints,
...(parsedTopology ? { topology: parsedTopology } : {}),
};
}
function parsePipelineEdge(value: unknown): PipelineEdge {
if (!isRecord(value)) {
throw new Error("Pipeline edge must be an object.");
}
const validEvents: NonNullable<PipelineEdge["on"]>[] = [
"success",
"validation_fail",
"failure",
"always",
];
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;
const when: RouteCondition[] = [];
if (rawWhen !== undefined) {
if (!Array.isArray(rawWhen)) {
throw new Error('Pipeline edge field "when" must be an array when provided.');
}
for (const condition of rawWhen) {
when.push(parseCondition(condition));
}
}
return {
from: readString(value, "from"),
to: readString(value, "to"),
...(on ? { on } : {}),
...(event ? { event } : {}),
...(when.length > 0 ? { when } : {}),
};
}
function parsePipeline(value: unknown): PipelineGraph {
if (!isRecord(value)) {
throw new Error("Manifest pipeline must be an object.");
}
const nodesValue = value.nodes;
if (!Array.isArray(nodesValue) || nodesValue.length === 0) {
throw new Error("Manifest pipeline.nodes must be a non-empty array.");
}
const edgesValue = value.edges;
if (!Array.isArray(edgesValue)) {
throw new Error("Manifest pipeline.edges must be an array.");
}
const nodes = nodesValue.map(parsePipelineNode);
const edges = edgesValue.map(parsePipelineEdge);
return {
entryNodeId: readString(value, "entryNodeId"),
nodes,
edges,
};
}
function parseTopologies(value: unknown): TopologyKind[] {
if (!Array.isArray(value) || value.length === 0) {
throw new Error("Manifest topologies must be a non-empty array.");
}
const valid = new Set<TopologyKind>(["hierarchical", "parallel", "retry-unrolled", "sequential"]);
const result: TopologyKind[] = [];
for (const item of value) {
if (typeof item !== "string" || !valid.has(item as TopologyKind)) {
throw new Error("Manifest topologies contains an unsupported topology kind.");
}
result.push(item as TopologyKind);
}
return result;
}
function parseTopologyConstraints(value: unknown): TopologyConstraint {
if (!isRecord(value)) {
throw new Error("Manifest topologyConstraints must be an object.");
}
const maxDepth = readOptionalInteger(value, "maxDepth", { min: 1 });
const maxRetries = readOptionalInteger(value, "maxRetries", { min: 0 });
return {
maxDepth: maxDepth ?? 4,
maxRetries: maxRetries ?? 2,
};
}
export const AgentManifestSchema = z.object({
schemaVersion: z.literal("1"),
topologies: z.array(TopologyKindSchema).min(1, "Manifest topologies must be a non-empty array."),
personas: z.array(ManifestPersonaSchema).min(1, "Manifest personas must be a non-empty array."),
relationships: z.array(RelationshipEdgeSchema),
pipeline: PipelineGraphSchema,
topologyConstraints: TopologyConstraintSchema,
});
export type AgentManifest = z.infer<typeof AgentManifestSchema>;
function assertNoDuplicates(items: string[], label: string): void {
const seen = new Set<string>();
for (const item of items) {
if (seen.has(item)) {
throw new Error(`${label} contains duplicate id \"${item}\".`);
throw new Error(`${label} contains duplicate id "${item}".`);
}
seen.add(item);
}
@@ -409,20 +124,20 @@ function assertPipelineDag(pipeline: PipelineGraph): void {
}
if (!nodeIds.has(pipeline.entryNodeId)) {
throw new Error(`Pipeline entry node \"${pipeline.entryNodeId}\" is not defined.`);
throw new Error(`Pipeline entry node "${pipeline.entryNodeId}" is not defined.`);
}
for (const edge of pipeline.edges) {
if (!nodeIds.has(edge.from)) {
throw new Error(`Pipeline edge references unknown from node \"${edge.from}\".`);
throw new Error(`Pipeline edge references unknown from node "${edge.from}".`);
}
if (!nodeIds.has(edge.to)) {
throw new Error(`Pipeline edge references unknown to node \"${edge.to}\".`);
throw new Error(`Pipeline edge references unknown to node "${edge.to}".`);
}
const neighbors = adjacency.get(edge.from);
if (!neighbors) {
throw new Error(`Internal DAG error for node \"${edge.from}\".`);
throw new Error(`Internal DAG error for node "${edge.from}".`);
}
neighbors.push(edge.to);
const currentInDegree = indegree.get(edge.to);
@@ -503,34 +218,16 @@ function assertRelationshipDag(relationships: RelationshipEdge[]): void {
}
export function parseAgentManifest(input: unknown): AgentManifest {
if (!isRecord(input)) {
throw new Error("AgentManifest must be an object.");
let manifest: AgentManifest;
try {
manifest = AgentManifestSchema.parse(input);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error("Manifest invalid: " + error.issues.map((e: any) => e.message).join(", "));
}
throw error;
}
const schemaVersion = readString(input, "schemaVersion");
if (schemaVersion !== "1") {
throw new Error(`Unsupported AgentManifest schemaVersion \"${schemaVersion}\".`);
}
const personasValue = input.personas;
if (!Array.isArray(personasValue) || personasValue.length === 0) {
throw new Error("Manifest personas must be a non-empty array.");
}
const relationshipsValue = input.relationships;
if (!Array.isArray(relationshipsValue)) {
throw new Error("Manifest relationships must be an array.");
}
const manifest: AgentManifest = {
schemaVersion: "1",
topologies: parseTopologies(input.topologies),
personas: personasValue.map(parsePersona),
relationships: relationshipsValue.map(parseRelationship),
pipeline: parsePipeline(input.pipeline),
topologyConstraints: parseTopologyConstraints(input.topologyConstraints),
};
assertNoDuplicates(
manifest.personas.map((persona) => persona.id),
"Manifest personas",
@@ -545,12 +242,12 @@ export function parseAgentManifest(input: unknown): AgentManifest {
for (const relation of manifest.relationships) {
if (!personaIds.has(relation.parentPersonaId)) {
throw new Error(
`Relationship references unknown parent persona \"${relation.parentPersonaId}\".`,
`Relationship references unknown parent persona "${relation.parentPersonaId}".`,
);
}
if (!personaIds.has(relation.childPersonaId)) {
throw new Error(
`Relationship references unknown child persona \"${relation.childPersonaId}\".`,
`Relationship references unknown child persona "${relation.childPersonaId}".`,
);
}
}
@@ -559,7 +256,7 @@ export function parseAgentManifest(input: unknown): AgentManifest {
for (const node of manifest.pipeline.nodes) {
if (!personaIds.has(node.personaId)) {
throw new Error(`Pipeline node \"${node.id}\" references unknown persona \"${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)) {