File size: 4,311 Bytes
5f43c7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Deterministic dataflow layout for Mode B. NO model. Pure geometry over the
// already-decided provenance the engine emitted on each ToolCall.
//
// Edge taxonomy (NON-NEGOTIABLE #4 — proven vs hypothesis ALWAYS separated):
//   - "spine"  : sequence edge query->t0->t1->... (structural, neutral grey)
//   - "proven" : value-flow cross-link. A verbatim flowValue from an earlier
//                tool_result reappeared in this tool's input. SOLID, orange.
//                Asserted by the engine (provenance:'indirect' + flowValue).
//   - "hypo"   : proximity-only link (no verbatim value pinned). DOTTED, muted.
//                A hypothesis the human judges; never rendered as fact.
//
// We resolve a proven cross-link to a concrete *source node* by walking backward
// to the most recent earlier tool whose name == sourceTool. If none is found we
// downgrade to a proximity hypothesis edge (dotted) rather than invent a source.

export function buildDataflow(turn) {
  const tools = turn.tools || [];

  // Node 0 is always the user query (the root of the per-turn graph).
  const nodes = [
    {
      key: "q",
      kind: "query",
      idx: -1,
      label: turn.origin === "system" ? "Agent work (system turn)" : "Your query",
      sub: (turn.prompt || "").replace(/\s+/g, " ").trim().slice(0, 90),
    },
    ...tools.map((tl, idx) => ({
      key: "t" + idx,
      kind: "tool",
      idx,
      tool: tl,
      name: tl.mcp ? `${tl.mcp.server}:${tl.mcp.tool}` : tl.name,
      rawName: tl.name,
      indirect: tl.provenance === "indirect",
      errored: !!tl.errored,
    })),
  ];

  const edges = [];

  // Spine: sequence backbone. query -> first tool, then tool i -> tool i+1.
  for (let i = 0; i < tools.length; i++) {
    edges.push({ from: i === 0 ? "q" : "t" + (i - 1), to: "t" + i, type: "spine" });
  }

  // Cross-links for indirect tools. These are DRAWN ON DEMAND (per selected/
  // hovered node) so a 49-edge turn doesn't render as a permanent tangle — the
  // UI-SPEC asks for "the highlighted causal path", i.e. focus-driven, not a
  // hairball. Each link records its consumer (`to`) and, when we can pin the
  // source within THIS turn, its producer (`from`). Cross-turn sources stay
  // `from:null` (external) — the node still shows its provenance, but we never
  // draw a misleading full-height arc back to the query root.
  for (let i = 0; i < tools.length; i++) {
    const tl = tools[i];
    if (tl.provenance !== "indirect") continue;

    // Walk backward to the most recent earlier tool matching sourceTool name.
    let srcIdx = -1;
    if (tl.sourceTool) {
      for (let j = i - 1; j >= 0; j--) {
        if (tools[j].name === tl.sourceTool) {
          srcIdx = j;
          break;
        }
      }
    }

    if (tl.flowValue && srcIdx >= 0) {
      // PROVEN value-flow: a verbatim value crossed from srcIdx's result to i's
      // input, both inside this turn. Drawable solid orange arc.
      edges.push({
        from: "t" + srcIdx,
        to: "t" + i,
        type: "proven",
        flowValue: tl.flowValue,
        sourceTool: tl.sourceTool,
      });
    } else if (tl.flowValue) {
      // Proven by the engine, but the producing tool_result lives in an EARLIER
      // turn (or no same-name node here). Record it for the detail panel/legend
      // count, but mark it external so it is NOT drawn as a long arc.
      edges.push({
        from: null,
        to: "t" + i,
        type: "proven",
        flowValue: tl.flowValue,
        sourceTool: tl.sourceTool,
        external: true,
      });
    } else {
      // No verbatim value pinned -> proximity HYPOTHESIS only. Dotted, and only
      // drawn when its consumer node is focused.
      edges.push({ from: srcIdx >= 0 ? "t" + srcIdx : null, to: "t" + i, type: "hypo", sourceTool: tl.sourceTool, external: srcIdx < 0 });
    }
  }

  return { nodes, edges };
}

// Stats for the legend / header strip of a turn graph.
export function turnEntityCounts(turn) {
  const tools = turn.tools || [];
  const proven = tools.filter((t) => t.provenance === "indirect" && t.flowValue).length;
  const direct = tools.filter((t) => t.provenance === "direct").length;
  const errored = tools.filter((t) => t.errored).length;
  return { proven, direct, errored, total: tools.length };
}