/ ENGINEERING CASE

Engineering case · Agent Graph Inspector

A multi-agent run, read as a graph and not as a log

A multi-agent run leaves one transcript per agent and a single wall-clock number. This tool turns that journal into one HTML page where the timings, the bottleneck and the missing agents are visible and checkable.

Python 3.9+No dependenciesOne file in, one page outMIT

01 PROBLEM

A multi-agent run leaves a journal, not a picture. You get N transcripts and a total time. You cannot see what actually ran in parallel, which stage set the total, or whether the step that fans in heard back from everyone it fanned out to.

The last one is the dangerous one. If some agents start and never record a result, the run's summary still reads as finished and the output is quietly incomplete. In a flat log those agents are just missing lines. A run I found on my own machine had 108 agents started and 33 with no result: a session limit.

02 INPUT AND OUTPUT CONTRACT

Input is one Workflow run directory (wf_*). build_graph.py reads three kinds of files from it:

Output is one self-contained HTML file with three linked views: chain against graph on one scale, a wall-clock timeline, and the execution map. --json also dumps the parsed graph as nodes, edges, meta and domains. Each node carries start, end, dur, layer, model, the question and the output.

python3 build_graph.py --list                        # runs found on this machine
python3 build_graph.py <run-dir> -o inspector.html   # build the page

Two rules keep it honest. A field the run did not record stays empty. And a node with a started row but no result row gets failed and is drawn dashed. One catch on the way: a model message is written to a transcript twice with different uuids, so a naive token sum is exactly double. The parser keeps one usage record per message id.

03 ONE HARD DECISION

The journal says who started and who finished. It does not say who fed whom. So the edges have to come from somewhere. The alternative was to make the user describe the topology by hand. The tool supports that (--groups), but the default is to infer it from timestamps, because a tool that needs a hand-drawn graph before it shows anything does not get used.

The inference is one rule: a node moves to the next layer if it started after the whole previous layer had finished.

layer, horizon = 1, None
for node in sorted(nodes, key=lambda n: n["start"]):
    if horizon is not None and node["start"] >= horizon - 0.5:
        layer += 1
        horizon = None
    node["layer"] = layer
    horizon = node["end"] if horizon is None else max(horizon, node["end"])

This matches parallel() barriers and pipeline stages. It is a heuristic, and the tool says so in two places: a run whose stages overlap is split differently than its script reads, and the page footer states topology inferred or topology manual, so a reader always knows which one they are looking at.

The critical path follows from it. Start at the node that finished last and walk back through the predecessor that finished last. In the demo that chain is 3 nodes, 59.4 s of the 84.7 s wall-clock. Speed-up is defined plainly as the sum of durations over wall-clock.

04 REPRODUCIBLE CHECK

The repo holds no raw journals, because they contain prompts and answers verbatim. fixtures/run-9nodes-reference.json holds the parsed 9-agent reference run, and demo.html embeds the parsed 25-agent run. The script below recomputes the totals from the stored per-node timings and re-runs the repo's own infer_layers to compare with the stored layers.

import json, sys
sys.path.insert(0, ".")
import build_graph as b

def stats(nodes):
    seq = sum(x["dur"] for x in nodes)
    wall = max(x["end"] for x in nodes) - min(x["start"] for x in nodes)
    return round(seq, 1), round(wall, 1), round(seq / wall, 2)

ref = json.load(open("fixtures/run-9nodes-reference.json"))
n = ref["nodes"]; stored = [x["layer"] for x in n]
print("9 nodes ", stats(n), "layers re-inferred == stored:",
      (b.infer_layers(n) and [x["layer"] for x in n] == stored))

html = open("demo.html").read()
i = html.index("const DATA = ") + len("const DATA = ")
demo, _ = json.JSONDecoder().raw_decode(html[i:])
n = demo["nodes"]; stored = [x["layer"] for x in n]
print("25 nodes", stats(n), "layers re-inferred == stored:",
      (b.infer_layers(n) and [x["layer"] for x in n] == stored))

Run from the repo root at commit a2ee054 (Python 3.14.8), it prints:

9 nodes  (99.4, 52.7, 1.89) layers re-inferred == stored: True
25 nodes (392.1, 84.7, 4.63) layers re-inferred == stored: True
RunAs a chainAs a graphSpeed-up
9 agents99.4 s52.7 s1.89x
25 agents392.1 s84.7 s4.63x

What this proves: the arithmetic and the layering. What it does not: the journal parser itself. The parser was checked against the raw journals when I wrote it, and I did not rerun that for this page.

05 LIMITS

06 WHO DID WHAT

I designed and built it with AI-assisted coding. The decisions, the verification and the reference numbers are mine.

Hiring for AI or backend work, or have a project in mind?