@matt82198/aesop 0.1.0-beta.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +158 -5
- package/LICENSE +66 -21
- package/README.md +97 -33
- package/aesop.config.example.json +6 -0
- package/bin/CLAUDE.md +12 -3
- package/bin/cli.js +211 -40
- package/daemons/CLAUDE.md +1 -1
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +208 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/autonomous-swe.md +149 -0
- package/docs/reproduce.md +121 -0
- package/hooks/CLAUDE.md +28 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +133 -20
- package/package.json +15 -7
- package/scan/CLAUDE.md +30 -0
- package/skills/CLAUDE.md +1 -0
- package/state_store/ingest.py +18 -1
- package/state_store/projections.py +78 -4
- package/state_store/store.py +102 -7
- package/tools/CLAUDE.md +25 -18
- package/tools/alert_bridge.py +14 -10
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +215 -34
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +24 -21
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +359 -90
- package/tools/self_stats.py +292 -4
- package/tools/status.js +187 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_prboard.py +344 -0
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +2 -0
- package/ui/agents.py +332 -33
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +125 -32
- package/ui/config.py +22 -2
- package/ui/cost.py +98 -3
- package/ui/csrf.py +2 -1
- package/ui/handler.py +209 -21
- package/ui/sse.py +102 -16
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/web/.gitattributes +0 -13
- package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
- package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
- package/ui/web/index.html +0 -13
- package/ui/web/package-lock.json +0 -2225
- package/ui/web/package.json +0 -26
- package/ui/web/src/App.test.tsx +0 -74
- package/ui/web/src/App.tsx +0 -142
- package/ui/web/src/CONTRIBUTING-UI.md +0 -49
- package/ui/web/src/components/AgentRow.css +0 -187
- package/ui/web/src/components/AgentRow.test.tsx +0 -209
- package/ui/web/src/components/AgentRow.tsx +0 -207
- package/ui/web/src/components/AgentsPanel.css +0 -108
- package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
- package/ui/web/src/components/AgentsPanel.tsx +0 -58
- package/ui/web/src/components/AlertsPanel.css +0 -88
- package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
- package/ui/web/src/components/AlertsPanel.tsx +0 -67
- package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
- package/ui/web/src/components/BacklogPanel.tsx +0 -122
- package/ui/web/src/components/CostChart.css +0 -110
- package/ui/web/src/components/CostChart.test.tsx +0 -144
- package/ui/web/src/components/CostChart.tsx +0 -152
- package/ui/web/src/components/CostTable.css +0 -93
- package/ui/web/src/components/CostTable.test.tsx +0 -165
- package/ui/web/src/components/CostTable.tsx +0 -94
- package/ui/web/src/components/EventsFeed.css +0 -68
- package/ui/web/src/components/EventsFeed.test.tsx +0 -36
- package/ui/web/src/components/EventsFeed.tsx +0 -31
- package/ui/web/src/components/HealthHeader.css +0 -137
- package/ui/web/src/components/HealthHeader.test.tsx +0 -278
- package/ui/web/src/components/HealthHeader.tsx +0 -281
- package/ui/web/src/components/InboxForm.css +0 -135
- package/ui/web/src/components/InboxForm.test.tsx +0 -208
- package/ui/web/src/components/InboxForm.tsx +0 -116
- package/ui/web/src/components/MessagesTail.module.css +0 -144
- package/ui/web/src/components/MessagesTail.test.tsx +0 -176
- package/ui/web/src/components/MessagesTail.tsx +0 -94
- package/ui/web/src/components/ReposPanel.css +0 -90
- package/ui/web/src/components/ReposPanel.test.tsx +0 -45
- package/ui/web/src/components/ReposPanel.tsx +0 -67
- package/ui/web/src/components/Scorecard.css +0 -106
- package/ui/web/src/components/Scorecard.test.tsx +0 -117
- package/ui/web/src/components/Scorecard.tsx +0 -85
- package/ui/web/src/components/Timeline.module.css +0 -151
- package/ui/web/src/components/Timeline.test.tsx +0 -215
- package/ui/web/src/components/Timeline.tsx +0 -99
- package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
- package/ui/web/src/components/TrackerBoard.tsx +0 -107
- package/ui/web/src/components/TrackerCard.test.tsx +0 -180
- package/ui/web/src/components/TrackerCard.tsx +0 -160
- package/ui/web/src/components/TrackerForm.test.tsx +0 -189
- package/ui/web/src/components/TrackerForm.tsx +0 -144
- package/ui/web/src/lib/api.ts +0 -218
- package/ui/web/src/lib/format.test.ts +0 -89
- package/ui/web/src/lib/format.ts +0 -103
- package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
- package/ui/web/src/lib/sanitizeUrl.ts +0 -38
- package/ui/web/src/lib/types.ts +0 -230
- package/ui/web/src/lib/useHashRoute.test.ts +0 -60
- package/ui/web/src/lib/useHashRoute.ts +0 -23
- package/ui/web/src/lib/useSSE.ts +0 -175
- package/ui/web/src/main.tsx +0 -10
- package/ui/web/src/styles/global.css +0 -179
- package/ui/web/src/styles/theme.css +0 -184
- package/ui/web/src/styles/work.css +0 -572
- package/ui/web/src/test/fixtures.ts +0 -385
- package/ui/web/src/test/setup.ts +0 -49
- package/ui/web/src/views/Activity.module.css +0 -43
- package/ui/web/src/views/Activity.test.tsx +0 -89
- package/ui/web/src/views/Activity.tsx +0 -31
- package/ui/web/src/views/Cost.css +0 -87
- package/ui/web/src/views/Cost.test.tsx +0 -142
- package/ui/web/src/views/Cost.tsx +0 -54
- package/ui/web/src/views/Overview.css +0 -51
- package/ui/web/src/views/Overview.test.tsx +0 -76
- package/ui/web/src/views/Overview.tsx +0 -46
- package/ui/web/src/views/Work.test.tsx +0 -82
- package/ui/web/src/views/Work.tsx +0 -79
- package/ui/web/src/vite-env.d.ts +0 -10
- package/ui/web/tsconfig.json +0 -22
- package/ui/web/vite.config.ts +0 -25
- package/ui/web/vitest.config.ts +0 -12
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
bench_runner.py — Held-out benchmark scorer for fleet subagent capability claims.
|
|
4
|
+
|
|
5
|
+
Wave-26 context: an external critique observed that the claim "Haiku is sufficient
|
|
6
|
+
for fleet subagent work" was unmeasured and, where it *was* measured at all, was
|
|
7
|
+
self-graded by the fleet (agents grading agents) with results parked in a private
|
|
8
|
+
MEMORY.md. That is not evidence. This module is the MEASUREMENT APPARATUS, not a
|
|
9
|
+
verdict: it loads a fixed set of held-out tasks (bench/tasks.jsonl), scores a
|
|
10
|
+
model runner's outputs against externally-fixed ground truth (bench/ground_truth.jsonl)
|
|
11
|
+
by exact string match or regex match, and prints a per-model accuracy table.
|
|
12
|
+
|
|
13
|
+
It ships with a MOCK runner (`mock_runner`) so the SCORING LOGIC can be tested and
|
|
14
|
+
demonstrated completely offline, without calling any model or spending any tokens.
|
|
15
|
+
The mock runner is a small deterministic heuristic (regex/string-parsing stand-in).
|
|
16
|
+
Its accuracy says NOTHING about how Haiku, Sonnet, or Opus would actually score —
|
|
17
|
+
see bench/README.md for how to wire a real model runner.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python tools/bench_runner.py # run the mock runner, print table
|
|
21
|
+
python tools/bench_runner.py --runner mock
|
|
22
|
+
python tools/bench_runner.py --tasks path/to/tasks.jsonl --ground-truth path/to/gt.jsonl
|
|
23
|
+
|
|
24
|
+
Cost axis (wave-32): accuracy alone can't settle "is Haiku good enough" — the
|
|
25
|
+
whole cost thesis is "equal quality at ~1/3 the cost," which requires reporting
|
|
26
|
+
cost *alongside* accuracy. A runner may therefore return either a bare string
|
|
27
|
+
(text only, backward-compatible) or a `(text, usage)` pair, where usage carries
|
|
28
|
+
a token count and/or a latency figure. run_bench() records per-task usage,
|
|
29
|
+
summarize_cost() aggregates it, and the tables print accuracy and cost together.
|
|
30
|
+
This module still never calls a real model or spends a token; usage is supplied
|
|
31
|
+
by whatever runner the caller wires in.
|
|
32
|
+
|
|
33
|
+
Exit codes: 0 always on a completed run (this is a measurement tool, not a gate).
|
|
34
|
+
Programmatic API: load_tasks(), load_ground_truth(), score_output(), run_bench(),
|
|
35
|
+
normalize_runner_output(), summarize_cost(), build_summary(), print_comparison().
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import argparse
|
|
40
|
+
import json
|
|
41
|
+
import re
|
|
42
|
+
import sys
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
45
|
+
|
|
46
|
+
BENCH_DIR = Path(__file__).resolve().parent.parent / "bench"
|
|
47
|
+
DEFAULT_TASKS_PATH = BENCH_DIR / "tasks.jsonl"
|
|
48
|
+
DEFAULT_GROUND_TRUTH_PATH = BENCH_DIR / "ground_truth.jsonl"
|
|
49
|
+
|
|
50
|
+
REQUIRED_TASK_FIELDS = ("id", "category", "match", "prompt")
|
|
51
|
+
VALID_MATCH_TYPES = ("exact", "regex")
|
|
52
|
+
|
|
53
|
+
# A model runner is any callable that takes a prompt string and returns either:
|
|
54
|
+
# * the model's raw text response (a str), OR
|
|
55
|
+
# * a (text, usage) pair, where `usage` is a token/latency figure — a dict
|
|
56
|
+
# like {"tokens": 812, "latency_ms": 430.0}, or a bare int/float taken as a
|
|
57
|
+
# token count.
|
|
58
|
+
# The bare-str form is the original contract and stays fully supported; runners
|
|
59
|
+
# that don't measure cost simply keep returning a string. Real runners
|
|
60
|
+
# (Haiku/Sonnet/Opus/etc.) are wired by the caller; this module never imports a
|
|
61
|
+
# model SDK itself.
|
|
62
|
+
ModelRunner = Callable[[str], object]
|
|
63
|
+
|
|
64
|
+
# Normalized per-task usage record: either field may be None when the runner did
|
|
65
|
+
# not report it. A task where the runner returned a bare string has usage=None.
|
|
66
|
+
USAGE_KEYS = ("tokens", "latency_ms")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def normalize_runner_output(output: object) -> Tuple[Optional[str], Optional[dict]]:
|
|
70
|
+
"""Normalize a runner's return value into (text, usage).
|
|
71
|
+
|
|
72
|
+
Accepts, in order:
|
|
73
|
+
* str (or None) -> (output, None) [original contract]
|
|
74
|
+
* (text, usage) 2-tuple -> (text, normalized_usage)
|
|
75
|
+
where usage may be:
|
|
76
|
+
- a dict with any of USAGE_KEYS -> those fields (others None)
|
|
77
|
+
- an int/float -> {"tokens": int(usage), "latency_ms": None}
|
|
78
|
+
- None -> None
|
|
79
|
+
|
|
80
|
+
Any other shape is a runner bug and raises ValueError, so a malformed runner
|
|
81
|
+
fails loudly rather than silently scoring blank.
|
|
82
|
+
"""
|
|
83
|
+
if output is None or isinstance(output, str):
|
|
84
|
+
return output, None
|
|
85
|
+
if isinstance(output, tuple):
|
|
86
|
+
if len(output) != 2:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"runner returned a {len(output)}-tuple; expected a bare str or a "
|
|
89
|
+
f"(text, usage) 2-tuple"
|
|
90
|
+
)
|
|
91
|
+
text, usage = output
|
|
92
|
+
if text is not None and not isinstance(text, str):
|
|
93
|
+
raise ValueError(f"runner (text, usage) tuple has non-str text: {type(text).__name__}")
|
|
94
|
+
return text, _normalize_usage(usage)
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"runner returned unsupported type {type(output).__name__}; expected a bare "
|
|
97
|
+
f"str or a (text, usage) 2-tuple"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _normalize_usage(usage: object) -> Optional[dict]:
|
|
102
|
+
if usage is None:
|
|
103
|
+
return None
|
|
104
|
+
if isinstance(usage, bool):
|
|
105
|
+
# bool is an int subclass; a bool token count is almost certainly a bug.
|
|
106
|
+
raise ValueError("usage token count must be a number, not a bool")
|
|
107
|
+
if isinstance(usage, (int, float)):
|
|
108
|
+
return {"tokens": int(usage), "latency_ms": None}
|
|
109
|
+
if isinstance(usage, dict):
|
|
110
|
+
return {
|
|
111
|
+
"tokens": usage.get("tokens"),
|
|
112
|
+
"latency_ms": usage.get("latency_ms"),
|
|
113
|
+
}
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"usage must be a dict, an int/float token count, or None; got {type(usage).__name__}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def load_jsonl(path: Path) -> List[dict]:
|
|
120
|
+
"""Load a JSON-Lines file into a list of dicts. Blank lines are skipped."""
|
|
121
|
+
items: List[dict] = []
|
|
122
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
123
|
+
for lineno, raw_line in enumerate(f, start=1):
|
|
124
|
+
line = raw_line.strip()
|
|
125
|
+
if not line:
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
items.append(json.loads(line))
|
|
129
|
+
except json.JSONDecodeError as exc:
|
|
130
|
+
raise ValueError(f"{path}:{lineno}: invalid JSON: {exc}") from exc
|
|
131
|
+
return items
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_tasks(tasks_path: Optional[Path] = None) -> List[dict]:
|
|
135
|
+
"""Load the task list and validate required fields are present."""
|
|
136
|
+
path = Path(tasks_path) if tasks_path else DEFAULT_TASKS_PATH
|
|
137
|
+
tasks = load_jsonl(path)
|
|
138
|
+
for task in tasks:
|
|
139
|
+
missing = [f for f in REQUIRED_TASK_FIELDS if f not in task]
|
|
140
|
+
if missing:
|
|
141
|
+
raise ValueError(f"task {task.get('id', '<unknown>')} missing fields: {missing}")
|
|
142
|
+
if task["match"] not in VALID_MATCH_TYPES:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"task {task['id']} has invalid match type {task['match']!r}; "
|
|
145
|
+
f"expected one of {VALID_MATCH_TYPES}"
|
|
146
|
+
)
|
|
147
|
+
return tasks
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_ground_truth(ground_truth_path: Optional[Path] = None) -> Dict[str, dict]:
|
|
151
|
+
"""Load ground truth entries keyed by task id."""
|
|
152
|
+
path = Path(ground_truth_path) if ground_truth_path else DEFAULT_GROUND_TRUTH_PATH
|
|
153
|
+
entries = load_jsonl(path)
|
|
154
|
+
by_id: Dict[str, dict] = {}
|
|
155
|
+
for entry in entries:
|
|
156
|
+
if "id" not in entry:
|
|
157
|
+
raise ValueError(f"ground truth entry missing 'id': {entry}")
|
|
158
|
+
by_id[entry["id"]] = entry
|
|
159
|
+
return by_id
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def score_output(output: Optional[str], ground_truth_entry: dict, match_type: str) -> bool:
|
|
163
|
+
"""Score a single model output against its ground truth entry.
|
|
164
|
+
|
|
165
|
+
exact: case-insensitive, whitespace-trimmed string equality against
|
|
166
|
+
ground_truth_entry["expected"].
|
|
167
|
+
regex: re.search of ground_truth_entry["expected_regex"] against the
|
|
168
|
+
trimmed output (search, not fullmatch — callers anchor with ^/$
|
|
169
|
+
in the pattern when they need a full-string match).
|
|
170
|
+
"""
|
|
171
|
+
if output is None:
|
|
172
|
+
return False
|
|
173
|
+
trimmed = output.strip()
|
|
174
|
+
if match_type == "regex":
|
|
175
|
+
pattern = ground_truth_entry.get("expected_regex")
|
|
176
|
+
if pattern is None:
|
|
177
|
+
raise ValueError("ground truth entry for a regex task is missing 'expected_regex'")
|
|
178
|
+
return re.search(pattern, trimmed) is not None
|
|
179
|
+
if match_type == "exact":
|
|
180
|
+
expected = ground_truth_entry.get("expected")
|
|
181
|
+
if expected is None:
|
|
182
|
+
raise ValueError("ground truth entry for an exact task is missing 'expected'")
|
|
183
|
+
return trimmed.lower() == str(expected).strip().lower()
|
|
184
|
+
raise ValueError(f"unknown match type: {match_type!r}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def run_bench(
|
|
188
|
+
tasks: List[dict],
|
|
189
|
+
ground_truth: Dict[str, dict],
|
|
190
|
+
runner: ModelRunner,
|
|
191
|
+
) -> Tuple[List[dict], float]:
|
|
192
|
+
"""Run every task through `runner`, score against `ground_truth`.
|
|
193
|
+
|
|
194
|
+
Returns (per_task_results, accuracy) where accuracy is correct/total
|
|
195
|
+
(0.0 for an empty task list, never a division error).
|
|
196
|
+
"""
|
|
197
|
+
results: List[dict] = []
|
|
198
|
+
correct = 0
|
|
199
|
+
for task in tasks:
|
|
200
|
+
tid = task["id"]
|
|
201
|
+
if tid not in ground_truth:
|
|
202
|
+
raise KeyError(f"no ground truth entry for task id {tid!r}")
|
|
203
|
+
gt_entry = ground_truth[tid]
|
|
204
|
+
text, usage = normalize_runner_output(runner(task["prompt"]))
|
|
205
|
+
ok = score_output(text, gt_entry, task["match"])
|
|
206
|
+
if ok:
|
|
207
|
+
correct += 1
|
|
208
|
+
results.append(
|
|
209
|
+
{
|
|
210
|
+
"id": tid,
|
|
211
|
+
"category": task.get("category", ""),
|
|
212
|
+
"output": text,
|
|
213
|
+
"correct": ok,
|
|
214
|
+
# Cost axis: None when the runner returned a bare string.
|
|
215
|
+
"usage": usage,
|
|
216
|
+
"tokens": usage.get("tokens") if usage else None,
|
|
217
|
+
"latency_ms": usage.get("latency_ms") if usage else None,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
accuracy = correct / len(tasks) if tasks else 0.0
|
|
221
|
+
return results, accuracy
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def summarize_cost(results: List[dict]) -> dict:
|
|
225
|
+
"""Aggregate the per-task cost axis recorded by run_bench.
|
|
226
|
+
|
|
227
|
+
Returns a dict with total/average tokens and latency across the tasks that
|
|
228
|
+
reported them. Fields are None when no task reported that figure, so a
|
|
229
|
+
string-only (cost-free) run summarizes cleanly to all-None rather than 0.
|
|
230
|
+
"""
|
|
231
|
+
token_vals = [r["tokens"] for r in results if r.get("tokens") is not None]
|
|
232
|
+
latency_vals = [r["latency_ms"] for r in results if r.get("latency_ms") is not None]
|
|
233
|
+
n = len(results)
|
|
234
|
+
|
|
235
|
+
def _avg(vals):
|
|
236
|
+
return (sum(vals) / len(vals)) if vals else None
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"n_tasks": n,
|
|
240
|
+
"n_with_tokens": len(token_vals),
|
|
241
|
+
"n_with_latency": len(latency_vals),
|
|
242
|
+
"total_tokens": sum(token_vals) if token_vals else None,
|
|
243
|
+
"avg_tokens": _avg(token_vals),
|
|
244
|
+
"total_latency_ms": sum(latency_vals) if latency_vals else None,
|
|
245
|
+
"avg_latency_ms": _avg(latency_vals),
|
|
246
|
+
"has_cost": bool(token_vals or latency_vals),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_summary(model_name: str, results: List[dict], accuracy: float) -> dict:
|
|
251
|
+
"""Combine accuracy and cost into one per-model row for a comparison table."""
|
|
252
|
+
n = len(results)
|
|
253
|
+
n_correct = sum(1 for r in results if r["correct"])
|
|
254
|
+
row = {
|
|
255
|
+
"model": model_name,
|
|
256
|
+
"n_tasks": n,
|
|
257
|
+
"n_correct": n_correct,
|
|
258
|
+
"accuracy": accuracy,
|
|
259
|
+
}
|
|
260
|
+
row.update(summarize_cost(results))
|
|
261
|
+
return row
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Mock runner — zero-cost, deterministic, offline. Exists ONLY to prove the
|
|
266
|
+
# scoring pipeline works end to end without a network call or an API key.
|
|
267
|
+
# Do not read its accuracy as evidence about any real model's capability.
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
_FILE_RE = re.compile(r"File:\s*(\S+)")
|
|
271
|
+
_FAILED_TEST_RE = re.compile(r"FAILED\s+\S+::(\w+)")
|
|
272
|
+
_ISSUE_NUM_RE = re.compile(r"#(\d+)")
|
|
273
|
+
_PR_TITLE_RE = re.compile(r"Title:\s*(\w+):")
|
|
274
|
+
_EXC_TYPE_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*(?:Error|Exception)):")
|
|
275
|
+
_VERSION_RE = re.compile(r"\[(\d+\.\d+\.\d+)\]")
|
|
276
|
+
_IDENTIFIER_RE = re.compile(r"Identifier:\s*(\S+)")
|
|
277
|
+
_FILE_LINE_RE = re.compile(r"([A-Za-z0-9_\-./]+\.py:\d+)")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _classify_file_path(path: str) -> str:
|
|
281
|
+
filename = path.rsplit("/", 1)[-1]
|
|
282
|
+
if path.startswith("tests/") or "/tests/" in path or filename.startswith("test_"):
|
|
283
|
+
return "test"
|
|
284
|
+
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
|
|
285
|
+
if ext in {"md", "rst", "txt"}:
|
|
286
|
+
return "docs"
|
|
287
|
+
if ext in {"json", "yaml", "yml", "toml", "ini", "cfg"}:
|
|
288
|
+
return "config"
|
|
289
|
+
return "code"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _snake_to_camel(identifier: str) -> str:
|
|
293
|
+
parts = identifier.split("_")
|
|
294
|
+
if not parts:
|
|
295
|
+
return identifier
|
|
296
|
+
return parts[0] + "".join(p.capitalize() for p in parts[1:])
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def mock_runner(prompt: str) -> str:
|
|
300
|
+
"""Deterministic offline stand-in for a real model runner.
|
|
301
|
+
|
|
302
|
+
Handles extraction/classification/transform tasks with plain regex and
|
|
303
|
+
string logic (no judgment required). Deliberately does NOT attempt the
|
|
304
|
+
semantic "is this a real bug" judgment task and always answers "no" for
|
|
305
|
+
it — a cheap default that is sometimes wrong, so the scorer has at least
|
|
306
|
+
one guaranteed miss to prove it distinguishes correct from incorrect
|
|
307
|
+
(a mock that always scored 100% would not exercise the failure path).
|
|
308
|
+
"""
|
|
309
|
+
m = _FILE_RE.search(prompt)
|
|
310
|
+
if m and "Classify this changed file path" in prompt:
|
|
311
|
+
return _classify_file_path(m.group(1))
|
|
312
|
+
|
|
313
|
+
m = _FAILED_TEST_RE.search(prompt)
|
|
314
|
+
if m and "failing test function" in prompt:
|
|
315
|
+
return m.group(1)
|
|
316
|
+
|
|
317
|
+
if "issue/PR number" in prompt:
|
|
318
|
+
m = _ISSUE_NUM_RE.search(prompt)
|
|
319
|
+
if m:
|
|
320
|
+
return m.group(1)
|
|
321
|
+
|
|
322
|
+
m = _PR_TITLE_RE.search(prompt)
|
|
323
|
+
if m and "PR title" in prompt:
|
|
324
|
+
return m.group(1)
|
|
325
|
+
|
|
326
|
+
if "exception class name" in prompt:
|
|
327
|
+
matches = _EXC_TYPE_RE.findall(prompt)
|
|
328
|
+
if matches:
|
|
329
|
+
return matches[-1]
|
|
330
|
+
|
|
331
|
+
if "is the finding a real bug" in prompt:
|
|
332
|
+
return "no"
|
|
333
|
+
|
|
334
|
+
m = _VERSION_RE.search(prompt)
|
|
335
|
+
if m and "semantic version" in prompt:
|
|
336
|
+
return m.group(1)
|
|
337
|
+
|
|
338
|
+
m = _IDENTIFIER_RE.search(prompt)
|
|
339
|
+
if m and "camelCase" in prompt:
|
|
340
|
+
return _snake_to_camel(m.group(1))
|
|
341
|
+
|
|
342
|
+
m = _FILE_LINE_RE.search(prompt)
|
|
343
|
+
if m and "path:line" in prompt:
|
|
344
|
+
return m.group(1)
|
|
345
|
+
|
|
346
|
+
return ""
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
RUNNERS: Dict[str, ModelRunner] = {
|
|
350
|
+
"mock": mock_runner,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _fmt(value, suffix: str = "") -> str:
|
|
355
|
+
return "-" if value is None else f"{value:g}{suffix}"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def print_table(model_name: str, results: List[dict], accuracy: float, stream=None) -> None:
|
|
359
|
+
stream = stream or sys.stdout
|
|
360
|
+
cost = summarize_cost(results)
|
|
361
|
+
show_cost = cost["has_cost"]
|
|
362
|
+
print(f"\nBenchmark results -- runner: {model_name}", file=stream)
|
|
363
|
+
print("-" * 72, file=stream)
|
|
364
|
+
if show_cost:
|
|
365
|
+
print(f"{'id':<6}{'category':<28}{'result':<8}{'tokens':<10}{'latency_ms':<12}", file=stream)
|
|
366
|
+
for r in results:
|
|
367
|
+
mark = "PASS" if r["correct"] else "FAIL"
|
|
368
|
+
print(
|
|
369
|
+
f"{r['id']:<6}{r['category']:<28}{mark:<8}"
|
|
370
|
+
f"{_fmt(r.get('tokens')):<10}{_fmt(r.get('latency_ms')):<12}",
|
|
371
|
+
file=stream,
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
print(f"{'id':<6}{'category':<28}{'result':<8}", file=stream)
|
|
375
|
+
for r in results:
|
|
376
|
+
mark = "PASS" if r["correct"] else "FAIL"
|
|
377
|
+
print(f"{r['id']:<6}{r['category']:<28}{mark:<8}", file=stream)
|
|
378
|
+
print("-" * 72, file=stream)
|
|
379
|
+
n = len(results)
|
|
380
|
+
n_correct = sum(1 for r in results if r["correct"])
|
|
381
|
+
print(f"Accuracy: {n_correct}/{n} = {accuracy:.1%}", file=stream)
|
|
382
|
+
if show_cost:
|
|
383
|
+
# Cost axis printed right under accuracy so the two are read together.
|
|
384
|
+
print(
|
|
385
|
+
f"Cost: total_tokens={_fmt(cost['total_tokens'])} "
|
|
386
|
+
f"avg_tokens/task={_fmt(round(cost['avg_tokens'], 1) if cost['avg_tokens'] is not None else None)} "
|
|
387
|
+
f"total_latency_ms={_fmt(cost['total_latency_ms'])} "
|
|
388
|
+
f"avg_latency_ms/task={_fmt(round(cost['avg_latency_ms'], 1) if cost['avg_latency_ms'] is not None else None)}",
|
|
389
|
+
file=stream,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def print_comparison(summaries: List[dict], stream=None) -> None:
|
|
394
|
+
"""Print accuracy AND cost side by side across models.
|
|
395
|
+
|
|
396
|
+
Each element of `summaries` is a build_summary() row. This is the table that
|
|
397
|
+
lets "equal accuracy at a fraction of the cost" be shown rather than
|
|
398
|
+
asserted: models line up in rows, accuracy and token/latency cost in columns.
|
|
399
|
+
"""
|
|
400
|
+
stream = stream or sys.stdout
|
|
401
|
+
print("\nModel comparison -- accuracy vs cost", file=stream)
|
|
402
|
+
print("-" * 72, file=stream)
|
|
403
|
+
print(
|
|
404
|
+
f"{'model':<12}{'accuracy':<12}{'avg_tokens':<12}{'total_tokens':<14}{'avg_latency_ms':<16}",
|
|
405
|
+
file=stream,
|
|
406
|
+
)
|
|
407
|
+
for s in summaries:
|
|
408
|
+
acc = f"{s['n_correct']}/{s['n_tasks']}={s['accuracy']:.0%}"
|
|
409
|
+
avg_tok = _fmt(round(s["avg_tokens"], 1) if s.get("avg_tokens") is not None else None)
|
|
410
|
+
tot_tok = _fmt(s.get("total_tokens"))
|
|
411
|
+
avg_lat = _fmt(round(s["avg_latency_ms"], 1) if s.get("avg_latency_ms") is not None else None)
|
|
412
|
+
print(f"{s['model']:<12}{acc:<12}{avg_tok:<12}{tot_tok:<14}{avg_lat:<16}", file=stream)
|
|
413
|
+
print("-" * 72, file=stream)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
417
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
418
|
+
parser.add_argument(
|
|
419
|
+
"--runner",
|
|
420
|
+
default="mock",
|
|
421
|
+
choices=sorted(RUNNERS.keys()),
|
|
422
|
+
help="Which registered model runner to score (default: mock, offline/zero-cost).",
|
|
423
|
+
)
|
|
424
|
+
parser.add_argument("--tasks", default=None, help="Override path to tasks.jsonl")
|
|
425
|
+
parser.add_argument("--ground-truth", default=None, help="Override path to ground_truth.jsonl")
|
|
426
|
+
args = parser.parse_args(argv)
|
|
427
|
+
|
|
428
|
+
tasks = load_tasks(args.tasks)
|
|
429
|
+
ground_truth = load_ground_truth(args.ground_truth)
|
|
430
|
+
runner = RUNNERS[args.runner]
|
|
431
|
+
|
|
432
|
+
results, accuracy = run_bench(tasks, ground_truth, runner)
|
|
433
|
+
print_table(args.runner, results, accuracy)
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
if __name__ == "__main__":
|
|
438
|
+
sys.exit(main())
|
package/tools/buildlog.py
CHANGED
|
@@ -33,13 +33,10 @@ import sys
|
|
|
33
33
|
from datetime import datetime
|
|
34
34
|
from pathlib import Path
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return Path(os.environ["AESOP_STATE_ROOT"])
|
|
41
|
-
# Default to ./state (relative to cwd)
|
|
42
|
-
return Path.cwd() / "state"
|
|
36
|
+
try:
|
|
37
|
+
from common import get_state_dir
|
|
38
|
+
except ImportError:
|
|
39
|
+
from tools.common import get_state_dir
|
|
43
40
|
|
|
44
41
|
|
|
45
42
|
def get_git_head(repo_path):
|