Documentation
¶
Overview ¶
Package streamjson reads token usage out of the JSONL transcripts agent CLIs already write to disk.
The envelopes differ per agent and change between releases, so this does not model any one of them. It walks the decoded JSON and picks up values by key, which means an agent that renames its wrapper keeps working and an agent that renames its usage fields degrades to "no numbers" instead of to wrong numbers. Nothing here guesses: a key that is not recognized contributes nothing.
Package usagewatch reads live token counts out of the transcripts agent CLIs already write to disk.
Agents differ in what they print to stdout: some report token usage as they stream, some only at exit, some never. They agree on something else, though, which is that they keep a structured session transcript, and that transcript carries per-message usage with timestamps. Tailing it gives a live rate without root, without intercepting anyone's network traffic, and without asking the agent to behave differently.
The design constraints that shape everything here:
- Only count what happened during this review. Transcripts are per session and sessions outlive reviews (--continue-sessions reuses them), so the watcher records where each file ended when it attached and reads only what is appended after that.
- Attribute the transcript to the right review. Both supported agents record their working directory in the transcript, and every review runs with a distinct directory in worktree mode, so the cwd is the key.
- Never invent a number. An agent whose transcript cannot be found, parsed, or attributed simply reports nothing, and the dashboard shows no rate.
Index ¶
- func Agents() []string
- func ConnectedTo(pid int, endpoints []netip.AddrPort) bool
- func DefinitionsPath() string
- func EnableOpenCodeDB(on bool) bool
- func LoadDefinitions(path string) error
- func Peers(pid int) []netip.AddrPort
- func Rate(prev, cur Sample) (float64, bool)
- func RegisterSpec(tool string, spec Spec) error
- func Supported(tool string) bool
- type Process
- type Sample
- type Spec
- type Watcher
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Agents ¶
func Agents() []string
Agents lists every agent name this package knows, built in and defined.
func ConnectedTo ¶
ConnectedTo reports whether a process holds a connection to any of the given endpoints, which is how a monitor decides that an agent's tokens are already being counted somewhere else.
Endpoints are matched on port plus address, with loopback spellings treated as equal: an engine advertised as 127.0.0.1:11434 and a connection to ::1:11434 are the same engine.
func DefinitionsPath ¶
func DefinitionsPath() string
DefinitionsPath is where agent definitions live by default. It follows gauntlet's location so one file serves both tools.
func EnableOpenCodeDB ¶
EnableOpenCodeDB turns reading of opencode's SQLite session store on or off.
It is gated twice on purpose. The build tag `sqlite` decides whether the database driver is linked in at all, since it is a large dependency for one agent, and this switch decides whether a program that has it actually opens the operator's session database. Neither gate implies the other.
It reports whether this build can read it: false means the binary was compiled without `-tags sqlite`, and nothing was enabled.
func LoadDefinitions ¶
LoadDefinitions reads agent definitions from a JSON file, teaching this package about agents it was not compiled to know, including where they keep their transcripts:
{"myagent": {"usage": {"roots": ["~/.myagent/sessions"]}}}
A missing file is not an error, since most machines have none. A malformed one is: running with a half-loaded agent set is worse than refusing.
func Peers ¶
Peers lists the TCP endpoints a process is connected to.
It exists to answer one question a token monitor has to get right: is this agent generating through an engine that is already being measured? An agent pointed at a local llama.cpp or vLLM produces tokens the engine reports too, so counting both doubles the number. The connection is the evidence, and it needs no configuration or guesswork about model names.
Linux only, and best effort: an empty result means "cannot tell", which a caller should treat as "assume it is not the same engine" rather than as a statement about the process.
func Rate ¶
Rate returns tokens per second between two samples, and whether it could be computed at all. It never extrapolates: without two readings and a positive span there is no rate to report.
func RegisterSpec ¶
RegisterSpec adds a transcript adapter for a defined agent.
Types ¶
type Process ¶
type Process struct {
PID int
// Tool is the agent name (claude, codex, pi, …).
Tool string
// Dir is the process's working directory, which is what attributes a
// transcript to it.
Dir string
// Started is when the process began, as far as the OS reports it.
Started time.Time
}
Process is one running agent CLI.
func Discover ¶
func Discover() []Process
Discover lists the agent CLIs running on this machine, so a monitor can show what is generating right now without being told.
It walks /proc rather than shelling out to pgrep: the answer is three reads per process, and spawning a process to count processes is how a monitor ends up measuring itself. On systems without /proc it returns nothing, which callers should treat as "cannot tell" rather than "nothing is running".
type Sample ¶
type Sample struct {
Output int
Thinking int
Total int
// At is when the reading was taken, so successive samples make a rate.
At time.Time
}
Sample is cumulative usage observed since the watcher attached. Thinking is the reasoning share of Output, which the agents report separately: it is what the model spent before it wrote anything the user sees.
type Spec ¶
type Spec struct {
// Roots are directories to search, with ~ expanded.
Roots []string `json:"roots"`
// Suffix filters transcript files (default ".jsonl").
Suffix string `json:"suffix,omitempty"`
// Cumulative says the counters already include everything before them, so
// the first value seen becomes a baseline. Default is per message.
Cumulative bool `json:"cumulative,omitempty"`
// HeaderCwd says the working directory appears once in a session header
// rather than on every record, so ownership is decided from the head of
// the file. Without it, a transcript whose usage lines carry no cwd is
// attributed by location alone.
HeaderCwd bool `json:"header_cwd,omitempty"`
}
Spec describes where a defined agent keeps its transcripts, so live usage works for agents gauntlet was not compiled to know about (pi and the CLIs built on it, in-house wrappers). The records are parsed generically: any JSONL whose objects carry recognizable token counters works, and one whose objects do not simply reports nothing.
type Watcher ¶
type Watcher struct {
// contains filtered or unexported fields
}
Watcher tails one agent's transcripts for one review.
func Watch ¶
Watch starts reading usage for one agent working in one directory. It returns nil when that agent keeps no readable transcript, which callers should treat as "no rate available" rather than an error. Files that already exist are read from their current end, so a session resumed from an earlier review contributes only what it adds from now on.
func (*Watcher) Poll ¶
Poll reads whatever the transcripts have gained since the last read and returns the total. Callers use it for a final synchronous read once the agent has exited, since the last records land after the process is gone.
func (*Watcher) Run ¶
Run polls until the context is canceled, calling onChange whenever the observed usage grows. It is meant to run in its own goroutine.