@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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 +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- 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/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -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/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -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/agents.py +183 -0
- package/ui/api/__init__.py +84 -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/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Self-building stats counter for aesop README.
|
|
4
|
+
|
|
5
|
+
Computes git-derived metrics (verifiable by anyone who clones) and reads session telemetry
|
|
6
|
+
from docs/self-stats-data.json. All hard metrics in output carry verification markers.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python self_stats.py [--repo PATH] [--data-file PATH] [--markdown|--json]
|
|
10
|
+
|
|
11
|
+
Output modes:
|
|
12
|
+
default - Human-readable table
|
|
13
|
+
--markdown - README block with <!-- SELF-STATS:START/END --> markers (markdown verification comments)
|
|
14
|
+
--json - Machine-readable JSON object
|
|
15
|
+
|
|
16
|
+
All hard metrics (percentages, multipliers, dollar amounts) in markdown output include
|
|
17
|
+
<!-- metrics-verified: <source> --> markers for the metrics_gate.py CI gate.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Optional, Dict, Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GitStats:
|
|
31
|
+
"""Compute statistics from git repository."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, repo_root: str = "."):
|
|
34
|
+
"""Initialize with repo root path."""
|
|
35
|
+
self.repo_root = Path(repo_root)
|
|
36
|
+
self._merged_prs = None
|
|
37
|
+
self._total_commits = None
|
|
38
|
+
self._project_age_days = None
|
|
39
|
+
self._wave_count = None
|
|
40
|
+
self._insertions_deletions = None
|
|
41
|
+
self._files_tracked = None
|
|
42
|
+
self._distinct_coauthors = None
|
|
43
|
+
|
|
44
|
+
def _run_git(self, *args, check=True) -> str:
|
|
45
|
+
"""Run git command in repo, return stdout."""
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
["git"] + list(args),
|
|
49
|
+
cwd=str(self.repo_root),
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
errors="replace",
|
|
54
|
+
check=check,
|
|
55
|
+
)
|
|
56
|
+
return (result.stdout or "").strip()
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def merged_prs(self) -> int:
|
|
62
|
+
"""Count merge commits with 'Merge pull request #' in message."""
|
|
63
|
+
if self._merged_prs is not None:
|
|
64
|
+
return self._merged_prs
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Get all commit messages
|
|
68
|
+
output = self._run_git("log", "--format=%B", check=False)
|
|
69
|
+
if not output:
|
|
70
|
+
self._merged_prs = 0
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
# Count lines matching "Merge pull request #"
|
|
74
|
+
count = output.count("Merge pull request #")
|
|
75
|
+
self._merged_prs = count
|
|
76
|
+
return count
|
|
77
|
+
except Exception:
|
|
78
|
+
self._merged_prs = 0
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def total_commits(self) -> int:
|
|
83
|
+
"""Total commit count."""
|
|
84
|
+
if self._total_commits is not None:
|
|
85
|
+
return self._total_commits
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
output = self._run_git("rev-list", "--count", "HEAD", check=False)
|
|
89
|
+
count = int(output) if output else 0
|
|
90
|
+
self._total_commits = count
|
|
91
|
+
return count
|
|
92
|
+
except (ValueError, Exception):
|
|
93
|
+
self._total_commits = 0
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def project_age_days(self) -> Optional[int]:
|
|
98
|
+
"""Project age in days (first commit to now)."""
|
|
99
|
+
if self._project_age_days is not None:
|
|
100
|
+
return self._project_age_days
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Get timestamp of the earliest commit (root) — --reverse lists
|
|
104
|
+
# oldest first, so the first line is the project's birth.
|
|
105
|
+
output = self._run_git(
|
|
106
|
+
"log", "--reverse", "--format=%cI", check=False
|
|
107
|
+
)
|
|
108
|
+
if not output:
|
|
109
|
+
self._project_age_days = None
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
first_commit_iso = output.split("\n", 1)[0].strip()
|
|
113
|
+
if not first_commit_iso:
|
|
114
|
+
self._project_age_days = None
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# Parse ISO format timestamp
|
|
118
|
+
first_commit_dt = datetime.fromisoformat(first_commit_iso.replace("Z", "+00:00"))
|
|
119
|
+
now_dt = datetime.now(timezone.utc)
|
|
120
|
+
age_days = (now_dt - first_commit_dt).days
|
|
121
|
+
|
|
122
|
+
self._project_age_days = age_days
|
|
123
|
+
return age_days
|
|
124
|
+
except Exception:
|
|
125
|
+
self._project_age_days = None
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def wave_count(self) -> int:
|
|
130
|
+
"""Count of distinct waves (parse wave labels or release tags)."""
|
|
131
|
+
if self._wave_count is not None:
|
|
132
|
+
return self._wave_count
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# First try parsing wave labels from merge commit messages
|
|
136
|
+
output = self._run_git("log", "--format=%B", check=False)
|
|
137
|
+
if output:
|
|
138
|
+
# Count lines like "wave-N" (case insensitive)
|
|
139
|
+
import re
|
|
140
|
+
waves = set()
|
|
141
|
+
for match in re.finditer(r"wave[_-]?(\d+)", output, re.IGNORECASE):
|
|
142
|
+
waves.add(int(match.group(1)))
|
|
143
|
+
if waves:
|
|
144
|
+
self._wave_count = len(waves)
|
|
145
|
+
return len(waves)
|
|
146
|
+
|
|
147
|
+
# Fallback: count release tags (v*)
|
|
148
|
+
tags = self._run_git("tag", "-l", "v*", check=False)
|
|
149
|
+
tag_count = len([t for t in tags.split("\n") if t.strip()])
|
|
150
|
+
self._wave_count = tag_count
|
|
151
|
+
return tag_count
|
|
152
|
+
except Exception:
|
|
153
|
+
self._wave_count = 0
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def insertions_deletions(self) -> int:
|
|
158
|
+
"""Total insertions + deletions across all commits."""
|
|
159
|
+
if self._insertions_deletions is not None:
|
|
160
|
+
return self._insertions_deletions
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
output = self._run_git(
|
|
164
|
+
"log", "--numstat", "--format=%H", check=False
|
|
165
|
+
)
|
|
166
|
+
if not output:
|
|
167
|
+
self._insertions_deletions = 0
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
total = 0
|
|
171
|
+
for line in output.split("\n"):
|
|
172
|
+
parts = line.split("\t")
|
|
173
|
+
if len(parts) >= 2:
|
|
174
|
+
try:
|
|
175
|
+
# Skip lines that are commit hashes or other non-numstat data
|
|
176
|
+
insertions = int(parts[0])
|
|
177
|
+
deletions = int(parts[1])
|
|
178
|
+
total += insertions + deletions
|
|
179
|
+
except ValueError:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
self._insertions_deletions = total
|
|
183
|
+
return total
|
|
184
|
+
except Exception:
|
|
185
|
+
self._insertions_deletions = 0
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def files_tracked(self) -> int:
|
|
190
|
+
"""Count of tracked files."""
|
|
191
|
+
if self._files_tracked is not None:
|
|
192
|
+
return self._files_tracked
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
output = self._run_git("ls-files", check=False)
|
|
196
|
+
count = len([f for f in output.split("\n") if f.strip()])
|
|
197
|
+
self._files_tracked = count
|
|
198
|
+
return count
|
|
199
|
+
except Exception:
|
|
200
|
+
self._files_tracked = 0
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def distinct_coauthors(self) -> int:
|
|
205
|
+
"""Count of distinct authors including co-authors."""
|
|
206
|
+
if self._distinct_coauthors is not None:
|
|
207
|
+
return self._distinct_coauthors
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Get all authors
|
|
211
|
+
output = self._run_git("log", "--format=%an", check=False)
|
|
212
|
+
authors = set()
|
|
213
|
+
if output:
|
|
214
|
+
for author in output.split("\n"):
|
|
215
|
+
if author.strip():
|
|
216
|
+
authors.add(author.strip())
|
|
217
|
+
|
|
218
|
+
# Get all co-authors from commit messages
|
|
219
|
+
commit_msg = self._run_git("log", "--format=%B", check=False)
|
|
220
|
+
if commit_msg:
|
|
221
|
+
import re
|
|
222
|
+
for match in re.finditer(r"Co-Authored-By:\s*(.+?)(?:\n|$)", commit_msg):
|
|
223
|
+
coauthor = match.group(1).strip()
|
|
224
|
+
if coauthor:
|
|
225
|
+
authors.add(coauthor)
|
|
226
|
+
|
|
227
|
+
count = len(authors)
|
|
228
|
+
self._distinct_coauthors = count
|
|
229
|
+
return count
|
|
230
|
+
except Exception:
|
|
231
|
+
self._distinct_coauthors = 0
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class SessionTelemetry:
|
|
236
|
+
"""Load session telemetry from JSON file."""
|
|
237
|
+
|
|
238
|
+
def __init__(self, data_file: str = "docs/self-stats-data.json"):
|
|
239
|
+
"""Initialize with data file path."""
|
|
240
|
+
self.data_file = Path(data_file)
|
|
241
|
+
self._data = None
|
|
242
|
+
self._load()
|
|
243
|
+
|
|
244
|
+
def _load(self):
|
|
245
|
+
"""Load JSON data, silently ignore missing/invalid files."""
|
|
246
|
+
if not self.data_file.exists():
|
|
247
|
+
self._data = {}
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
with open(self.data_file) as f:
|
|
252
|
+
self._data = json.load(f)
|
|
253
|
+
except (json.JSONDecodeError, IOError):
|
|
254
|
+
self._data = {}
|
|
255
|
+
|
|
256
|
+
def _get(self, key: str) -> Optional[Any]:
|
|
257
|
+
"""Get field, return None if missing or null."""
|
|
258
|
+
if not self._data:
|
|
259
|
+
return None
|
|
260
|
+
value = self._data.get(key)
|
|
261
|
+
return value if value is not None else None
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def total_sessions(self) -> Optional[int]:
|
|
265
|
+
return self._get("total_sessions")
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def total_turns(self) -> Optional[int]:
|
|
269
|
+
return self._get("total_turns")
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def total_user_prompts(self) -> Optional[int]:
|
|
273
|
+
return self._get("total_user_prompts")
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def max_tokens_single_turn(self) -> Optional[int]:
|
|
277
|
+
return self._get("max_tokens_single_turn")
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def cumulative_agent_runs(self) -> Optional[int]:
|
|
281
|
+
return self._get("cumulative_agent_runs")
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def cumulative_tokens(self) -> Optional[int]:
|
|
285
|
+
return self._get("cumulative_tokens")
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def total_coding_hours(self) -> Optional[float]:
|
|
289
|
+
return self._get("total_coding_hours")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class StatsCounter:
|
|
293
|
+
"""Combine git and telemetry stats, format for output."""
|
|
294
|
+
|
|
295
|
+
def __init__(self, repo_root: str = ".", data_file: str = None):
|
|
296
|
+
"""Initialize with repo root and optional data file."""
|
|
297
|
+
self.git = GitStats(repo_root=repo_root)
|
|
298
|
+
if data_file is None:
|
|
299
|
+
# Infer from repo root
|
|
300
|
+
data_file = str(Path(repo_root) / "docs" / "self-stats-data.json")
|
|
301
|
+
self.telemetry = SessionTelemetry(data_file=data_file)
|
|
302
|
+
|
|
303
|
+
def table(self) -> str:
|
|
304
|
+
"""Human-readable table format."""
|
|
305
|
+
lines = []
|
|
306
|
+
lines.append("")
|
|
307
|
+
lines.append("=" * 50)
|
|
308
|
+
lines.append("Aesop Self-Building Stats")
|
|
309
|
+
lines.append("=" * 50)
|
|
310
|
+
lines.append("")
|
|
311
|
+
|
|
312
|
+
# Git-derived stats
|
|
313
|
+
lines.append("Repository Metrics:")
|
|
314
|
+
if self.git.merged_prs > 0:
|
|
315
|
+
lines.append(f" Merged PRs: {self.git.merged_prs}")
|
|
316
|
+
if self.git.total_commits > 0:
|
|
317
|
+
lines.append(f" Total Commits: {self.git.total_commits}")
|
|
318
|
+
if self.git.project_age_days is not None and self.git.project_age_days >= 0:
|
|
319
|
+
lines.append(f" Project Age (days): {self.git.project_age_days}")
|
|
320
|
+
if self.git.wave_count > 0:
|
|
321
|
+
lines.append(f" Wave Count: {self.git.wave_count}")
|
|
322
|
+
if self.git.insertions_deletions > 0:
|
|
323
|
+
lines.append(f" Insertions+Deletions: {self.git.insertions_deletions}")
|
|
324
|
+
if self.git.files_tracked > 0:
|
|
325
|
+
lines.append(f" Files Tracked: {self.git.files_tracked}")
|
|
326
|
+
if self.git.distinct_coauthors > 0:
|
|
327
|
+
lines.append(f" Distinct Co-authors: {self.git.distinct_coauthors}")
|
|
328
|
+
|
|
329
|
+
# Session telemetry (only if present)
|
|
330
|
+
if any([
|
|
331
|
+
self.telemetry.total_sessions,
|
|
332
|
+
self.telemetry.total_turns,
|
|
333
|
+
self.telemetry.cumulative_tokens,
|
|
334
|
+
]):
|
|
335
|
+
lines.append("")
|
|
336
|
+
lines.append("Session Telemetry:")
|
|
337
|
+
if self.telemetry.total_sessions is not None:
|
|
338
|
+
lines.append(f" Total Sessions: {self.telemetry.total_sessions}")
|
|
339
|
+
if self.telemetry.total_turns is not None:
|
|
340
|
+
lines.append(f" Total Turns: {self.telemetry.total_turns}")
|
|
341
|
+
if self.telemetry.total_user_prompts is not None:
|
|
342
|
+
lines.append(f" User Prompts: {self.telemetry.total_user_prompts}")
|
|
343
|
+
if self.telemetry.cumulative_agent_runs is not None:
|
|
344
|
+
lines.append(f" Agent Runs: {self.telemetry.cumulative_agent_runs}")
|
|
345
|
+
if self.telemetry.cumulative_tokens is not None:
|
|
346
|
+
lines.append(f" Total Tokens: {self.telemetry.cumulative_tokens}")
|
|
347
|
+
if self.telemetry.max_tokens_single_turn is not None:
|
|
348
|
+
lines.append(f" Max Tokens/Turn: {self.telemetry.max_tokens_single_turn}")
|
|
349
|
+
if self.telemetry.total_coding_hours is not None:
|
|
350
|
+
lines.append(f" Coding Hours: {self.telemetry.total_coding_hours}")
|
|
351
|
+
|
|
352
|
+
lines.append("")
|
|
353
|
+
lines.append("=" * 50)
|
|
354
|
+
lines.append("")
|
|
355
|
+
|
|
356
|
+
return "\n".join(lines)
|
|
357
|
+
|
|
358
|
+
def markdown(self) -> str:
|
|
359
|
+
"""Markdown output with verification markers for hard metrics."""
|
|
360
|
+
lines = []
|
|
361
|
+
lines.append("<!-- SELF-STATS:START -->")
|
|
362
|
+
lines.append("")
|
|
363
|
+
lines.append("## Aesop builds itself")
|
|
364
|
+
lines.append("")
|
|
365
|
+
lines.append(
|
|
366
|
+
"Aesop is built entirely by its own `/buildsystem` wave cycle—running parallel Haiku fleets "
|
|
367
|
+
"across ranked backlog items, verifying merges, auditing orchestration health. "
|
|
368
|
+
"These stats are the receipts: all numbers computed LIVE from git, verified by anyone who clones."
|
|
369
|
+
)
|
|
370
|
+
lines.append("")
|
|
371
|
+
|
|
372
|
+
# Build stat rows
|
|
373
|
+
rows = []
|
|
374
|
+
|
|
375
|
+
if self.git.merged_prs > 0:
|
|
376
|
+
rows.append(
|
|
377
|
+
f"| Merged PRs | {self.git.merged_prs} <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
378
|
+
)
|
|
379
|
+
if self.git.total_commits > 0:
|
|
380
|
+
rows.append(
|
|
381
|
+
f"| Total Commits | {self.git.total_commits} <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
382
|
+
)
|
|
383
|
+
if self.git.project_age_days is not None and self.git.project_age_days >= 0:
|
|
384
|
+
rows.append(
|
|
385
|
+
f"| Project Age | {self.git.project_age_days} days <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
386
|
+
)
|
|
387
|
+
if self.git.wave_count > 0:
|
|
388
|
+
rows.append(
|
|
389
|
+
f"| Waves | {self.git.wave_count} <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
390
|
+
)
|
|
391
|
+
if self.git.insertions_deletions > 0:
|
|
392
|
+
rows.append(
|
|
393
|
+
f"| Insertions + Deletions | {self.git.insertions_deletions:,} <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
394
|
+
)
|
|
395
|
+
if self.git.files_tracked > 0:
|
|
396
|
+
rows.append(
|
|
397
|
+
f"| Files Tracked | {self.git.files_tracked} <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
398
|
+
)
|
|
399
|
+
if self.git.distinct_coauthors > 0:
|
|
400
|
+
rows.append(
|
|
401
|
+
f"| Distinct Co-authors | {self.git.distinct_coauthors} <!-- metrics-verified: self_stats.py (git log) --> |"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Session telemetry
|
|
405
|
+
if self.telemetry.total_sessions is not None:
|
|
406
|
+
rows.append(
|
|
407
|
+
f"| Sessions | {self.telemetry.total_sessions} <!-- metrics-verified: docs/self-stats-data.json --> |"
|
|
408
|
+
)
|
|
409
|
+
if self.telemetry.total_turns is not None:
|
|
410
|
+
rows.append(
|
|
411
|
+
f"| Total Turns | {self.telemetry.total_turns} <!-- metrics-verified: docs/self-stats-data.json --> |"
|
|
412
|
+
)
|
|
413
|
+
if self.telemetry.cumulative_tokens is not None:
|
|
414
|
+
rows.append(
|
|
415
|
+
f"| Cumulative Tokens | {self.telemetry.cumulative_tokens:,} <!-- metrics-verified: docs/self-stats-data.json --> |"
|
|
416
|
+
)
|
|
417
|
+
if self.telemetry.total_coding_hours is not None:
|
|
418
|
+
rows.append(
|
|
419
|
+
f"| Coding Hours | {self.telemetry.total_coding_hours} <!-- metrics-verified: docs/self-stats-data.json --> |"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Only add table if we have rows
|
|
423
|
+
if rows:
|
|
424
|
+
lines.append("| Metric | Value |")
|
|
425
|
+
lines.append("| --- | --- |")
|
|
426
|
+
lines.extend(rows)
|
|
427
|
+
lines.append("")
|
|
428
|
+
|
|
429
|
+
lines.append("<!-- SELF-STATS:END -->")
|
|
430
|
+
lines.append("")
|
|
431
|
+
|
|
432
|
+
return "\n".join(lines)
|
|
433
|
+
|
|
434
|
+
def json(self) -> str:
|
|
435
|
+
"""Machine-readable JSON output."""
|
|
436
|
+
data = {
|
|
437
|
+
"git": {
|
|
438
|
+
"merged_prs": self.git.merged_prs,
|
|
439
|
+
"total_commits": self.git.total_commits,
|
|
440
|
+
"project_age_days": self.git.project_age_days,
|
|
441
|
+
"wave_count": self.git.wave_count,
|
|
442
|
+
"insertions_deletions": self.git.insertions_deletions,
|
|
443
|
+
"files_tracked": self.git.files_tracked,
|
|
444
|
+
"distinct_coauthors": self.git.distinct_coauthors,
|
|
445
|
+
},
|
|
446
|
+
"telemetry": {
|
|
447
|
+
"total_sessions": self.telemetry.total_sessions,
|
|
448
|
+
"total_turns": self.telemetry.total_turns,
|
|
449
|
+
"total_user_prompts": self.telemetry.total_user_prompts,
|
|
450
|
+
"max_tokens_single_turn": self.telemetry.max_tokens_single_turn,
|
|
451
|
+
"cumulative_agent_runs": self.telemetry.cumulative_agent_runs,
|
|
452
|
+
"cumulative_tokens": self.telemetry.cumulative_tokens,
|
|
453
|
+
"total_coding_hours": self.telemetry.total_coding_hours,
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
return json.dumps(data, indent=2)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def main():
|
|
460
|
+
"""CLI entry point."""
|
|
461
|
+
parser = argparse.ArgumentParser(
|
|
462
|
+
description=__doc__,
|
|
463
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
464
|
+
)
|
|
465
|
+
parser.add_argument(
|
|
466
|
+
"--repo",
|
|
467
|
+
default=".",
|
|
468
|
+
help="Repository root (default: current directory)"
|
|
469
|
+
)
|
|
470
|
+
parser.add_argument(
|
|
471
|
+
"--data-file",
|
|
472
|
+
help="Path to docs/self-stats-data.json (auto-detected if not specified)"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
mode_group = parser.add_mutually_exclusive_group()
|
|
476
|
+
mode_group.add_argument(
|
|
477
|
+
"--markdown",
|
|
478
|
+
action="store_true",
|
|
479
|
+
help="Output markdown block with START/END markers"
|
|
480
|
+
)
|
|
481
|
+
mode_group.add_argument(
|
|
482
|
+
"--json",
|
|
483
|
+
action="store_true",
|
|
484
|
+
help="Output machine-readable JSON"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
args = parser.parse_args()
|
|
488
|
+
|
|
489
|
+
counter = StatsCounter(repo_root=args.repo, data_file=args.data_file)
|
|
490
|
+
|
|
491
|
+
# Use UTF-8 for output to handle emojis
|
|
492
|
+
import io
|
|
493
|
+
import sys
|
|
494
|
+
if hasattr(sys.stdout, 'buffer'):
|
|
495
|
+
out = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
496
|
+
else:
|
|
497
|
+
out = sys.stdout
|
|
498
|
+
|
|
499
|
+
if args.markdown:
|
|
500
|
+
out.write(counter.markdown())
|
|
501
|
+
elif args.json:
|
|
502
|
+
out.write(counter.json())
|
|
503
|
+
else:
|
|
504
|
+
out.write(counter.table())
|
|
505
|
+
out.flush()
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
if __name__ == "__main__":
|
|
509
|
+
main()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sum token usage across a session's main thread and subagent transcripts.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python sum_session_tokens.py <session_dir> [<main_transcript>]
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
session_dir: Path to session directory (e.g., .../f1bfdcf9-2ece-42f3-8798-c9066146f6a1)
|
|
10
|
+
main_transcript: Path to main-thread JSONL transcript (auto-detected if not provided)
|
|
11
|
+
|
|
12
|
+
Extracts per-agent totals of input_tokens, output_tokens, cache_read_input_tokens,
|
|
13
|
+
cache_creation_input_tokens from all .output files in <session_dir>/tasks/ plus
|
|
14
|
+
the main thread. Outputs a table with per-agent summaries and totals.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def extract_usage(line_obj):
|
|
25
|
+
"""Extract usage fields from a message object."""
|
|
26
|
+
if "message" not in line_obj or "usage" not in line_obj["message"]:
|
|
27
|
+
return None
|
|
28
|
+
usage = line_obj["message"]["usage"]
|
|
29
|
+
return {
|
|
30
|
+
"input_tokens": usage.get("input_tokens", 0),
|
|
31
|
+
"output_tokens": usage.get("output_tokens", 0),
|
|
32
|
+
"cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
|
|
33
|
+
"cache_creation_input_tokens": usage.get("cache_creation_input_tokens", 0),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_model(line_obj):
|
|
38
|
+
"""Extract model name from a message object."""
|
|
39
|
+
if "message" not in line_obj:
|
|
40
|
+
return None
|
|
41
|
+
msg = line_obj["message"]
|
|
42
|
+
if "model" in msg:
|
|
43
|
+
return msg["model"]
|
|
44
|
+
# Fallback: check if it's in content metadata
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_transcript(filepath, agent_name=None):
|
|
49
|
+
"""
|
|
50
|
+
Parse a JSONL transcript file and sum usage per message.
|
|
51
|
+
Returns (agent_name, model, total_usage_dict).
|
|
52
|
+
"""
|
|
53
|
+
total_usage = {
|
|
54
|
+
"input_tokens": 0,
|
|
55
|
+
"output_tokens": 0,
|
|
56
|
+
"cache_read_input_tokens": 0,
|
|
57
|
+
"cache_creation_input_tokens": 0,
|
|
58
|
+
}
|
|
59
|
+
model = None
|
|
60
|
+
message_count = 0
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
64
|
+
for line in f:
|
|
65
|
+
line = line.strip()
|
|
66
|
+
if not line:
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
obj = json.loads(line)
|
|
70
|
+
usage = extract_usage(obj)
|
|
71
|
+
if usage:
|
|
72
|
+
for key in total_usage:
|
|
73
|
+
total_usage[key] += usage.get(key, 0)
|
|
74
|
+
message_count += 1
|
|
75
|
+
if model is None:
|
|
76
|
+
model = extract_model(obj)
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
pass
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Warning: Failed to parse {filepath}: {e}", file=sys.stderr)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if message_count == 0:
|
|
84
|
+
return None # No usage data
|
|
85
|
+
|
|
86
|
+
return (agent_name or filepath.stem, model, total_usage)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
if len(sys.argv) < 2:
|
|
91
|
+
print(f"Usage: {sys.argv[0]} <session_dir> [<main_transcript>]", file=sys.stderr)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
session_dir = Path(sys.argv[1])
|
|
95
|
+
if not session_dir.exists():
|
|
96
|
+
print(f"Error: Session directory not found: {session_dir}", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
# Find main transcript
|
|
100
|
+
if len(sys.argv) >= 3:
|
|
101
|
+
main_transcript = Path(sys.argv[2])
|
|
102
|
+
else:
|
|
103
|
+
# Auto-detect: look for largest .jsonl matching session ID in parent or memory dirs
|
|
104
|
+
session_id = session_dir.name
|
|
105
|
+
proj_dirs = [
|
|
106
|
+
session_dir.parent, # If session_dir is already in projects/
|
|
107
|
+
Path.home() / ".claude" / "projects" / session_dir.parent.name,
|
|
108
|
+
]
|
|
109
|
+
main_transcript = None
|
|
110
|
+
for proj_dir in proj_dirs:
|
|
111
|
+
if proj_dir.exists():
|
|
112
|
+
candidates = list(proj_dir.glob(f"{session_id}*.jsonl"))
|
|
113
|
+
if candidates:
|
|
114
|
+
main_transcript = max(candidates, key=lambda p: p.stat().st_size)
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
# Collect usage data
|
|
118
|
+
results = {}
|
|
119
|
+
|
|
120
|
+
# Parse main thread
|
|
121
|
+
if main_transcript and main_transcript.exists():
|
|
122
|
+
result = parse_transcript(main_transcript, "orchestrator (main thread)")
|
|
123
|
+
if result:
|
|
124
|
+
name, model, usage = result
|
|
125
|
+
results[name] = (model or "Fable/Opus (inferred)", usage)
|
|
126
|
+
|
|
127
|
+
# Parse subagent tasks
|
|
128
|
+
tasks_dir = session_dir / "tasks"
|
|
129
|
+
if tasks_dir.exists():
|
|
130
|
+
for output_file in sorted(tasks_dir.glob("*.output")):
|
|
131
|
+
result = parse_transcript(output_file)
|
|
132
|
+
if result:
|
|
133
|
+
name, model, usage = result
|
|
134
|
+
results[name] = (model or "Unknown", usage)
|
|
135
|
+
|
|
136
|
+
if not results:
|
|
137
|
+
print("No usage data found in transcripts.", file=sys.stderr)
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
# Print results
|
|
141
|
+
print("\n" + "=" * 100)
|
|
142
|
+
print("SESSION TOKEN USAGE SUMMARY")
|
|
143
|
+
print("=" * 100)
|
|
144
|
+
print(f"Timestamp: {datetime.now().isoformat()}")
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
print(f"{'Agent':<35} {'Model':<15} {'Input':>12} {'Output':>12} {'CacheRead':>12} {'CacheCreate':>12} {'Total':>12}")
|
|
148
|
+
print("-" * 112)
|
|
149
|
+
|
|
150
|
+
grand_total = {
|
|
151
|
+
"input_tokens": 0,
|
|
152
|
+
"output_tokens": 0,
|
|
153
|
+
"cache_read_input_tokens": 0,
|
|
154
|
+
"cache_creation_input_tokens": 0,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for agent_name in sorted(results.keys()):
|
|
158
|
+
model, usage = results[agent_name]
|
|
159
|
+
inp = usage["input_tokens"]
|
|
160
|
+
out = usage["output_tokens"]
|
|
161
|
+
cache_read = usage["cache_read_input_tokens"]
|
|
162
|
+
cache_create = usage["cache_creation_input_tokens"]
|
|
163
|
+
total = inp + out + cache_read + cache_create
|
|
164
|
+
|
|
165
|
+
print(
|
|
166
|
+
f"{agent_name:<35} {model:<15} {inp:>12,} {out:>12,} {cache_read:>12,} {cache_create:>12,} {total:>12,}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
for key in grand_total:
|
|
170
|
+
grand_total[key] += usage[key]
|
|
171
|
+
|
|
172
|
+
print("-" * 112)
|
|
173
|
+
grand_inp = grand_total["input_tokens"]
|
|
174
|
+
grand_out = grand_total["output_tokens"]
|
|
175
|
+
grand_cache_read = grand_total["cache_read_input_tokens"]
|
|
176
|
+
grand_cache_create = grand_total["cache_creation_input_tokens"]
|
|
177
|
+
grand_sum = grand_inp + grand_out + grand_cache_read + grand_cache_create
|
|
178
|
+
|
|
179
|
+
print(
|
|
180
|
+
f"{'TOTAL':<35} {'':<15} {grand_inp:>12,} {grand_out:>12,} {grand_cache_read:>12,} {grand_cache_create:>12,} {grand_sum:>12,}"
|
|
181
|
+
)
|
|
182
|
+
print("=" * 100)
|
|
183
|
+
print()
|
|
184
|
+
|
|
185
|
+
# Summary by category
|
|
186
|
+
print("SUMMARY BY CATEGORY:")
|
|
187
|
+
print(f" Input tokens (fresh): {grand_inp:>12,}")
|
|
188
|
+
print(f" Output tokens: {grand_out:>12,}")
|
|
189
|
+
print(f" Cache read tokens: {grand_cache_read:>12,}")
|
|
190
|
+
print(f" Cache creation tokens: {grand_cache_create:>12,}")
|
|
191
|
+
print(f" TOTAL: {grand_sum:>12,}")
|
|
192
|
+
print()
|
|
193
|
+
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
sys.exit(main())
|