Skip to content

Big & long-running graphs

Toy agents are easy to look at. The runs you actually need to debug are 40 model steps, 200 tool calls, and a fan-out that would draw a hairball in any span-per-call view.

One node per logical node. A tool called 200 times is one box with 200 instances, not 200 boxes. Graph size scales with the size of your code, not the length of the run — a 40-step loop over six tools draws about seven nodes whether it runs once or a hundred times.

That single decision is why GraphMind stays readable where a trace waterfall does not. Select the node, then scroll its instances.

Nodes appear as they are reached and light up while executing, so the canvas is a status display rather than a wall of text. The important motion — what is running now, what just failed — is what your eye is drawn to.

The command palette (K) jumps to any node by name, and carries every view action. Collapse all (C) folds sub-agents and chains into summary cards; the error-path filter (E) dims everything that is not an ancestor of a failure; the timeline (T) shows what actually overlapped and where the wall clock went. See the viewer.

Pause-on-error is your filter. In a 200-step run you do not want to watch; you want to be told. Leave run mode on with only the error breakpoint armed and let the agent stop itself.

The client keeps a bounded ring buffer of events (2000 by default, drop-oldest). Attach a viewer mid-run and the whole buffer replays oldest-first with the original sequence numbers — you get the history you missed, and the viewer deduplicates on (runId, seq).

For a genuinely long run, raise the buffer so nothing is dropped before you get there:

const gm = graphmind({ app: 'nightly-batch', bufferSize: 20_000 });

The buffer only governs what a late viewer can replay — once a viewer is attached, events are persisted to SQLite as they arrive and are never lost.

Holding a gate costs nothing structurally — the adapter awaits before it starts the work, so no connection is open and no partial request is waiting. Two practical guards:

  • SDK timeouts are neutralised while a debugger is attached (adapters chain rather than replace abort signals, and filter timeout-driven aborts). There are limits with the Vercel AI SDK’s outer loop — see the details — so prefer removing timeout configs while debugging.

  • pauseTimeoutMs auto-continues a gate nobody resumed. Unset by default (hold forever). Set it when you leave an agent running under a debugger unattended:

    const gm = graphmind({ app: 'nightly-batch', pauseTimeoutMs: 5 * 60_000 });

Concurrent tool calls hold independent gates. Ten parallel calls that all break give you ten separate pauses, each with its own state and its own resume — you are not forced into one global “paused” mode where resuming one resumes all.

For that to attribute correctly, every event must carry its instanceId. The adapters in this repo do; if you are writing one, do not skip it — recency-based attribution mis-assigns results exactly in the concurrent case you most want to debug.

A tool that returns a megabyte of JSON makes for a slow inspector and an expensive agent (that blob is re-sent to the model on every subsequent step). Two options:

  • Summarise at the source. Usually the right fix — the model did not want the megabyte either, and your token bill agrees.
  • Let the inspector do the work. Payloads render as a collapsible tree, so a large object stays navigable; the MCP tools compact payloads to a preview cap for the same reason.

The run list is most-recent-first, with per-run status, source (live / import / demo), event count and error count, so a bad run stands out without opening it. Name your runs (gm.run('nightly-batch', …)) and they stay identifiable a week later.

To pull one out of the pile programmatically, the MCP tools list_runs and find_errors answer “which recent run broke, and where” without opening the viewer at all.