Spaces:
Sleeping
Sleeping
| /** | |
| * 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"); | |
| } | |