claude-code-tracker 1.2.4 → 1.4.0-beta.4
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/README.md +26 -2
- package/bin/claude-tracker-cost.js +20 -0
- package/bin/claude-tracker-setup +10 -0
- package/install.js +21 -0
- package/install.sh +26 -2
- package/package.json +7 -3
- package/skills/view-tracking/SKILL.md +54 -0
- package/src/__pycache__/cost.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_compactions.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_friction.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_skills.cpython-312.pyc +0 -0
- package/src/__pycache__/platform_utils.cpython-312.pyc +0 -0
- package/src/__pycache__/storage.cpython-312.pyc +0 -0
- package/src/__pycache__/write-agent.cpython-312.pyc +0 -0
- package/src/__pycache__/write-turns.cpython-312.pyc +0 -0
- package/src/backfill.py +47 -52
- package/src/cost-summary.py +57 -11
- package/src/cost.py +7 -0
- package/src/export-json.py +27 -0
- package/src/generate-charts.py +691 -12
- package/src/init-templates.py +26 -0
- package/src/init-templates.sh +3 -3
- package/src/parse_compactions.py +112 -0
- package/src/parse_friction.py +277 -0
- package/src/parse_skills.py +133 -0
- package/src/patch-durations.py +14 -114
- package/src/platform_utils.py +36 -0
- package/src/stop-hook.js +26 -0
- package/src/stop-hook.sh +27 -155
- package/src/storage.py +538 -0
- package/src/subagent-stop-hook.sh +37 -0
- package/src/update-prompts-index.py +177 -20
- package/src/write-agent.py +113 -0
- package/src/write-turns.py +130 -0
- package/uninstall.js +20 -0
package/src/generate-charts.py
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Generates tracking/charts.html from
|
|
3
|
+
Generates tracking/charts.html from SQLite storage + key-prompts/ folder.
|
|
4
4
|
Called by stop-hook.sh after each session update.
|
|
5
5
|
|
|
6
|
-
Usage: python3 generate-charts.py <
|
|
6
|
+
Usage: python3 generate-charts.py <tracking_dir_or_tokens.json> <output.html>
|
|
7
7
|
"""
|
|
8
8
|
import sys, json, os, re, glob
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
13
|
+
import storage
|
|
14
|
+
|
|
15
|
+
arg1 = sys.argv[1]
|
|
12
16
|
output_file = sys.argv[2]
|
|
13
17
|
|
|
18
|
+
# Backward compat: accept either tokens.json path or tracking directory
|
|
19
|
+
if arg1.endswith('.json'):
|
|
20
|
+
tracking_dir = os.path.dirname(os.path.abspath(arg1))
|
|
21
|
+
else:
|
|
22
|
+
tracking_dir = os.path.abspath(arg1)
|
|
23
|
+
|
|
24
|
+
data = storage.get_all_turns(tracking_dir)
|
|
25
|
+
agent_data = storage.get_all_agents(tracking_dir)
|
|
26
|
+
skill_data = storage.get_all_skills(tracking_dir)
|
|
27
|
+
|
|
28
|
+
friction_data = storage.get_all_friction(tracking_dir)
|
|
29
|
+
compaction_data = storage.get_all_compactions(tracking_dir)
|
|
30
|
+
|
|
14
31
|
def format_duration(seconds):
|
|
15
32
|
if seconds <= 0:
|
|
16
33
|
return "0m"
|
|
@@ -21,9 +38,6 @@ def format_duration(seconds):
|
|
|
21
38
|
return f"{h}h {m}m"
|
|
22
39
|
return f"{m}m {s}s"
|
|
23
40
|
|
|
24
|
-
with open(tokens_file) as f:
|
|
25
|
-
data = json.load(f)
|
|
26
|
-
|
|
27
41
|
if not data:
|
|
28
42
|
sys.exit(0)
|
|
29
43
|
|
|
@@ -72,6 +86,7 @@ total_turns = len(data)
|
|
|
72
86
|
total_sessions = len({e.get("session_id") for e in data})
|
|
73
87
|
sessions_with_data = len({e.get("session_id") for e in data if e.get("total_tokens", 0) > 0})
|
|
74
88
|
total_output = sum(e.get("output_tokens", 0) for e in data)
|
|
89
|
+
total_input = sum(e.get("input_tokens", 0) for e in data)
|
|
75
90
|
total_cache_read = sum(e.get("cache_read_tokens", 0) for e in data)
|
|
76
91
|
total_all_tokens = sum(e.get("total_tokens", 0) for e in data)
|
|
77
92
|
cache_pct = round(total_cache_read / total_all_tokens * 100, 1) if total_all_tokens > 0 else 0
|
|
@@ -81,7 +96,7 @@ avg_duration = total_duration // total_turns if total_turns > 0 else 0
|
|
|
81
96
|
project_name = data[0].get("project", "Project") if data else "Project"
|
|
82
97
|
|
|
83
98
|
# --- Count total human messages per date from JSONL transcripts ---
|
|
84
|
-
project_dir = os.path.dirname(os.path.dirname(
|
|
99
|
+
project_dir = os.path.dirname(os.path.dirname(tracking_dir)) # project root
|
|
85
100
|
# Claude Code slugifies paths as: replace every "/" with "-" (keeping leading slash → leading dash)
|
|
86
101
|
transcripts_dir = os.path.expanduser(
|
|
87
102
|
"~/.claude/projects/" + project_dir.replace("/", "-")
|
|
@@ -107,7 +122,7 @@ if os.path.isdir(transcripts_dir):
|
|
|
107
122
|
os.path.getmtime(jf)).strftime("%Y-%m-%d")
|
|
108
123
|
|
|
109
124
|
try:
|
|
110
|
-
with open(jf) as f:
|
|
125
|
+
with open(jf, encoding='utf-8') as f:
|
|
111
126
|
for line in f:
|
|
112
127
|
try:
|
|
113
128
|
obj = json.loads(line)
|
|
@@ -147,7 +162,7 @@ total_human_msgs = sum(human_by_date.values())
|
|
|
147
162
|
total_trivial_msgs = sum(trivial_by_date.values())
|
|
148
163
|
|
|
149
164
|
# --- Aggregate prompt data from key-prompts/ folder ---
|
|
150
|
-
prompts_dir = os.path.join(
|
|
165
|
+
prompts_dir = os.path.join(tracking_dir, "key-prompts")
|
|
151
166
|
prompt_files = sorted(glob.glob(os.path.join(prompts_dir, "????-??-??.md")))
|
|
152
167
|
|
|
153
168
|
prompt_by_date = {} # date -> {total, by_category}
|
|
@@ -155,7 +170,7 @@ all_categories = set()
|
|
|
155
170
|
|
|
156
171
|
for f in prompt_files:
|
|
157
172
|
date = os.path.splitext(os.path.basename(f))[0]
|
|
158
|
-
content = open(f).read()
|
|
173
|
+
content = open(f, encoding='utf-8').read()
|
|
159
174
|
cats = re.findall(r'^\*\*Category\*\*: (\S+)', content, re.MULTILINE)
|
|
160
175
|
by_cat = defaultdict(int)
|
|
161
176
|
for c in cats:
|
|
@@ -176,6 +191,8 @@ cache_read_by_date_js = json.dumps([by_date[d]["cache_read"] for d in dates])
|
|
|
176
191
|
opus_by_date_js = json.dumps([round(by_date[d]["opus_cost"], 4) for d in dates])
|
|
177
192
|
sonnet_by_date_js = json.dumps([round(by_date[d]["sonnet_cost"], 4) for d in dates])
|
|
178
193
|
duration_by_date_js = json.dumps([by_date[d]["duration"] for d in dates])
|
|
194
|
+
input_by_date_js = json.dumps([by_date[d]["input"] for d in dates])
|
|
195
|
+
cache_create_by_date_js = json.dumps([by_date[d]["cache_create"] for d in dates])
|
|
179
196
|
|
|
180
197
|
cumul_labels_js = json.dumps([f"{c['date']} {c['session_id']}#{c['turn_index']}" for c in cumulative])
|
|
181
198
|
cumul_values_js = json.dumps([c["cumulative_cost"] for c in cumulative])
|
|
@@ -285,6 +302,617 @@ donut_labels_js = json.dumps(list(cat_totals.keys()))
|
|
|
285
302
|
donut_values_js = json.dumps(list(cat_totals.values()))
|
|
286
303
|
donut_colors_js = json.dumps([CAT_COLORS.get(c, DEFAULT_COLOR) for c in cat_totals])
|
|
287
304
|
|
|
305
|
+
# --- Agent data aggregation ---
|
|
306
|
+
by_agent_type = defaultdict(lambda: {"cost": 0, "count": 0, "turns": 0})
|
|
307
|
+
for a in agent_data:
|
|
308
|
+
t = a.get('agent_type', 'unknown')
|
|
309
|
+
by_agent_type[t]["cost"] += a.get('estimated_cost_usd', 0)
|
|
310
|
+
by_agent_type[t]["count"] += 1
|
|
311
|
+
by_agent_type[t]["turns"] += a.get('turns', 0)
|
|
312
|
+
|
|
313
|
+
agent_types_sorted = sorted(by_agent_type.keys(), key=lambda t: -by_agent_type[t]["cost"])
|
|
314
|
+
total_agent_cost = sum(a.get('estimated_cost_usd', 0) for a in agent_data)
|
|
315
|
+
total_agent_invocations = len(agent_data)
|
|
316
|
+
|
|
317
|
+
agent_labels_js = json.dumps(agent_types_sorted)
|
|
318
|
+
agent_costs_js = json.dumps([round(by_agent_type[t]["cost"], 4) for t in agent_types_sorted])
|
|
319
|
+
agent_counts_js = json.dumps([by_agent_type[t]["count"] for t in agent_types_sorted])
|
|
320
|
+
agent_cpt_js = json.dumps([
|
|
321
|
+
round(by_agent_type[t]["cost"] / by_agent_type[t]["turns"], 4)
|
|
322
|
+
if by_agent_type[t]["turns"] > 0 else 0
|
|
323
|
+
for t in agent_types_sorted
|
|
324
|
+
])
|
|
325
|
+
|
|
326
|
+
# Conditional HTML blocks
|
|
327
|
+
if agent_data:
|
|
328
|
+
agents_stat_html = f''' <div class="stat">
|
|
329
|
+
<div class="stat-label">Agent cost</div>
|
|
330
|
+
<div class="stat-value">${total_agent_cost:.2f}</div>
|
|
331
|
+
<div class="stat-sub">{total_agent_invocations} invocations</div>
|
|
332
|
+
</div>'''
|
|
333
|
+
agents_section_html = f'''<div class="section">
|
|
334
|
+
<div class="section-header agents">Agents</div>
|
|
335
|
+
<div class="grid">
|
|
336
|
+
<div class="card">
|
|
337
|
+
<h2>Cost by agent type</h2>
|
|
338
|
+
<canvas id="agentCost"></canvas>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="card">
|
|
341
|
+
<h2>Invocations by agent type</h2>
|
|
342
|
+
<canvas id="agentCount"></canvas>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="card">
|
|
345
|
+
<h2>Cost per turn by agent type</h2>
|
|
346
|
+
<canvas id="agentCPT"></canvas>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
'''
|
|
352
|
+
agents_js_constants = f'''const AGENT_LABELS = {agent_labels_js};
|
|
353
|
+
const AGENT_COSTS = {agent_costs_js};
|
|
354
|
+
const AGENT_COUNTS = {agent_counts_js};
|
|
355
|
+
const AGENT_CPT = {agent_cpt_js};'''
|
|
356
|
+
agents_js_charts = '''
|
|
357
|
+
new Chart(document.getElementById('agentCost'), {
|
|
358
|
+
type: 'bar',
|
|
359
|
+
data: { labels: AGENT_LABELS,
|
|
360
|
+
datasets: [{ label: 'Cost ($)', data: AGENT_COSTS,
|
|
361
|
+
backgroundColor: '#f97316', borderRadius: 4 }] },
|
|
362
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins,
|
|
363
|
+
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.y.toFixed(4) } } } }
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
new Chart(document.getElementById('agentCount'), {
|
|
367
|
+
type: 'bar',
|
|
368
|
+
data: { labels: AGENT_LABELS,
|
|
369
|
+
datasets: [{ label: 'Invocations', data: AGENT_COUNTS,
|
|
370
|
+
backgroundColor: '#fb923c', borderRadius: 4 }] },
|
|
371
|
+
options: baseOpts
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
new Chart(document.getElementById('agentCPT'), {
|
|
375
|
+
type: 'bar',
|
|
376
|
+
data: { labels: AGENT_LABELS,
|
|
377
|
+
datasets: [{ label: 'Cost/turn', data: AGENT_CPT,
|
|
378
|
+
backgroundColor: '#fbbf24', borderRadius: 4 }] },
|
|
379
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins,
|
|
380
|
+
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.y.toFixed(4) + '/turn' } } } }
|
|
381
|
+
});'''
|
|
382
|
+
else:
|
|
383
|
+
agents_stat_html = ''
|
|
384
|
+
agents_section_html = ''
|
|
385
|
+
agents_js_constants = ''
|
|
386
|
+
agents_js_charts = ''
|
|
387
|
+
|
|
388
|
+
# --- Skill data aggregation ---
|
|
389
|
+
by_skill_name = defaultdict(lambda: {"count": 0, "success": 0, "fail": 0, "duration": 0})
|
|
390
|
+
skill_by_date = defaultdict(int)
|
|
391
|
+
for sk in skill_data:
|
|
392
|
+
name = sk.get('skill_name', 'unknown')
|
|
393
|
+
by_skill_name[name]["count"] += 1
|
|
394
|
+
if sk.get('success', 1):
|
|
395
|
+
by_skill_name[name]["success"] += 1
|
|
396
|
+
else:
|
|
397
|
+
by_skill_name[name]["fail"] += 1
|
|
398
|
+
by_skill_name[name]["duration"] += sk.get('duration_seconds', 0)
|
|
399
|
+
skill_by_date[sk.get('date', 'unknown')] += 1
|
|
400
|
+
|
|
401
|
+
skill_names_sorted = sorted(by_skill_name.keys(), key=lambda n: -by_skill_name[n]["count"])
|
|
402
|
+
total_skill_invocations = sum(by_skill_name[n]["count"] for n in by_skill_name)
|
|
403
|
+
total_skill_success = sum(by_skill_name[n]["success"] for n in by_skill_name)
|
|
404
|
+
skill_success_rate = round(total_skill_success / total_skill_invocations * 100, 1) if total_skill_invocations > 0 else 0
|
|
405
|
+
|
|
406
|
+
skill_labels_js = json.dumps(skill_names_sorted)
|
|
407
|
+
skill_counts_js = json.dumps([by_skill_name[n]["count"] for n in skill_names_sorted])
|
|
408
|
+
skill_success_js = json.dumps([by_skill_name[n]["success"] for n in skill_names_sorted])
|
|
409
|
+
skill_fail_js = json.dumps([by_skill_name[n]["fail"] for n in skill_names_sorted])
|
|
410
|
+
|
|
411
|
+
skill_timeline_dates = sorted(skill_by_date.keys())
|
|
412
|
+
skill_timeline_dates_js = json.dumps(skill_timeline_dates)
|
|
413
|
+
skill_timeline_values_js = json.dumps([skill_by_date[d] for d in skill_timeline_dates])
|
|
414
|
+
|
|
415
|
+
if skill_data:
|
|
416
|
+
skills_stat_html = f''' <div class="stat">
|
|
417
|
+
<div class="stat-label">Skill invocations</div>
|
|
418
|
+
<div class="stat-value">{total_skill_invocations}</div>
|
|
419
|
+
<div class="stat-sub">{skill_success_rate}% success rate</div>
|
|
420
|
+
</div>'''
|
|
421
|
+
skills_section_html = f'''<div class="section">
|
|
422
|
+
<div class="section-header skills">Skills</div>
|
|
423
|
+
<div class="grid">
|
|
424
|
+
<div class="card">
|
|
425
|
+
<h2>Invocations by skill</h2>
|
|
426
|
+
<canvas id="skillCount"></canvas>
|
|
427
|
+
</div>
|
|
428
|
+
<div class="card">
|
|
429
|
+
<h2>Success / fail by skill</h2>
|
|
430
|
+
<canvas id="skillSuccess"></canvas>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="card wide">
|
|
433
|
+
<h2>Skill invocations over time</h2>
|
|
434
|
+
<canvas id="skillTimeline"></canvas>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
'''
|
|
440
|
+
skills_js_constants = f'''const SKILL_LABELS = {skill_labels_js};
|
|
441
|
+
const SKILL_COUNTS = {skill_counts_js};
|
|
442
|
+
const SKILL_SUCCESS = {skill_success_js};
|
|
443
|
+
const SKILL_FAIL = {skill_fail_js};
|
|
444
|
+
const SKILL_TIMELINE_DATES = {skill_timeline_dates_js};
|
|
445
|
+
const SKILL_TIMELINE_VALUES = {skill_timeline_values_js};'''
|
|
446
|
+
skills_js_charts = '''
|
|
447
|
+
new Chart(document.getElementById('skillCount'), {
|
|
448
|
+
type: 'bar',
|
|
449
|
+
data: { labels: SKILL_LABELS,
|
|
450
|
+
datasets: [{ label: 'Invocations', data: SKILL_COUNTS,
|
|
451
|
+
backgroundColor: '#f59e0b', borderRadius: 4 }] },
|
|
452
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
new Chart(document.getElementById('skillSuccess'), {
|
|
456
|
+
type: 'bar',
|
|
457
|
+
data: { labels: SKILL_LABELS,
|
|
458
|
+
datasets: [
|
|
459
|
+
{ label: 'Success', data: SKILL_SUCCESS, backgroundColor: '#f59e0b', borderRadius: 2 },
|
|
460
|
+
{ label: 'Fail', data: SKILL_FAIL, backgroundColor: '#92400e', borderRadius: 2 }
|
|
461
|
+
] },
|
|
462
|
+
options: { ...baseOpts, scales: { ...baseOpts.scales,
|
|
463
|
+
x: { ...baseOpts.scales.x, stacked: true },
|
|
464
|
+
y: { ...baseOpts.scales.y, stacked: true } } }
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
new Chart(document.getElementById('skillTimeline'), {
|
|
468
|
+
type: 'line',
|
|
469
|
+
data: {
|
|
470
|
+
labels: SKILL_TIMELINE_DATES,
|
|
471
|
+
datasets: [{ label: 'Invocations', data: SKILL_TIMELINE_VALUES,
|
|
472
|
+
borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.15)',
|
|
473
|
+
fill: true, tension: 0.3, pointRadius: 3 }]
|
|
474
|
+
},
|
|
475
|
+
options: baseOpts
|
|
476
|
+
});'''
|
|
477
|
+
else:
|
|
478
|
+
skills_stat_html = ''
|
|
479
|
+
skills_section_html = ''
|
|
480
|
+
skills_js_constants = ''
|
|
481
|
+
skills_js_charts = ''
|
|
482
|
+
|
|
483
|
+
# --- Friction data aggregation ---
|
|
484
|
+
FRICTION_CAT_COLORS = {
|
|
485
|
+
"permission_denied": "#dc2626",
|
|
486
|
+
"hook_blocked": "#b91c1c",
|
|
487
|
+
"cascade_error": "#f97316",
|
|
488
|
+
"command_failed": "#eab308",
|
|
489
|
+
"tool_error": "#ef4444",
|
|
490
|
+
"correction": "#8b5cf6",
|
|
491
|
+
"retry": "#06b6d4",
|
|
492
|
+
}
|
|
493
|
+
FRICTION_DEFAULT_COLOR = "#94a3b8"
|
|
494
|
+
|
|
495
|
+
friction_by_date = defaultdict(lambda: {"main": 0, "subagent": 0})
|
|
496
|
+
friction_by_category = defaultdict(int)
|
|
497
|
+
friction_by_tool = defaultdict(int)
|
|
498
|
+
for fe in friction_data:
|
|
499
|
+
d = fe.get('date', 'unknown')
|
|
500
|
+
src = fe.get('source', 'main')
|
|
501
|
+
if src == 'subagent':
|
|
502
|
+
friction_by_date[d]["subagent"] += 1
|
|
503
|
+
else:
|
|
504
|
+
friction_by_date[d]["main"] += 1
|
|
505
|
+
friction_by_category[fe.get('category', 'unknown')] += 1
|
|
506
|
+
tn = fe.get('tool_name')
|
|
507
|
+
if tn:
|
|
508
|
+
friction_by_tool[tn] += 1
|
|
509
|
+
|
|
510
|
+
friction_by_skill = defaultdict(int)
|
|
511
|
+
for fe in friction_data:
|
|
512
|
+
sk = fe.get('skill')
|
|
513
|
+
if sk:
|
|
514
|
+
friction_by_skill[sk] += 1
|
|
515
|
+
has_skill_data = bool(friction_by_skill)
|
|
516
|
+
|
|
517
|
+
retry_events = [fe for fe in friction_data if fe.get('category') == 'retry']
|
|
518
|
+
retry_total = len(retry_events)
|
|
519
|
+
retry_resolved = sum(1 for fe in retry_events if fe.get('resolved'))
|
|
520
|
+
retry_rate = round(retry_resolved / retry_total * 100, 1) if retry_total > 0 else 0
|
|
521
|
+
|
|
522
|
+
total_friction = len(friction_data)
|
|
523
|
+
friction_rate = round(total_friction / total_turns * 100, 1) if total_turns > 0 else 0
|
|
524
|
+
|
|
525
|
+
friction_dates_sorted = sorted(friction_by_date.keys())
|
|
526
|
+
friction_dates_js = json.dumps(friction_dates_sorted)
|
|
527
|
+
friction_main_by_date_js = json.dumps([friction_by_date[d]["main"] for d in friction_dates_sorted])
|
|
528
|
+
friction_sub_by_date_js = json.dumps([friction_by_date[d]["subagent"] for d in friction_dates_sorted])
|
|
529
|
+
|
|
530
|
+
friction_cats_sorted = sorted(friction_by_category.keys(), key=lambda c: -friction_by_category[c])
|
|
531
|
+
friction_cat_labels_js = json.dumps(friction_cats_sorted)
|
|
532
|
+
friction_cat_values_js = json.dumps([friction_by_category[c] for c in friction_cats_sorted])
|
|
533
|
+
friction_cat_colors_js = json.dumps([FRICTION_CAT_COLORS.get(c, FRICTION_DEFAULT_COLOR) for c in friction_cats_sorted])
|
|
534
|
+
|
|
535
|
+
friction_tools_sorted = sorted(friction_by_tool.keys(), key=lambda t: -friction_by_tool[t])
|
|
536
|
+
friction_tool_labels_js = json.dumps(friction_tools_sorted)
|
|
537
|
+
friction_tool_values_js = json.dumps([friction_by_tool[t] for t in friction_tools_sorted])
|
|
538
|
+
|
|
539
|
+
friction_skills_sorted = sorted(friction_by_skill.keys(), key=lambda s: -friction_by_skill[s])
|
|
540
|
+
friction_skill_labels_js = json.dumps(friction_skills_sorted)
|
|
541
|
+
friction_skill_values_js = json.dumps([friction_by_skill[s] for s in friction_skills_sorted])
|
|
542
|
+
|
|
543
|
+
# Friction rate trend per day (events per 100 prompts)
|
|
544
|
+
friction_rate_dates = sorted(set(friction_dates_sorted) & set(dates))
|
|
545
|
+
friction_rate_values = []
|
|
546
|
+
for d in friction_rate_dates:
|
|
547
|
+
day_friction = friction_by_date[d]["main"] + friction_by_date[d]["subagent"]
|
|
548
|
+
day_turns = by_date[d]["turns"] if d in by_date else 0
|
|
549
|
+
friction_rate_values.append(round(day_friction / day_turns * 100, 1) if day_turns > 0 else 0)
|
|
550
|
+
friction_rate_dates_js = json.dumps(friction_rate_dates)
|
|
551
|
+
friction_rate_values_js = json.dumps(friction_rate_values)
|
|
552
|
+
|
|
553
|
+
if friction_data:
|
|
554
|
+
friction_stat_html = f''' <div class="stat">
|
|
555
|
+
<div class="stat-label">Friction events</div>
|
|
556
|
+
<div class="stat-value">{total_friction}</div>
|
|
557
|
+
<div class="stat-sub">{friction_rate} per 100 prompts</div>
|
|
558
|
+
</div>'''
|
|
559
|
+
retry_stat_html = f''' <div class="stat">
|
|
560
|
+
<div class="stat-label">Retry resolution</div>
|
|
561
|
+
<div class="stat-value">{retry_rate}%</div>
|
|
562
|
+
<div class="stat-sub">{retry_resolved} of {retry_total} retries succeeded</div>
|
|
563
|
+
</div>''' if retry_total > 0 else ''
|
|
564
|
+
friction_skill_card = '''
|
|
565
|
+
<div class="card">
|
|
566
|
+
<h2>Friction by skill</h2>
|
|
567
|
+
<canvas id="frictionSkill"></canvas>
|
|
568
|
+
</div>''' if has_skill_data else ''
|
|
569
|
+
friction_section_html = f'''<div class="section">
|
|
570
|
+
<div class="section-header friction">Friction</div>
|
|
571
|
+
<div class="grid">
|
|
572
|
+
<div class="card">
|
|
573
|
+
<h2>Friction per day</h2>
|
|
574
|
+
<canvas id="frictionDay"></canvas>
|
|
575
|
+
</div>
|
|
576
|
+
<div class="card">
|
|
577
|
+
<h2>Friction by category</h2>
|
|
578
|
+
<canvas id="frictionCat"></canvas>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="card">
|
|
581
|
+
<h2>Friction by tool</h2>
|
|
582
|
+
<canvas id="frictionTool"></canvas>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="card">
|
|
585
|
+
<h2>Friction rate trend</h2>
|
|
586
|
+
<canvas id="frictionRate"></canvas>
|
|
587
|
+
</div>{friction_skill_card}
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
'''
|
|
592
|
+
friction_js_constants = f'''const FRICTION_DATES = {friction_dates_js};
|
|
593
|
+
const FRICTION_MAIN = {friction_main_by_date_js};
|
|
594
|
+
const FRICTION_SUB = {friction_sub_by_date_js};
|
|
595
|
+
const FRICTION_CAT_LABELS = {friction_cat_labels_js};
|
|
596
|
+
const FRICTION_CAT_VALUES = {friction_cat_values_js};
|
|
597
|
+
const FRICTION_CAT_COLORS = {friction_cat_colors_js};
|
|
598
|
+
const FRICTION_TOOL_LABELS = {friction_tool_labels_js};
|
|
599
|
+
const FRICTION_TOOL_VALUES = {friction_tool_values_js};
|
|
600
|
+
const FRICTION_RATE_DATES = {friction_rate_dates_js};
|
|
601
|
+
const FRICTION_RATE_VALUES = {friction_rate_values_js};''' + (f'''
|
|
602
|
+
const FRICTION_SKILL_LABELS = {friction_skill_labels_js};
|
|
603
|
+
const FRICTION_SKILL_VALUES = {friction_skill_values_js};''' if has_skill_data else '')
|
|
604
|
+
friction_js_charts = '''
|
|
605
|
+
// Friction per day (stacked bar)
|
|
606
|
+
new Chart(document.getElementById('frictionDay'), {
|
|
607
|
+
type: 'bar',
|
|
608
|
+
data: {
|
|
609
|
+
labels: FRICTION_DATES,
|
|
610
|
+
datasets: [
|
|
611
|
+
{ label: 'Main session', data: FRICTION_MAIN, backgroundColor: '#ef4444', borderRadius: 2 },
|
|
612
|
+
{ label: 'Subagent', data: FRICTION_SUB, backgroundColor: '#f97316', borderRadius: 2 }
|
|
613
|
+
]
|
|
614
|
+
},
|
|
615
|
+
options: { ...baseOpts, scales: { ...baseOpts.scales,
|
|
616
|
+
x: { ...baseOpts.scales.x, stacked: true },
|
|
617
|
+
y: { ...baseOpts.scales.y, stacked: true } } }
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Friction by category (horizontal bar)
|
|
621
|
+
new Chart(document.getElementById('frictionCat'), {
|
|
622
|
+
type: 'bar',
|
|
623
|
+
data: {
|
|
624
|
+
labels: FRICTION_CAT_LABELS,
|
|
625
|
+
datasets: [{ label: 'Events', data: FRICTION_CAT_VALUES,
|
|
626
|
+
backgroundColor: FRICTION_CAT_COLORS, borderRadius: 4 }]
|
|
627
|
+
},
|
|
628
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Friction by tool (bar)
|
|
632
|
+
new Chart(document.getElementById('frictionTool'), {
|
|
633
|
+
type: 'bar',
|
|
634
|
+
data: {
|
|
635
|
+
labels: FRICTION_TOOL_LABELS,
|
|
636
|
+
datasets: [{ label: 'Events', data: FRICTION_TOOL_VALUES,
|
|
637
|
+
backgroundColor: '#ef4444', borderRadius: 4 }]
|
|
638
|
+
},
|
|
639
|
+
options: baseOpts
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Friction rate trend (line)
|
|
643
|
+
new Chart(document.getElementById('frictionRate'), {
|
|
644
|
+
type: 'line',
|
|
645
|
+
data: {
|
|
646
|
+
labels: FRICTION_RATE_DATES,
|
|
647
|
+
datasets: [{ label: 'Per 100 prompts', data: FRICTION_RATE_VALUES,
|
|
648
|
+
borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.15)',
|
|
649
|
+
fill: true, tension: 0.3, pointRadius: 3 }]
|
|
650
|
+
},
|
|
651
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins,
|
|
652
|
+
tooltip: { callbacks: { label: ctx => ' ' + ctx.parsed.y + ' per 100 prompts' } } } }
|
|
653
|
+
});''' + ('''
|
|
654
|
+
|
|
655
|
+
// Friction by skill (horizontal bar)
|
|
656
|
+
new Chart(document.getElementById('frictionSkill'), {
|
|
657
|
+
type: 'bar',
|
|
658
|
+
data: {
|
|
659
|
+
labels: FRICTION_SKILL_LABELS,
|
|
660
|
+
datasets: [{ label: 'Events', data: FRICTION_SKILL_VALUES,
|
|
661
|
+
backgroundColor: '#c084fc', borderRadius: 4 }]
|
|
662
|
+
},
|
|
663
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
664
|
+
});''' if has_skill_data else '')
|
|
665
|
+
else:
|
|
666
|
+
friction_stat_html = ''
|
|
667
|
+
retry_stat_html = ''
|
|
668
|
+
friction_section_html = ''
|
|
669
|
+
friction_js_constants = ''
|
|
670
|
+
friction_js_charts = ''
|
|
671
|
+
|
|
672
|
+
# --- Error tracking ---
|
|
673
|
+
ERROR_CATEGORIES = {'tool_error', 'command_failed', 'cascade_error'}
|
|
674
|
+
|
|
675
|
+
def classify_error(event):
|
|
676
|
+
detail = (event.get('detail') or '').strip()
|
|
677
|
+
if 'InputValidationError' in detail:
|
|
678
|
+
return 'validation_error'
|
|
679
|
+
if 'No such file' in detail or 'FileNotFoundError' in detail:
|
|
680
|
+
return 'file_not_found'
|
|
681
|
+
if 'timed out' in detail.lower() or 'timeout' in detail.lower():
|
|
682
|
+
return 'timeout'
|
|
683
|
+
if 'UnicodeDecodeError' in detail or 'encoding' in detail.lower():
|
|
684
|
+
return 'encoding_error'
|
|
685
|
+
if event.get('category') == 'command_failed':
|
|
686
|
+
import re as _re
|
|
687
|
+
m = _re.search(r'Exit code (\d+)', detail)
|
|
688
|
+
if m:
|
|
689
|
+
return f'exit_code_{m.group(1)}'
|
|
690
|
+
return 'generic'
|
|
691
|
+
|
|
692
|
+
error_events = [fe for fe in friction_data if fe.get('category') in ERROR_CATEGORIES]
|
|
693
|
+
|
|
694
|
+
error_by_type = defaultdict(int)
|
|
695
|
+
error_by_tool = defaultdict(int)
|
|
696
|
+
error_by_date = defaultdict(int)
|
|
697
|
+
for ee in error_events:
|
|
698
|
+
error_by_type[classify_error(ee)] += 1
|
|
699
|
+
tn = ee.get('tool_name')
|
|
700
|
+
if tn:
|
|
701
|
+
error_by_tool[tn] += 1
|
|
702
|
+
error_by_date[ee.get('date', 'unknown')] += 1
|
|
703
|
+
|
|
704
|
+
total_errors = len(error_events)
|
|
705
|
+
error_rate = round(total_errors / total_turns * 100, 1) if total_turns > 0 else 0
|
|
706
|
+
|
|
707
|
+
if error_events:
|
|
708
|
+
error_types_sorted = sorted(error_by_type.keys(), key=lambda t: -error_by_type[t])
|
|
709
|
+
error_tools_sorted = sorted(error_by_tool.keys(), key=lambda t: -error_by_tool[t])
|
|
710
|
+
error_dates_sorted = sorted(error_by_date.keys())
|
|
711
|
+
|
|
712
|
+
error_type_labels_js = json.dumps(error_types_sorted)
|
|
713
|
+
error_type_values_js = json.dumps([error_by_type[t] for t in error_types_sorted])
|
|
714
|
+
error_tool_labels_js = json.dumps(error_tools_sorted)
|
|
715
|
+
error_tool_values_js = json.dumps([error_by_tool[t] for t in error_tools_sorted])
|
|
716
|
+
error_dates_js = json.dumps(error_dates_sorted)
|
|
717
|
+
error_date_values_js = json.dumps([error_by_date[d] for d in error_dates_sorted])
|
|
718
|
+
|
|
719
|
+
error_stat_html = f''' <div class="stat">
|
|
720
|
+
<div class="stat-label">Errors</div>
|
|
721
|
+
<div class="stat-value">{total_errors}</div>
|
|
722
|
+
<div class="stat-sub">{error_rate} per 100 prompts</div>
|
|
723
|
+
</div>'''
|
|
724
|
+
error_section_html = f'''<div class="section">
|
|
725
|
+
<div class="section-header errors">Errors</div>
|
|
726
|
+
<div class="grid">
|
|
727
|
+
<div class="card">
|
|
728
|
+
<h2>Error types</h2>
|
|
729
|
+
<canvas id="errorTypes"></canvas>
|
|
730
|
+
</div>
|
|
731
|
+
<div class="card">
|
|
732
|
+
<h2>Errors by tool</h2>
|
|
733
|
+
<canvas id="errorTools"></canvas>
|
|
734
|
+
</div>
|
|
735
|
+
<div class="card wide">
|
|
736
|
+
<h2>Error trend per day</h2>
|
|
737
|
+
<canvas id="errorTrend"></canvas>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
'''
|
|
743
|
+
error_js_constants = f'''const ERROR_TYPE_LABELS = {error_type_labels_js};
|
|
744
|
+
const ERROR_TYPE_VALUES = {error_type_values_js};
|
|
745
|
+
const ERROR_TOOL_LABELS = {error_tool_labels_js};
|
|
746
|
+
const ERROR_TOOL_VALUES = {error_tool_values_js};
|
|
747
|
+
const ERROR_DATES = {error_dates_js};
|
|
748
|
+
const ERROR_DATE_VALUES = {error_date_values_js};'''
|
|
749
|
+
error_js_charts = '''
|
|
750
|
+
// Error types (horizontal bar)
|
|
751
|
+
new Chart(document.getElementById('errorTypes'), {
|
|
752
|
+
type: 'bar',
|
|
753
|
+
data: {
|
|
754
|
+
labels: ERROR_TYPE_LABELS,
|
|
755
|
+
datasets: [{ label: 'Errors', data: ERROR_TYPE_VALUES,
|
|
756
|
+
backgroundColor: '#fb7185', borderRadius: 4 }]
|
|
757
|
+
},
|
|
758
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Errors by tool (bar)
|
|
762
|
+
new Chart(document.getElementById('errorTools'), {
|
|
763
|
+
type: 'bar',
|
|
764
|
+
data: {
|
|
765
|
+
labels: ERROR_TOOL_LABELS,
|
|
766
|
+
datasets: [{ label: 'Errors', data: ERROR_TOOL_VALUES,
|
|
767
|
+
backgroundColor: '#e11d48', borderRadius: 4 }]
|
|
768
|
+
},
|
|
769
|
+
options: baseOpts
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Error trend per day (line)
|
|
773
|
+
new Chart(document.getElementById('errorTrend'), {
|
|
774
|
+
type: 'line',
|
|
775
|
+
data: {
|
|
776
|
+
labels: ERROR_DATES,
|
|
777
|
+
datasets: [{ label: 'Errors', data: ERROR_DATE_VALUES,
|
|
778
|
+
borderColor: '#e11d48', backgroundColor: 'rgba(225,29,72,0.15)',
|
|
779
|
+
fill: true, tension: 0.3, pointRadius: 3 }]
|
|
780
|
+
},
|
|
781
|
+
options: baseOpts
|
|
782
|
+
});'''
|
|
783
|
+
else:
|
|
784
|
+
error_stat_html = ''
|
|
785
|
+
error_section_html = ''
|
|
786
|
+
error_js_constants = ''
|
|
787
|
+
error_js_charts = ''
|
|
788
|
+
|
|
789
|
+
# --- Compaction data aggregation ---
|
|
790
|
+
compaction_by_date = defaultdict(lambda: {"auto": 0, "manual": 0})
|
|
791
|
+
compaction_tokens = []
|
|
792
|
+
compaction_per_session = defaultdict(int)
|
|
793
|
+
for ce in compaction_data:
|
|
794
|
+
d = ce.get('date', 'unknown')
|
|
795
|
+
trig = ce.get('trigger', 'auto')
|
|
796
|
+
if trig == 'manual':
|
|
797
|
+
compaction_by_date[d]["manual"] += 1
|
|
798
|
+
else:
|
|
799
|
+
compaction_by_date[d]["auto"] += 1
|
|
800
|
+
pre_t = ce.get('pre_tokens', 0)
|
|
801
|
+
if pre_t > 0:
|
|
802
|
+
compaction_tokens.append({"x": ce.get('timestamp', '')[:10] if ce.get('timestamp') else 'unknown', "y": pre_t})
|
|
803
|
+
compaction_per_session[ce.get('session_id', '')] += 1
|
|
804
|
+
|
|
805
|
+
total_compactions = len(compaction_data)
|
|
806
|
+
total_auto = sum(1 for c in compaction_data if c.get('trigger', 'auto') == 'auto')
|
|
807
|
+
total_manual = total_compactions - total_auto
|
|
808
|
+
avg_pre_tokens = round(sum(c.get('pre_tokens', 0) for c in compaction_data) / total_compactions) if total_compactions > 0 else 0
|
|
809
|
+
sessions_with_compaction = len([s for s in compaction_per_session if compaction_per_session[s] > 0])
|
|
810
|
+
pct_sessions_compacted = round(sessions_with_compaction / total_sessions * 100, 1) if total_sessions > 0 else 0
|
|
811
|
+
avg_per_session = round(total_compactions / total_sessions, 1) if total_sessions > 0 else 0
|
|
812
|
+
|
|
813
|
+
compact_dates_sorted = sorted(compaction_by_date.keys())
|
|
814
|
+
compact_dates_js = json.dumps(compact_dates_sorted)
|
|
815
|
+
compact_auto_js = json.dumps([compaction_by_date[d]["auto"] for d in compact_dates_sorted])
|
|
816
|
+
compact_manual_js = json.dumps([compaction_by_date[d]["manual"] for d in compact_dates_sorted])
|
|
817
|
+
compact_scatter_js = json.dumps(compaction_tokens)
|
|
818
|
+
|
|
819
|
+
compact_dist = {"0": 0, "1": 0, "2": 0, "3+": 0}
|
|
820
|
+
all_session_ids = {e.get("session_id") for e in data}
|
|
821
|
+
for sid in all_session_ids:
|
|
822
|
+
cnt = compaction_per_session.get(sid, 0)
|
|
823
|
+
if cnt == 0:
|
|
824
|
+
compact_dist["0"] += 1
|
|
825
|
+
elif cnt == 1:
|
|
826
|
+
compact_dist["1"] += 1
|
|
827
|
+
elif cnt == 2:
|
|
828
|
+
compact_dist["2"] += 1
|
|
829
|
+
else:
|
|
830
|
+
compact_dist["3+"] += 1
|
|
831
|
+
compact_dist_labels_js = json.dumps(list(compact_dist.keys()))
|
|
832
|
+
compact_dist_values_js = json.dumps(list(compact_dist.values()))
|
|
833
|
+
|
|
834
|
+
if compaction_data:
|
|
835
|
+
compaction_stat_html = f''' <div class="stat">
|
|
836
|
+
<div class="stat-label">Compactions</div>
|
|
837
|
+
<div class="stat-value">{total_compactions}</div>
|
|
838
|
+
<div class="stat-sub">{avg_per_session}/session, {pct_sessions_compacted}% of sessions</div>
|
|
839
|
+
</div>'''
|
|
840
|
+
compaction_section_html = f'''<div class="section">
|
|
841
|
+
<div class="section-header compaction">Context Compactions</div>
|
|
842
|
+
<div class="grid">
|
|
843
|
+
<div class="card">
|
|
844
|
+
<h2>Compactions per day</h2>
|
|
845
|
+
<canvas id="compactDay"></canvas>
|
|
846
|
+
</div>
|
|
847
|
+
<div class="card">
|
|
848
|
+
<h2>Pre-compaction token count</h2>
|
|
849
|
+
<canvas id="compactTokens"></canvas>
|
|
850
|
+
</div>
|
|
851
|
+
<div class="card">
|
|
852
|
+
<h2>Compactions per session</h2>
|
|
853
|
+
<canvas id="compactDist"></canvas>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
'''
|
|
859
|
+
compaction_js_constants = f'''const COMPACT_DATES = {compact_dates_js};
|
|
860
|
+
const COMPACT_AUTO = {compact_auto_js};
|
|
861
|
+
const COMPACT_MANUAL = {compact_manual_js};
|
|
862
|
+
const COMPACT_SCATTER = {compact_scatter_js};
|
|
863
|
+
const COMPACT_DIST_LABELS = {compact_dist_labels_js};
|
|
864
|
+
const COMPACT_DIST_VALUES = {compact_dist_values_js};'''
|
|
865
|
+
compaction_js_charts = '''
|
|
866
|
+
// Compactions per day (stacked bar)
|
|
867
|
+
new Chart(document.getElementById('compactDay'), {
|
|
868
|
+
type: 'bar',
|
|
869
|
+
data: {
|
|
870
|
+
labels: COMPACT_DATES,
|
|
871
|
+
datasets: [
|
|
872
|
+
{ label: 'Auto', data: COMPACT_AUTO, backgroundColor: '#8b5cf6', borderRadius: 2 },
|
|
873
|
+
{ label: 'Manual', data: COMPACT_MANUAL, backgroundColor: '#c084fc', borderRadius: 2 }
|
|
874
|
+
]
|
|
875
|
+
},
|
|
876
|
+
options: { ...baseOpts, scales: { ...baseOpts.scales,
|
|
877
|
+
x: { ...baseOpts.scales.x, stacked: true },
|
|
878
|
+
y: { ...baseOpts.scales.y, stacked: true } } }
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Pre-compaction token count (scatter)
|
|
882
|
+
new Chart(document.getElementById('compactTokens'), {
|
|
883
|
+
type: 'scatter',
|
|
884
|
+
data: {
|
|
885
|
+
datasets: [{ label: 'Pre-tokens', data: COMPACT_SCATTER,
|
|
886
|
+
backgroundColor: '#a78bfa', pointRadius: 5 }]
|
|
887
|
+
},
|
|
888
|
+
options: { ...baseOpts,
|
|
889
|
+
scales: {
|
|
890
|
+
x: { type: 'category', labels: [...new Set(COMPACT_SCATTER.map(p => p.x))],
|
|
891
|
+
ticks: { color: TEXT, font: { size: 10 } }, grid: { color: GRID } },
|
|
892
|
+
y: { ticks: { color: TEXT, font: { size: 10 },
|
|
893
|
+
callback: v => (v/1000).toFixed(0) + 'K' }, grid: { color: GRID } }
|
|
894
|
+
},
|
|
895
|
+
plugins: { ...baseOpts.plugins,
|
|
896
|
+
tooltip: { callbacks: { label: ctx => ' ' + ctx.parsed.y.toLocaleString() + ' tokens' } } }
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Compactions per session distribution
|
|
901
|
+
new Chart(document.getElementById('compactDist'), {
|
|
902
|
+
type: 'bar',
|
|
903
|
+
data: {
|
|
904
|
+
labels: COMPACT_DIST_LABELS,
|
|
905
|
+
datasets: [{ label: 'Sessions', data: COMPACT_DIST_VALUES,
|
|
906
|
+
backgroundColor: '#8b5cf6', borderRadius: 4 }]
|
|
907
|
+
},
|
|
908
|
+
options: baseOpts
|
|
909
|
+
});'''
|
|
910
|
+
else:
|
|
911
|
+
compaction_stat_html = ''
|
|
912
|
+
compaction_section_html = ''
|
|
913
|
+
compaction_js_constants = ''
|
|
914
|
+
compaction_js_charts = ''
|
|
915
|
+
|
|
288
916
|
html = f"""<!DOCTYPE html>
|
|
289
917
|
<html lang="en">
|
|
290
918
|
<head>
|
|
@@ -314,6 +942,11 @@ html = f"""<!DOCTYPE html>
|
|
|
314
942
|
.section-header.cost {{ border-left: 3px solid #6366f1; color: #818cf8; }}
|
|
315
943
|
.section-header.time {{ border-left: 3px solid #34d399; color: #34d399; }}
|
|
316
944
|
.section-header.prompts {{ border-left: 3px solid #a78bfa; color: #a78bfa; }}
|
|
945
|
+
.section-header.agents {{ border-left: 3px solid #f97316; color: #fb923c; }}
|
|
946
|
+
.section-header.friction {{ border-left: 3px solid #ef4444; color: #f87171; }}
|
|
947
|
+
.section-header.errors {{ border-left: 3px solid #e11d48; color: #fb7185; }}
|
|
948
|
+
.section-header.skills {{ border-left: 3px solid #f59e0b; color: #fbbf24; }}
|
|
949
|
+
.section-header.compaction {{ border-left: 3px solid #8b5cf6; color: #a78bfa; }}
|
|
317
950
|
.grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
|
|
318
951
|
.card {{ background: #1e2330; border: 1px solid #2d3748; border-radius: 10px;
|
|
319
952
|
padding: 16px; }}
|
|
@@ -350,6 +983,11 @@ html = f"""<!DOCTYPE html>
|
|
|
350
983
|
<div class="stat-value">{total_output:,}</div>
|
|
351
984
|
<div class="stat-sub"> </div>
|
|
352
985
|
</div>
|
|
986
|
+
<div class="stat">
|
|
987
|
+
<div class="stat-label">Input tokens</div>
|
|
988
|
+
<div class="stat-value">{total_input:,}</div>
|
|
989
|
+
<div class="stat-sub"> </div>
|
|
990
|
+
</div>
|
|
353
991
|
<div class="stat">
|
|
354
992
|
<div class="stat-label">Cache read share</div>
|
|
355
993
|
<div class="stat-value">{cache_pct}%</div>
|
|
@@ -370,6 +1008,12 @@ html = f"""<!DOCTYPE html>
|
|
|
370
1008
|
<div class="stat-value">{overall_efficiency}%</div>
|
|
371
1009
|
<div class="stat-sub">key / non-trivial (higher = better)</div>
|
|
372
1010
|
</div>
|
|
1011
|
+
{agents_stat_html}
|
|
1012
|
+
{skills_stat_html}
|
|
1013
|
+
{friction_stat_html}
|
|
1014
|
+
{retry_stat_html}
|
|
1015
|
+
{error_stat_html}
|
|
1016
|
+
{compaction_stat_html}
|
|
373
1017
|
</div>
|
|
374
1018
|
|
|
375
1019
|
<div class="section">
|
|
@@ -396,10 +1040,15 @@ html = f"""<!DOCTYPE html>
|
|
|
396
1040
|
<canvas id="modelStack"></canvas>
|
|
397
1041
|
</div>
|
|
398
1042
|
|
|
1043
|
+
<div class="card wide">
|
|
1044
|
+
<h2>Token composition per day</h2>
|
|
1045
|
+
<canvas id="tokenComp"></canvas>
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
399
1048
|
</div>
|
|
400
1049
|
</div>
|
|
401
1050
|
|
|
402
|
-
<div class="section">
|
|
1051
|
+
{agents_section_html}{skills_section_html}{friction_section_html}{error_section_html}{compaction_section_html}<div class="section">
|
|
403
1052
|
<div class="section-header prompts">Key Prompts</div>
|
|
404
1053
|
<div class="grid">
|
|
405
1054
|
|
|
@@ -477,6 +1126,9 @@ const DATES = {dates_js};
|
|
|
477
1126
|
const COST_BY_DATE = {cost_by_date_js};
|
|
478
1127
|
const SESSIONS_BY_DATE = {sessions_by_date_js};
|
|
479
1128
|
const OUTPUT_BY_DATE = {output_by_date_js};
|
|
1129
|
+
const INPUT_BY_DATE = {input_by_date_js};
|
|
1130
|
+
const CACHE_CREATE_BY_DATE = {cache_create_by_date_js};
|
|
1131
|
+
const CACHE_READ_BY_DATE = {cache_read_by_date_js};
|
|
480
1132
|
const OPUS_BY_DATE = {opus_by_date_js};
|
|
481
1133
|
const SONNET_BY_DATE = {sonnet_by_date_js};
|
|
482
1134
|
const CUMUL_LABELS = {cumul_labels_js};
|
|
@@ -501,6 +1153,11 @@ const AVG_DURATION_BY_DATE = {avg_duration_by_date_js};
|
|
|
501
1153
|
const SCATTER_DATA = {scatter_data_js};
|
|
502
1154
|
const TPM_DATA = {tpm_data_js};
|
|
503
1155
|
const DUR_HIST_RANGES = {dur_hist_ranges_js};
|
|
1156
|
+
{agents_js_constants}
|
|
1157
|
+
{skills_js_constants}
|
|
1158
|
+
{friction_js_constants}
|
|
1159
|
+
{error_js_constants}
|
|
1160
|
+
{compaction_js_constants}
|
|
504
1161
|
|
|
505
1162
|
function formatDuration(s) {{
|
|
506
1163
|
if (s <= 0) return '0s';
|
|
@@ -576,6 +1233,23 @@ new Chart(document.getElementById('modelStack'), {{
|
|
|
576
1233
|
tooltip: {{ callbacks: {{ label: ctx => ' $' + ctx.parsed.y.toFixed(2) }} }} }} }}
|
|
577
1234
|
}});
|
|
578
1235
|
|
|
1236
|
+
// Token composition per day (stacked bar)
|
|
1237
|
+
new Chart(document.getElementById('tokenComp'), {{
|
|
1238
|
+
type: 'bar',
|
|
1239
|
+
data: {{
|
|
1240
|
+
labels: DATES,
|
|
1241
|
+
datasets: [
|
|
1242
|
+
{{ label: 'Input', data: INPUT_BY_DATE, backgroundColor: '#6366f1', borderRadius: 2 }},
|
|
1243
|
+
{{ label: 'Cache creation', data: CACHE_CREATE_BY_DATE, backgroundColor: '#f59e0b', borderRadius: 2 }},
|
|
1244
|
+
{{ label: 'Cache read', data: CACHE_READ_BY_DATE, backgroundColor: '#22d3ee', borderRadius: 2 }},
|
|
1245
|
+
{{ label: 'Output', data: OUTPUT_BY_DATE, backgroundColor: '#a78bfa', borderRadius: 2 }}
|
|
1246
|
+
]
|
|
1247
|
+
}},
|
|
1248
|
+
options: {{ ...baseOpts, scales: {{ ...baseOpts.scales,
|
|
1249
|
+
x: {{ ...baseOpts.scales.x, stacked: true }},
|
|
1250
|
+
y: {{ ...baseOpts.scales.y, stacked: true }} }} }}
|
|
1251
|
+
}});
|
|
1252
|
+
|
|
579
1253
|
// Session duration per day
|
|
580
1254
|
new Chart(document.getElementById('durationDay'), {{
|
|
581
1255
|
type: 'bar',
|
|
@@ -757,10 +1431,15 @@ new Chart(document.getElementById('promptStack'), {{
|
|
|
757
1431
|
}}
|
|
758
1432
|
}}
|
|
759
1433
|
}});
|
|
1434
|
+
{agents_js_charts}
|
|
1435
|
+
{skills_js_charts}
|
|
1436
|
+
{friction_js_charts}
|
|
1437
|
+
{error_js_charts}
|
|
1438
|
+
{compaction_js_charts}
|
|
760
1439
|
</script>
|
|
761
1440
|
</body>
|
|
762
1441
|
</html>
|
|
763
1442
|
"""
|
|
764
1443
|
|
|
765
|
-
with open(output_file, "w") as f:
|
|
1444
|
+
with open(output_file, "w", encoding='utf-8') as f:
|
|
766
1445
|
f.write(html)
|