first commit

This commit is contained in:
2026-02-23 12:06:13 -05:00
commit 53af0d44cd
33 changed files with 6483 additions and 0 deletions

506
src/agents/manifest.ts Normal file
View File

@@ -0,0 +1,506 @@
import { isRecord } from "./types.js";
export type ToolClearancePolicy = {
allowlist: string[];
banlist: string[];
};
export type ManifestPersona = {
id: string;
displayName: string;
systemPromptTemplate: string;
toolClearance: ToolClearancePolicy;
};
export type RelationshipConstraint = {
maxDepth?: number;
maxChildren?: number;
};
export type RelationshipEdge = {
parentPersonaId: string;
childPersonaId: string;
constraints?: RelationshipConstraint;
};
export type RouteCondition =
| {
type: "always";
}
| {
type: "state_flag";
key: string;
equals: boolean;
}
| {
type: "history_has_event";
event: string;
}
| {
type: "file_exists";
path: string;
};
export type PipelineConstraint = {
maxRetries?: number;
};
export type PipelineNode = {
id: string;
actorId: string;
personaId: string;
constraints?: PipelineConstraint;
};
export type PipelineEdge = {
from: string;
to: string;
on:
| "success"
| "validation_fail"
| "failure"
| "always"
| "onTaskComplete"
| "onValidationFail";
when?: RouteCondition[];
};
export type PipelineGraph = {
entryNodeId: string;
nodes: PipelineNode[];
edges: PipelineEdge[];
};
export type TopologyKind = "hierarchical" | "retry-unrolled" | "sequential";
export type TopologyConstraint = {
maxDepth: number;
maxRetries: number;
};
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 {
if (!isRecord(value)) {
throw new Error("Manifest persona toolClearance must be an object.");
}
return {
allowlist: readStringArray(value, "allowlist"),
banlist: readStringArray(value, "banlist"),
};
}
function parsePersona(value: unknown): ManifestPersona {
if (!isRecord(value)) {
throw new Error("Manifest persona entry must be an object.");
}
return {
id: readString(value, "id"),
displayName: readString(value, "displayName"),
systemPromptTemplate: readString(value, "systemPromptTemplate"),
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 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,
};
}
function parsePipelineEdge(value: unknown): PipelineEdge {
if (!isRecord(value)) {
throw new Error("Pipeline edge must be an object.");
}
const on = readString(value, "on");
const validEvents: PipelineEdge["on"][] = [
"success",
"validation_fail",
"failure",
"always",
"onTaskComplete",
"onValidationFail",
];
if (!validEvents.includes(on as PipelineEdge["on"])) {
throw new Error(`Pipeline edge field \"on\" has unsupported event \"${on}\".`);
}
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 as PipelineEdge["on"],
...(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", "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,
};
}
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}\".`);
}
seen.add(item);
}
}
function assertPipelineDag(pipeline: PipelineGraph): void {
const adjacency = new Map<string, string[]>();
const indegree = new Map<string, number>();
const nodeIds = new Set<string>(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<string, string[]>();
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<string>();
const visited = new Set<string>();
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 {
if (!isRecord(input)) {
throw new Error("AgentManifest must be an object.");
}
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",
);
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}\".`);
}
}
assertPipelineDag(manifest.pipeline);
return manifest;
}