A lightweight, correct-by-construction cost-capture layer for local AI-agent
session transcripts. Pure Python stdlib only (sqlite3, argparse, json,
os) — no third-party dependencies, nothing to download, ~15 s full sync.
A sync / report / sql CLI over a typed SQLite ledger (package:
agentfinops):
python -m agentfinops sync # incremental ingest (mtime watermark)
python -m agentfinops report --by project # cost by project (also: model | session_id)
python -m agentfinops sql "SELECT model, sum(output_tokens) FROM messages GROUP BY model"Off-the-shelf capture tools (e.g. pond) get the raw ingest right but leave two
correctness footguns and ship no cost semantics. agent-finops is our own
recreation of that approach, refined to fix each weakness at the structural
level rather than papering over it at query time.
| # | Weakness elsewhere | Fix here | Locus |
|---|---|---|---|
| R1 | one row per JSONL line → Claude Code repeats cumulative usage across a multi-tool turn's lines → a naive SUM double-counts ~2.6× |
dedup at ingest on (session_id, message_id), keeping the max-output row; a plain SUM is already correct — the footgun is structurally impossible |
store.py UPSERT |
| R2 | token counters buried in a JSON blob → fragile json_get paths |
first-class typed columns | store.py schema |
| R3 | no cost/pricing semantics | built-in pricing table + cost formula, 1h/5m cache-write split priced separately, unpriced model = named gap not silent $0 | pricing.py, report.py |
| R4 | heavy binary + embedding-model download for search we don't need | pure stdlib, ~15 s full sync, nothing to download | whole package |
| R5 | no sub-agent / workflow-boundary concept | key sessions on real sessionId + capture is_sidechain / parent_uuid / source_agent |
adapter.py |
| R6 | (the one strength worth keeping) | lossless raw_json fallback column |
store.py |
Every billable assistant message becomes one row in messages, with token
counters, model, timestamp, and the 1h/5m cache-write split as typed
columns — queryable directly, no blob-path fragility. De-duplication is
enforced at ingest by PRIMARY KEY (session_id, message_id) and an UPSERT that
keeps the row with the greatest output_tokens (the authoritative/completed
emission of a turn that Claude Code spreads across several JSONL lines). Because
of that, a plain aggregate over messages is already correct — there is no
consumer-side dedup query to forget.
Costs are applied per row from a small, declarative pricing table
(pricing.py), at list-price defaults. The formula prices the 1-hour and
5-minute cache writes separately (2.00× and 1.25× the input rate) and cache
reads at 0.10×. A model absent from the table is priced at $0 and surfaced as
a named gap in the report — never a silent zero folded into a total.
sync— incremental ingest of~/.claude/projects/**/*.jsonl(only re-reads files whose mtime moved past the stored watermark;--forceto re-read all).report --by model|project|session_id— cost + token report over the ledger.sql "<SELECT ...>"— read-only SQL surface over the typed columns (SELECT/WITH only).
Dogfooded over a real local corpus: sync over 2,553 files → 84,561 deduped
messages / 1,729 logical sessions in ~15 s, ≈ $9,836 indicative list-price
cost. The double-count footgun is provably absent —
SELECT count(*), count(DISTINCT session_id||message_id) FROM messages returns
equal counts — and GROUP BY is_sidechain cleanly separates main-agent from
sub-agent spend.
This is the capture + cost-semantics layer. Boundary-semantics attribution (sub-agent-spawn cost roll-up, cache amortization, "who must justify this spend"), a budget-guard, and billing are deliberately out of scope for this package.