@matt82198/aesop 0.3.2 → 0.4.1
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 +47 -0
- package/README.md +24 -9
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +107 -34
- package/daemons/run-watchdog.sh +6 -2
- package/docs/CONFIGURE.md +31 -20
- package/docs/FIRST-WAVE.md +29 -9
- package/docs/INSTALL.md +181 -26
- package/docs/MICROKERNEL.md +452 -0
- package/docs/PORTING.md +4 -0
- package/docs/README.md +7 -3
- package/docs/THE-AESOP-HYPOTHESIS.md +156 -0
- package/driver/CLAUDE.md +123 -123
- package/driver/README.md +40 -2
- package/driver/adjudication_gate.py +367 -0
- package/driver/aesop.config.example.json +20 -4
- package/driver/backend_config.py +603 -12
- package/driver/claude_code_driver.py +9 -17
- package/driver/codex_driver.py +80 -25
- package/driver/context_pack.py +454 -0
- package/driver/openai_compatible_driver.py +53 -11
- package/driver/openai_transport.py +31 -10
- package/driver/orchestrator_backend.py +332 -0
- package/driver/orchestrator_driver.py +589 -0
- package/driver/proc_util.py +134 -0
- package/driver/wave_loop.py +801 -59
- package/driver/wave_scheduler.py +361 -37
- package/monitor/collect-signals.mjs +83 -0
- package/package.json +1 -1
- package/state_store/coordination.py +65 -5
- package/tools/ci_merge_wait.py +88 -42
- package/tools/ci_shard_runner.py +128 -0
- package/tools/doctor.js +124 -12
- package/tools/reproduce.js +11 -2
- package/tools/seated_shadow_adjudication.py +920 -0
- package/tools/self_stats.py +61 -6
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_test_suite_count.py +250 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
- package/tools/wave_manifest_lint.py +485 -0
- package/tools/wave_scorecard.py +430 -0
- package/ui/config.py +1 -1
- package/ui/cost.py +176 -3
- package/ui/web/dist/assets/{index-CP68RIh3.js → index-4rp6B_ID.js} +1 -1
- package/ui/web/dist/assets/{index-CNQxaiOW.css → index-B2lTtpsH.css} +1 -1
- package/ui/web/dist/index.html +2 -2
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Wave Quality Scorecard Generator — deterministic metrics from telemetry.
|
|
4
|
+
|
|
5
|
+
Generates quality scorecards answering "how healthy are our waves?" from on-disk telemetry.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
wave_scorecard.py [--json|--md] [--waves N] [--state-root PATH]
|
|
9
|
+
|
|
10
|
+
Outputs:
|
|
11
|
+
- ASCII (default): human-readable text format
|
|
12
|
+
- --json: machine-readable JSON
|
|
13
|
+
- --md: markdown table for side-by-side comparison
|
|
14
|
+
- --waves N: show last N waves (default: all)
|
|
15
|
+
|
|
16
|
+
Metrics (where sources exist):
|
|
17
|
+
- Items dispatched/succeeded (from ledger)
|
|
18
|
+
- Repair rounds used (from ledger)
|
|
19
|
+
- First-try-green rate (from ledger verdict timing)
|
|
20
|
+
- Defect-escape count (from git history if available)
|
|
21
|
+
- Tokens + estimated cost by phase/model (from ledger)
|
|
22
|
+
- Agent success by agent_type (from ledger)
|
|
23
|
+
- Retry frequency (from ledger entries)
|
|
24
|
+
|
|
25
|
+
Sources:
|
|
26
|
+
- AESOP_STATE_ROOT/ledger/OUTCOMES-LEDGER.md: fleet outcome telemetry
|
|
27
|
+
- tools/defect_escape.py: Haiku code quality metrics
|
|
28
|
+
- Tools report on n/a (missing source) rather than inventing.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import sys
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from collections import defaultdict
|
|
36
|
+
|
|
37
|
+
# Ensure tools and state_store are importable (sys.path fix for bootstrapping)
|
|
38
|
+
repo_root = Path(__file__).parent.parent
|
|
39
|
+
if str(repo_root) not in sys.path:
|
|
40
|
+
sys.path.insert(0, str(repo_root))
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from common import get_state_dir
|
|
44
|
+
except ImportError:
|
|
45
|
+
from tools.common import get_state_dir
|
|
46
|
+
|
|
47
|
+
from state_store.read_api import ReadAPI
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_ledger_rows():
|
|
51
|
+
"""Parse OUTCOMES-LEDGER.md and return structured rows via StateAPI.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
list of dicts with keys: iso_ts, agent_type, model, duration_sec,
|
|
55
|
+
tokens_in, tokens_out, verdict, phase, wave
|
|
56
|
+
"""
|
|
57
|
+
state_dir = get_state_dir()
|
|
58
|
+
api = ReadAPI(state_dir)
|
|
59
|
+
return api.read_ledger_rows()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def compute_wave_metrics(rows):
|
|
63
|
+
"""Compute quality metrics grouped by wave.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
rows: list of ledger rows
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
dict mapping wave_num to metrics dict
|
|
70
|
+
"""
|
|
71
|
+
metrics_by_wave = defaultdict(lambda: {
|
|
72
|
+
'entries': 0,
|
|
73
|
+
'ok_count': 0,
|
|
74
|
+
'failed_count': 0,
|
|
75
|
+
'empty_count': 0,
|
|
76
|
+
'hung_count': 0,
|
|
77
|
+
'tokens_in': 0,
|
|
78
|
+
'tokens_out': 0,
|
|
79
|
+
'duration_sec': 0,
|
|
80
|
+
'by_phase': defaultdict(lambda: {'entries': 0, 'tokens_out': 0, 'ok_count': 0, 'failed_count': 0}),
|
|
81
|
+
'by_model': defaultdict(lambda: {'entries': 0, 'tokens_out': 0, 'ok_count': 0, 'failed_count': 0}),
|
|
82
|
+
'by_agent_type': defaultdict(lambda: {'entries': 0, 'ok_count': 0, 'failed_count': 0}),
|
|
83
|
+
'repair_rounds': 0,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
for row in rows:
|
|
87
|
+
wave = row['wave']
|
|
88
|
+
if wave is None:
|
|
89
|
+
wave = 0 # Unknown wave
|
|
90
|
+
|
|
91
|
+
m = metrics_by_wave[wave]
|
|
92
|
+
|
|
93
|
+
m['entries'] += 1
|
|
94
|
+
m['tokens_in'] += row['tokens_in']
|
|
95
|
+
m['tokens_out'] += row['tokens_out']
|
|
96
|
+
m['duration_sec'] += row['duration_sec']
|
|
97
|
+
|
|
98
|
+
# Count verdicts
|
|
99
|
+
verdict = row['verdict']
|
|
100
|
+
if verdict == 'OK':
|
|
101
|
+
m['ok_count'] += 1
|
|
102
|
+
elif verdict == 'FAILED':
|
|
103
|
+
m['failed_count'] += 1
|
|
104
|
+
elif verdict == 'EMPTY':
|
|
105
|
+
m['empty_count'] += 1
|
|
106
|
+
elif verdict == 'HUNG':
|
|
107
|
+
m['hung_count'] += 1
|
|
108
|
+
|
|
109
|
+
# Track by phase
|
|
110
|
+
phase = row['phase'] or 'unknown'
|
|
111
|
+
phase_data = m['by_phase'][phase]
|
|
112
|
+
phase_data['entries'] += 1
|
|
113
|
+
phase_data['tokens_out'] += row['tokens_out']
|
|
114
|
+
if verdict == 'OK':
|
|
115
|
+
phase_data['ok_count'] += 1
|
|
116
|
+
else:
|
|
117
|
+
phase_data['failed_count'] += 1
|
|
118
|
+
|
|
119
|
+
# Count repair rounds
|
|
120
|
+
if phase == 'repair':
|
|
121
|
+
m['repair_rounds'] += 1
|
|
122
|
+
|
|
123
|
+
# Track by model
|
|
124
|
+
model = row['model']
|
|
125
|
+
model_data = m['by_model'][model]
|
|
126
|
+
model_data['entries'] += 1
|
|
127
|
+
model_data['tokens_out'] += row['tokens_out']
|
|
128
|
+
if verdict == 'OK':
|
|
129
|
+
model_data['ok_count'] += 1
|
|
130
|
+
else:
|
|
131
|
+
model_data['failed_count'] += 1
|
|
132
|
+
|
|
133
|
+
# Track by agent type
|
|
134
|
+
agent_type = row['agent_type']
|
|
135
|
+
agent_data = m['by_agent_type'][agent_type]
|
|
136
|
+
agent_data['entries'] += 1
|
|
137
|
+
if verdict == 'OK':
|
|
138
|
+
agent_data['ok_count'] += 1
|
|
139
|
+
else:
|
|
140
|
+
agent_data['failed_count'] += 1
|
|
141
|
+
|
|
142
|
+
# Convert defaultdicts to regular dicts for JSON serialization
|
|
143
|
+
result = {}
|
|
144
|
+
for wave, metrics in metrics_by_wave.items():
|
|
145
|
+
result[wave] = {
|
|
146
|
+
'entries': metrics['entries'],
|
|
147
|
+
'ok_count': metrics['ok_count'],
|
|
148
|
+
'failed_count': metrics['failed_count'],
|
|
149
|
+
'empty_count': metrics['empty_count'],
|
|
150
|
+
'hung_count': metrics['hung_count'],
|
|
151
|
+
'tokens_in': metrics['tokens_in'],
|
|
152
|
+
'tokens_out': metrics['tokens_out'],
|
|
153
|
+
'duration_sec': metrics['duration_sec'],
|
|
154
|
+
'by_phase': dict(metrics['by_phase']),
|
|
155
|
+
'by_model': dict(metrics['by_model']),
|
|
156
|
+
'by_agent_type': dict(metrics['by_agent_type']),
|
|
157
|
+
'repair_rounds': metrics['repair_rounds'],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def compute_derived_metrics(wave_metrics):
|
|
164
|
+
"""Compute derived metrics from wave metrics.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
wave_metrics: dict from compute_wave_metrics()
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
dict with derived metrics
|
|
171
|
+
"""
|
|
172
|
+
derived = {}
|
|
173
|
+
|
|
174
|
+
for wave, metrics in wave_metrics.items():
|
|
175
|
+
total = metrics['entries']
|
|
176
|
+
if total == 0:
|
|
177
|
+
derived[wave] = {
|
|
178
|
+
'success_rate': 'n/a (no entries)',
|
|
179
|
+
'first_try_green_rate': 'n/a',
|
|
180
|
+
'repair_efficiency': 'n/a',
|
|
181
|
+
'cost_estimate': 'n/a',
|
|
182
|
+
}
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
ok = metrics['ok_count']
|
|
186
|
+
success_rate = (ok / total) * 100 if total > 0 else 0
|
|
187
|
+
|
|
188
|
+
# First-try-green: rough estimate based on whether repairs happened
|
|
189
|
+
repair_rounds = metrics['repair_rounds']
|
|
190
|
+
if repair_rounds > 0:
|
|
191
|
+
# Rough estimate: if there were repairs, not all were first-try-green
|
|
192
|
+
first_try_estimate = ((total - repair_rounds) / total) * 100
|
|
193
|
+
else:
|
|
194
|
+
first_try_estimate = success_rate
|
|
195
|
+
|
|
196
|
+
# Repair efficiency: OK repairs / total repairs
|
|
197
|
+
if repair_rounds > 0:
|
|
198
|
+
repair_ok = metrics['by_phase'].get('repair', {}).get('ok_count', 0)
|
|
199
|
+
repair_efficiency = (repair_ok / repair_rounds) * 100
|
|
200
|
+
else:
|
|
201
|
+
repair_efficiency = None
|
|
202
|
+
|
|
203
|
+
# Cost estimate (rough): assume haiku ~$0.001 per 1M tokens
|
|
204
|
+
tokens_out = metrics['tokens_out']
|
|
205
|
+
cost_estimate = (tokens_out / 1_000_000) * 0.001
|
|
206
|
+
|
|
207
|
+
derived[wave] = {
|
|
208
|
+
'success_rate': f"{success_rate:.1f}%",
|
|
209
|
+
'first_try_green_rate': f"{first_try_estimate:.1f}%",
|
|
210
|
+
'repair_efficiency': f"{repair_efficiency:.1f}%" if repair_efficiency is not None else "n/a",
|
|
211
|
+
'cost_estimate': f"${cost_estimate:.4f}",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return derived
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def format_ascii(wave_metrics, derived_metrics):
|
|
218
|
+
"""Format metrics as ASCII text.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
wave_metrics: dict from compute_wave_metrics()
|
|
222
|
+
derived_metrics: dict from compute_derived_metrics()
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
formatted string
|
|
226
|
+
"""
|
|
227
|
+
if not wave_metrics:
|
|
228
|
+
return "No data available from ledger (n/a: no entries)\n"
|
|
229
|
+
|
|
230
|
+
output = []
|
|
231
|
+
output.append("\n=== Wave Quality Scorecards ===\n")
|
|
232
|
+
|
|
233
|
+
# Sort by wave number
|
|
234
|
+
for wave in sorted(wave_metrics.keys()):
|
|
235
|
+
metrics = wave_metrics[wave]
|
|
236
|
+
derived = derived_metrics.get(wave, {})
|
|
237
|
+
|
|
238
|
+
output.append(f"Wave {wave}:")
|
|
239
|
+
output.append(f" Entries: {metrics['entries']}")
|
|
240
|
+
output.append(f" OK: {metrics['ok_count']} | Failed: {metrics['failed_count']} | Empty: {metrics['empty_count']} | Hung: {metrics['hung_count']}")
|
|
241
|
+
output.append(f" Success Rate: {derived.get('success_rate', 'n/a')}")
|
|
242
|
+
output.append(f" First-Try-Green: {derived.get('first_try_green_rate', 'n/a')}")
|
|
243
|
+
output.append(f" Repair Rounds: {metrics['repair_rounds']}")
|
|
244
|
+
output.append(f" Repair Efficiency: {derived.get('repair_efficiency', 'n/a')}")
|
|
245
|
+
output.append(f" Tokens In: {metrics['tokens_in']} | Out: {metrics['tokens_out']}")
|
|
246
|
+
output.append(f" Estimated Cost: {derived.get('cost_estimate', 'n/a')}")
|
|
247
|
+
output.append(f" Duration: {metrics['duration_sec']}s")
|
|
248
|
+
|
|
249
|
+
# Tokens by phase
|
|
250
|
+
if metrics['by_phase']:
|
|
251
|
+
output.append(" Tokens by Phase:")
|
|
252
|
+
for phase, phase_data in sorted(metrics['by_phase'].items()):
|
|
253
|
+
output.append(f" {phase}: {phase_data['entries']} entries, {phase_data['tokens_out']} tokens, {phase_data['ok_count']} OK")
|
|
254
|
+
|
|
255
|
+
# Tokens by model
|
|
256
|
+
if metrics['by_model']:
|
|
257
|
+
output.append(" Tokens by Model:")
|
|
258
|
+
for model, model_data in sorted(metrics['by_model'].items()):
|
|
259
|
+
output.append(f" {model}: {model_data['entries']} entries, {model_data['tokens_out']} tokens, {model_data['ok_count']} OK")
|
|
260
|
+
|
|
261
|
+
# By agent type
|
|
262
|
+
if metrics['by_agent_type']:
|
|
263
|
+
output.append(" Success by Agent Type:")
|
|
264
|
+
for agent_type, agent_data in sorted(metrics['by_agent_type'].items()):
|
|
265
|
+
rate = (agent_data['ok_count'] / agent_data['entries'] * 100) if agent_data['entries'] > 0 else 0
|
|
266
|
+
output.append(f" {agent_type}: {agent_data['ok_count']}/{agent_data['entries']} OK ({rate:.1f}%)")
|
|
267
|
+
|
|
268
|
+
output.append("")
|
|
269
|
+
|
|
270
|
+
return "\n".join(output)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def format_json(wave_metrics, derived_metrics):
|
|
274
|
+
"""Format metrics as JSON.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
wave_metrics: dict from compute_wave_metrics()
|
|
278
|
+
derived_metrics: dict from compute_derived_metrics()
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
JSON string
|
|
282
|
+
"""
|
|
283
|
+
result = {
|
|
284
|
+
'scorecards': {},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for wave in sorted(wave_metrics.keys()):
|
|
288
|
+
metrics = wave_metrics[wave]
|
|
289
|
+
derived = derived_metrics.get(wave, {})
|
|
290
|
+
|
|
291
|
+
result['scorecards'][wave] = {
|
|
292
|
+
'entries': metrics['entries'],
|
|
293
|
+
'verdicts': {
|
|
294
|
+
'ok': metrics['ok_count'],
|
|
295
|
+
'failed': metrics['failed_count'],
|
|
296
|
+
'empty': metrics['empty_count'],
|
|
297
|
+
'hung': metrics['hung_count'],
|
|
298
|
+
},
|
|
299
|
+
'success_rate': derived.get('success_rate', 'n/a'),
|
|
300
|
+
'first_try_green_rate': derived.get('first_try_green_rate', 'n/a'),
|
|
301
|
+
'repair_rounds': metrics['repair_rounds'],
|
|
302
|
+
'repair_efficiency': derived.get('repair_efficiency', 'n/a'),
|
|
303
|
+
'tokens': {
|
|
304
|
+
'in': metrics['tokens_in'],
|
|
305
|
+
'out': metrics['tokens_out'],
|
|
306
|
+
},
|
|
307
|
+
'cost_estimate': derived.get('cost_estimate', 'n/a'),
|
|
308
|
+
'duration_sec': metrics['duration_sec'],
|
|
309
|
+
'by_phase': metrics['by_phase'],
|
|
310
|
+
'by_model': metrics['by_model'],
|
|
311
|
+
'by_agent_type': metrics['by_agent_type'],
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if not result['scorecards']:
|
|
315
|
+
result['error'] = "No data available from ledger"
|
|
316
|
+
|
|
317
|
+
return json.dumps(result, indent=2)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def format_markdown(wave_metrics, derived_metrics):
|
|
321
|
+
"""Format metrics as markdown table (side-by-side).
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
wave_metrics: dict from compute_wave_metrics()
|
|
325
|
+
derived_metrics: dict from compute_derived_metrics()
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
markdown string
|
|
329
|
+
"""
|
|
330
|
+
if not wave_metrics:
|
|
331
|
+
return "No data available from ledger (n/a: no entries)\n"
|
|
332
|
+
|
|
333
|
+
output = []
|
|
334
|
+
output.append("\n## Wave Quality Scorecards\n")
|
|
335
|
+
|
|
336
|
+
# Sort by wave number
|
|
337
|
+
sorted_waves = sorted(wave_metrics.keys())
|
|
338
|
+
|
|
339
|
+
# Header
|
|
340
|
+
header = "| Metric" + "".join(f" | Wave {w}" for w in sorted_waves) + " |"
|
|
341
|
+
separator = "|--------" + "".join("|--------" for _ in sorted_waves) + "|"
|
|
342
|
+
|
|
343
|
+
output.append(header)
|
|
344
|
+
output.append(separator)
|
|
345
|
+
|
|
346
|
+
# Metrics rows
|
|
347
|
+
metrics_to_show = [
|
|
348
|
+
('entries', 'Entries'),
|
|
349
|
+
('ok_count', 'OK Count'),
|
|
350
|
+
('failed_count', 'Failed Count'),
|
|
351
|
+
('repair_rounds', 'Repair Rounds'),
|
|
352
|
+
('tokens_out', 'Tokens Out'),
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
for metric_key, metric_name in metrics_to_show:
|
|
356
|
+
row = f"| {metric_name}"
|
|
357
|
+
for wave in sorted_waves:
|
|
358
|
+
metrics = wave_metrics[wave]
|
|
359
|
+
value = metrics.get(metric_key, 0)
|
|
360
|
+
row += f" | {value}"
|
|
361
|
+
row += " |"
|
|
362
|
+
output.append(row)
|
|
363
|
+
|
|
364
|
+
# Derived metrics rows
|
|
365
|
+
derived_to_show = [
|
|
366
|
+
('success_rate', 'Success Rate'),
|
|
367
|
+
('first_try_green_rate', 'First-Try-Green'),
|
|
368
|
+
('repair_efficiency', 'Repair Efficiency'),
|
|
369
|
+
('cost_estimate', 'Est. Cost'),
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
for derived_key, derived_name in derived_to_show:
|
|
373
|
+
row = f"| {derived_name}"
|
|
374
|
+
for wave in sorted_waves:
|
|
375
|
+
derived = derived_metrics.get(wave, {})
|
|
376
|
+
value = derived.get(derived_key, 'n/a')
|
|
377
|
+
row += f" | {value}"
|
|
378
|
+
row += " |"
|
|
379
|
+
output.append(row)
|
|
380
|
+
|
|
381
|
+
output.append("")
|
|
382
|
+
return "\n".join(output)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def main():
|
|
386
|
+
"""Main entry point."""
|
|
387
|
+
import argparse
|
|
388
|
+
|
|
389
|
+
parser = argparse.ArgumentParser(
|
|
390
|
+
description="Wave Quality Scorecard Generator",
|
|
391
|
+
epilog="Output formats: ASCII (default), --json (machine-readable), --md (markdown table)"
|
|
392
|
+
)
|
|
393
|
+
parser.add_argument("--json", action="store_true", help="Output JSON")
|
|
394
|
+
parser.add_argument("--md", action="store_true", help="Output markdown table")
|
|
395
|
+
parser.add_argument("--waves", type=int, help="Show last N waves (default: all)")
|
|
396
|
+
parser.add_argument("--state-root", help="Path to state directory (overrides AESOP_STATE_ROOT)")
|
|
397
|
+
|
|
398
|
+
args = parser.parse_args()
|
|
399
|
+
|
|
400
|
+
# Override state root if provided
|
|
401
|
+
if args.state_root:
|
|
402
|
+
os.environ['AESOP_STATE_ROOT'] = args.state_root
|
|
403
|
+
|
|
404
|
+
# Parse ledger
|
|
405
|
+
rows = parse_ledger_rows()
|
|
406
|
+
|
|
407
|
+
# Filter to last N waves if requested
|
|
408
|
+
if args.waves:
|
|
409
|
+
if rows:
|
|
410
|
+
max_wave = max((r['wave'] for r in rows if r['wave'] is not None), default=0)
|
|
411
|
+
min_wave = max(0, max_wave - args.waves + 1)
|
|
412
|
+
rows = [r for r in rows if r['wave'] is None or r['wave'] >= min_wave]
|
|
413
|
+
|
|
414
|
+
# Compute metrics
|
|
415
|
+
wave_metrics = compute_wave_metrics(rows)
|
|
416
|
+
derived_metrics = compute_derived_metrics(wave_metrics)
|
|
417
|
+
|
|
418
|
+
# Format output
|
|
419
|
+
if args.json:
|
|
420
|
+
print(format_json(wave_metrics, derived_metrics))
|
|
421
|
+
elif args.md:
|
|
422
|
+
print(format_markdown(wave_metrics, derived_metrics))
|
|
423
|
+
else:
|
|
424
|
+
print(format_ascii(wave_metrics, derived_metrics))
|
|
425
|
+
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == '__main__':
|
|
430
|
+
sys.exit(main())
|
package/ui/config.py
CHANGED
|
@@ -52,7 +52,7 @@ def reload():
|
|
|
52
52
|
with open(derived_config_file) as f:
|
|
53
53
|
derived_config = json.load(f)
|
|
54
54
|
if "aesop_root" in derived_config:
|
|
55
|
-
AESOP_ROOT = Path(derived_config["aesop_root"])
|
|
55
|
+
AESOP_ROOT = Path(derived_config["aesop_root"]).expanduser()
|
|
56
56
|
except Exception:
|
|
57
57
|
# Silently ignore config errors here; will attempt full load below
|
|
58
58
|
pass
|
package/ui/cost.py
CHANGED
|
@@ -155,13 +155,16 @@ def get_cost_summary():
|
|
|
155
155
|
|
|
156
156
|
Extended fields (additively):
|
|
157
157
|
- per_week_costs: dict of "YYYY-Www" -> week cost/token totals and model mix
|
|
158
|
+
- per_wave_costs: dict of "wave-N" -> wave cost/token totals and model mix
|
|
159
|
+
- per_agent_costs: dict of agent_type -> agent cost/token totals and model mix
|
|
158
160
|
- verdict_weighted_cost: cost-per-outcome metrics (cost per OK, weighted by verdict distribution)
|
|
159
161
|
- model_mix_trend: per-day model usage distribution (%)
|
|
160
162
|
|
|
161
163
|
Returns:
|
|
162
164
|
dict: CostSummary with models, daily_totals, overall_scorecard,
|
|
163
165
|
skipped_lines, has_pricing, estimates_by_model, per_week_costs,
|
|
164
|
-
verdict_weighted_cost, model_mix_trend
|
|
166
|
+
per_wave_costs, per_agent_costs, verdict_weighted_cost, model_mix_trend
|
|
167
|
+
(or error field if invalid).
|
|
165
168
|
"""
|
|
166
169
|
import sys
|
|
167
170
|
from datetime import datetime, timedelta
|
|
@@ -188,6 +191,8 @@ def get_cost_summary():
|
|
|
188
191
|
"has_pricing": False,
|
|
189
192
|
"estimates_by_model": {},
|
|
190
193
|
"per_week_costs": {},
|
|
194
|
+
"per_wave_costs": {},
|
|
195
|
+
"per_agent_costs": {},
|
|
191
196
|
"verdict_weighted_cost": {
|
|
192
197
|
"cost_per_ok": 0.0,
|
|
193
198
|
"cost_per_failed": 0.0,
|
|
@@ -294,14 +299,28 @@ def get_cost_summary():
|
|
|
294
299
|
result["skipped_lines"] += 1
|
|
295
300
|
continue
|
|
296
301
|
|
|
297
|
-
#
|
|
302
|
+
# Extract optional wave and phase fields (if present)
|
|
303
|
+
wave = None
|
|
304
|
+
phase = None
|
|
305
|
+
try:
|
|
306
|
+
if len(parts) > 8:
|
|
307
|
+
phase = parts[8].strip() if len(parts) > 8 else None
|
|
308
|
+
if len(parts) > 9:
|
|
309
|
+
wave = parts[9].strip() if len(parts) > 9 else None
|
|
310
|
+
except (IndexError, ValueError):
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
# Store ledger entry for per-week/per-wave/per-agent calculation
|
|
298
314
|
ledger_entries.append({
|
|
299
315
|
"timestamp": timestamp,
|
|
300
316
|
"date_str": date_str,
|
|
301
317
|
"model": model,
|
|
318
|
+
"agent_type": agent_type,
|
|
302
319
|
"tokens_in": tokens_in,
|
|
303
320
|
"tokens_out": tokens_out,
|
|
304
|
-
"verdict": verdict
|
|
321
|
+
"verdict": verdict,
|
|
322
|
+
"wave": wave,
|
|
323
|
+
"phase": phase
|
|
305
324
|
})
|
|
306
325
|
|
|
307
326
|
# Aggregate by model
|
|
@@ -367,6 +386,8 @@ def get_cost_summary():
|
|
|
367
386
|
|
|
368
387
|
# Calculate per-week costs and model mix trend
|
|
369
388
|
_calculate_weekly_costs(result, pricing_map, ledger_entries)
|
|
389
|
+
_calculate_per_wave_costs(result, pricing_map, ledger_entries)
|
|
390
|
+
_calculate_per_agent_costs(result, pricing_map, ledger_entries)
|
|
370
391
|
_calculate_verdict_weighted_cost(result, pricing_map)
|
|
371
392
|
_calculate_model_mix_trend(result)
|
|
372
393
|
|
|
@@ -551,6 +572,158 @@ def _calculate_model_mix_trend(result):
|
|
|
551
572
|
result["model_mix_trend"] = model_mix_trend
|
|
552
573
|
|
|
553
574
|
|
|
575
|
+
def _calculate_per_wave_costs(result, pricing_map, ledger_entries):
|
|
576
|
+
"""Calculate per-wave cost rollup from ledger entries.
|
|
577
|
+
|
|
578
|
+
Groups ledger entries by wave number and aggregates per-model tokens for each wave.
|
|
579
|
+
If pricing is available, includes cost estimates.
|
|
580
|
+
|
|
581
|
+
Modifies result["per_wave_costs"] in-place with structure:
|
|
582
|
+
{
|
|
583
|
+
"wave-N": {
|
|
584
|
+
"tokens_in": int,
|
|
585
|
+
"tokens_out": int,
|
|
586
|
+
"model_tokens": {"model": int, ...},
|
|
587
|
+
"cost": float (if pricing available)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
"""
|
|
591
|
+
if not ledger_entries:
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
# Group ledger entries by wave and aggregate per-model within each wave
|
|
595
|
+
waves = {}
|
|
596
|
+
for entry in ledger_entries:
|
|
597
|
+
if not entry.get("wave"):
|
|
598
|
+
# Skip entries without wave info
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
wave_key = f"wave-{entry['wave']}"
|
|
602
|
+
|
|
603
|
+
if wave_key not in waves:
|
|
604
|
+
waves[wave_key] = {
|
|
605
|
+
"tokens_in": 0,
|
|
606
|
+
"tokens_out": 0,
|
|
607
|
+
"model_tokens": {},
|
|
608
|
+
"cost": 0.0
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# Aggregate this entry's tokens into the wave
|
|
612
|
+
waves[wave_key]["tokens_in"] += entry["tokens_in"]
|
|
613
|
+
waves[wave_key]["tokens_out"] += entry["tokens_out"]
|
|
614
|
+
|
|
615
|
+
# Track per-model tokens within this wave
|
|
616
|
+
model = entry["model"]
|
|
617
|
+
if model not in waves[wave_key]["model_tokens"]:
|
|
618
|
+
waves[wave_key]["model_tokens"][model] = 0
|
|
619
|
+
waves[wave_key]["model_tokens"][model] += entry["tokens_in"] + entry["tokens_out"]
|
|
620
|
+
|
|
621
|
+
# If pricing available, calculate cost per wave based on that wave's model mix
|
|
622
|
+
if pricing_map:
|
|
623
|
+
for wave_key, wave_data in waves.items():
|
|
624
|
+
total_cost = 0.0
|
|
625
|
+
for model, total_tokens in wave_data["model_tokens"].items():
|
|
626
|
+
if model in pricing_map:
|
|
627
|
+
pricing = pricing_map[model]
|
|
628
|
+
input_price = pricing.get("input_per_mtok", 0.0)
|
|
629
|
+
output_price = pricing.get("output_per_mtok", 0.0)
|
|
630
|
+
# Find entries for this model in this wave
|
|
631
|
+
model_entries_in_wave = [
|
|
632
|
+
e for e in ledger_entries
|
|
633
|
+
if e.get("wave") == wave_key.replace("wave-", "") and e["model"] == model
|
|
634
|
+
]
|
|
635
|
+
if model_entries_in_wave:
|
|
636
|
+
wave_model_tokens_in = sum(e["tokens_in"] for e in model_entries_in_wave)
|
|
637
|
+
wave_model_tokens_out = sum(e["tokens_out"] for e in model_entries_in_wave)
|
|
638
|
+
model_cost = (wave_model_tokens_in * input_price + wave_model_tokens_out * output_price) / 1_000_000
|
|
639
|
+
else:
|
|
640
|
+
# Fallback: if we can't find the entries, use 1:2 ratio estimate
|
|
641
|
+
tokens_in_estimate = total_tokens * 0.333
|
|
642
|
+
tokens_out_estimate = total_tokens * 0.667
|
|
643
|
+
model_cost = (tokens_in_estimate * input_price + tokens_out_estimate * output_price) / 1_000_000
|
|
644
|
+
total_cost += model_cost
|
|
645
|
+
wave_data["cost"] = total_cost
|
|
646
|
+
|
|
647
|
+
result["per_wave_costs"] = waves
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _calculate_per_agent_costs(result, pricing_map, ledger_entries):
|
|
651
|
+
"""Calculate per-agent-type cost rollup from ledger entries.
|
|
652
|
+
|
|
653
|
+
Groups ledger entries by agent_type and aggregates per-model tokens for each agent type.
|
|
654
|
+
If pricing is available, includes cost estimates.
|
|
655
|
+
|
|
656
|
+
Modifies result["per_agent_costs"] in-place with structure:
|
|
657
|
+
{
|
|
658
|
+
"agent_type": {
|
|
659
|
+
"tokens_in": int,
|
|
660
|
+
"tokens_out": int,
|
|
661
|
+
"model_tokens": {"model": int, ...},
|
|
662
|
+
"runs": int,
|
|
663
|
+
"verdicts": {"OK": int, "FAILED": int, ...},
|
|
664
|
+
"cost": float (if pricing available)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
"""
|
|
668
|
+
if not ledger_entries:
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
# Group ledger entries by agent_type and aggregate per-model within each agent type
|
|
672
|
+
agents = {}
|
|
673
|
+
for entry in ledger_entries:
|
|
674
|
+
agent_type = entry.get("agent_type", "unknown")
|
|
675
|
+
|
|
676
|
+
if agent_type not in agents:
|
|
677
|
+
agents[agent_type] = {
|
|
678
|
+
"tokens_in": 0,
|
|
679
|
+
"tokens_out": 0,
|
|
680
|
+
"model_tokens": {},
|
|
681
|
+
"runs": 0,
|
|
682
|
+
"verdicts": {"OK": 0, "FAILED": 0, "EMPTY": 0, "HUNG": 0},
|
|
683
|
+
"cost": 0.0
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
# Aggregate this entry's tokens into the agent type
|
|
687
|
+
agents[agent_type]["tokens_in"] += entry["tokens_in"]
|
|
688
|
+
agents[agent_type]["tokens_out"] += entry["tokens_out"]
|
|
689
|
+
agents[agent_type]["runs"] += 1
|
|
690
|
+
agents[agent_type]["verdicts"][entry["verdict"]] += 1
|
|
691
|
+
|
|
692
|
+
# Track per-model tokens within this agent type
|
|
693
|
+
model = entry["model"]
|
|
694
|
+
if model not in agents[agent_type]["model_tokens"]:
|
|
695
|
+
agents[agent_type]["model_tokens"][model] = 0
|
|
696
|
+
agents[agent_type]["model_tokens"][model] += entry["tokens_in"] + entry["tokens_out"]
|
|
697
|
+
|
|
698
|
+
# If pricing available, calculate cost per agent type based on that type's model mix
|
|
699
|
+
if pricing_map:
|
|
700
|
+
for agent_type, agent_data in agents.items():
|
|
701
|
+
total_cost = 0.0
|
|
702
|
+
for model, total_tokens in agent_data["model_tokens"].items():
|
|
703
|
+
if model in pricing_map:
|
|
704
|
+
pricing = pricing_map[model]
|
|
705
|
+
input_price = pricing.get("input_per_mtok", 0.0)
|
|
706
|
+
output_price = pricing.get("output_per_mtok", 0.0)
|
|
707
|
+
# Find entries for this model from this agent type
|
|
708
|
+
model_entries_for_agent = [
|
|
709
|
+
e for e in ledger_entries
|
|
710
|
+
if e.get("agent_type") == agent_type and e["model"] == model
|
|
711
|
+
]
|
|
712
|
+
if model_entries_for_agent:
|
|
713
|
+
agent_model_tokens_in = sum(e["tokens_in"] for e in model_entries_for_agent)
|
|
714
|
+
agent_model_tokens_out = sum(e["tokens_out"] for e in model_entries_for_agent)
|
|
715
|
+
model_cost = (agent_model_tokens_in * input_price + agent_model_tokens_out * output_price) / 1_000_000
|
|
716
|
+
else:
|
|
717
|
+
# Fallback: if we can't find the entries, use 1:2 ratio estimate
|
|
718
|
+
tokens_in_estimate = total_tokens * 0.333
|
|
719
|
+
tokens_out_estimate = total_tokens * 0.667
|
|
720
|
+
model_cost = (tokens_in_estimate * input_price + tokens_out_estimate * output_price) / 1_000_000
|
|
721
|
+
total_cost += model_cost
|
|
722
|
+
agent_data["cost"] = total_cost
|
|
723
|
+
|
|
724
|
+
result["per_agent_costs"] = agents
|
|
725
|
+
|
|
726
|
+
|
|
554
727
|
def _load_pricing_config():
|
|
555
728
|
"""Load pricing map from aesop.config.json at call time.
|
|
556
729
|
|