floe-guard 0.8.0 → 0.9.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
@@ -64,6 +64,23 @@ declare namespace pricing {
64
64
  * same epsilon handling, same fail-closed default.
65
65
  */
66
66
 
67
+ /**
68
+ * A two-dimensional in-flight hold (USD + tokens) returned by
69
+ * {@link BudgetGuard.reserve} when a token ceiling or an active
70
+ * {@link BudgetGuard.step} is involved. A dumb value handle — pass it straight
71
+ * back to {@link BudgetGuard.settle} / {@link BudgetGuard.release}. When neither
72
+ * tokens nor a step are involved, `reserve` returns a plain `number` instead
73
+ * (byte-for-byte the old behaviour), so {@link ReservationHandle} is the union.
74
+ */
75
+ interface BudgetReservation {
76
+ readonly usd: number;
77
+ readonly tokens: number;
78
+ }
79
+ /**
80
+ * A plain `number` when USD-only (backward compatible); a
81
+ * {@link BudgetReservation} when a token ceiling or an active step is in play.
82
+ */
83
+ type ReservationHandle = number | BudgetReservation;
67
84
  /**
68
85
  * One priced spend event in the guard's per-call ledger.
69
86
  *
@@ -103,7 +120,9 @@ interface BudgetGuardOptions {
103
120
  /**
104
121
  * Optional callback invoked with `(spentUsd, limitUsd)` right before
105
122
  * {@link BudgetExceeded} is thrown. Defaults to printing the
106
- * `BUDGET EXCEEDED — call blocked` banner to stderr.
123
+ * `BUDGET EXCEEDED — call blocked` banner to stderr. Fires on **USD** ceiling
124
+ * crossings only; a token-ceiling block ({@link TokenBudgetExceeded}, aggregate
125
+ * or step) is thrown without invoking it, since the callback is dollar-shaped.
107
126
  */
108
127
  onBlock?: (spentUsd: number, limitUsd: number) => void;
109
128
  /**
@@ -118,6 +137,11 @@ interface BudgetGuardOptions {
118
137
  * unaffected. Default: keep every event.
119
138
  */
120
139
  maxLogEvents?: number;
140
+ /**
141
+ * Aggregate token ceiling. `null`/undefined disables the token dimension
142
+ * entirely (a pure USD guard, unchanged). `0` blocks the first token.
143
+ */
144
+ tokenLimit?: number;
121
145
  }
122
146
  /**
123
147
  * A context-aware spend signal for the single local budget.
@@ -158,6 +182,19 @@ interface BudgetAdvisory {
158
182
  * expectedCost; `advisory()` always sets it.
159
183
  */
160
184
  estCallsRemaining?: number | null;
185
+ /**
186
+ * Aggregate token utilization in basis points (0..10000), mirroring usedBps
187
+ * for the token ceiling. null when no `tokenLimit` is set (nothing to signal).
188
+ */
189
+ tokenUsedBps?: number | null;
190
+ /** Tokens left before the aggregate token ceiling. null when no `tokenLimit`. */
191
+ remainingTokens?: number | null;
192
+ /**
193
+ * Per-step headroom for the innermost active step — present (non-null) ONLY
194
+ * while a step() is active AND that dimension is capped. null otherwise.
195
+ */
196
+ stepRemainingUsd?: number | null;
197
+ stepRemainingTokens?: number | null;
161
198
  }
