agentusage

package
v0.4.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 17 Imported by: 1

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

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

func ConnectedTo(pid int, endpoints []netip.AddrPort) bool

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

func EnableOpenCodeDB(on bool) bool

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

func LoadDefinitions(path string) error

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

func Peers(pid int) []netip.AddrPort

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

func Rate(prev, cur Sample) (float64, bool)

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

func RegisterSpec(tool string, spec Spec) error

RegisterSpec adds a transcript adapter for a defined agent.

func Supported

func Supported(tool string) bool

Supported reports whether live usage can be read for an 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.

func (Sample) Empty

func (s Sample) Empty() bool

Empty reports whether nothing has been observed yet.

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

func Watch(tool, dir string, since time.Time) *Watcher

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) Dir

func (w *Watcher) Dir() string

Dir is the working directory this watcher attributes usage to.

func (*Watcher) Poll

func (w *Watcher) Poll() Sample

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) Read

func (w *Watcher) Read() Sample

Read takes one reading now, including whatever an agent wrote as it exited.

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context, every time.Duration, onChange func(Sample))

Run polls until the context is canceled, calling onChange whenever the observed usage grows. It is meant to run in its own goroutine.

func (*Watcher) Sample

func (w *Watcher) Sample() Sample

Sample returns the usage observed so far.

func (*Watcher) Tool

func (w *Watcher) Tool() string

Tool is the agent this watcher follows.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL