Skip to content

About

Provider-agnostic cost & agent-tree explorer for AI coding sessions. Zero deps.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

agent-atlas

Provider-agnostic cost & agent-tree explorer for AI coding sessions. React 19 + Vite UI, backed by a Node 22+ server. The only outbound request is the price-catalogue refresh (see Pricing; AGENT_ATLAS_PRICING=offline disables it). No telemetry, and binds 127.0.0.1 by default.

Reads the transcripts your agent harness already writes, builds a nested tree of every agent that ran (main → subagents → their subagents, any depth), prices every node from its own token usage, and rolls costs up: total = own + Σ children.total.

main — (session)                              opus-5 high 19.4h 251c  $323.17 (own $81.76 + sub $241.41)
├─ lib-researcher — Survey tracing options    opus-5 xhigh 12m 41c    $6.08 (own $3.80 + sub $2.28)
│  ├─ general-purpose — Research Langfuse…    opus-5 xhigh 5m 19c     $1.07
…

Usage

Install and build the browser client once:

npm install
npm run build

Then use the CLI:

node src/cli.mjs list [--days 7 | --all] [--json]      # sessions, cost-so-far, LIVE badge
node src/cli.mjs tree <session-id|path> [--json|--html] # expandable tree (terminal/JSON/HTML)
node src/cli.mjs export <id> --out tree.html            # standalone HTML, no server needed
node src/cli.mjs serve [--host 127.0.0.1] [--port 4747] [--interval 10] [--token X]

Web view: / is a Vite-built React application and /session/<id> is its React detail route. The list uses a worker-backed session index, server-side filtering/sorting/aggregates, paged JSON and bounded virtual rows, so the initial UI payload and mounted DOM do not grow with the complete history. Detail data is fetched only after navigating to a session.

Compatibility endpoints remain available: /api/sessions and /api/tree/<id>. The React UI uses additive /api/ui/sessions and /api/ui/session/<id> endpoints. With --token X, HTML and API requests require ?t=X; fingerprinted static browser assets are intentionally public and contain no transcript data.

Live sessions

Transcripts are append-only JSONL, so ingestion is incremental and idempotent:

  1. Per file we keep a byte offset and parse only new, complete lines (a partial trailing line waits for the next pass).
  2. Aggregation dedups by message.id keeping the MAX-usage variant — the same rule the harness uses for its own streaming duplicates — so re-reading any range converges to the same numbers. A live session's cost is a monotone lower bound of its final cost.
  3. There is no "session closed" marker on disk, and none is needed: a session is LIVE if its file changed in the last 2 minutes; its tree is simply the tree so far, and live pages auto-refresh.

Change detection is a stat scan every --interval seconds. Parsing/indexing runs in a worker thread, so the HTTP listener and health endpoint become available before the initial corpus index completes. The UI reports this transient indexing state rather than presenting partial totals as final.

Providers

src/providers/<name>.mjs implements: discover() → session refs, buildTree(ref, cache) → tree, and refFromPath(absPath) → a ref or null ("not mine"). src/providers/index.mjs is the registry: it concatenates discover() over the providers and dispatches buildTree on ref.provider. A ref is {provider, id, project, path, mtimeMs, size}.

tree <path> / export <path> now only accept a path a provider recognises (today: an existing *.jsonl transcript). Any other real path used to be parsed as a transcript and rendered as a garbage tree; it is no session matches now. Session ids are unchanged.

Implemented:

  • claude (Claude Code): sessions in ~/.claude/projects/<proj>/<uuid>.jsonl (override root with AGENT_ATLAS_CLAUDE_ROOT); harness subagents from <uuid>/subagents/agent-*.jsonl + .meta.json sidecars (agentType, description, parentAgentId, spawnDepth); cost per node computed from per-message usage with the 5m/1h cache-tier split.
  • codex (Codex CLI): Codex's own stores under ~/.codex (override the root with AGENT_ATLAS_CODEX_ROOT, then CODEX_HOME — these name the home, not a sessions directory, because the three inputs are siblings): state_5.sqlite (threads, thread_spawn_edges), thread_history_1.sqlite (thread_turns, thread_items) and the rollout JSONL under sessions/, which is the only place a per-response token breakdown exists. Read-only, via node:sqlite. Cost is per API call, priced with that call's own turn model — a thread that switched models mid-session is not priced at one of them. No dollar figure exists anywhere in Codex's store, so every row is list-price × tokens flagged computed. A thread that is the child of a thread_spawn_edges row nests inside its parent's tree with via: "spawn" (not "cli", which means "spotted in a Claude transcript", and not "harness"). A nested spawn child is not addressable on its own — tree <child-id> and GET /session/<child-id> do not resolve it, the same way a Claude harness subagent's own file is not a top-level session. Archived threads are still listed: atlas audits what is on disk. On a host that has Codex data, Node prints one ExperimentalWarning: SQLite is an experimental feature… line to stderr per process, the first time the store is opened. NODE_NO_WARNINGS=1 silences it for --json pipelines. On a host that never ran Codex, nothing is read, no warning is printed, and nothing about Claude sessions changes. codex-auto-review is Codex's internal Guardian model for automatic permission checks, as described in Codex's model catalogue. Atlas labels it as internal, retains its recorded id, and does not substitute the parent agent's model or invent a price when the catalogues have none. If turn contexts are absent, Atlas preserves models declared in thread settings or SQLite metadata for display (modelSource records that origin). Those declarations never price calls lacking their own turn model. If no model was saved, the UI says model not recorded; the model that authored an instruction template is not evidence of which model executed a session. The default minimum-cost filter keeps unavailable and partial estimates visible; it only hides fully priced cheap sessions.

