Documentation
¶
Overview ¶
Package reasonix implements an EVENT-LEVEL adapter for Reasonix (esengine/DeepSeek-Reasonix, MIT).
SURFACE. Reasonix ships a purpose-built usage ledger and this adapter reads that and nothing else:
${REASONIX_STATE_HOME:-${REASONIX_HOME:-~/.reasonix}}/stats/YYYY-MM-DD.jsonl
One append-only JSONL file per writer-local day, one record per completed request. The session transcripts under <root>/projects/**/sessions/ are deliberately NOT read: they carry prompts, reasoning and tool arguments, and they lack the exact counters these records carry. Reasonix's own registry entry says the same thing in the other direction — the transcript overlaps these records and is excluded.
CRITICAL: strictly observational. Files are opened O_RDONLY, never written, never locked, never rotated. Reasonix guards its own appends with <root>/stats/.append.lock; this adapter never touches it.
Records are EVENTS, not cumulative counters ¶
This is the bug class that eats OTEL-shaped and session-state surfaces, so it is settled by evidence rather than by shape. Two proofs, both from this machine:
- Reasonix indexes its own stats files into a SQLite catalog whose record table is keyed PRIMARY KEY(file_path, byte_offset) with columns source/model_ref/provider/prompt/completion/reasoning/cache_hit/ cache_miss/total/requests/turns, and whose day rollup is built by SUMMING those columns. A cumulative re-export summed that way would be nonsense, so the vendor's own reader treats every line as a delta.
- Two consecutive live records on this machine read prompt=6207 then prompt=5718. A running total does not go down.
So the collector adds these up; it never diffs them and never takes a max.
Identity is the LINE's CONTENT; the byte offset is only a gate ¶
The dedup key is a hash of the record's own bytes. It carries no path and no read position, so a re-read from zero — a lost checkpoint, a moved REASONIX_HOME, a symlinked root re-pointed — re-derives exactly the same keys and collapses in the store. A key minted from a byte offset would recount every line after any rewrite, and a position is not an identity.
Collision safety comes from the record itself: `ts` is RFC3339 with NANOSECOND precision and sits beside the full counter tuple, so two distinct requests cannot produce identical bytes. The only line that can collide with an already-stored line is a genuine duplicate write, which is exactly what deduplication is for. The trade is deliberate and in the conservative direction the ledger always takes: this can only ever UNDER-count, and an append-only ledger can never take an over-count back.
The byte offset survives as the incremental CHECKPOINT, where being wrong costs a re-read rather than a fact. Offsets are safe on THIS surface — daily files are append-only and every record is newline-terminated (the writer guards record boundaries) — unlike the re-read-in-full transcript surfaces, where a poll re-walks the whole tree and a position-derived key would recount on every pass. The checkpoint is trusted only on pure growth; a shrink or a same-size rewrite re-reads from zero.
Cost: the captured rows are unpriced ¶
The captured Ollama rows carry cost_complete=false and incomplete_reason="no_price", with no amount. This adapter stamps no cost: CostMicroUSD remains nil, and the pricing ladder can value the counters. The exact v1.25.3 writer can also emit optional quoted cost amounts and currencies. They are not mapped here; unknown currencies must never be interpreted as USD. See testdata/live-1.25.3.provenance.md for the versioned writer evidence and the captured surface's limits.
Names and counters ONLY ¶
The record shape is content-free by construction — no prompt, no result, no tool input, no cwd, not even a session id — and the decode is an ALLOW-LIST of the seventeen fields below, so a content field added upstream contributes nothing until this package is taught about it on purpose. Every string field read here is an enum or an identifier: a model ref, a surface name ("cli"), a status ("unavailable"), a reason ("no_price"). privacy.no_raw is satisfied by construction, not by a switch.
Index ¶
- Constants
- func New() adapter.Adapter
- func StatsDir(cfg adapter.DiscoverConfig) string
- type Adapter
- func (Adapter) Capabilities() model.ToolCapability
- func (a Adapter) Collect(ctx context.Context, src adapter.Source) (adapter.Observation, error)
- func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)
- func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)
- func (Adapter) DisplayName() string
- func (Adapter) ID() string
Constants ¶
const ( StateHomeEnv = "REASONIX_STATE_HOME" HomeEnv = "REASONIX_HOME" )
StateHomeEnv and HomeEnv name the environment variables that move the Reasonix state root, and with it every stats file this adapter reads. StateHomeEnv is checked FIRST; HomeEnv is the fallback; ~/.reasonix is the default. They are exported because a caller that copies this process's discovery into another one — the systemd units internal/service writes — has to know that the environment, not the defaults, decided what gets collected.
Variables ¶
This section is empty.
Functions ¶
func StatsDir ¶
func StatsDir(cfg adapter.DiscoverConfig) string
StatsDir resolves the stats directory for one discovery config, applying the full three-step root resolution. Exported for the CLI's `sources`/`doctor` surfaces, which report where an adapter is looking without collecting.
Types ¶
type Adapter ¶
type Adapter struct{}
Adapter reads Reasonix daily usage ledgers. Read-only.
func (Adapter) Capabilities ¶
func (Adapter) Capabilities() model.ToolCapability
Capabilities declares what this project can say about Reasonix.
Cost is COMPUTED: nothing here calls SetCost, so a row is valued from the public rate card or left unpriced. There is NO activity at all — this adapter references model.ActivityEvent nowhere.
func (Adapter) CollectIncremental ¶
func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)
CollectIncremental tail-reads what is new since cp: an unchanged size+mtime opens nothing; pure growth seeks to the stored offset; a shrink or a same-size rewrite re-reads from zero, which is free of consequence because the keys are content hashes and re-derive identically.
func (Adapter) Discover ¶
func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)
Discover lists the daily ledger files under the resolved stats directory. A missing root is not an error: Reasonix simply is not installed here.
The root is NOT symlink-resolved, and does not need to be: dedup keys are hashes of a record's own bytes, so a re-pointed root cannot mint a new identity for a line already stored.
func (Adapter) DisplayName ¶
DisplayName returns the human-friendly name.