162
199
  declare class BudgetGuard {
163
200
  readonly limitUsd: number;
@@ -165,6 +202,14 @@ declare class BudgetGuard {
165
202
  priceOverrides?: Record<string, ManualPrice>;
166
203
  failClosed: boolean;
167
204
  nearLimitBps: number;
205
+ /** Aggregate token ceiling, or null when the token dimension is disabled. */
206
+ readonly tokenLimit: number | null;
207
+ /** Aggregate tokens accrued (prompt + completion + cache buckets). */
208
+ spentTokens: number;
209
+ /** Tokens held in-flight — the token twin of `reserved`. */
210
+ private reservedTokens;
211
+ /** Step stack: innermost step is last. Empty when no step() is active. */
212
+ private readonly steps;
168
213
  private readonly onBlock;
169
214
  /**
170
215
  * Costs of the most recent priced LLM call and tool call, tracked
@@ -203,7 +248,9 @@ declare class BudgetGuard {
203
248
  * Note: `check` is a non-binding peek. For parallel calls, use `reserve()` /
204
249
  * `settle()`, which hold the estimate across the await.
205
250
  */
206
- check(estimatedNextCost?: number): void;
251
+ check(estimatedNextCost?: number, options?: {
252
+ estimatedTokens?: number;
253
+ }): void;
207
254
  /**
208
255
  * Atomically check the ceiling AND hold the estimated cost in flight.
209
256
  *
@@ -215,7 +262,9 @@ declare class BudgetGuard {
215
262
  * `estimatedCost` defaults to the costlier of the last LLM call and the last
216
263
  * tool call.
217
264
  */
218
- reserve(estimatedCost?: number): number;
265
+ reserve(estimatedCost?: number, options?: {
266
+ estimatedTokens?: number;
267
+ }): ReservationHandle;
219
268
  /**
220
269
  * Release a reservation and record the actual cost. `record` is `settle` with
221
270
  * no reservation. Returns the USD cost of this call; unpriceable-model handling
@@ -226,7 +275,7 @@ declare class BudgetGuard {
226
275
  * in lockstep with `spentUsd`.
227
276
  */
228
277
  settle(model: string, promptTokens: number, completionTokens: number, options?: {
229
- reserved?: number;
278
+ reserved?: ReservationHandle;
230
279
  price?: ManualPrice;
231
280
  label?: string;
232
281
  }): number;
@@ -258,7 +307,7 @@ declare class BudgetGuard {
258
307
  * worth falling back to. Pass the returned handle to
259
308
  * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
260
309
  */
261
- reserveTool(estimatedCost: number): number;
310
+ reserveTool(estimatedCost: number): ReservationHandle;
262
311
  /**
263
312
  * Release a reservation and record a tool call's actual cost.
264
313
  *
@@ -274,7 +323,7 @@ declare class BudgetGuard {
274
323
  * Returns `costUsd`.
275
324
  */
276
325
  settleTool(tool: string, costUsd: number, options?: {
277
- reserved?: number;
326
+ reserved?: ReservationHandle;
278
327
  label?: string;
279
328
  }): number;
280
329
  /**
@@ -292,7 +341,7 @@ declare class BudgetGuard {
292
341
  * Drop an in-flight reservation without recording spend (e.g. the call failed
293
342
  * before producing usage). Safe to call with `0`.
294
343
  */
295
- release(reserved: number): void;
344
+ release(reserved: ReservationHandle): void;
296
345
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
297
346
  get remainingUsd(): number;
298
347
  /**
@@ -338,6 +387,50 @@ declare class BudgetGuard {
338
387
  * responsibility.
339
388
  */
340
389
  private consumeReservation;
390
+ /**
391
+ * The USD amount of a handle, validated. Both shapes are checked — a raw
392
+ * number and a {@link BudgetReservation}'s `usd`/`tokens` fields — so a bad
393
+ * hand-rolled handle can't corrupt the in-flight tally.
394
+ */
395
+ /**
396
+ * Validate a caller-supplied token estimate. Rejects a fraction, NaN,
397
+ * Infinity, or boolean (via `Number.isInteger`) so it can't disable the
398
+ * integer token hard-stops or corrupt the in-flight token tally. `undefined`
399
+ * (the default) is fine; a negative integer is clamped by `Math.max(0, ...)`,
400
+ * matching the lenient USD estimate. Mirrors Python's `_validate_token_estimate`.
401
+ */
402
+ private validateTokenEstimate;
403
+ private reservedUsdOf;
404
+ /**
405
+ * Accrue a settled call into the innermost active step (no-op when none).
406
+ * Sequential-loop contract: the innermost step owns the call.
407
+ */
408
+ private accrueStep;
409
+ /**
410
+ * The ONE choke point, across two dimensions and two scopes. Returns the first
411
+ * ceiling that blocks as `[dimension, scope]` — dimension `"usd" | "tokens"`,
412
+ * scope `"aggregate" | "step"` — or `null` if the call fits everywhere.
413
+ * Aggregate is checked before step (the guard-wide ceiling is the hard limit).
414
+ */
415
+ private blockingCross;
416
+ /** Notify + throw the right error for a [dimension, scope] block. */
417
+ private raiseBlock;
418
+ /**
419
+ * Scope a per-step USD and/or token cap for a **sequential agent loop**.
420
+ *
421
+ * Runs `fn` with a step pushed onto the guard's stack; the
422
+ * enforcement/accrual path honours the innermost active step *on top of* the
423
+ * aggregate ceilings. A call that would cross the step's `maxUsd` / `maxTokens`
424
+ * is hard-blocked ({@link BudgetExceeded} / {@link TokenBudgetExceeded} with
425
+ * `scope: "step"`) even if the aggregate budget has room. `fn` receives the
426
+ * SAME guard, so no adapter needs to know about steps. The step is popped when
427
+ * `fn` settles or throws. **Not for concurrent parallel steps on one guard** —
428
+ * that's out of scope for issue #46; use one guard per parallel branch.
429
+ */
430
+ step<T>(options: {
431
+ maxUsd?: number;
432
+ maxTokens?: number;
433
+ }, fn: (guard: BudgetGuard) => T): T;
341
434
  private appendEvent;
342
435
  /**
343
436
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
@@ -442,7 +535,25 @@ declare class FloeGuardError extends Error {
442
535
  declare class BudgetExceeded extends FloeGuardError {
443
536
  readonly spentUsd: number;
444
537
  readonly limitUsd: number;
445
- constructor(spentUsd: number, limitUsd: number);
538
+ constructor(spentUsd: number, limitUsd: number, message?: string);
539
+ }
540
+ /**
541
+ * Thrown before a call that would cross a token ceiling (aggregate or step).
542
+ *
543
+ * The token twin of {@link BudgetExceeded} — it extends it so the same retry
544
+ * logic that treats a USD block as terminal (`!(error instanceof
545
+ * BudgetExceeded)`) treats a token block as terminal too, with no extra wiring.
546
+ * `spentUsd` / `limitUsd` are inherited but not meaningful here; the token
547
+ * fields are the payload.
548
+ *
549
+ * `scope` is `"aggregate"` (the guard-wide `tokenLimit`) or `"step"` (the
550
+ * innermost active {@link BudgetGuard.step} cap).
551
+ */
552
+ declare class TokenBudgetExceeded extends BudgetExceeded {
553
+ readonly spentTokens: number;
554
+ readonly limitTokens: number;
555
+ readonly scope: "aggregate" | "step";
556
+ constructor(spentTokens: number, limitTokens: number, scope: "aggregate" | "step");
446
557
  }
447
558
  /**
448
559
  * Thrown when a model cannot be priced and the guard is fail-closed.
@@ -559,4 +670,4 @@ interface BudgetRetryOptions<T> {
559
670
  */
560
671
  declare function withBudgetRetry<T>(guard: BudgetGuard, call: () => T | Promise<T>, options?: BudgetRetryOptions<T>): Promise<T>;
561
672
 
562
- export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, type BudgetRetryOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type RetryPlan, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice, withBudgetRetry };
673
+ export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, type BudgetReservation, type BudgetRetryOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type ReservationHandle, type RetryPlan, type SpendEvent, TokenBudgetExceeded, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice, withBudgetRetry };
package/dist/index.d.ts CHANGED
@@ -64,6 +64,23 @@ declare namespace pricing {
64
64
  * same epsilon handling, same fail-closed default.
65
65
  */
66
66
 
67
+ /**
68
+ * A two-dimensional in-flight hold (USD + tokens) returned by
69
+ * {@link BudgetGuard.reserve} when a token ceiling or an active
70
+ * {@link BudgetGuard.step} is involved. A dumb value handle — pass it straight
71
+ * back to {@link BudgetGuard.settle} / {@link BudgetGuard.release}. When neither
72
+ * tokens nor a step are involved, `reserve` returns a plain `number` instead
73
+ * (byte-for-byte the old behaviour), so {@link ReservationHandle} is the union.
74
+ */
75
+ interface BudgetReservation {
76
+ readonly usd: number;
77
+ readonly tokens: number;
78
+ }
79
+ /**
80
+ * A plain `number` when USD-only (backward compatible); a
81
+ * {@link BudgetReservation} when a token ceiling or an active step is in play.
82
+ */
83
+ type ReservationHandle = number | BudgetReservation;
67
84
  /**
68
85
  * One priced spend event in the guard's per-call ledger.
69
86
  *
@@ -103,7 +120,9 @@ interface BudgetGuardOptions {
103
120
  /**
104
121
  * Optional callback invoked with `(spentUsd, limitUsd)` right before
105
122
  * {@link BudgetExceeded} is thrown. Defaults to printing the
106
- * `BUDGET EXCEEDED — call blocked` banner to stderr.
123
+ * `BUDGET EXCEEDED — call blocked` banner to stderr. Fires on **USD** ceiling
124
+ * crossings only; a token-ceiling block ({@link TokenBudgetExceeded}, aggregate
125
+ * or step) is thrown without invoking it, since the callback is dollar-shaped.
107
126
  */
108
127
  onBlock?: (spentUsd: number, limitUsd: number) => void;
109
128
  /**
@@ -118,6 +137,11 @@ interface BudgetGuardOptions {
118
137
  * unaffected. Default: keep every event.
119
138
  */
120
139
  maxLogEvents?: number;
140
+ /**
141
+ * Aggregate token ceiling. `null`/undefined disables the token dimension
142
+ * entirely (a pure USD guard, unchanged). `0` blocks the first token.
143
+ */
144
+ tokenLimit?: number;
121
145
  }
122
146
  /**
123
147
  * A context-aware spend signal for the single local budget.
@@ -158,6 +182,19 @@ interface BudgetAdvisory {
158
182
  * expectedCost; `advisory()` always sets it.
159
183
  */
160
184
  estCallsRemaining?: number | null;
185
+ /**
186
+ * Aggregate token utilization in basis points (0..10000), mirroring usedBps
187
+ * for the token ceiling. null when no `tokenLimit` is set (nothing to signal).
188
+ */
189
+ tokenUsedBps?: number | null;
190
+ /** Tokens left before the aggregate token ceiling. null when no `tokenLimit`. */
191
+ remainingTokens?: number | null;
192
+ /**
193
+ * Per-step headroom for the innermost active step — present (non-null) ONLY
194
+ * while a step() is active AND that dimension is capped. null otherwise.
195
+ */
196
+ stepRemainingUsd?: number | null;
197
+ stepRemainingTokens?: number | null;
161
198
  }
