AUDIT / src /lib /agent /TaskGraph.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
7.71 kB
/**
* TaskGraph.ts — Directed Acyclic Graph for agent task orchestration (FASE 4)
*
* Replaces the monolithic prompt with:
* Planner → Task Graph → Executors → Verifier → Memory Update
*
* Supports:
* - Dependency-based execution ordering (topological sort)
* - Parallel execution of independent nodes
* - Node status tracking (pending → ready → running → done/failed/skipped)
*/
export type NodeType =
| "plan" // Meta-node representing the overall goal
| "tool_call" // Execute a specific tool
| "verify" // Verify a tool result
| "synthesize" // Aggregate results into final answer
| "memory"; // Persist findings to memory
export type NodeStatus =
| "pending" // Waiting for deps
| "ready" // All deps done, can execute
| "running" // Currently executing
| "done" // Completed successfully
| "failed" // Execution failed
| "skipped"; // Skipped due to upstream failure
export interface TaskNode {
id: string;
type: NodeType;
label: string;
tool?: string;
input?: Record<string, unknown>;
deps: string[]; // IDs of nodes that must complete before this
status: NodeStatus;
result?: string;
error?: string;
startedAt?: number;
finishedAt?: number;
priority?: number; // Fix 7 (S421): posizione nel piano strutturato (basso = prima)
}
export interface TaskEdge {
from: string;
to: string;
}
export interface TaskGraph {
id: string;
goal: string;
nodes: Map<string, TaskNode>;
edges: TaskEdge[];
createdAt: number;
}
// ─── Graph builder ────────────────────────────────────────────────────────────
let _nodeCounter = 0;
function nodeId(prefix = "n"): string {
return `${prefix}-${Date.now()}-${++_nodeCounter}`;
}
export interface GraphSpec {
goal: string;
steps: Array<{
tool: string;
label: string;
input?: Record<string, unknown>;
dependsOn?: number[]; // 0-based indices of earlier steps
verify?: boolean;
priority?: number; // Fix 7 (S421): posizione nel piano strutturato (basso = prima)
}>;
synthesize?: boolean;
memorize?: boolean;
}
export function buildGraph(spec: GraphSpec): TaskGraph {
const graph: TaskGraph = {
id: nodeId("graph"),
goal: spec.goal,
nodes: new Map(),
edges: [],
createdAt: Date.now(),
};
// Plan node (root)
const planNode: TaskNode = {
id: "plan-root",
type: "plan",
label: `Goal: ${spec.goal.slice(0, 80)}`,
deps: [],
status: "done", // plan is already done (we are the planner)
result: spec.goal,
};
graph.nodes.set(planNode.id, planNode);
// Tool call nodes
const stepIds: string[] = [];
for (const [i, step] of spec.steps.entries()) {
const id = nodeId("tool");
stepIds.push(id);
const deps = step.dependsOn
? step.dependsOn.map(idx => stepIds[idx]).filter(Boolean)
: i === 0
? ["plan-root"]
: [stepIds[i - 1]];
const node: TaskNode = {
id,
type: "tool_call",
label: step.label,
tool: step.tool,
input: step.input ?? {},
deps,
status: "pending",
priority: step.priority, // Fix 7 (S421): propaga dal piano strutturato
};
graph.nodes.set(id, node);
for (const dep of deps) {
graph.edges.push({ from: dep, to: id });
}
// Optional verify node
if (step.verify) {
const vid = nodeId("verify");
const vnode: TaskNode = {
id: vid,
type: "verify",
label: `Verifica: ${step.label}`,
tool: step.tool,
deps: [id],
status: "pending",
};
graph.nodes.set(vid, vnode);
graph.edges.push({ from: id, to: vid });
// Replace the tool step id in stepIds so subsequent deps point to the verify node
stepIds[i] = vid;
}
}
// Memory node (optional)
let lastLayerIds = stepIds.slice(-3);
if (spec.memorize) {
const mid = nodeId("mem");
const mnode: TaskNode = {
id: mid,
type: "memory",
label: "Memorizza risultati",
deps: lastLayerIds,
status: "pending",
};
graph.nodes.set(mid, mnode);
for (const dep of lastLayerIds) {
graph.edges.push({ from: dep, to: mid });
}
lastLayerIds = [mid];
}
// Synthesize node (always last)
if (spec.synthesize !== false) {
const sid = nodeId("synth");
const snode: TaskNode = {
id: sid,
type: "synthesize",
label: "Sintetizza e rispondi",
deps: lastLayerIds,
status: "pending",
};
graph.nodes.set(sid, snode);
for (const dep of lastLayerIds) {
graph.edges.push({ from: dep, to: sid });
}
}
return graph;
}
// ─── Graph utilities ──────────────────────────────────────────────────────────
/** Returns nodes in parallel layers (each layer can execute concurrently) */
export function topologicalLayers(graph: TaskGraph): string[][] {
const inDegree = new Map<string, number>();
const adjList = new Map<string, string[]>();
for (const node of graph.nodes.values()) {
if (!inDegree.has(node.id)) inDegree.set(node.id, 0);
adjList.set(node.id, []);
}
for (const edge of graph.edges) {
adjList.get(edge.from)?.push(edge.to);
inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
}
const layers: string[][] = [];
let current = [...inDegree.entries()].filter(([, d]) => d === 0).map(([id]) => id);
while (current.length > 0) {
layers.push(current);
const next: string[] = [];
for (const id of current) {
for (const child of adjList.get(id) ?? []) {
const deg = (inDegree.get(child) ?? 1) - 1;
inDegree.set(child, deg);
if (deg === 0) next.push(child);
}
}
current = next;
}
return layers;
}
/** Returns all nodes whose deps are done and status is still pending */
export function getReadyNodes(graph: TaskGraph): TaskNode[] {
const ready: TaskNode[] = [];
for (const node of graph.nodes.values()) {
if (node.status !== "pending") continue;
const depsOk = node.deps.every(dep => {
const d = graph.nodes.get(dep);
return d?.status === "done" || d?.status === "skipped";
});
if (depsOk) ready.push(node);
}
return ready;
}
/** Mark a node and propagate skip to dependents if it failed */
export function markNode(graph: TaskGraph, id: string, status: NodeStatus, result?: string, error?: string) {
const node = graph.nodes.get(id);
if (!node) return;
node.status = status;
if (result !== undefined) node.result = result;
if (error !== undefined) node.error = error;
node.finishedAt = Date.now();
if (status === "failed") {
// Skip all transitive dependents
const skip = (nid: string) => {
for (const n of graph.nodes.values()) {
if (n.deps.includes(nid) && (n.status === "pending" || n.status === "ready")) {
n.status = "skipped";
n.error = `Saltato: dipendenza "${nid}" fallita`;
skip(n.id);
}
}
};
skip(id);
}
}
export function graphDone(graph: TaskGraph): boolean {
return [...graph.nodes.values()].every(
n => n.status === "done" || n.status === "failed" || n.status === "skipped",
);
}
export function collectResults(graph: TaskGraph): string {
const results: string[] = [];
for (const node of graph.nodes.values()) {
if (node.type === "tool_call" && node.result) {
results.push(`[${node.label}]\n${node.result}`);
}
}
return results.join("\n\n---\n\n");
}