File size: 7,713 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/**
 * 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");
}