Spaces:
Running on Zero
Running on Zero
| // 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 }; | |
| } | |