Planned (design notes, not yet implemented):

  • cursor: nothing to read — Cursor persists no billable usage locally and its headless CLI reports no cost (open feature request). Cursor-launched work has no local trace at all today.

Transcript detection needs no cooperation from whoever launched the agent, which is why it is the only mechanism here: when a Bash tool_use whose command mentions codex returns a tool_result carrying codex's own session id: <uuid> line, agent-atlas emits one CLI:codex leaf per distinct uuid per tree, under the agent whose transcript it was read from. It is a stub — cost is always 0 / n/a — and the banner fields it carries (tokensUsed, sandbox, workdir, approval, codexVersion, under reported) are harvested only from a complete printed banner; a grep or log re-read yields the session id and nothing else. Those reported.* key names are provisional, and may be replaced if a real codex provider module ever reads Codex's own store. Two known false negatives: a launch whose output the harness spilled to <session-dir>/tool-results/<name>.txt (that sibling file is never opened), and a launch run as a background shell and read back via BashOutput (that tool call carries no command to anchor on). Neither leaves a trace in the tree.

Pricing

Prices are not tables in the source. src/pricing/ resolves a model's token prices at runtime, so a model that ships tomorrow is priced as soon as a catalogue lists it.

  • Sources, in order: your local overrides, then LiteLLM's model_prices_and_context_window.json, then OpenRouter's /api/v1/models (only for models LiteLLM does not list yet). Input, output, cache read and cache write (5m and 1h) are priced separately where the source has them.
  • Cache policy: each source is re-read at most every 24 h (1 h back-off after a failure), in the background for serve and for at most 2 s per source in list/tree/export. The result is cached at $XDG_CACHE_HOME/agent-atlas/prices.json (AGENT_ATLAS_PRICES_CACHE). A failed, slow or implausible (< 50 models) refresh changes nothing; the last good catalogue, else the snapshot bundled with the release (npm run prices:snapshot), keeps answering however old. Models an upstream delists keep their last price. AGENT_ATLAS_PRICING=offline never touches the network.
  • Overrides: $XDG_CONFIG_HOME/agent-atlas/prices.json (AGENT_ATLAS_PRICES_OVERRIDE), {"model": {"in": 5, "out": 25, "cr": 0.5, "cw5": 6.25, "cw1h": 10}} in $/MTok; unlisted classes are unpublished. Wins over every source, re-read live.
  • Matching: provider prefixes, date suffixes, @version, -v1:0, [1m] and dots/dashes are normalised (anthropic/claude-opus-5.5 = claude-opus-5-5-20260921). No fuzzy or nearest-version fallback: a guess would be a silent wrong number.
  • Provenance: every node carries cost.provenance (model, source, matched key, origin cache/bundled/override, fetched-at, URL); the session view shows it in the "prices" tile.
  • Unknown is not zero: a model no source knows is n/a (or a lower bound ≥ $x next to priced calls), is listed per session, and is never hidden by the min $ filter. A token class the source does not publish (e.g. OpenAI cache writes) makes the result partial, not free.
  • Not modelled: long-context tiers (> 200k/272k), batch/priority tiers, and promotional windows. A session is priced at the current catalogue price, so a later price change re-prices history. These are estimated API list-price costs, not subscription charges or invoice totals. Reproducible historical pricing is tracked separately in #31.

Caveats

  • The harness sweeps transcripts after cleanupPeriodDays (default 30). v1 is in-memory over what's on disk; durable retention would add a store later.
  • The initial worker index remains corpus-sized; it is deliberately asynchronous so it does not delay the HTTP server.

Docker

Published to GHCR by CI on every push to main (and semver tags):

docker run --rm -p 4747:4747 \
  -v "$HOME:/data/home" -e AGENT_ATLAS_CLAUDE_ROOT=/data/home/.claude/projects \
  -e AGENT_ATLAS_CODEX_ROOT=/data/home/.codex \
  ghcr.io/daimonbot/agent-atlas:latest

The image runs as user node (uid 1000). Its default CODEX_HOME is /data/home/.codex, matching the shared-home mount above; set CODEX_HOME or AGENT_ATLAS_CODEX_ROOT for a different location (the Atlas override wins). Pricing caches default to /tmp/agent-atlas-cache, so a read-only root filesystem needs only a writable /tmp mount for that cache. Harness databases are always opened read-only; SQLite may need its own writable sidecar files as described below.

The mount is deliberately not :ro. Codex stores use SQLite WAL mode. A read-only mount works when the required readable -wal and -shm files are already maintained by the Codex writer; otherwise SQLite may need to create the -shm sidecar and opening can fail. Every Atlas connection uses readOnly: true, so Atlas never writes the databases themselves. Allow the minimum required sidecar-file writes when configuring a new mount. For a Claude-only installation, mount :ro and point AGENT_ATLAS_CODEX_ROOT at an empty directory to disable Codex discovery.

About

Provider-agnostic cost & agent-tree explorer for AI coding sessions. Zero deps.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages