package main import ( "fmt" "io" "sort" "strings" "text/tabwriter" ) // report prints the numbers METRICS.md wants: the headline pass rate, and // enough per-task and per-tool detail to tell which change caused a move. func report(w io.Writer, results []Result) { if len(results) == 0 { return } fmt.Fprintln(w, "\n"+strings.Repeat("─", 78)) byTask := map[string][]Result{} var order []string for _, r := range results { if _, seen := byTask[r.Task]; !seen { order = append(order, r.Task) } byTask[r.Task] = append(byTask[r.Task], r) } sort.Strings(order) tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "TASK\tPASS\tTURNS\tIN\tOUT\tCACHED\tWALL") for _, name := range order { rs := byTask[name] fmt.Fprintf(tw, "%s\t%d/%d\t%.1f\t%.0f\t%.0f\t%.0f\t%.1fs\n", name, countPass(rs), len(rs), mean(rs, func(r Result) float64 { return float64(r.Turns) }), mean(rs, func(r Result) float64 { return float64(r.Usage.InputTokens) }), mean(rs, func(r Result) float64 { return float64(r.Usage.OutputTokens) }), mean(rs, func(r Result) float64 { return float64(r.Usage.CacheReadInputTokens) }), mean(rs, func(r Result) float64 { return float64(r.WallMS) / 1000 })) } tw.Flush() // A task counts as solved if every repeat passed; anything less is a task // the harness cannot do reliably, which is the number worth reporting. var solved int for _, name := range order { if countPass(byTask[name]) == len(byTask[name]) { solved++ } } fmt.Fprintln(w, strings.Repeat("─", 78)) calls, fails := toolStats(results) fmt.Fprintf(w, "pass rate %d/%d tasks solved on every run (%d/%d runs passed)\n", solved, len(order), countPass(results), len(results)) fmt.Fprintf(w, "edit success %s\n", rate(calls["edit_file"]-fails["edit_file"], calls["edit_file"])) fmt.Fprintf(w, "mean turns %.1f\n", mean(results, func(r Result) float64 { return float64(r.Turns) })) fmt.Fprintf(w, "mean tokens %.0f in (%.0f cached), %.0f out\n", mean(results, func(r Result) float64 { return float64(r.Usage.InputTokens) }), mean(results, func(r Result) float64 { return float64(r.Usage.CacheReadInputTokens) }), mean(results, func(r Result) float64 { return float64(r.Usage.OutputTokens) })) fmt.Fprintf(w, "mean cost %s\n", costCell(results)) fmt.Fprintf(w, "mean wall %.1fs\n", mean(results, func(r Result) float64 { return float64(r.WallMS) / 1000 })) fmt.Fprintln(w, "\ntool calls (failures in brackets)") tw = tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) var names []string for name := range calls { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Fprintf(tw, " %s\t%d\t[%d]\t%s ok\n", name, calls[name], fails[name], rate(calls[name]-fails[name], calls[name])) } tw.Flush() fmt.Fprintln(w, "\nMETRICS.md row:") fmt.Fprintf(w, "| | %d/%d | %s | %.1f | %s |\n", solved, len(order), rate(calls["edit_file"]-fails["edit_file"], calls["edit_file"]), mean(results, func(r Result) float64 { return float64(r.Turns) }), costCell(results)) } // cost prices one run. Pricing lives in the llm package so the REPL's running // total and this table cannot drift apart. func cost(r Result) float64 { c, _ := r.Usage.Cost(r.Model) return c } // costKnown reports whether every run had priceable usage, so an unpriced // model produces a blank rather than $0.0000. func costKnown(results []Result) bool { for _, r := range results { if _, ok := r.Usage.Cost(r.Model); !ok { return false } } return len(results) > 0 } func costCell(results []Result) string { if !costKnown(results) { return "n/a" } return fmt.Sprintf("$%.4f", mean(results, cost)) } func toolStats(results []Result) (calls, fails map[string]int) { calls, fails = map[string]int{}, map[string]int{} for _, r := range results { for _, ev := range r.Events { if ev.Type != "tool_call" { continue } calls[ev.Tool]++ if ev.OK != nil && !*ev.OK { fails[ev.Tool]++ } } } return calls, fails } func countPass(rs []Result) int { var n int for _, r := range rs { if r.Pass { n++ } } return n } func mean(rs []Result, f func(Result) float64) float64 { if len(rs) == 0 { return 0 } var sum float64 for _, r := range rs { sum += f(r) } return sum / float64(len(rs)) } func rate(ok, total int) string { if total == 0 { return "n/a" } return fmt.Sprintf("%.0f%%", 100*float64(ok)/float64(total)) }