floe-guard 0.4.0 → 0.5.0

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/dist/index.d.cts CHANGED
@@ -68,7 +68,8 @@ declare namespace pricing {
68
68
  * One priced spend event in the guard's per-call ledger.
69
69
  *
70
70
  * Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
71
- * {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
71
+ * {@link BudgetGuard.recordTool} / {@link BudgetGuard.settleTool} that accrues
72
+ * spend appends exactly one event, so
72
73
  * the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
73
74
  * evicted old events). The schema is identical in the Python
74
75
  * package (`SpendEvent` in `src/floe_guard/guard.py`) and
@@ -149,13 +150,26 @@ declare class BudgetGuard {
149
150
  failClosed: boolean;
150
151
  nearLimitBps: number;
151
152
  private readonly onBlock;
152
- /** Cost of the most recent priced call, used to predict the next one. */
153
- private lastCost;
153
+ /**
154
+ * Costs of the most recent priced LLM call and tool call, tracked
155
+ * SEPARATELY: the default next-call prediction is the max of the two, so a
156
+ * cheap tool call can't shrink the estimate right before an expensive LLM
157
+ * call (or vice versa) — conservative beats one-call-too-late.
158
+ */
159
+ private lastLlmCost;
160
+ private lastToolCost;
154
161
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
155
162
  private reserved;
156
163
  /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
157
164
  private readonly spendEvents;
158
165
  private readonly maxLogEvents?;
166
+ /**
167
+ * Per-tool running totals (settleTool/recordTool) — the tool side of the one
168
+ * shared ceiling, exposed via the toolCosts getter. null-prototype: tool
169
+ * names are caller-supplied strings, so a "__proto__" name is stored as
170
+ * plain data instead of mutating the object's prototype.
171
+ */
172
+ private readonly toolCostTotals;
159
173
  /**
160
174
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
161
175
  */
@@ -164,7 +178,8 @@ declare class BudgetGuard {
164
178
  * Throw {@link BudgetExceeded} if the next call would cross the ceiling.
165
179
  *
166
180
  * Call this immediately before each LLM request. The "next call" is estimated
167
- * from the last recorded call's cost (override with `estimatedNextCost`); the
181
+ * conservatively as the costlier of the last LLM call and the last tool call
182
+ * (override with `estimatedNextCost`); the
168
183
  * first call is always allowed unless the ceiling is already met. In-flight
169
184
  * reservations count toward the total, so this stays correct alongside
170
185
  * {@link BudgetGuard.reserve}.
@@ -181,7 +196,8 @@ declare class BudgetGuard {
181
196
  * the same stale total. Throws {@link BudgetExceeded} (without reserving) if
182
197
  * the reservation would cross the ceiling. Returns the reservation handle to
183
198
  * pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
184
- * `estimatedCost` defaults to the last call's cost.
199
+ * `estimatedCost` defaults to the costlier of the last LLM call and the last
200
+ * tool call.
185
201
  */
186
202
  reserve(estimatedCost?: number): number;
187
203
  /**
@@ -209,16 +225,49 @@ declare class BudgetGuard {
209
225
  price?: ManualPrice;
210
226
  label?: string;
211
227
  }): number;
228
+ /**
229
+ * Atomically check the ceiling AND hold a tool call's cost in flight.
230
+ *
231
+ * The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
232
+ * than the LLM path, because a paid tool's price is usually known exactly
233
+ * before the call, so the pre-call hard-stop is precise rather than
234
+ * estimated:
235
+ *
236
+ * const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
237
+ * const result = await apollo.peopleLookup(...);
238
+ * guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
239
+ *
240
+ * Throws {@link BudgetExceeded} (without reserving) if the call would cross
241
+ * the ceiling. The estimate is required — tools have no last-cost prediction
242
+ * worth falling back to. Pass the returned handle to
243
+ * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
244
+ */
245
+ reserveTool(estimatedCost: number): number;
246
+ /**
247
+ * Release a reservation and record a tool call's actual cost.
248
+ *
249
+ * `recordTool` is `settleTool` with no reservation. The caller supplies the
250
+ * cost — tools have no token usage to price. Accrues into the same
251
+ * `spentUsd` ceiling as tokens, tallies the per-tool total
252
+ * ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
253
+ * estimate (tracked separately from the LLM side; the default prediction is
254
+ * the max of the two, so a tool-hammering loop's plain `check()` stops
255
+ * BEFORE the crossing call without a cheap tool shrinking the LLM
256
+ * prediction), and appends
257
+ * a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
258
+ * Returns `costUsd`.
259
+ */
260
+ settleTool(tool: string, costUsd: number, options?: {
261
+ reserved?: number;
262
+ label?: string;
263
+ }): number;
212
264
  /**
213
265
  * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
214
266
  *
215
- * Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
216
- * same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
217
- * `check()` / `reserve()` see them) and appends a `kind: "tool"`
218
- * {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
219
- * cost: tools have no token usage to price. Deliberately does NOT update the
220
- * next-call estimate — that predicts the next *LLM* call, and a tool's price
221
- * would skew it. Returns `costUsd`.
267
+ * Post-hoc accrual for costs only known after the call (metered APIs); when
268
+ * the price is known up front, {@link BudgetGuard.reserveTool} /
269
+ * {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
270
+ * `settleTool` for the full contract. Returns `costUsd`.
222
271
  */
223
272
  recordTool(tool: string, costUsd: number, options?: {
224
273
  label?: string;
@@ -230,10 +279,17 @@ declare class BudgetGuard {
230
279
  release(reserved: number): void;
231
280
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
232
281
  get remainingUsd(): number;
282
+ /**
283
+ * Per-tool running USD totals, keyed by the name given to `settleTool()` /
284
+ * `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
285
+ * Makes the token/tool split of the one shared ceiling inspectable
286
+ * (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
287
+ */
288
+ get toolCosts(): Record<string, number>;
233
289
  /**
234
290
  * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
235
- * `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
236
- * it cannot corrupt the ledger.
291
+ * `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
292
+ * snapshot copy: mutating it cannot corrupt the ledger.
237
293
  */
238
294
  get spendLog(): SpendEvent[];
239
295
  /**
@@ -247,6 +303,25 @@ declare class BudgetGuard {
247
303
  * Python `2.5e-06`.) Empty ledger yields `""`.
248
304
  */
249
305
  exportLog(): string;
306
+ /**
307
+ * The default next-call prediction when the caller supplies no estimate.
308
+ * Conservative: the costlier of the last LLM call and the last tool call — a
309
+ * mixed loop predicts the pricier kind, which at worst blocks one call early
310
+ * (fail-closed) rather than letting a crossing call through because the LAST
311
+ * event happened to be cheap.
312
+ */
313
+ private defaultEstimate;
314
+ /**
315
+ * Subtract a settled/released hold from the in-flight tally. A handle larger
316
+ * than EVERYTHING currently held cannot have come from a matching
317
+ * `reserve()` — throwing beats silently clamping, which would free OTHER
318
+ * callers' holds and fail the ceiling open. The epsilon absorbs float dust
319
+ * from accumulating and draining many holds; per-caller over-release (a
320
+ * handle within the total but larger than the caller's own hold) is
321
+ * undetectable without per-handle tracking and remains the caller's
322
+ * responsibility.
323
+ */
324
+ private consumeReservation;
250
325
  private appendEvent;
251
326
  /**
252
327
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
package/dist/index.d.ts CHANGED
@@ -68,7 +68,8 @@ declare namespace pricing {
68
68
  * One priced spend event in the guard's per-call ledger.
69
69
  *
70
70
  * Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
71
- * {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
71
+ * {@link BudgetGuard.recordTool} / {@link BudgetGuard.settleTool} that accrues
72
+ * spend appends exactly one event, so
72
73
  * the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
73
74
  * evicted old events). The schema is identical in the Python
74
75
  * package (`SpendEvent` in `src/floe_guard/guard.py`) and
@@ -149,13 +150,26 @@ declare class BudgetGuard {
149
150
  failClosed: boolean;
150
151
  nearLimitBps: number;
151
152
  private readonly onBlock;
152
- /** Cost of the most recent priced call, used to predict the next one. */
153
- private lastCost;
153
+ /**
154
+ * Costs of the most recent priced LLM call and tool call, tracked
155
+ * SEPARATELY: the default next-call prediction is the max of the two, so a
156
+ * cheap tool call can't shrink the estimate right before an expensive LLM
157
+ * call (or vice versa) — conservative beats one-call-too-late.
158
+ */
159
+ private lastLlmCost;
160
+ private lastToolCost;
154
161
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
155
162
  private reserved;
156
163
  /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
157
164
  private readonly spendEvents;
158
165
  private readonly maxLogEvents?;
166
+ /**
167
+ * Per-tool running totals (settleTool/recordTool) — the tool side of the one
168
+ * shared ceiling, exposed via the toolCosts getter. null-prototype: tool
169
+ * names are caller-supplied strings, so a "__proto__" name is stored as
170
+ * plain data instead of mutating the object's prototype.
171
+ */
172
+ private readonly toolCostTotals;
159
173
  /**
160
174
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
161
175
  */
@@ -164,7 +178,8 @@ declare class BudgetGuard {
164
178
  * Throw {@link BudgetExceeded} if the next call would cross the ceiling.
165
179
  *
166
180
  * Call this immediately before each LLM request. The "next call" is estimated
167
- * from the last recorded call's cost (override with `estimatedNextCost`); the
181
+ * conservatively as the costlier of the last LLM call and the last tool call
182
+ * (override with `estimatedNextCost`); the
168
183
  * first call is always allowed unless the ceiling is already met. In-flight
169
184
  * reservations count toward the total, so this stays correct alongside
170
185
  * {@link BudgetGuard.reserve}.
@@ -181,7 +196,8 @@ declare class BudgetGuard {
181
196
  * the same stale total. Throws {@link BudgetExceeded} (without reserving) if
182
197
  * the reservation would cross the ceiling. Returns the reservation handle to
183
198
  * pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
184
- * `estimatedCost` defaults to the last call's cost.
199
+ * `estimatedCost` defaults to the costlier of the last LLM call and the last
200
+ * tool call.
185
201
  */
186
202
  reserve(estimatedCost?: number): number;
187
203
  /**
@@ -209,16 +225,49 @@ declare class BudgetGuard {
209
225
  price?: ManualPrice;
210
226
  label?: string;
211
227
  }): number;
228
+ /**
229
+ * Atomically check the ceiling AND hold a tool call's cost in flight.
230
+ *
231
+ * The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
232
+ * than the LLM path, because a paid tool's price is usually known exactly
233
+ * before the call, so the pre-call hard-stop is precise rather than
234
+ * estimated:
235
+ *
236
+ * const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
237
+ * const result = await apollo.peopleLookup(...);
238
+ * guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
239
+ *
240
+ * Throws {@link BudgetExceeded} (without reserving) if the call would cross
241
+ * the ceiling. The estimate is required — tools have no last-cost prediction
242
+ * worth falling back to. Pass the returned handle to
243
+ * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
244
+ */
245
+ reserveTool(estimatedCost: number): number;
246
+ /**
247
+ * Release a reservation and record a tool call's actual cost.
248
+ *
249
+ * `recordTool` is `settleTool` with no reservation. The caller supplies the
250
+ * cost — tools have no token usage to price. Accrues into the same
251
+ * `spentUsd` ceiling as tokens, tallies the per-tool total
252
+ * ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
253
+ * estimate (tracked separately from the LLM side; the default prediction is
254
+ * the max of the two, so a tool-hammering loop's plain `check()` stops
255
+ * BEFORE the crossing call without a cheap tool shrinking the LLM
256
+ * prediction), and appends
257
+ * a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
258
+ * Returns `costUsd`.
259
+ */
260
+ settleTool(tool: string, costUsd: number, options?: {
261
+ reserved?: number;
262
+ label?: string;
263
+ }): number;
212
264
  /**
213
265
  * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
214
266
  *
215
- * Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
216
- * same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
217
- * `check()` / `reserve()` see them) and appends a `kind: "tool"`
218
- * {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
219
- * cost: tools have no token usage to price. Deliberately does NOT update the
220
- * next-call estimate — that predicts the next *LLM* call, and a tool's price
221
- * would skew it. Returns `costUsd`.
267
+ * Post-hoc accrual for costs only known after the call (metered APIs); when
268
+ * the price is known up front, {@link BudgetGuard.reserveTool} /
269
+ * {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
270
+ * `settleTool` for the full contract. Returns `costUsd`.
222
271
  */
223
272
  recordTool(tool: string, costUsd: number, options?: {
224
273
  label?: string;
@@ -230,10 +279,17 @@ declare class BudgetGuard {
230
279
  release(reserved: number): void;
231
280
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
232
281
  get remainingUsd(): number;
282
+ /**
283
+ * Per-tool running USD totals, keyed by the name given to `settleTool()` /
284
+ * `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
285
+ * Makes the token/tool split of the one shared ceiling inspectable
286
+ * (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
287
+ */
288
+ get toolCosts(): Record<string, number>;
233
289
  /**
234
290
  * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
235
- * `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
236
- * it cannot corrupt the ledger.
291
+ * `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
292
+ * snapshot copy: mutating it cannot corrupt the ledger.
237
293
  */
238
294
  get spendLog(): SpendEvent[];
239
295
  /**
@@ -247,6 +303,25 @@ declare class BudgetGuard {
247
303
  * Python `2.5e-06`.) Empty ledger yields `""`.
248
304
  */
249
305
  exportLog(): string;
306
+ /**
307
+ * The default next-call prediction when the caller supplies no estimate.
308
+ * Conservative: the costlier of the last LLM call and the last tool call — a
309
+ * mixed loop predicts the pricier kind, which at worst blocks one call early
310
+ * (fail-closed) rather than letting a crossing call through because the LAST
311
+ * event happened to be cheap.
312
+ */
313
+ private defaultEstimate;
314
+ /**
315
+ * Subtract a settled/released hold from the in-flight tally. A handle larger
316
+ * than EVERYTHING currently held cannot have come from a matching
317
+ * `reserve()` — throwing beats silently clamping, which would free OTHER
318
+ * callers' holds and fail the ceiling open. The epsilon absorbs float dust
319
+ * from accumulating and draining many holds; per-caller over-release (a
320
+ * handle within the total but larger than the caller's own hold) is
321
+ * undetectable without per-handle tracking and remains the caller's
322
+ * responsibility.
323
+ */
324
+ private consumeReservation;
250
325
  private appendEvent;
251
326
  /**
252
327
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
package/dist/index.js CHANGED
@@ -963,13 +963,26 @@ var BudgetGuard = class {
963
963
  failClosed;
964
964
  nearLimitBps;
965
965
  onBlock;
966
- /** Cost of the most recent priced call, used to predict the next one. */
967
- lastCost = 0;
966
+ /**
967
+ * Costs of the most recent priced LLM call and tool call, tracked
968
+ * SEPARATELY: the default next-call prediction is the max of the two, so a
969
+ * cheap tool call can't shrink the estimate right before an expensive LLM
970
+ * call (or vice versa) — conservative beats one-call-too-late.
971
+ */
972
+ lastLlmCost = 0;
973
+ lastToolCost = 0;
968
974
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
969
975
  reserved = 0;
970
976
  /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
971
977
  spendEvents = [];
972
978
  maxLogEvents;
979
+ /**
980
+ * Per-tool running totals (settleTool/recordTool) — the tool side of the one
981
+ * shared ceiling, exposed via the toolCosts getter. null-prototype: tool
982
+ * names are caller-supplied strings, so a "__proto__" name is stored as
983
+ * plain data instead of mutating the object's prototype.
984
+ */
985
+ toolCostTotals = /* @__PURE__ */ Object.create(null);
973
986
  /**
974
987
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
975
988
  */
@@ -1001,7 +1014,8 @@ var BudgetGuard = class {
1001
1014
  * Throw {@link BudgetExceeded} if the next call would cross the ceiling.
1002
1015
  *
1003
1016
  * Call this immediately before each LLM request. The "next call" is estimated
1004
- * from the last recorded call's cost (override with `estimatedNextCost`); the
1017
+ * conservatively as the costlier of the last LLM call and the last tool call
1018
+ * (override with `estimatedNextCost`); the
1005
1019
  * first call is always allowed unless the ceiling is already met. In-flight
1006
1020
  * reservations count toward the total, so this stays correct alongside
1007
1021
  * {@link BudgetGuard.reserve}.
@@ -1010,7 +1024,7 @@ var BudgetGuard = class {
1010
1024
  * `settle()`, which hold the estimate across the await.
1011
1025
  */
1012
1026
  check(estimatedNextCost) {
1013
- const rawEstimate = estimatedNextCost === void 0 ? this.lastCost : estimatedNextCost;
1027
+ const rawEstimate = estimatedNextCost === void 0 ? this.defaultEstimate() : estimatedNextCost;
1014
1028
  if (!Number.isFinite(rawEstimate)) {
1015
1029
  throw new RangeError(
1016
1030
  `estimatedNextCost must be a finite number, got ${rawEstimate}`
@@ -1031,10 +1045,11 @@ var BudgetGuard = class {
1031
1045
  * the same stale total. Throws {@link BudgetExceeded} (without reserving) if
1032
1046
  * the reservation would cross the ceiling. Returns the reservation handle to
1033
1047
  * pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
1034
- * `estimatedCost` defaults to the last call's cost.
1048
+ * `estimatedCost` defaults to the costlier of the last LLM call and the last
1049
+ * tool call.
1035
1050
  */
1036
1051
  reserve(estimatedCost) {
1037
- const rawEstimate = estimatedCost === void 0 ? this.lastCost : estimatedCost;
1052
+ const rawEstimate = estimatedCost === void 0 ? this.defaultEstimate() : estimatedCost;
1038
1053
  if (!Number.isFinite(rawEstimate)) {
1039
1054
  throw new RangeError(
1040
1055
  `estimatedCost must be a finite number, got ${rawEstimate}`
@@ -1086,13 +1101,13 @@ var BudgetGuard = class {
1086
1101
  throw err;
1087
1102
  }
1088
1103
  if (reserved) {
1089
- this.reserved = Math.max(0, this.reserved - reserved);
1104
+ this.consumeReservation(reserved);
1090
1105
  }
1091
1106
  this.spentUsd += cost;
1092
1107
  if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
1093
1108
  this.spentUsd = this.limitUsd;
1094
1109
  }
1095
- this.lastCost = cost;
1110
+ this.lastLlmCost = cost;
1096
1111
  this.appendEvent({
1097
1112
  timestamp: Date.now() / 1e3,
1098
1113
  kind: "llm",
@@ -1122,24 +1137,64 @@ var BudgetGuard = class {
1122
1137
  });
1123
1138
  }
1124
1139
  /**
1125
- * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
1140
+ * Atomically check the ceiling AND hold a tool call's cost in flight.
1141
+ *
1142
+ * The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
1143
+ * than the LLM path, because a paid tool's price is usually known exactly
1144
+ * before the call, so the pre-call hard-stop is precise rather than
1145
+ * estimated:
1126
1146
  *
1127
- * Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
1128
- * same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
1129
- * `check()` / `reserve()` see them) and appends a `kind: "tool"`
1130
- * {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
1131
- * cost: tools have no token usage to price. Deliberately does NOT update the
1132
- * next-call estimate — that predicts the next *LLM* call, and a tool's price
1133
- * would skew it. Returns `costUsd`.
1147
+ * const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
1148
+ * const result = await apollo.peopleLookup(...);
1149
+ * guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
1150
+ *
1151
+ * Throws {@link BudgetExceeded} (without reserving) if the call would cross
1152
+ * the ceiling. The estimate is required — tools have no last-cost prediction
1153
+ * worth falling back to. Pass the returned handle to
1154
+ * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
1134
1155
  */
1135
- recordTool(tool, costUsd, options = {}) {
1156
+ reserveTool(estimatedCost) {
1157
+ if (estimatedCost === void 0) {
1158
+ throw new RangeError("reserveTool requires an estimated cost, got undefined");
1159
+ }
1160
+ if (!Number.isFinite(estimatedCost) || estimatedCost < 0) {
1161
+ throw new RangeError(
1162
+ `estimatedCost must be a finite, non-negative number, got ${estimatedCost}`
1163
+ );
1164
+ }
1165
+ return this.reserve(estimatedCost);
1166
+ }
1167
+ /**
1168
+ * Release a reservation and record a tool call's actual cost.
1169
+ *
1170
+ * `recordTool` is `settleTool` with no reservation. The caller supplies the
1171
+ * cost — tools have no token usage to price. Accrues into the same
1172
+ * `spentUsd` ceiling as tokens, tallies the per-tool total
1173
+ * ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
1174
+ * estimate (tracked separately from the LLM side; the default prediction is
1175
+ * the max of the two, so a tool-hammering loop's plain `check()` stops
1176
+ * BEFORE the crossing call without a cheap tool shrinking the LLM
1177
+ * prediction), and appends
1178
+ * a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
1179
+ * Returns `costUsd`.
1180
+ */
1181
+ settleTool(tool, costUsd, options = {}) {
1136
1182
  if (!Number.isFinite(costUsd) || costUsd < 0) {
1137
1183
  throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
1138
1184
  }
1185
+ const reserved = options.reserved ?? 0;
1186
+ if (!Number.isFinite(reserved) || reserved < 0) {
1187
+ throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
1188
+ }
1189
+ if (reserved) {
1190
+ this.consumeReservation(reserved);
1191
+ }
1139
1192
  this.spentUsd += costUsd;
1140
1193
  if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
1141
1194
  this.spentUsd = this.limitUsd;
1142
1195
  }
1196
+ this.lastToolCost = costUsd;
1197
+ this.toolCostTotals[tool] = (this.toolCostTotals[tool] ?? 0) + costUsd;
1143
1198
  this.appendEvent({
1144
1199
  timestamp: Date.now() / 1e3,
1145
1200
  kind: "tool",
@@ -1147,10 +1202,22 @@ var BudgetGuard = class {
1147
1202
  promptTokens: null,
1148
1203
  completionTokens: null,
1149
1204
  costUsd,
1150
- ...options.label !== void 0 ? { label: options.label } : {}
1205
+ ...options.label !== void 0 ? { label: options.label } : {},
1206
+ ...reserved ? { reserved } : {}
1151
1207
  });
1152
1208
  return costUsd;
1153
1209
  }
1210
+ /**
1211
+ * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
1212
+ *
1213
+ * Post-hoc accrual for costs only known after the call (metered APIs); when
1214
+ * the price is known up front, {@link BudgetGuard.reserveTool} /
1215
+ * {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
1216
+ * `settleTool` for the full contract. Returns `costUsd`.
1217
+ */
1218
+ recordTool(tool, costUsd, options = {}) {
1219
+ return this.settleTool(tool, costUsd, { reserved: 0, label: options.label });
1220
+ }
1154
1221
  /**
1155
1222
  * Drop an in-flight reservation without recording spend (e.g. the call failed
1156
1223
  * before producing usage). Safe to call with `0`.
@@ -1160,16 +1227,25 @@ var BudgetGuard = class {
1160
1227
  throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
1161
1228
  }
1162
1229
  if (!reserved) return;
1163
- this.reserved = Math.max(0, this.reserved - reserved);
1230
+ this.consumeReservation(reserved);
1164
1231
  }
1165
1232
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
1166
1233
  get remainingUsd() {
1167
1234
  return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
1168
1235
  }
1236
+ /**
1237
+ * Per-tool running USD totals, keyed by the name given to `settleTool()` /
1238
+ * `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
1239
+ * Makes the token/tool split of the one shared ceiling inspectable
1240
+ * (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
1241
+ */
1242
+ get toolCosts() {
1243
+ return { ...this.toolCostTotals };
1244
+ }
1169
1245
  /**
1170
1246
  * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
1171
- * `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
1172
- * it cannot corrupt the ledger.
1247
+ * `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
1248
+ * snapshot copy: mutating it cannot corrupt the ledger.
1173
1249
  */
1174
1250
  get spendLog() {
1175
1251
  return [...this.spendEvents];
@@ -1200,6 +1276,34 @@ var BudgetGuard = class {
1200
1276
  `;
1201
1277
  }).join("");
1202
1278
  }
1279
+ /**
1280
+ * The default next-call prediction when the caller supplies no estimate.
1281
+ * Conservative: the costlier of the last LLM call and the last tool call — a
1282
+ * mixed loop predicts the pricier kind, which at worst blocks one call early
1283
+ * (fail-closed) rather than letting a crossing call through because the LAST
1284
+ * event happened to be cheap.
1285
+ */
1286
+ defaultEstimate() {
1287
+ return Math.max(this.lastLlmCost, this.lastToolCost);
1288
+ }
1289
+ /**
1290
+ * Subtract a settled/released hold from the in-flight tally. A handle larger
1291
+ * than EVERYTHING currently held cannot have come from a matching
1292
+ * `reserve()` — throwing beats silently clamping, which would free OTHER
1293
+ * callers' holds and fail the ceiling open. The epsilon absorbs float dust
1294
+ * from accumulating and draining many holds; per-caller over-release (a
1295
+ * handle within the total but larger than the caller's own hold) is
1296
+ * undetectable without per-handle tracking and remains the caller's
1297
+ * responsibility.
1298
+ */
1299
+ consumeReservation(reserved) {
1300
+ if (reserved > this.reserved + EPS) {
1301
+ throw new RangeError(
1302
+ `reserved handle (${reserved}) exceeds total in-flight reservations (${this.reserved}) \u2014 a handle must come from a matching reserve()`
1303
+ );
1304
+ }
1305
+ this.reserved = Math.max(0, this.reserved - reserved);
1306
+ }
1203
1307
  appendEvent(event) {
1204
1308
  this.spendEvents.push(Object.freeze(event));
1205
1309
  if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {