/** * 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; 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; 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; 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(); const adjList = new Map(); 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"); }