The shared control plane for agent traces and spend.
Chronicle and TokenOps talk HTTP. Only this process owns SQLite. Agents never open the database file.
One UI, one SQLite: budgets and policies on the left, a run halted in-path by progress_guard on the right. Chronicle traces live in the next tab.
AgentPlane Control is the HTTP plane Chronicle and TokenOps share. Sidecars post envelopes and register runs; the plane stores them; Admin, Chronicle, and TokenOps tabs read the same file. No NFS, no two processes fighting over WAL, no "which DB did that agent open."
Why · Architecture · Install · Quick start · Sidecars · Comparison · Design
- SQLite lives here. Nowhere else. Chronicle and TokenOps are HTTP clients. They never receive a DB path.
- One pane for both products. Admin keys, Chronicle waterfalls, TokenOps budgets / policies / breaches — same process, same file.
- Ingest is a contract, not a dump. Chronicle writes
POST /v1/envelopes:batch(batch_size=1= flush now) and replaysGET /v1/traces/{id}/envelopes. TokenOps registers runs, ledger, and governance over/v1/*. - Auth when you need it. Empty key table = local anonymous (all scopes, tenant
local). Create sidecar keys in Admin, or seedCONTROL_PLANE_API_KEYS. - pip in, serve, done. No Postgres, no login screen, no separate dashboard server.
The plane is one FastAPI process. Agents stay agents.
flowchart LR
subgraph AGENTS["Agent processes"]
C["Chronicle sidecar<br/>RemoteStore"]
T["TokenOps sidecar<br/>HttpStore"]
end
subgraph PLANE["Control plane (:8800)"]
API["HTTP /v1"]
UI["Admin · Chronicle · TokenOps"]
DB[("SQLite CONTROL_PLANE_DB")]
API --> DB
UI --> API
end
C -->|"POST /v1/envelopes:batch"| API
C -->|"GET /v1/traces/{id}/envelopes"| API
T -->|"POST /v1/runs · ledger · governance"| API
| Piece | Owns | Does not own |
|---|---|---|
This plane (control-plane serve) |
SQLite, HTTP API, HTML UI | Agent loops, LLM calls, record-and-replay, in-path halt |
| Chronicle sidecar | Boundaries, envelopes, fixtures | The database file |
| TokenOps sidecar | tokenops_run, wrap_complete, ledger client |
The database file |
Design notes: docs/DESIGN.md.
Chronicle tab: newest traces first. Click a row for the time-based waterfall.
pip install agentplane-control-planePrerequisites: Python 3.10+. Sidecars are separate packages: agent-chronicle>=0.4.0, agent-tokenops>=0.2.0.
PyPI name is agentplane-control-plane; import is control_plane; CLI is control-plane.
See RELEASING.md for releases.
From a clone:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"control-plane start # runs in the background; safe to re-run (no-op if already up)
control-plane status # pid, port, version, /health
control-plane stopOpen http://127.0.0.1:8800/ — Admin, Chronicle, TokenOps. No login.
One managed instance per machine user; start/stop/status track it via a small
state file (platformdirs user-state dir) and a PID — stop never touches a process
it didn't start. Logs go to a file next to the state (path printed by start), since
the process is detached from your terminal.
Prefer the foreground, un-managed form for scripting, containers, or when you want Ctrl-C to stop it:
control-plane serve --port 8800 --db control_plane.dbcontrol-plane ui is a pointer, not a second server: the HTML UI is served with the API.
For hosting (a shared team plane, a demo environment) rather than a local dev loop:
docker compose up -d # builds the image, starts on :8800, persists SQLite in a volume
docker compose logs -f
docker compose down # add -v to also drop the data volumeOr without Compose:
docker build -t agentplane-control-plane .
docker run -d --name control-plane -p 8800:8800 -v control-plane-data:/data agentplane-control-planeThe image's entrypoint is control-plane serve (foreground, PID 1) — not start.
Docker/Kubernetes is already the process supervisor here (restart policy, health
checks via the built-in HEALTHCHECK hitting /health, log collection from
stdout/stderr); start/stop/status are for running the plane directly on a
developer's machine, where nothing else is supervising the process. Don't run them
inside the container — a background/detached mode would exit PID 1 as soon as it
spawned its child, and the container would exit with it.
Set CONTROL_PLANE_API_KEYS before exposing the container beyond localhost — the
default (empty) is anonymous, all-scopes access.
Point both libraries at the same origin. Do not set a SQLite path on the agents.
export CONTROL_PLANE_URL=http://127.0.0.1:8800
# TokenOps also honors TOKENOPS_URL; leave TOKENOPS_EMBEDDED unsetChronicle — flush every envelope to the plane:
import os
import chronicle
from chronicle import RemoteStore
store = RemoteStore(os.environ["CONTROL_PLANE_URL"], batch_size=1)
with chronicle.record("my-run", store=store):
...TokenOps — register runs and share the ledger over HTTP:
from tokenops import ControlPlaneClient, tokenops_run
client = ControlPlaneClient.from_env() # CONTROL_PLANE_URL or TOKENOPS_URL
with tokenops_run(client=client) as bound:
...Auth off until you create a key. Then:
export CONTROL_PLANE_API_KEYS='chron:local:ingest+read,tops:local:ingest+read,admin:local:read+admin'
export CONTROL_PLANE_API_KEY=chron # Chronicle / TokenOps sidecarOr create keys in the Admin tab (secret shown once).
Every route has one caller class. Agents do not scrape the UI.
| Route | Caller |
|---|---|
POST /v1/envelopes:batch |
Chronicle sidecar (only ingest API) |
GET /v1/traces/{id}/envelopes |
Chronicle sidecar (fixture replay) and Chronicle waterfall |
GET /v1/traces |
Chronicle UI search |
POST /v1/runs, ledger, governance, run-records |
TokenOps sidecar |
| segments / budgets / policies, admin keys | UI |
This is not a gateway, not a SaaS, and not a replacement for Chronicle or TokenOps. It is the shared store + UI those two already assume.
| AgentPlane Control | TokenOps embedded SQLite | Langfuse / Phoenix | |
|---|---|---|---|
| Primary focus | Shared plane (traces + spend) | Governance in one process | Observe / traces |
| Agents open the DB file | No | Yes (same TOKENOPS_DB) |
N/A (hosted or collector) |
| Chronicle + TokenOps one UI | Yes | TokenOps UI only | No |
| In-path halt / mutate | Via TokenOps sidecar | Yes | No (analytics) |
| Record-and-replay fixtures | Via Chronicle sidecar | No | No |
| Requires hosted SaaS | No | No | Often |
What this does not do: call models, wrap complete, record boundaries, or host a multi-tenant cloud for you. Fail-closed auth is opt-in (create keys). The HTTP contract is still 0.x.
Command and env reference
| Variable | Purpose |
|---|---|
CONTROL_PLANE_DB |
SQLite path (or control-plane serve --db) |
CONTROL_PLANE_URL |
Sidecar base URL (http://127.0.0.1:8800) |
CONTROL_PLANE_API_KEYS |
Seed keys: name:tenant:scope+scope |
CONTROL_PLANE_API_KEY |
Bearer the sidecar sends |
CONTROL_PLANE_CONFIG |
Governance YAML seed (else packaged default.yaml) |
TOKENOPS_URL |
TokenOps alias for the same origin |
TOKENOPS_API_KEY |
TokenOps alias for the Bearer |
TOKENOPS_EMBEDDED |
Must be unset when using this plane |
control-plane serve [--host 127.0.0.1] [--port 8800] [--db PATH] [--reload]
The plane is early (0.x). Near-term:
- Harden SQLite under concurrent sidecar writes (
busy_timeout, WAL discipline). - Admin: persist the browser key so a refresh does not 401.
- Keep the HTTP contract stable enough for Chronicle 0.4 and TokenOps 0.2.
Ideas welcome via GitHub issues.
| agentplane-control-plane | tokenops | agent-chronicle | notes |
|---|---|---|---|
| 0.1.x | ≤ 0.2.1 | ≥ 0.3.0 | single-op /v1/ledger/*, PUT /v1/run-records |
| 0.2.x | ≤ 0.2.1 and <next> |
≥ 0.3.0 | additive — old clients keep working; adds precheck / events:batch, run_state, data_scope |
| 0.3.x | <next>+ only |
≥ 0.3.0 | breaking — drops run_registrations, PUT /v1/run-records, legacy ledger wrappers |
- 0.2.0 → 0.3.0:
run_registrationsfolded intorunsand dropped;PUT /v1/run-records(create_run) removed;PATCH /v1/run-recordsrejectssteps/cost_micros(0.2.x only ignores them); legacy/v1/ledger/halt/*and single-op/v1/ledger/{spent,inflight}/*writes removed. Runs as an automaticPRAGMA user_versionv3 migration. Released only aftertokenops <next>stops callingcreate_run.
Full contract: docs/api-contract.md.
- API contract — the TokenOps ⇄ control-plane wire spec
- Design — storage, callers, scopes, keys
- Releasing — Trusted Publishing to PyPI
- Changelog
- Chronicle · TokenOps
Issues and PRs are welcome.
pip install -e ".[dev]"
ruff check control_plane tests
pytest -vThanks to everyone who has contributed.
If this is the missing box between your agents and the database, please ⭐ star the repo so more people can find it.
Built by Susheem Koul and Tisha Chawla
