package agent import ( "path" "strings" ) // This file is the single authoritative definition of the local per-invocation // performance metrics and their boundaries. Every count, category, and timing // split recorded to agent_invocations is defined here so the semantics live in // exactly one place; the codex adapter fills them from its event stream, the // pipeline records them, and `no-mistakes stats` renders them, all against // these definitions. Nothing here reads or stores prompts, outputs, diffs, or // raw command arguments - only bounded counts, categories, and durations. // ToolCategory is a bounded bucket for a single tool sub-command. The set is // fixed and low-cardinality so the histogram stays bounded and privacy-safe: // we categorize a command's intent by its leading verb and never store the // command text itself. type ToolCategory string const ( // ToolWait is a wait/poll call that produces no work: sleeping, waiting on // a background job, or polling a slow subprocess (e.g. codex write_stdin). ToolWait ToolCategory = "wait" // ToolTestLint runs a test suite or a linter/formatter. ToolTestLint ToolCategory = "test_lint" // ToolEdit mutates the working tree (patch/apply, file writes, moves). ToolEdit ToolCategory = "edit" // ToolRead inspects the working tree without mutating it (cat, grep, ls). ToolRead ToolCategory = "read" // ToolGit is any git invocation. ToolGit ToolCategory = "git" // ToolOther is anything not matched by the buckets above. ToolOther ToolCategory = "other" ) // ToolCategoryCounts is the bounded histogram of classified tool sub-commands // for one invocation. Because a compound command (`go test && git commit`) // contributes one count per sub-command, the sum of these fields can exceed // InvocationMetrics.ToolCalls, which counts whole tool invocations. type ToolCategoryCounts struct { Wait int TestLint int Edit int Read int Git int Other int } // Add increments the bucket for category. func (c *ToolCategoryCounts) Add(category ToolCategory) { switch category { case ToolWait: c.Wait++ case ToolTestLint: c.TestLint++ case ToolEdit: c.Edit++ case ToolRead: c.Read++ case ToolGit: c.Git++ default: c.Other++ } } // Total returns the number of classified sub-commands. func (c ToolCategoryCounts) Total() int { return c.Wait + c.TestLint + c.Edit + c.Read + c.Git + c.Other } // InvocationMetrics is the bounded activity evidence an adapter extracts from // one invocation's event stream. A nil *InvocationMetrics means the adapter // reported nothing (recorded as NULL, never a fabricated zero); a non-nil value // means every field is meaningful, including a genuine zero. type InvocationMetrics struct { // ModelRoundtrips counts the model-authored items in the turn (assistant // messages plus tool calls). It is a live-stream proxy for productive model // round-trips: because codex batches an exec into a single turn and does not // surface internal poll round-trips as items, every counted item is // productive work, not "are-we-there-yet" polling. ModelRoundtrips int // ToolCalls counts whole tool invocations (one command_execution item is one // tool call regardless of how many sub-commands it chains). ToolCalls int // ToolCategories is the per-sub-command histogram (see ToolCategoryCounts). ToolCategories ToolCategoryCounts // SubprocessWaitMS is the wall-clock spent inside tool subprocesses, // measured by the reader as the sum of each tool item's started->completed // interval. Combined with the invocation duration it separates subprocess // wait from model/reasoning time (see ModelTimeMS). SubprocessWaitMS int64 } // ModelTimeMS is the authoritative split of invocation wall-clock into // model/reasoning time: the invocation duration minus the time spent waiting on // tool subprocesses. It never goes negative. func ModelTimeMS(durationMS, subprocessWaitMS int64) int64 { if subprocessWaitMS <= 0 { return durationMS } if subprocessWaitMS >= durationMS { return 0 } return durationMS - subprocessWaitMS } // FreshInputTokens is the non-cached portion of an invocation's reported input: // the input tokens that were not served from the provider's prompt cache. It is // the honest per-invocation cost signal, separated from cache reads. It never // goes negative. func FreshInputTokens(inputTokens, cacheReadTokens int) int { if cacheReadTokens <= 0 { return inputTokens } if cacheReadTokens >= inputTokens { return 0 } return inputTokens - cacheReadTokens } // PerRoundTokens converts a token counter into the per-round amount for one // invocation. Some adapters (codex) report usage cumulatively across a resumed // durable session, so round N's raw counter includes rounds 1..N-1; there the // per-round amount is current minus the same session's previous cumulative. // Adapters that report per-invocation usage (cumulative == false), and the // first invocation of any session (priorCumulative <= 0), report current as-is. // A cumulative counter that appears to shrink is treated as non-cumulative for // that row rather than fabricating a negative or oversized delta. func PerRoundTokens(current, priorCumulative int, cumulative bool) int { if !cumulative || priorCumulative <= 0 || current < priorCumulative { return current } return current - priorCumulative } // ClassifyToolCommand classifies one tool invocation's command into one bucket // per chained sub-command. It unwraps a shell wrapper (`bash -lc '