Dashboard (Visual Studio / Graph Debugger)
The dashboard is a live visual debugger for StateGraph runs, served by ChronosOS at
/dashboard/. It lets you watch a run's graph execute, inspect per-node state, rewind
to any checkpoint (time-travel), resume a paused run, and see per-session cost — all
from a browser, with no other tooling.
os/server.go ──► /dashboard/ static UI (HTML/JS/CSS, no build step, no CDN)
──► /api/dashboard/* checkpoints · graph topology · cost · resume · time-travel
Session listing, traces, live streaming, and HITL approvals are not duplicated
under /api/dashboard/ — the UI calls the existing /api/sessions, /api/traces,
/api/agui/stream, and /api/approval/* endpoints directly.
Enabling it
The dashboard is on by default on any chronosos.Server:
s := chronosos.New(":8420", store)
// http://localhost:8420/dashboard/
Disable it on a hardened deployment with WithDashboard(false), the same pattern as
WithSwagger(false).
Wiring graph rendering and resume/time-travel
The dashboard's graph view and its resume/time-travel actions need to know which
compiled graph a session belongs to. Register your graphs, keyed by the agent id
that produced the session (storage.Session.AgentID):
s := chronosos.NewWithOptions(":8420", store,
chronosos.WithGraphs(dashboard.GraphRegistry{
"expense-approver": compiledGraph,
}),
)
Without WithGraphs, the dashboard still lists sessions, checkpoints, and cost; the
graph view and the Resume/time-travel buttons return 501 Not Implemented.
Cost reporting
Wire an already-configured engine/hooks.CostTracker (the same one you attach to your
agent's hook chain) to enable the per-session token/cost panel:
costTracker := hooks.NewCostTracker(priceTable)
// ... agent.WithHooks(hooks.Chain{costTracker, ...}) ...
s := chronosos.NewWithOptions(":8420", store,
chronosos.WithGraphs(graphs),
chronosos.WithCostTracker(costTracker),
)
Without WithCostTracker, the cost panel shows "no cost tracker configured" rather
than a fabricated $0.
Starting a run
POST /api/dashboard/runs {"agent_id": ..., "input": {...}} begins a brand-new
run against a registered graph — input is optional; the session id is always
generated by the server (never caller-supplied, to avoid a cross-tenant
existence oracle or id-squatting through a client-chosen id) and returned in
the response. This is the only way to start a run through ChronosOS's HTTP
surface itself: everywhere else
(including Resume and time-travel above), a session has to already exist because
some in-process caller created it by calling graph.Runner.Run directly, the
way examples/dashboard/ does before starting the server. A
YAML agent marked durable: true (see
YAML Agents & the Dashboard) has no such in-process
caller, so this endpoint is how its sessions get created at all.
curl -X POST http://localhost:8420/api/dashboard/runs \
-H 'Content-Type: application/json' \
-d '{"agent_id": "expense-approver", "input": {"amount": 4200}}'
Resuming a paused run
graph.Runner mirrors a run's status (running/paused/completed/failed) onto
storage.Session.Status, so the session list shows which sessions are paused without
loading a checkpoint. Clicking Resume on a paused session calls
POST /api/dashboard/resume {"session_id": ...}, which builds a fresh graph.Runner
against the registered graph and calls Resume — the same path
sdk/agent.Agent.Resume and graph.Runner.Resume use.
Time-travel
The checkpoint list (GET /api/dashboard/checkpoints?session_id=) shows every
checkpoint in a session's history. Clicking one calls
POST /api/dashboard/timetravel {"checkpoint_id": ...}, which rewinds execution to
that checkpoint's node and state and re-runs from there — graph.Runner.ResumeFromCheckpoint
under the hood. The original history is untouched; re-running from an earlier
checkpoint simply continues forward from that point.
Live updates
The graph view highlights the currently executing node by subscribing to the
standardized AG-UI stream (/api/agui/stream?session=<id>, see the AG-UI guide)
and listening for STEP_STARTED events.
Auth
Every /api/dashboard/* call goes through the same auth/tenant chain as every other
/api/ route (a session or checkpoint from another tenant is invisible, exactly like
/api/sessions and /api/traces). The static UI shell itself is served without
auth — like the Swagger UI — so a bearer token or API key can be entered from the page
before making any API call:
GET /dashboard/ → always reachable (static HTML/JS/CSS only)
GET /api/dashboard/* → requires auth when auth is enabled
Paste a token into the input in the dashboard's header; it's stored in
localStorage and attached as Authorization: Bearer <token> (JWTs) or X-Api-Key
(anything else) to every fetch call.
Caveat: browsers' EventSource cannot set custom headers, so the live
/api/agui/stream connection only works unauthenticated, or behind a reverse proxy
that injects the header for that path. The rest of the dashboard (sessions,
checkpoints, graph, cost, resume, time-travel) works fully authenticated either way —
only the live node-highlighting stream is affected.
Full example
See examples/dashboard/
for a runnable, key-free demo: it runs a small expense-approval workflow to its
human-in-the-loop gate, then serves the dashboard so you can inspect the paused
session, time-travel through its checkpoints, and resume it to completion.
go run ./examples/dashboard/
See also
- YAML Agents & the Dashboard — declaring a durable graph in YAML instead of Go, and registering it automatically via
chronos serve - The ChronosOS Server —
chronos serve, auth, andchronos auth token