Files
wehub-resource-sync 26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

456 lines
14 KiB
Go

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 '<script>'`),
// splits the script on command separators, and classifies each sub-command by
// its leading verb. It is a deliberately simple, allocation-light heuristic -
// the same leading-verb approach the efficiency audits used - and it never
// retains the command text.
func ClassifyToolCommand(command string) []ToolCategory {
script := unwrapShellCommand(command)
subs := splitCompoundCommand(script)
if len(subs) == 0 {
return nil
}
categories := make([]ToolCategory, 0, len(subs))
for _, sub := range subs {
categories = append(categories, classifySubcommand(sub))
}
return categories
}
// shellNames are the shells codex/agents wrap tool commands in.
var shellNames = map[string]bool{
"sh": true, "bash": true, "zsh": true, "dash": true, "ksh": true, "fish": true,
}
// commandWrappers are prefixes that precede the real verb without changing its
// classification (env assignments are handled separately).
var commandWrappers = map[string]bool{
"sudo": true, "env": true, "time": true, "nice": true, "nohup": true,
"xargs": true, "command": true, "builtin": true, "exec": true, "stdbuf": true,
}
// unwrapShellCommand extracts the inner script from a `<shell> -lc '<script>'`
// wrapper. Fields inside the quoted script are re-joined with single spaces,
// which is lossless for leading-verb classification. A command with no
// recognized shell wrapper is returned unchanged.
func unwrapShellCommand(command string) string {
fields := strings.Fields(command)
if len(fields) < 3 {
return command
}
if !shellNames[path.Base(fields[0])] {
return command
}
for i := 1; i < len(fields)-1; i++ {
if isShellCommandFlag(fields[i]) {
return trimMatchingQuotes(strings.Join(fields[i+1:], " "))
}
}
return command
}
// isShellCommandFlag reports whether flag is a `-c`-family shell flag
// (`-c`, `-lc`, `-lic`, `-ic`, ...) that introduces an inline script.
func isShellCommandFlag(flag string) bool {
if len(flag) < 2 || flag[0] != '-' || len(flag) > 5 {
return false
}
body := flag[1:]
if !strings.HasSuffix(body, "c") {
return false
}
for _, r := range body {
switch r {
case 'c', 'l', 'i', 'e', 'x':
default:
return false
}
}
return true
}
func trimMatchingQuotes(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '\'' && s[len(s)-1] == '\'') || (s[0] == '"' && s[len(s)-1] == '"') {
return s[1 : len(s)-1]
}
}
return s
}
// splitCompoundCommand splits a script into sub-commands on the shell operators
// that sequence separate commands: &&, ||, ;, |, and newlines.
func splitCompoundCommand(script string) []string {
var subs []string
start := 0
var quote rune
escaped := false
runes := []rune(script)
for i := 0; i < len(runes); i++ {
r := runes[i]
if escaped {
escaped = false
continue
}
if r == '\\' && quote != '\'' {
escaped = true
continue
}
if quote != 0 {
if r == quote {
quote = 0
}
continue
}
if r == '\'' || r == '"' {
quote = r
continue
}
separatorWidth := 0
switch r {
case '\n', ';':
separatorWidth = 1
case '|':
separatorWidth = 1
if i+1 < len(runes) && runes[i+1] == '|' {
separatorWidth = 2
}
case '&':
if i+1 < len(runes) && runes[i+1] == '&' {
separatorWidth = 2
}
}
if separatorWidth == 0 {
continue
}
if sub := strings.TrimSpace(string(runes[start:i])); sub != "" {
subs = append(subs, sub)
}
start = i + separatorWidth
i += separatorWidth - 1
}
if sub := strings.TrimSpace(string(runes[start:])); sub != "" {
subs = append(subs, sub)
}
return subs
}
// testLintVerbs are standalone linters, formatters, and test runners.
var testLintVerbs = map[string]bool{
"pytest": true, "vitest": true, "jest": true, "mocha": true, "rspec": true,
"phpunit": true, "ctest": true, "shellcheck": true, "eslint": true,
"prettier": true, "ruff": true, "flake8": true, "pylint": true, "mypy": true,
"black": true, "isort": true, "golangci-lint": true, "gofmt": true,
"goimports": true, "staticcheck": true, "revive": true, "vet": true,
"tsc": true, "clippy": true, "rubocop": true, "standardrb": true,
"checkstyle": true, "detekt": true, "ktlint": true, "swiftlint": true,
"stylelint": true, "biome": true,
}
// readVerbs inspect without mutating.
var readVerbs = map[string]bool{
"cat": true, "head": true, "tail": true, "less": true, "more": true,
"grep": true, "egrep": true, "fgrep": true, "rg": true, "ag": true,
"ls": true, "find": true, "fd": true, "wc": true, "cut": true, "sort": true,
"uniq": true, "tr": true, "awk": true, "jq": true, "yq": true, "stat": true,
"file": true, "tree": true, "nl": true, "tac": true, "column": true,
"diff": true, "cmp": true, "xxd": true, "od": true, "bat": true,
"readlink": true, "dirname": true, "basename": true, "pwd": true,
"which": true, "whereis": true, "realpath": true, "hexdump": true,
}
// editVerbs mutate the working tree.
var editVerbs = map[string]bool{
"apply_patch": true, "patch": true, "tee": true, "cp": true, "mv": true,
"rm": true, "mkdir": true, "rmdir": true, "touch": true, "chmod": true,
"chown": true, "ln": true, "install": true, "dd": true, "truncate": true,
}
// waitVerbs poll or idle.
var waitVerbs = map[string]bool{
"sleep": true, "wait": true, "write_stdin": true, "watch": true,
"until": true,
}
func classifySubcommand(sub string) ToolCategory {
verb, rest := leadingVerb(sub)
if verb == "" {
return ToolOther
}
switch {
case verb == "git":
return ToolGit
case waitVerbs[verb]:
return ToolWait
case verb == "sed":
if hasInPlaceFlag(rest) {
return ToolEdit
}
return ToolRead
case editVerbs[verb]:
return ToolEdit
case testLintVerbs[verb]:
return ToolTestLint
case verb == "go":
if firstArg(rest) == "test" || firstArg(rest) == "vet" {
return ToolTestLint
}
return ToolOther
case verb == "cargo":
if a := firstArg(rest); a == "test" || a == "clippy" || a == "fmt" {
return ToolTestLint
}
return ToolOther
case verb == "npm" || verb == "pnpm" || verb == "yarn" || verb == "bun":
if scriptRunnerIsTestLint(rest) {
return ToolTestLint
}
return ToolOther
case verb == "make":
if makeTargetIsTestLint(rest) {
return ToolTestLint
}
return ToolOther
case verb == "gradle" || verb == "./gradlew" || verb == "gradlew":
if a := firstArg(rest); a == "test" || a == "check" || a == "lint" {
return ToolTestLint
}
return ToolOther
case readVerbs[verb]:
return ToolRead
default:
return ToolOther
}
}
// leadingVerb returns the base command verb and the remaining tokens, skipping
// leading `VAR=value` env assignments and command wrappers (sudo, env, time,
// ...). The verb is reduced to its basename so `/usr/bin/git` classifies as git.
func leadingVerb(sub string) (verb string, rest []string) {
tokens := strings.Fields(sub)
i := 0
for i < len(tokens) {
tok := tokens[i]
if isEnvAssignment(tok) {
i++
continue
}
base := path.Base(tok)
if commandWrappers[base] {
i++
// `env`/`xargs` may be followed by their own flags; skip a following
// flag token so the real verb is found.
for i < len(tokens) && strings.HasPrefix(tokens[i], "-") {
i++
}
continue
}
return base, tokens[i+1:]
}
return "", nil
}
func isEnvAssignment(tok string) bool {
eq := strings.IndexByte(tok, '=')
if eq <= 0 {
return false
}
for _, r := range tok[:eq] {
if !(r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) {
return false
}
}
return true
}
func firstArg(rest []string) string {
for _, tok := range rest {
if strings.HasPrefix(tok, "-") {
continue
}
return tok
}
return ""
}
func hasInPlaceFlag(rest []string) bool {
for _, tok := range rest {
if tok == "-i" || strings.HasPrefix(tok, "-i.") || strings.HasPrefix(tok, "--in-place") {
return true
}
}
return false
}
// scriptRunnerIsTestLint reports whether an npm/pnpm/yarn/bun invocation runs a
// test or lint script (`npm test`, `pnpm run lint`, `yarn lint`, ...).
func scriptRunnerIsTestLint(rest []string) bool {
for _, tok := range rest {
if strings.HasPrefix(tok, "-") || tok == "run" || tok == "exec" {
continue
}
return isTestLintWord(tok)
}
return false
}
func makeTargetIsTestLint(rest []string) bool {
for _, tok := range rest {
if strings.HasPrefix(tok, "-") {
continue
}
if isTestLintWord(tok) {
return true
}
}
return false
}
func isTestLintWord(word string) bool {
switch word {
case "test", "tests", "lint", "lints", "check", "checks", "vet", "e2e",
"typecheck", "format", "fmt", "verify":
return true
}
return strings.HasPrefix(word, "test") || strings.HasPrefix(word, "lint")
}