1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! On-disk budget derivations (ARCH §6, §8): spend, wall, and depth for
//! a conversation and its descent. Every value is a pure function of the
//! `<conv-repo>/steps/` tree (or the branch name) at read time — the
//! harness stores no running counter (PRINCIPLES "Single source of
//! truth"). Callers re-derive on every check.
//!
//! **Branch and its descent.** A conversation's spend and wall span its
//! own `steps/<branch>/` records *and* every descended subagent's
//! `steps/<branch>-*/` (hyphenated descent, ARCH §2.2) — the same prefix
//! walk `litany stop` uses to cascade a stop (§2.9).
use crateStepMeta;
use Event;
use fs;
use Path;
/// Conv-repo subdir holding per-conversation step records (ARCH §2.2).
const STEPS_DIR: &str = "steps";
/// Per-step JSONL of `v=1` events (ARCH §2.3, §4.4).
const RESPONSE_FILE: &str = "response.json";
/// Per-step metadata carrying the `started_at`/`ended_at` span (§2.3).
const META_FILE: &str = "meta.json";
/// Zero-padded step-sequence width (`001`, `002`, …) per ARCH §2.3.
const STEP_SEQ_WIDTH: usize = 3;
/// Sum `Usage` tokens across *every* attempt segment of *every*
/// `response.json` under `branch` and its descent (ARCH §6 "Every
/// attempt segment counts": failed and superseded attempts are billed).
/// A `None` counter contributes 0 — never a fabricated value.
/// Wall-clock seconds summed per step from `meta.json`'s
/// `started_at`→`ended_at` across `branch` and its descent. Each span
/// already covers the backoff sleeps between that step's attempts (ARCH
/// §2.10, §4.4 "Fd held open for the whole model call"), so wall counts
/// sleeping as well as streaming (§6 "wall is wall").
/// Dispatch depth of `branch`: a root agent is one `<ts>-<short>` id
/// (one hyphen) at depth 0; each dispatch appends `-<ts>-<short>` (two
/// more hyphens), so depth = hyphens / 2 (ARCH §6 "The depth boundary",
/// over the §2.3 hyphenated descent). Relies on the id token format —
/// the compact timestamp and short id are both hyphen-free (clock.rs /
/// ARCH §2.3).
/// Walk `steps/<branch>/` and every `steps/<branch>-*/` conv-id dir,
/// folding `per_step` over each 3-digit step subdir and summing. A
/// missing `steps/` tree (or an entry that is not a readable directory)
/// contributes 0 — the derivation never panics on a partial tree.
/// Fold `per_step` over the 3-digit step subdirs of one conv-id dir.
/// A conv-id entry that is not a readable directory contributes 0.
/// Sum `Usage` tokens over every event line of one step's
/// `response.json`. A missing file contributes 0; a malformed or
/// forward-compat line is skipped (the `v=1` tolerate-unknown contract,
/// §4.4). Every `Usage` line across every segment is counted (§6).
/// One `Usage` event's tokens, folded so a cached slice that already
/// sits INSIDE the prompt counter is billed once (ARCH §6 "The cached
/// slice is billed once"): `max(input, cache_read + cache_write) +
/// output`.
///
/// brazen's canonical `Usage` reports each provider's own counters
/// unaltered (brazen `specs/architecture.md` §3.2), and providers
/// disagree about overlap: Anthropic's prompt counters are disjoint
/// slices (`input_tokens` beside `cache_read_input_tokens` /
/// `cache_creation_input_tokens`), while the OpenAI-shaped and Google
/// decoders map a prompt counter that CONTAINS the cached one
/// (`prompt_tokens` ⊇ `prompt_tokens_details.cached_tokens`,
/// `input_tokens` ⊇ `input_tokens_details.cached_tokens`,
/// `promptTokenCount` ⊇ `cachedContentTokenCount`). Nothing on the
/// `Usage` event says which shape it is, and a step record carries no
/// protocol, so the fold takes the larger of the two readings of the
/// prompt rather than their sum: exact where the slice is contained,
/// a floor (never an over-statement) where the counters are disjoint,
/// and plain `input + output` where no cache counter is reported.
///
/// A floor rather than a ceiling on purpose — spend is what was really
/// consumed, and billing a prompt twice ends a conversation before the
/// ceiling its operator declared. Collapses back to the plain sum if
/// brazen ever guarantees disjoint slices (litany bl-68f5 / brazen
/// bl-d192). Each `None` field is 0; non-`Usage` events carry no tokens.
/// The `started_at`→`ended_at` span of one step (seconds). A missing or
/// malformed `meta.json`, or an unparseable timestamp, contributes 0.
/// Seconds from `start` to `end` (RFC3339 / ISO-8601 — the clock.rs
/// format). An unparseable pair, or `end` before `start`, is 0 —
/// defensive against a partially-written or malformed record.