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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//! Call-scoped measurement of physical provider dispatches.
//!
//! How many provider requests a terminal represents has exactly one authority:
//! the observed transport boundary, which records one dispatch here per
//! request. Every other surface reads this ledger rather than inferring a
//! count from tokens, attempts, or error text.
//!
//! Before this module the counter lived only on the agent-session host, so
//! `record_provider_dispatch` was a silent no-op whenever no agent session was
//! open. `agent_loop` reported a correct count while a bare `llm_call`
//! measured nothing, and the structured envelope then fell back to asserting
//! one unknown attempt for a terminal that never dispatched. A budget refusal
//! that made no request was reported as one unpriced call, which is what
//! charged a downstream host's cost meter a conservative reserve per refusal
//! (burin-labs/harn#8529).
//!
//! The ledger is a `tokio::task_local`, matching `raw_provider_capture` and
//! `cost_route` in this same layer: a scope follows its future across `.await`
//! and across spawned work instead of pinning to whichever thread happened to
//! poll it.
use std::future::Future;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
tokio::task_local! {
static PROVIDER_DISPATCH_LEDGER: ProviderDispatchLedger;
}
/// Physical provider dispatches observed inside one open measurement scope.
///
/// Cloning shares the same underlying count, so the scope's owner and the
/// deep call sites that record into it see one number. A scope opened inside
/// another keeps a link to it, and a dispatch is recorded on every scope up
/// that chain: the inner call reports its own requests while the enclosing
/// call still sees the total.
#[derive(Clone, Debug, Default)]
pub(crate) struct ProviderDispatchLedger {
dispatches: Arc<AtomicI64>,
parent: Option<Box<ProviderDispatchLedger>>,
}
impl ProviderDispatchLedger {
/// Dispatches recorded so far. Zero here is a measured zero: the scope was
/// open and nothing dispatched.
pub(crate) fn observed(&self) -> i64 {
self.dispatches.load(Ordering::Relaxed)
}
fn record(&self) {
self.dispatches.fetch_add(1, Ordering::Relaxed);
if let Some(parent) = self.parent.as_deref() {
parent.record();
}
}
/// The same counter, linked under whatever scope is currently open.
fn nested_under_current(&self) -> Self {
Self {
dispatches: Arc::clone(&self.dispatches),
parent: PROVIDER_DISPATCH_LEDGER
.try_with(Clone::clone)
.ok()
.map(Box::new),
}
}
}
/// Run `future` with `ledger` as the open measurement scope.
///
/// Dispatches inside `future` land on `ledger` and on every enclosing scope,
/// so the caller reads exactly its own requests through its clone while an
/// outer measurement still totals them.
///
/// This is a plain `fn` that boxes `future` rather than an `async fn`. An
/// `async fn` wrapper embeds `F` inline in its own future, so every caller
/// awaiting it would carry the whole wrapped call future a second time on its
/// stack frame; the LLM call futures here are large enough that the doubling
/// trips the stack-frame budget. Boxing moves `F` to the heap, so the caller's
/// frame holds only a pointer. One heap allocation per provider call is
/// negligible against a network round trip.
pub(crate) fn with_provider_dispatch_ledger<F>(
ledger: ProviderDispatchLedger,
future: F,
) -> impl Future<Output = F::Output>
where
F: Future,
{
let scoped = ledger.nested_under_current();
PROVIDER_DISPATCH_LEDGER.scope(scoped, Box::pin(future))
}
/// Stamp the scope's measurement onto a thrown terminal.
///
/// `agent_loop` already exposes `provider_call_count` on its thrown terminal;
/// a bare `llm_call` throw carried nothing, so a consumer catching it could
/// not tell a refusal before dispatch from a failure after one. Only a dict
/// terminal can carry the field, and a field the producer already set wins.
pub(crate) fn stamp_thrown_terminal(
error: crate::value::VmError,
ledger: &ProviderDispatchLedger,
) -> crate::value::VmError {
use crate::value::{VmError, VmValue};
match error {
VmError::Thrown(VmValue::Dict(dict)) => {
let mut dict = (*dict).clone();
dict.entry(crate::value::intern_key("provider_call_count"))
.or_insert_with(|| VmValue::Int(ledger.observed()));
VmError::Thrown(VmValue::dict(dict))
}
other => other,
}
}
/// Dispatches measured by the open scope, or `None` when no scope is open.
///
/// `Some(0)` and `None` are deliberately different answers. The first says a
/// measurement ran and saw no request; the second says nobody measured. A
/// consumer that collapses them reintroduces the defect this module exists to
/// remove.
pub(crate) fn measured_dispatches() -> Option<i64> {
PROVIDER_DISPATCH_LEDGER
.try_with(ProviderDispatchLedger::observed)
.ok()
}
/// Record one physical provider dispatch against the open scope.
///
/// A dispatch with no scope open is deliberately not counted anywhere: the
/// absence of a scope is the absence of a measurement, and callers that need
/// one open it.
pub(crate) fn record_dispatch() {
let _ = PROVIDER_DISPATCH_LEDGER.try_with(ProviderDispatchLedger::record);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn an_open_scope_with_no_dispatch_measures_zero() {
let ledger = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(ledger.clone(), async {}).await;
assert_eq!(ledger.observed(), 0);
}
#[tokio::test]
async fn dispatches_reach_the_open_scope() {
let ledger = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(ledger.clone(), async {
record_dispatch();
record_dispatch();
})
.await;
assert_eq!(ledger.observed(), 2);
}
#[tokio::test]
async fn a_nested_scope_reports_its_own_share_and_the_outer_the_total() {
let outer = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(outer.clone(), async {
record_dispatch();
let inner = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(inner.clone(), async {
record_dispatch();
record_dispatch();
assert_eq!(measured_dispatches(), Some(2));
})
.await;
assert_eq!(inner.observed(), 2);
assert_eq!(measured_dispatches(), Some(3));
})
.await;
assert_eq!(outer.observed(), 3);
}
#[tokio::test]
async fn a_thrown_dict_terminal_is_stamped_and_a_producer_stamp_wins() {
use crate::value::{VmDictExt as _, VmError, VmValue};
let ledger = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(ledger.clone(), async {
record_dispatch();
})
.await;
let mut bare = crate::value::DictMap::new();
bare.put_str("category", "budget_exceeded");
let stamped = stamp_thrown_terminal(VmError::Thrown(VmValue::dict(bare)), &ledger);
let VmError::Thrown(VmValue::Dict(dict)) = stamped else {
panic!("dict terminal survives stamping");
};
assert!(matches!(
dict.get("provider_call_count"),
Some(VmValue::Int(1))
));
let mut owned = crate::value::DictMap::new();
owned.insert(
crate::value::intern_key("provider_call_count"),
VmValue::Int(7),
);
let kept = stamp_thrown_terminal(VmError::Thrown(VmValue::dict(owned)), &ledger);
let VmError::Thrown(VmValue::Dict(dict)) = kept else {
panic!("dict terminal survives stamping");
};
assert!(matches!(
dict.get("provider_call_count"),
Some(VmValue::Int(7))
));
let text = stamp_thrown_terminal(
VmError::Thrown(VmValue::String(arcstr::ArcStr::from("plain"))),
&ledger,
);
assert!(matches!(text, VmError::Thrown(VmValue::String(_))));
}
#[tokio::test]
async fn no_open_scope_reads_as_no_measurement_not_as_zero() {
assert_eq!(measured_dispatches(), None);
let ledger = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(ledger, async {
assert_eq!(measured_dispatches(), Some(0));
})
.await;
}
#[tokio::test]
async fn a_dispatch_outside_every_scope_is_not_attributed_to_a_later_one() {
record_dispatch();
let ledger = ProviderDispatchLedger::default();
with_provider_dispatch_ledger(ledger.clone(), async {}).await;
assert_eq!(ledger.observed(), 0);
}
}