Skip to content

Core concepts

Five ideas explain everything GraphMind does. None of them is deep, but the instance one and the gate one are where the design earns its keep.

A run is one top-level agent invocation — one gm.run('handle-ticket', …), one request, one job. It has an id, an app name, a start and end time, a status (ok, error, aborted), and everything that happened inside it.

Runs are established with AsyncLocalStorage, so every event your agent emits — however many awaits and callbacks deep — is attributed to the right run automatically. Concurrent runs in the same process stay cleanly separate.

You do not have to open a run. Events emitted outside one are collected into a single implicit run so nothing is lost. But naming your runs is what makes a run list readable a week later, and an explicit run is what carries the AbortController the debugger’s abort action uses.

This is the distinction that makes big graphs readable.

  • A node is a place in your code. Its nodeId is stable: tool:searchFlights, llm:step, agent:handle-ticket. Every execution of that code location maps to the same node.
  • An instance is one execution of that node. Its instanceId is unique per execution — a tool call id, a step index, a run id.

The canvas draws one node per logical node, and instances light it up. An agent that loops searchFlights eleven times gives you one node with eleven instances, not eleven boxes. This is the “graph is a projection of code” thesis, and it is why GraphMind stays legible on runs where a span-per-call view turns into confetti.

NodenodeIdinstanceId
Agent (a run)agent:<runName>the run id
Model stepllm:step<invocationId>:s<N>
Tool calltool:<toolName>the tool call id

Node kinds are agent, llm, tool, chain, retriever, server, resource, prompt and custom. The extra kinds exist so a run renders as itself rather than as generic boxes: chain / retriever for LangChain-shaped runs, and server / resource / prompt for MCP servers. MCP tool calls and sampling reuse tool and llm — they are the same concepts.

GraphMind does not ask you to declare a graph. It infers one from what actually executes, and adapters can pre-announce structure with a graph.hint event — the Vercel AI SDK adapter sends the whole tool roster on the first model step, so you see every tool greyed out before anything runs, then watch them come alive.

The consequence: the graph you look at is the code you wrote, not a parallel abstraction you have to keep in sync. Delete a tool and it disappears from the graph on the next run.

A gate is a cooperative pause point the adapter awaits. Each one sits at a point in a node’s lifecycle:

PointWhereFires when
beforeBefore the node’s work startsA breakpoint matches, or step mode is on
afterAfter work completes, before the value returnsStep mode
errorThe node threw, before the framework sees itAlways, by default
callbefore gateexecuteafter gatereturnerror gatethrows

While a gate is held, nothing is in flight: the adapter awaits the gate before it calls the provider or the tool, so a hold can last as long as you like. No socket is sitting open burning a timeout, and no partial request is waiting to be finished.

The viewer releases a gate with one of four actions:

ActionWhat the adapter does
continueProceed normally. At an error gate, rethrow the original error.
retryRe-run the node’s execution from the top.
injectSkip execution (or swallow the error) and use the value you supplied.
abortAbort the run’s AbortController — terminal, never retried by the SDK.

Gates are independent. Two concurrent tool calls hold two separate pauses, each resumable on its own.

GraphMind is instrumentation that ships in your codebase, so it is built to be harmless:

  • Disabled (GRAPHMIND_DISABLED=1, or NODE_ENV=production without GRAPHMIND=1): the wrappers are identity functions. No sockets, no buffering, no warnings — but gm.run still runs your function and still hands it a working context, so your code never has to branch.
  • Enabled but detached: gates take a fast path to a shared resolved promise (average under 1 ms in the test suite), and events go only to an in-memory ring buffer.
  • Attached, then disconnected: every held gate is released with continue immediately — measured under 100 ms — and breakpoints are forgotten until a new handshake re-arms them.
  • Internally broken: the session catches its own errors, degrades to a no-op, and logs one rate-limited warning. It never throws into your app. Errors thrown by your function inside gm.run are yours and propagate untouched.

Events are kept in a bounded ring buffer (2000 by default, drop-oldest). When a viewer attaches mid-run, the whole buffer is replayed oldest-first with the original sequence numbers, so you see the history you missed and the viewer deduplicates on (runId, seq) across reconnects.

Practically: you can start a long agent, notice it misbehaving, launch the debugger, and still get the graph from the beginning.

The server binds 127.0.0.1 and has no auth — it is a devtool, not a service; never expose the port. Runs are persisted to SQLite via node:sqlite (built into Node ≥ 22.13, zero native dependencies) at ~/.graphmind/graphmind.db, in WAL mode, keyed (run_id, seq).

Your prompts, tool arguments and outputs stay on your machine. The only thing that leaves is an anonymous telemetry ping naming the command you ran — one line to opt out of.

Because history is a plain local database, it stays readable when the viewer is closed: the MCP server queries it directly, so your coding agent can pull up the run that failed and reason about it with you.