import { z } from "zod"; import { isDomainEventType, type DomainEventType } from "./domain-events.js"; import { toolClearancePolicySchema, type ToolClearancePolicy as SecurityToolClearancePolicy, } from "../security/schemas.js"; export type ToolClearancePolicy = SecurityToolClearancePolicy; 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; 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; 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; 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; 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; export const NodeTopologyKindSchema = z.enum(["sequential", "parallel", "hierarchical", "retry-unrolled"]); export type NodeTopologyKind = z.infer; 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; 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; 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; 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; export const TopologyKindSchema = z.enum(["hierarchical", "parallel", "retry-unrolled", "sequential"]); export type TopologyKind = z.infer; 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; 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; function assertNoDuplicates(items: string[], label: string): void { const seen = new Set(); for (const item of items) { if (seen.has(item)) { throw new Error(`${label} contains duplicate id "${item}".`); } seen.add(item); } } function assertPipelineDag(pipeline: PipelineGraph): void { const adjacency = new Map(); const indegree = new Map(); const nodeIds = new Set(pipeline.nodes.map((node) => node.id)); for (const node of pipeline.nodes) { adjacency.set(node.id, []); indegree.set(node.id, 0); } if (!nodeIds.has(pipeline.entryNodeId)) { 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}".`); } if (!nodeIds.has(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}".`); } neighbors.push(edge.to); const currentInDegree = indegree.get(edge.to); indegree.set(edge.to, (currentInDegree ?? 0) + 1); } const queue: string[] = []; for (const [nodeId, degree] of indegree.entries()) { if (degree === 0) { queue.push(nodeId); } } let visited = 0; for (let cursor = 0; cursor < queue.length; cursor += 1) { const current = queue[cursor]; if (!current) { continue; } visited += 1; const neighbors = adjacency.get(current) ?? []; for (const neighbor of neighbors) { const degree = indegree.get(neighbor); if (degree === undefined) { continue; } const nextDegree = degree - 1; indegree.set(neighbor, nextDegree); if (nextDegree === 0) { queue.push(neighbor); } } } if (visited !== pipeline.nodes.length) { throw new Error("Pipeline graph must be a strict DAG (cycle detected)."); } } function assertRelationshipDag(relationships: RelationshipEdge[]): void { const adjacency = new Map(); for (const relationship of relationships) { const children = adjacency.get(relationship.parentPersonaId); if (children) { children.push(relationship.childPersonaId); } else { adjacency.set(relationship.parentPersonaId, [relationship.childPersonaId]); } if (!adjacency.has(relationship.childPersonaId)) { adjacency.set(relationship.childPersonaId, []); } } const visiting = new Set(); const visited = new Set(); const visit = (nodeId: string): void => { if (visiting.has(nodeId)) { throw new Error("Relationship graph must be acyclic (cycle detected)."); } if (visited.has(nodeId)) { return; } visiting.add(nodeId); for (const childId of adjacency.get(nodeId) ?? []) { visit(childId); } visiting.delete(nodeId); visited.add(nodeId); }; for (const nodeId of adjacency.keys()) { visit(nodeId); } } export function parseAgentManifest(input: unknown): AgentManifest { 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; } assertNoDuplicates( manifest.personas.map((persona) => persona.id), "Manifest personas", ); assertNoDuplicates( manifest.pipeline.nodes.map((node) => node.id), "Manifest pipeline.nodes", ); const personaIds = new Set(manifest.personas.map((persona) => persona.id)); for (const relation of manifest.relationships) { if (!personaIds.has(relation.parentPersonaId)) { throw new Error( `Relationship references unknown parent persona "${relation.parentPersonaId}".`, ); } if (!personaIds.has(relation.childPersonaId)) { throw new Error( `Relationship references unknown child persona "${relation.childPersonaId}".`, ); } } assertRelationshipDag(manifest.relationships); for (const node of manifest.pipeline.nodes) { 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); return manifest; }