162
199
  declare class BudgetGuard {
163
200
  readonly limitUsd: number;
@@ -165,6 +202,14 @@ declare class BudgetGuard {
165
202
  priceOverrides?: Record<string, ManualPrice>;
166
203
  failClosed: boolean;
167
204
  nearLimitBps: number;
205
+ /** Aggregate token ceiling, or null when the token dimension is disabled. */
206
+ readonly tokenLimit: number | null;
207
+ /** Aggregate tokens accrued (prompt + completion + cache buckets). */
208
+ spentTokens: number;
209
+ /** Tokens held in-flight — the token twin of `reserved`. */
210
+ private reservedTokens;
211
+ /** Step stack: innermost step is last. Empty when no step() is active. */
212
+ private readonly steps;
168
213
  private readonly onBlock;
169
214
  /**
170
215
  * Costs of the most recent priced LLM call and tool call, tracked
@@ -203,7 +248,9 @@ declare class BudgetGuard {
203
248
  * Note: `check` is a non-binding peek. For parallel calls, use `reserve()` /
204
249
  * `settle()`, which hold the estimate across the await.
205
250
  */
206
- check(estimatedNextCost?: number): void;
251
+ check(estimatedNextCost?: number, options?: {
252
+ estimatedTokens?: number;
253
+ }): void;
207
254
  /**
208
255
  * Atomically check the ceiling AND hold the estimated cost in flight.
209
256
  *
@@ -215,7 +262,9 @@ declare class BudgetGuard {
215
262
  * `estimatedCost` defaults to the costlier of the last LLM call and the last
216
263
  * tool call.
217
264
  */
218
- reserve(estimatedCost?: number): number;
265
+ reserve(estimatedCost?: number, options?: {
266
+ estimatedTokens?: number;
267
+ }): ReservationHandle;
219
268
  /**
220
269
  * Release a reservation and record the actual cost. `record` is `settle` with
221
270
  * no reservation. Returns the USD cost of this call; unpriceable-model handling
@@ -226,7 +275,7 @@ declare class BudgetGuard {
226
275
  * in lockstep with `spentUsd`.
227
276
  */
228
277
  settle(model: string, promptTokens: number, completionTokens: number, options?: {
229
- reserved?: number;
278
+ reserved?: ReservationHandle;
230
279
  price?: ManualPrice;
231
280
  label?: string;
232
281
  }): number;
@@ -258,7 +307,7 @@ declare class BudgetGuard {
258
307
  * worth falling back to. Pass the returned handle to
259
308
  * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
260
309
  */
261
- reserveTool(estimatedCost: number): number;
310
+ reserveTool(estimatedCost: number): ReservationHandle;
262
311
  /**
263
312
  * Release a reservation and record a tool call's actual cost.
264
313
  *
@@ -274,7 +323,7 @@ declare class BudgetGuard {
274
323
  * Returns `costUsd`.
275
324
  */
276
325
  settleTool(tool: string, costUsd: number, options?: {
277
- reserved?: number;
326
+ reserved?: ReservationHandle;
278
327
  label?: string;
279
328
  }): number;
280
329
  /**
@@ -292,7 +341,7 @@ declare class BudgetGuard {
292
341
  * Drop an in-flight reservation without recording spend (e.g. the call failed
293
342
  * before producing usage). Safe to call with `0`.
294
343
  */
295
- release(reserved: number): void;
344
+ release(reserved: ReservationHandle): void;
296
345
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
297
346
  get remainingUsd(): number;
298
347
  /**
@@ -338,6 +387,50 @@ declare class BudgetGuard {
338
387
  * responsibility.
339
388
  */
340
389
  private consumeReservation;
390
+ /**
391
+ * The USD amount of a handle, validated. Both shapes are checked — a raw
392
+ * number and a {@link BudgetReservation}'s `usd`/`tokens` fields — so a bad
393
+ * hand-rolled handle can't corrupt the in-flight tally.
394
+ */
395
+ /**
396
+ * Validate a caller-supplied token estimate. Rejects a fraction, NaN,
397
+ * Infinity, or boolean (via `Number.isInteger`) so it can't disable the
398
+ * integer token hard-stops or corrupt the in-flight token tally. `undefined`
399
+ * (the default) is fine; a negative integer is clamped by `Math.max(0, ...)`,
400
+ * matching the lenient USD estimate. Mirrors Python's `_validate_token_estimate`.
401
+ */
402
+ private validateTokenEstimate;
403
+ private reservedUsdOf;
404
+ /**
405
+ * Accrue a settled call into the innermost active step (no-op when none).
406
+ * Sequential-loop contract: the innermost step owns the call.
407
+ */
408
+ private accrueStep;
409
+ /**
410
+ * The ONE choke point, across two dimensions and two scopes. Returns the first
411
+ * ceiling that blocks as `[dimension, scope]` — dimension `"usd" | "tokens"`,
412
+ * scope `"aggregate" | "step"` — or `null` if the call fits everywhere.
413
+ * Aggregate is checked before step (the guard-wide ceiling is the hard limit).
414
+ */
415
+ private blockingCross;
416
+ /** Notify + throw the right error for a [dimension, scope] block. */
417
+ private raiseBlock;
418
+ /**
419
+ * Scope a per-step USD and/or token cap for a **sequential agent loop**.
420
+ *
421
+ * Runs `fn` with a step pushed onto the guard's stack; the
422
+ * enforcement/accrual path honours the innermost active step *on top of* the
423
+ * aggregate ceilings. A call that would cross the step's `maxUsd` / `maxTokens`
424
+ * is hard-blocked ({@link BudgetExceeded} / {@link TokenBudgetExceeded} with
425
+ * `scope: "step"`) even if the aggregate budget has room. `fn` receives the
426
+ * SAME guard, so no adapter needs to know about steps. The step is popped when
427
+ * `fn` settles or throws. **Not for concurrent parallel steps on one guard** —
428
+ * that's out of scope for issue #46; use one guard per parallel branch.
429
+ */
430
+ step<T>(options: {
431
+ maxUsd?: number;
432
+ maxTokens?: number;
433
+ }, fn: (guard: BudgetGuard) => T): T;
341
434
  private appendEvent;
342
435
  /**
343
436
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
@@ -442,7 +535,25 @@ declare class FloeGuardError extends Error {
442
535
  declare class BudgetExceeded extends FloeGuardError {
443
536
  readonly spentUsd: number;
444
537
  readonly limitUsd: number;
445
- constructor(spentUsd: number, limitUsd: number);
538
+ constructor(spentUsd: number, limitUsd: number, message?: string);
539
+ }
540
+ /**
541
+ * Thrown before a call that would cross a token ceiling (aggregate or step).
542
+ *
543
+ * The token twin of {@link BudgetExceeded} — it extends it so the same retry
544
+ * logic that treats a USD block as terminal (`!(error instanceof
545
+ * BudgetExceeded)`) treats a token block as terminal too, with no extra wiring.
546
+ * `spentUsd` / `limitUsd` are inherited but not meaningful here; the token
547
+ * fields are the payload.
548
+ *
549
+ * `scope` is `"aggregate"` (the guard-wide `tokenLimit`) or `"step"` (the
550
+ * innermost active {@link BudgetGuard.step} cap).
551
+ */
552
+ declare class TokenBudgetExceeded extends BudgetExceeded {
553
+ readonly spentTokens: number;
554
+ readonly limitTokens: number;
555
+ readonly scope: "aggregate" | "step";
556
+ constructor(spentTokens: number, limitTokens: number, scope: "aggregate" | "step");
446
557
  }
447
558
  /**
448
559
  * Thrown when a model cannot be priced and the guard is fail-closed.
@@ -559,4 +670,4 @@ interface BudgetRetryOptions<T> {
559
670
  */
560
671
  declare function withBudgetRetry<T>(guard: BudgetGuard, call: () => T | Promise<T>, options?: BudgetRetryOptions<T>): Promise<T>;
561
672
 
562
- export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, type BudgetRetryOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type RetryPlan, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice, withBudgetRetry };
673
+ export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, type BudgetReservation, type BudgetRetryOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type ReservationHandle, type RetryPlan, type SpendEvent, TokenBudgetExceeded, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice, withBudgetRetry };