agent-bios 0.9.3 → 0.9.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/claude/CLAUDE.md +1 -1
- package/claude/agents/workhorse.md +2 -2
- package/claude/guides/cli-multi-model-workflow.md +2 -2
- package/claude/guides/learning-flow.md +2 -2
- package/claude/settings.json +60 -0
- package/codex/AGENTS.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +2 -2
- package/codex/guides/learning-flow.md +2 -2
- package/config/agent-launch.toml +2 -2
- package/package.json +8 -2
- package/scripts/assemble.py +322 -0
- package/scripts/build-promotions.py +183 -0
- package/scripts/canary.sh +28 -0
- package/scripts/check-domains.py +242 -0
- package/scripts/check-parity.sh +2 -2
- package/scripts/ingest-learnings-export.py +258 -0
- package/scripts/install.sh +28 -0
- package/scripts/session-cost.py +38 -15
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Curation intake — map a dashboard learnings export to ledger candidates.
|
|
3
|
+
|
|
4
|
+
Phase 3 of the collection loop (design/collection-loop/PHASE3-CURATION-DESIGN.md).
|
|
5
|
+
The curator exports RECEIVED learnings from the dashboard as ledger-compatible
|
|
6
|
+
JSON (GET /api/exports/learnings — verbatim payloads + provenance); THIS script
|
|
7
|
+
does the DETERMINISTIC half of intake:
|
|
8
|
+
|
|
9
|
+
* validates each exported record against config/learning.schema.json — the
|
|
10
|
+
single validation source, reused from scripts/check-learning.py (no second
|
|
11
|
+
schema), so an invalid/verbatim-but-nonconforming payload is caught here;
|
|
12
|
+
* buckets each row: VALID → a ledger-candidate entry; REJECTED → schema or
|
|
13
|
+
domain-membership failure (with the reasons); DEFERRED → schema_version != 1
|
|
14
|
+
(Phase 2 stores v2+ verbatim for forward-compat; the v1 intake cannot map it
|
|
15
|
+
yet — it is NOT a reject, it is re-exportable once a v2-aware intake lands);
|
|
16
|
+
* flags candidates whose learning_id already appears in ledger.json (a
|
|
17
|
+
deterministic dedup warning — string membership, not a semantic judgment);
|
|
18
|
+
* emits a curation WORKLIST the curator then works through by hand.
|
|
19
|
+
|
|
20
|
+
It does NOT do the SEMANTIC half — triage the domain, classify type/layer/
|
|
21
|
+
mechanism, or judge novelty vs the full canon. Those stay with the curator
|
|
22
|
+
(capability boundary); see design/collection-loop/CURATION-INTAKE.md.
|
|
23
|
+
|
|
24
|
+
PII boundary: the worklist carries `_provenance.user_email` (D3.3 — visibility
|
|
25
|
+
into who contributes what). The worklist is a LOCAL artifact — never commit it;
|
|
26
|
+
when merging a candidate into the git-tracked ledger.json, keep `learning_id`
|
|
27
|
+
(non-PII dedup key) and DROP `_provenance` (see the procedure doc).
|
|
28
|
+
|
|
29
|
+
Input: a learnings-export JSON file (positional arg; '-' or omitted = stdin).
|
|
30
|
+
Output: the worklist JSON to stdout, or to --out FILE.
|
|
31
|
+
"""
|
|
32
|
+
import argparse
|
|
33
|
+
import importlib.util
|
|
34
|
+
import json
|
|
35
|
+
import pathlib
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
39
|
+
LEDGER = REPO / "design" / "session-distill" / "ledger.json"
|
|
40
|
+
FIXTURE = REPO / "design" / "collection-loop" / "fixtures" / "export-sample.json"
|
|
41
|
+
SUPPORTED_SCHEMA_VERSION = 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def die(msg, code=1):
|
|
45
|
+
print(f"ingest-learnings-export: {msg}", file=sys.stderr)
|
|
46
|
+
sys.exit(code)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_checker():
|
|
50
|
+
"""Reuse scripts/check-learning.py as the single validation source."""
|
|
51
|
+
path = REPO / "scripts" / "check-learning.py"
|
|
52
|
+
spec = importlib.util.spec_from_file_location("check_learning", path)
|
|
53
|
+
module = importlib.util.module_from_spec(spec)
|
|
54
|
+
spec.loader.exec_module(module)
|
|
55
|
+
return module
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_ledger_learning_ids(path=LEDGER):
|
|
59
|
+
"""learning_ids already present in the ledger (dedup key). Entries sourced
|
|
60
|
+
from earlier learnings carry a top-level `learning_id`; the historical
|
|
61
|
+
session-distill entries do not, so they simply contribute nothing here.
|
|
62
|
+
A missing/unreadable ledger is not fatal — dedup just finds nothing."""
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
65
|
+
except (OSError, json.JSONDecodeError):
|
|
66
|
+
return set()
|
|
67
|
+
out = set()
|
|
68
|
+
for e in data.get("entries", []):
|
|
69
|
+
lid = e.get("learning_id") if isinstance(e, dict) else None
|
|
70
|
+
if isinstance(lid, str) and lid:
|
|
71
|
+
out.add(lid.lower())
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def map_candidate(payload, row):
|
|
76
|
+
"""A validated export row -> a ledger-candidate entry (ledger.json shape).
|
|
77
|
+
Deterministic fields the record carries are copied; curator-only fields are
|
|
78
|
+
null for the curator to fill. `_provenance` is worklist-only (strip before
|
|
79
|
+
the ledger merge)."""
|
|
80
|
+
cls = payload.get("classification") or {}
|
|
81
|
+
return {
|
|
82
|
+
"id": None, # curator assigns (e.g. S4-02)
|
|
83
|
+
"learning_id": payload.get("learning_id"), # kept in ledger = dedup key
|
|
84
|
+
"lesson": payload.get("lesson"),
|
|
85
|
+
"strength": None, # curator: recurrence
|
|
86
|
+
"verdict": None, # curator: novel|partial|principle
|
|
87
|
+
"criteria": payload.get("criteria", []),
|
|
88
|
+
"supporting_sessions": payload.get("supporting_sessions", []),
|
|
89
|
+
"domain": payload.get("domain"),
|
|
90
|
+
"proposed_domain": payload.get("proposed_domain"),
|
|
91
|
+
"context": payload.get("context"), # curator-facing evidence note
|
|
92
|
+
"classification": {
|
|
93
|
+
"type": cls.get("type"),
|
|
94
|
+
"underlying_value": None,
|
|
95
|
+
"reformulation": None,
|
|
96
|
+
"meets_promotion_bar": cls.get("meets_bar"),
|
|
97
|
+
"layer": cls.get("layer"),
|
|
98
|
+
"mechanism": None,
|
|
99
|
+
"token_est": None,
|
|
100
|
+
"consumer_note": None,
|
|
101
|
+
"split": None,
|
|
102
|
+
"verification": None,
|
|
103
|
+
"proposed": False, # ledger convention: boolean
|
|
104
|
+
},
|
|
105
|
+
"status": "candidate",
|
|
106
|
+
"_provenance": {
|
|
107
|
+
"user_email": row.get("user_email"), # PII — worklist only, strip on merge
|
|
108
|
+
"received_at": row.get("received_at"), # server receipt time
|
|
109
|
+
"created": payload.get("created"), # user capture time (distinct)
|
|
110
|
+
"schema_version": payload.get("schema_version"),
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def process_export(export, checker, ledger_ids):
|
|
116
|
+
"""Bucket every export row. Deterministic: input order preserved, no
|
|
117
|
+
timestamps, so the same input yields byte-identical output."""
|
|
118
|
+
if not isinstance(export, dict) or not isinstance(export.get("learnings"), list):
|
|
119
|
+
die("not a learnings-export (expected an object with a `learnings` array)")
|
|
120
|
+
|
|
121
|
+
validator = checker.build_validator()
|
|
122
|
+
domain_values = checker.valid_domain_values()
|
|
123
|
+
|
|
124
|
+
entries, rejected, deferred, duplicates, warnings = [], [], [], [], []
|
|
125
|
+
for i, row in enumerate(export["learnings"]):
|
|
126
|
+
if not isinstance(row, dict) or not isinstance(row.get("payload"), dict):
|
|
127
|
+
rejected.append({"index": i, "learning_id": None,
|
|
128
|
+
"reasons": ["export row has no payload object"]})
|
|
129
|
+
continue
|
|
130
|
+
payload = row["payload"]
|
|
131
|
+
lid = payload.get("learning_id")
|
|
132
|
+
|
|
133
|
+
sv = payload.get("schema_version")
|
|
134
|
+
if sv != SUPPORTED_SCHEMA_VERSION:
|
|
135
|
+
deferred.append({"index": i, "learning_id": lid, "schema_version": sv,
|
|
136
|
+
"note": "re-export once a v%s-aware intake exists "
|
|
137
|
+
"(row stays available via includeExported)" % sv})
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
errors = checker.validate_record(payload, validator, domain_values)
|
|
141
|
+
if errors:
|
|
142
|
+
rejected.append({"index": i, "learning_id": lid, "reasons": errors})
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# server-bug detector: the export's domain column should mirror payload.domain.
|
|
146
|
+
if row.get("domain") != payload.get("domain"):
|
|
147
|
+
warnings.append({"index": i, "learning_id": lid,
|
|
148
|
+
"detail": "export domain column %r != payload.domain %r"
|
|
149
|
+
% (row.get("domain"), payload.get("domain"))})
|
|
150
|
+
|
|
151
|
+
cand = map_candidate(payload, row)
|
|
152
|
+
if isinstance(lid, str) and lid.lower() in ledger_ids:
|
|
153
|
+
cand["duplicate_in_ledger"] = True
|
|
154
|
+
duplicates.append({"index": i, "learning_id": lid})
|
|
155
|
+
entries.append(cand)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"source": "curation-intake",
|
|
159
|
+
"generated_from": export.get("source", "learnings-export"),
|
|
160
|
+
"counts": {"valid": len(entries), "rejected": len(rejected),
|
|
161
|
+
"deferred": len(deferred), "duplicates": len(duplicates),
|
|
162
|
+
"warnings": len(warnings)},
|
|
163
|
+
"warnings": warnings,
|
|
164
|
+
"rejected": rejected,
|
|
165
|
+
"deferred": deferred,
|
|
166
|
+
"entries": entries,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run(export, out_path=None):
|
|
171
|
+
worklist = process_export(export, load_checker(), load_ledger_learning_ids())
|
|
172
|
+
text = json.dumps(worklist, ensure_ascii=False, indent=2) + "\n"
|
|
173
|
+
if out_path and out_path != "-":
|
|
174
|
+
pathlib.Path(out_path).write_text(text, encoding="utf-8")
|
|
175
|
+
c = worklist["counts"]
|
|
176
|
+
print(f"ingest-learnings-export: wrote {out_path} "
|
|
177
|
+
f"(valid={c['valid']} rejected={c['rejected']} deferred={c['deferred']} "
|
|
178
|
+
f"duplicates={c['duplicates']})", file=sys.stderr)
|
|
179
|
+
else:
|
|
180
|
+
sys.stdout.write(text)
|
|
181
|
+
return worklist
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _self_test():
|
|
185
|
+
"""Verify bucketing against the committed export fixture (the cross-repo
|
|
186
|
+
contract artifact): valid rows map (cardinality > 0), a bad-domain row is
|
|
187
|
+
rejected with a domain reason (negative control), a v2 row is deferred not
|
|
188
|
+
rejected, mapping preserves context + meets_bar rename, and output is
|
|
189
|
+
deterministic. Exits non-zero on any failure."""
|
|
190
|
+
export = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
191
|
+
checker = load_checker()
|
|
192
|
+
w1 = process_export(export, checker, {"0f8c1c2a-4d1e-4abc-9def-000000000001"})
|
|
193
|
+
w2 = process_export(export, checker, {"0f8c1c2a-4d1e-4abc-9def-000000000001"})
|
|
194
|
+
|
|
195
|
+
valid_ids = {e["learning_id"] for e in w1["entries"]}
|
|
196
|
+
rej_reasons = " ".join(r for row in w1["rejected"] for r in row["reasons"])
|
|
197
|
+
deferred_svs = {d["schema_version"] for d in w1["deferred"]}
|
|
198
|
+
full = next((e for e in w1["entries"]
|
|
199
|
+
if e["learning_id"] == "0f8c1c2a-4d1e-4abc-9def-000000000002"), None)
|
|
200
|
+
|
|
201
|
+
checks = [
|
|
202
|
+
("valid rows mapped (cardinality > 0)", w1["counts"]["valid"] >= 3),
|
|
203
|
+
("bad-domain row rejected", w1["counts"]["rejected"] >= 1),
|
|
204
|
+
("reject reason names the domain (negative control)", "domain" in rej_reasons),
|
|
205
|
+
("v2 row deferred, not rejected", deferred_svs == {2}),
|
|
206
|
+
("deferred row absent from entries",
|
|
207
|
+
"0f8c1c2a-4d1e-4abc-9def-00000000000a" not in valid_ids),
|
|
208
|
+
("context preserved verbatim", full is not None and full["context"]
|
|
209
|
+
and "4분짜리" in full["context"]),
|
|
210
|
+
("meets_bar -> meets_promotion_bar",
|
|
211
|
+
full is not None and full["classification"]["meets_promotion_bar"] is True),
|
|
212
|
+
("classification.proposed is boolean false",
|
|
213
|
+
full is not None and full["classification"]["proposed"] is False),
|
|
214
|
+
("provenance carries user_email (worklist-only PII)",
|
|
215
|
+
full is not None and full["_provenance"]["user_email"] == "alice@day1company.co.kr"),
|
|
216
|
+
("provenance keeps both created and received_at",
|
|
217
|
+
full is not None and full["_provenance"]["created"] != full["_provenance"]["received_at"]),
|
|
218
|
+
("ledger dedup flags a known learning_id", w1["counts"]["duplicates"] == 1),
|
|
219
|
+
("deterministic (same input -> identical output)",
|
|
220
|
+
json.dumps(w1, ensure_ascii=False) == json.dumps(w2, ensure_ascii=False)),
|
|
221
|
+
]
|
|
222
|
+
failed = [name for name, ok in checks if not ok]
|
|
223
|
+
if failed:
|
|
224
|
+
for name in failed:
|
|
225
|
+
print(f"ingest-learnings-export --self-test: FAIL: {name}", file=sys.stderr)
|
|
226
|
+
sys.exit(1)
|
|
227
|
+
print(f"ingest-learnings-export --self-test: OK ({len(checks)} intake checks)")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main():
|
|
231
|
+
ap = argparse.ArgumentParser(description="Map a learnings export to ledger candidates.")
|
|
232
|
+
ap.add_argument("export", nargs="?", default="-",
|
|
233
|
+
help="learnings-export JSON file ('-' or omitted = stdin)")
|
|
234
|
+
ap.add_argument("--out", default=None, help="write the worklist here (default: stdout)")
|
|
235
|
+
ap.add_argument("--self-test", action="store_true",
|
|
236
|
+
help="run the intake self-test against the fixture and exit")
|
|
237
|
+
args = ap.parse_args()
|
|
238
|
+
|
|
239
|
+
if args.self_test:
|
|
240
|
+
_self_test()
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
if args.export == "-":
|
|
244
|
+
raw = sys.stdin.read()
|
|
245
|
+
else:
|
|
246
|
+
try:
|
|
247
|
+
raw = pathlib.Path(args.export).read_text(encoding="utf-8")
|
|
248
|
+
except OSError as e:
|
|
249
|
+
die(f"cannot read export {args.export!r}: {e}")
|
|
250
|
+
try:
|
|
251
|
+
export = json.loads(raw)
|
|
252
|
+
except json.JSONDecodeError as e:
|
|
253
|
+
die(f"export is not valid JSON: {e}")
|
|
254
|
+
run(export, args.out)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
main()
|
package/scripts/install.sh
CHANGED
|
@@ -23,6 +23,9 @@ set -euo pipefail
|
|
|
23
23
|
# or env var. Detach stdin so no child (the codex-helm dry-run, pip, git) can
|
|
24
24
|
# block forever on an inherited idle stdin — that is what hangs an install under
|
|
25
25
|
# CI, pipes, and background runs, where stdin stays open but never delivers.
|
|
26
|
+
# `learn` is the one subcommand whose payload IS stdin, so keep the caller's on
|
|
27
|
+
# fd 3 first and hand it back only there; every other path still sees /dev/null.
|
|
28
|
+
exec 3<&0 2>/dev/null || exec 3</dev/null # tolerate a caller that closed fd 0
|
|
26
29
|
exec </dev/null
|
|
27
30
|
|
|
28
31
|
# Resolve this script through symlinks before locating the package: npm links the
|
|
@@ -562,6 +565,16 @@ PY
|
|
|
562
565
|
else
|
|
563
566
|
log "note: managed venv/textual unavailable (numbered-prompt fallback applies)"
|
|
564
567
|
fi
|
|
568
|
+
# A file this installer executes but never ships is invisible from a clone and
|
|
569
|
+
# fatal on npm, so the payload gate runs wherever it exists (maintainer-side).
|
|
570
|
+
if [ -x "$REPO/scripts/check-package.sh" ]; then
|
|
571
|
+
if "$REPO/scripts/check-package.sh" >/dev/null 2>&1; then
|
|
572
|
+
info "npm payload OK"
|
|
573
|
+
else
|
|
574
|
+
log "npm payload incomplete; run scripts/check-package.sh"
|
|
575
|
+
fail=1
|
|
576
|
+
fi
|
|
577
|
+
fi
|
|
565
578
|
# Repo-internal mirror parity is a maintainer gate; only meaningful from a clone.
|
|
566
579
|
if [ -d "$REPO/ko" ] && [ -x "$REPO/scripts/check-parity.sh" ]; then
|
|
567
580
|
if "$REPO/scripts/check-parity.sh" >/dev/null 2>&1; then info "repo mirror parity OK"; else log "repo mirror parity FAILED"; fail=1; fi
|
|
@@ -688,6 +701,9 @@ agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
|
688
701
|
agent-bios install deploy into this environment (backs up + verifies)
|
|
689
702
|
agent-bios onboard interactive domain selection + packaged install + activation canary
|
|
690
703
|
agent-bios verify check the deployed state matches the source
|
|
704
|
+
agent-bios learn submit a session learning (reads the JSON record on
|
|
705
|
+
stdin; this is what the learn! flow calls, and it
|
|
706
|
+
works from any directory, unlike a repo-relative path)
|
|
691
707
|
agent-bios status show what is installed and where
|
|
692
708
|
agent-bios update git pull + reinstall (clone), or print the npm update line
|
|
693
709
|
agent-bios uninstall remove deployed files and the zsh hook
|
|
@@ -713,6 +729,18 @@ EOF
|
|
|
713
729
|
# ---- dispatch ------------------------------------------------------------
|
|
714
730
|
CMD="${1:-help}"
|
|
715
731
|
if [ $# -gt 0 ]; then shift; fi
|
|
732
|
+
|
|
733
|
+
# `learn` forwards its arguments and stdin straight to the collector, so it must
|
|
734
|
+
# bypass the flag parser below (which rejects anything it does not know). This
|
|
735
|
+
# subcommand is the only PATH-reachable entry to capture: the corpus guide used
|
|
736
|
+
# to invoke scripts/collect-learning.py relative to the cwd, which works from a
|
|
737
|
+
# clone and silently fails for every other install.
|
|
738
|
+
if [ "$CMD" = "learn" ]; then
|
|
739
|
+
collector="$REPO/scripts/collect-learning.py"
|
|
740
|
+
[ -f "$collector" ] || { log "learn: collector missing at $collector"; exit 1; }
|
|
741
|
+
exec python3 "$collector" "$@" <&3
|
|
742
|
+
fi
|
|
743
|
+
|
|
716
744
|
WITH=""
|
|
717
745
|
DOMAINS_ARG=""
|
|
718
746
|
DOMAINS_SET=0
|
package/scripts/session-cost.py
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"""Aggregate token usage & cost for a Claude Code session (main + subagents).
|
|
3
3
|
|
|
4
4
|
Usage: session-cost.py <session>.jsonl [...]
|
|
5
|
-
Reads the session transcript
|
|
5
|
+
Reads the session transcript, splitting main-loop from subagent (sidechain)
|
|
6
|
+
usage, and also picks up <session-dir>/subagents/agent-*.jsonl when present.
|
|
6
7
|
Prints per-source, per-model token sums, modeled cost, and wall-clock span.
|
|
7
8
|
"""
|
|
8
9
|
import json, sys, glob, os
|
|
@@ -28,9 +29,17 @@ def price_for(model):
|
|
|
28
29
|
return None
|
|
29
30
|
|
|
30
31
|
def scan(path):
|
|
31
|
-
"""-> {model: {in,out,cr,cw5,cw1,turns}}, (t_min, t_max)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
"""-> {(scope, model): {in,out,cr,cw5,cw1,turns}}, (t_min, t_max), {scope: agent_ids}
|
|
33
|
+
|
|
34
|
+
A single API response is written to the transcript several times as it
|
|
35
|
+
streams, each line carrying output_tokens *so far* (e.g. 2, 2, 2, 540).
|
|
36
|
+
Keep the record with the LARGEST output_tokens per message id: keeping the
|
|
37
|
+
first one instead under-reports subagent output by ~95%, because sidechain
|
|
38
|
+
messages get snapshotted far more often than main-loop ones do.
|
|
39
|
+
"""
|
|
40
|
+
best, tmin, tmax = {}, None, None
|
|
41
|
+
agents = {"main": set(), "sub": set()}
|
|
42
|
+
for line in open(path, errors="replace"):
|
|
34
43
|
try:
|
|
35
44
|
d = json.loads(line)
|
|
36
45
|
except json.JSONDecodeError:
|
|
@@ -42,11 +51,17 @@ def scan(path):
|
|
|
42
51
|
u, model = m.get("usage"), m.get("model")
|
|
43
52
|
if not u or not model or model == "<synthetic>":
|
|
44
53
|
continue
|
|
45
|
-
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
scope = "sub" if d.get("isSidechain") else "main"
|
|
55
|
+
if d.get("agentId"):
|
|
56
|
+
agents[scope].add(d["agentId"])
|
|
57
|
+
key = (scope, m.get("id") or d.get("requestId"))
|
|
58
|
+
prev = best.get(key)
|
|
59
|
+
if prev is None or u.get("output_tokens", 0) > prev[1].get("output_tokens", 0):
|
|
60
|
+
best[key] = (model, u)
|
|
61
|
+
|
|
62
|
+
agg = {}
|
|
63
|
+
for (scope, _), (model, u) in best.items():
|
|
64
|
+
a = agg.setdefault((scope, model), dict(inp=0, out=0, cr=0, cw5=0, cw1=0, turns=0))
|
|
50
65
|
a["inp"] += u.get("input_tokens", 0)
|
|
51
66
|
a["out"] += u.get("output_tokens", 0)
|
|
52
67
|
a["cr"] += u.get("cache_read_input_tokens", 0)
|
|
@@ -57,7 +72,7 @@ def scan(path):
|
|
|
57
72
|
else:
|
|
58
73
|
a["cw5"] += u.get("cache_creation_input_tokens", 0)
|
|
59
74
|
a["turns"] += 1
|
|
60
|
-
return agg, (tmin, tmax)
|
|
75
|
+
return agg, (tmin, tmax), agents
|
|
61
76
|
|
|
62
77
|
def cost(model, a):
|
|
63
78
|
p = price_for(model)
|
|
@@ -71,19 +86,27 @@ def fmt(n):
|
|
|
71
86
|
|
|
72
87
|
def report(session_path):
|
|
73
88
|
base = session_path[:-6] # strip .jsonl
|
|
74
|
-
sources = [(
|
|
89
|
+
sources = [(None, session_path)]
|
|
75
90
|
sources += [(os.path.basename(f)[:-6], f)
|
|
76
91
|
for f in sorted(glob.glob(os.path.join(base, "subagents", "*.jsonl")))]
|
|
77
92
|
print(f"\n=== {os.path.basename(session_path)} ===")
|
|
78
93
|
grand, unpriced = 0.0, []
|
|
79
|
-
hdr = f"{'source':<38}{'model':<22}{'turns':>6}{'input':>9}{'output':>9}
|
|
94
|
+
hdr = (f"{'source':<38}{'model':<22}{'turns':>6}{'input':>9}{'output':>9}"
|
|
95
|
+
f"{'cache_rd':>10}{'cache_wr':>10}{'cost$':>9}")
|
|
80
96
|
print(hdr); print("-" * len(hdr))
|
|
81
97
|
span_min = span_max = None
|
|
82
|
-
for
|
|
83
|
-
agg, (tmin, tmax) = scan(path)
|
|
98
|
+
for label, path in sources:
|
|
99
|
+
agg, (tmin, tmax), agents = scan(path)
|
|
84
100
|
if tmin:
|
|
85
101
|
span_min = min(span_min or tmin, tmin); span_max = max(span_max or tmax, tmax)
|
|
86
|
-
for model, a in agg.items():
|
|
102
|
+
for (scope, model), a in sorted(agg.items()):
|
|
103
|
+
if label is not None:
|
|
104
|
+
name = label
|
|
105
|
+
elif scope == "sub":
|
|
106
|
+
n = len(agents["sub"])
|
|
107
|
+
name = f"subagents (n={n})" if n else "subagents"
|
|
108
|
+
else:
|
|
109
|
+
name = "main"
|
|
87
110
|
c = cost(model, a)
|
|
88
111
|
cs = f"{c:9.2f}" if c is not None else " ?"
|
|
89
112
|
if c is None:
|