her / ui /src /dataflow.js
geekwrestler's picture
Squash history (purge pre-scrub demo session blobs)
5f43c7d
// 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 };
}