Vectrum
Sign In
146 lines · 4.4 KB · Go
package main

import (
	"fmt"
	"io"
	"sort"
	"strings"
	"text/tabwriter"

	"harness/internal/llm"
)

// 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      $%.4f\n", mean(results, func(r Result) float64 { return cost(r.Usage) }))
	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, "| <change> | %d/%d | %s | %.1f | $%.4f |\n",
		solved, len(order),
		rate(calls["edit_file"]-fails["edit_file"], calls["edit_file"]),
		mean(results, func(r Result) float64 { return float64(r.Turns) }),
		mean(results, func(r Result) float64 { return cost(r.Usage) }))
}

// Per-MTok rates for Claude Opus 5. Cache reads and writes use the standard
// 0.1x and 1.25x multipliers. Override these if you are running another model.
const (
	inputPerMTok      = 5.0
	outputPerMTok     = 25.0
	cacheReadPerMTok  = inputPerMTok * 0.1
	cacheWritePerMTok = inputPerMTok * 1.25
)

func cost(u llm.Usage) float64 {
	return (float64(u.InputTokens)*inputPerMTok +
		float64(u.OutputTokens)*outputPerMTok +
		float64(u.CacheReadInputTokens)*cacheReadPerMTok +
		float64(u.CacheCreationInputTokens)*cacheWritePerMTok) / 1_000_000
}

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