floe-guard 0.3.0 → 0.4.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
@@ -258,6 +258,77 @@ declare class BudgetGuard {
258
258
  advisory(): BudgetAdvisory;
259
259
  }
260
260
 
261
+ /**
262
+ * LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.
263
+ *
264
+ * BudgetGuard stops an agent before its next call crosses a USD ceiling;
265
+ * LatencyBudget stops it before the next call would blow an end-user SLA:
266
+ *
267
+ * ```ts
268
+ * const deadline = new LatencyBudget(5000);
269
+ * ...
270
+ * deadline.check(800); // throws DeadlineExceeded when projected over
271
+ * if (deadline.advisory().nearDeadline) useFasterModel();
272
+ * router.pick({ maxLatencyMs: deadline.remainingMs });
273
+ * ```
274
+ *
275
+ * Design notes (mirroring `src/floe_guard/latency.py`):
276
+ * - **Monotonic clock** — `performance.now()`, never wall time.
277
+ * - **Cooperative, not preemptive** — the guard provides the deadline signal;
278
+ * aborting a stalled in-flight call is the framework's job (AbortSignal).
279
+ * `check()` prevents the NEXT call from starting.
280
+ * - **Advisory symmetry** — `nearDeadline` / `usedBps` / `remainingMs` are the
281
+ * latency twin of BudgetGuard's `nearLimit` / `usedBps` / `remainingUsd`.
282
+ * - **In-process scope** — one instance per request/run; distributed latency
283
+ * tracking is out of scope.
284
+ */
285
+ /** A context-aware deadline signal — the latency twin of {@link BudgetAdvisory}.
286
+ * Soft by design; the hard-stop is {@link LatencyBudget.check}. */
287
+ interface LatencyAdvisory {
288
+ nearDeadline: boolean;
289
+ /** SLA consumed, basis points 0..10000 (8500 = 85%). */
290
+ usedBps: number;
291
+ remainingMs: number;
292
+ slaMs: number;
293
+ elapsedMs: number;
294
+ }
295
+ interface LatencyBudgetOptions {
296
+ /**
297
+ * Utilization (basis points, 0..10000) at which {@link LatencyBudget.advisory}
298
+ * flags `nearDeadline` so an agent can downshift to a faster path before the
299
+ * wall. Default 8000 (80%), matching BudgetGuard's `nearLimitBps`.
300
+ */
301
+ nearDeadlineBps?: number;
302
+ /** Invoked with `(elapsedMs, slaMs)` right before {@link DeadlineExceeded} is thrown. */
303
+ onBlock?: (elapsedMs: number, slaMs: number) => void;
304
+ /** Milliseconds-returning monotonic clock, injectable for tests. Defaults to `performance.now`. */
305
+ clock?: () => number;
306
+ }
307
+ declare class LatencyBudget {
308
+ readonly slaMs: number;
309
+ readonly nearDeadlineBps: number;
310
+ private readonly onBlock?;
311
+ private readonly clock;
312
+ private readonly startedAt;
313
+ /** The budget starts counting at construction — build it when the request
314
+ * (and its SLA) starts. */
315
+ constructor(slaMs: number, options?: LatencyBudgetOptions);
316
+ /** Milliseconds since construction (monotonic). */
317
+ get elapsedMs(): number;
318
+ /** Milliseconds left before the SLA, floored at 0 — the readable signal a
319
+ * router uses to pick a faster fallback or truncate work mid-chain. */
320
+ get remainingMs(): number;
321
+ /**
322
+ * Throw {@link DeadlineExceeded} when the projected elapsed time (now +
323
+ * `expectedMs` for the upcoming call) would blow the SLA. Call it
324
+ * immediately before each tool/model call; pass 0 to only gate on time
325
+ * already spent.
326
+ */
327
+ check(expectedMs?: number): void;
328
+ /** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */
329
+ advisory(): LatencyAdvisory;
330
+ }
331
+
261
332
  /**
262
333
  * Exceptions for floe-guard.
263
334
  *
@@ -293,6 +364,11 @@ declare class UnpriceableModelError extends FloeGuardError {
293
364
  readonly model: string;
294
365
  constructor(model: string);
295
366
  }
367
+ declare class DeadlineExceeded extends FloeGuardError {
368
+ readonly elapsedMs: number;
369
+ readonly slaMs: number;
370
+ constructor(elapsedMs: number, slaMs: number);
371
+ }
296
372
 
297
373
  /**
298
374
  * Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.
@@ -363,4 +439,4 @@ interface BudgetGuardMiddleware {
363
439
  */
364
440
  declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
365
441
 
366
- export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
442
+ export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
package/dist/index.d.ts CHANGED
@@ -258,6 +258,77 @@ declare class BudgetGuard {
258
258
  advisory(): BudgetAdvisory;
259
259
  }
260
260
 
261
+ /**
262
+ * LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.
263
+ *
264
+ * BudgetGuard stops an agent before its next call crosses a USD ceiling;
265
+ * LatencyBudget stops it before the next call would blow an end-user SLA:
266
+ *
267
+ * ```ts
268
+ * const deadline = new LatencyBudget(5000);
269
+ * ...
270
+ * deadline.check(800); // throws DeadlineExceeded when projected over
271
+ * if (deadline.advisory().nearDeadline) useFasterModel();
272
+ * router.pick({ maxLatencyMs: deadline.remainingMs });
273
+ * ```
274
+ *
275
+ * Design notes (mirroring `src/floe_guard/latency.py`):
276
+ * - **Monotonic clock** — `performance.now()`, never wall time.
277
+ * - **Cooperative, not preemptive** — the guard provides the deadline signal;
278
+ * aborting a stalled in-flight call is the framework's job (AbortSignal).
279
+ * `check()` prevents the NEXT call from starting.
280
+ * - **Advisory symmetry** — `nearDeadline` / `usedBps` / `remainingMs` are the
281
+ * latency twin of BudgetGuard's `nearLimit` / `usedBps` / `remainingUsd`.
282
+ * - **In-process scope** — one instance per request/run; distributed latency
283
+ * tracking is out of scope.
284
+ */
285
+ /** A context-aware deadline signal — the latency twin of {@link BudgetAdvisory}.
286
+ * Soft by design; the hard-stop is {@link LatencyBudget.check}. */
287
+ interface LatencyAdvisory {
288
+ nearDeadline: boolean;
289
+ /** SLA consumed, basis points 0..10000 (8500 = 85%). */
290
+ usedBps: number;
291
+ remainingMs: number;
292
+ slaMs: number;
293
+ elapsedMs: number;
294
+ }
295
+ interface LatencyBudgetOptions {
296
+ /**
297
+ * Utilization (basis points, 0..10000) at which {@link LatencyBudget.advisory}
298
+ * flags `nearDeadline` so an agent can downshift to a faster path before the
299
+ * wall. Default 8000 (80%), matching BudgetGuard's `nearLimitBps`.
300
+ */
301
+ nearDeadlineBps?: number;
302
+ /** Invoked with `(elapsedMs, slaMs)` right before {@link DeadlineExceeded} is thrown. */
303
+ onBlock?: (elapsedMs: number, slaMs: number) => void;
304
+ /** Milliseconds-returning monotonic clock, injectable for tests. Defaults to `performance.now`. */
305
+ clock?: () => number;
306
+ }
307
+ declare class LatencyBudget {
308
+ readonly slaMs: number;
309
+ readonly nearDeadlineBps: number;
310
+ private readonly onBlock?;
311
+ private readonly clock;
312
+ private readonly startedAt;
313
+ /** The budget starts counting at construction — build it when the request
314
+ * (and its SLA) starts. */
315
+ constructor(slaMs: number, options?: LatencyBudgetOptions);
316
+ /** Milliseconds since construction (monotonic). */
317
+ get elapsedMs(): number;
318
+ /** Milliseconds left before the SLA, floored at 0 — the readable signal a
319
+ * router uses to pick a faster fallback or truncate work mid-chain. */
320
+ get remainingMs(): number;
321
+ /**
322
+ * Throw {@link DeadlineExceeded} when the projected elapsed time (now +
323
+ * `expectedMs` for the upcoming call) would blow the SLA. Call it
324
+ * immediately before each tool/model call; pass 0 to only gate on time
325
+ * already spent.
326
+ */
327
+ check(expectedMs?: number): void;
328
+ /** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */
329
+ advisory(): LatencyAdvisory;
330
+ }
331
+
261
332
  /**
262
333
  * Exceptions for floe-guard.
263
334
  *
@@ -293,6 +364,11 @@ declare class UnpriceableModelError extends FloeGuardError {
293
364
  readonly model: string;
294
365
  constructor(model: string);
295
366
  }
367
+ declare class DeadlineExceeded extends FloeGuardError {
368
+ readonly elapsedMs: number;
369
+ readonly slaMs: number;
370
+ constructor(elapsedMs: number, slaMs: number);
371
+ }
296
372
 
297
373
  /**
298
374
  * Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.
@@ -363,4 +439,4 @@ interface BudgetGuardMiddleware {
363
439
  */
364
440
  declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
365
441
 
366
- export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
442
+ export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
package/dist/index.js CHANGED
@@ -34,6 +34,21 @@ var UnpriceableModelError = class extends FloeGuardError {
34
34
  this.model = model;
35
35
  }
36
36
  };
37
+ function roundHalfUp(ms) {
38
+ return Math.floor(ms + 0.5);
39
+ }
40
+ var DeadlineExceeded = class extends FloeGuardError {
41
+ elapsedMs;
42
+ slaMs;
43
+ constructor(elapsedMs, slaMs) {
44
+ super(
45
+ `DEADLINE EXCEEDED \u2014 call blocked (elapsed ${roundHalfUp(elapsedMs)}ms of ${roundHalfUp(slaMs)}ms SLA)`
46
+ );
47
+ this.name = "DeadlineExceeded";
48
+ this.elapsedMs = elapsedMs;
49
+ this.slaMs = slaMs;
50
+ }
51
+ };
37
52
 
38
53
  // src/pricing.ts
39
54
  var pricing_exports = {};
@@ -1222,6 +1237,68 @@ function defaultOnBlock(spentUsd, limitUsd) {
1222
1237
  );
1223
1238
  }
1224
1239
 
1240
+ // src/latency.ts
1241
+ var LatencyBudget = class {
1242
+ slaMs;
1243
+ nearDeadlineBps;
1244
+ onBlock;
1245
+ clock;
1246
+ startedAt;
1247
+ /** The budget starts counting at construction — build it when the request
1248
+ * (and its SLA) starts. */
1249
+ constructor(slaMs, options = {}) {
1250
+ if (!(Number.isFinite(slaMs) && slaMs > 0)) {
1251
+ throw new RangeError("slaMs must be > 0");
1252
+ }
1253
+ const nearDeadlineBps = options.nearDeadlineBps ?? 8e3;
1254
+ if (!Number.isInteger(nearDeadlineBps) || nearDeadlineBps < 0 || nearDeadlineBps > 1e4) {
1255
+ throw new RangeError("nearDeadlineBps must be an integer 0..10000");
1256
+ }
1257
+ this.slaMs = slaMs;
1258
+ this.nearDeadlineBps = nearDeadlineBps;
1259
+ this.onBlock = options.onBlock;
1260
+ this.clock = options.clock ?? (() => performance.now());
1261
+ this.startedAt = this.clock();
1262
+ }
1263
+ /** Milliseconds since construction (monotonic). */
1264
+ get elapsedMs() {
1265
+ return this.clock() - this.startedAt;
1266
+ }
1267
+ /** Milliseconds left before the SLA, floored at 0 — the readable signal a
1268
+ * router uses to pick a faster fallback or truncate work mid-chain. */
1269
+ get remainingMs() {
1270
+ return Math.max(0, this.slaMs - this.elapsedMs);
1271
+ }
1272
+ /**
1273
+ * Throw {@link DeadlineExceeded} when the projected elapsed time (now +
1274
+ * `expectedMs` for the upcoming call) would blow the SLA. Call it
1275
+ * immediately before each tool/model call; pass 0 to only gate on time
1276
+ * already spent.
1277
+ */
1278
+ check(expectedMs = 0) {
1279
+ if (!(Number.isFinite(expectedMs) && expectedMs >= 0)) {
1280
+ throw new RangeError("expectedMs must be >= 0");
1281
+ }
1282
+ const elapsed = this.elapsedMs;
1283
+ if (elapsed + expectedMs > this.slaMs) {
1284
+ this.onBlock?.(elapsed, this.slaMs);
1285
+ throw new DeadlineExceeded(elapsed, this.slaMs);
1286
+ }
1287
+ }
1288
+ /** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */
1289
+ advisory() {
1290
+ const elapsed = this.elapsedMs;
1291
+ const usedBps = elapsed > 0 ? Math.min(1e4, Math.round(elapsed * 1e4 / this.slaMs)) : 0;
1292
+ return {
1293
+ nearDeadline: usedBps >= this.nearDeadlineBps,
1294
+ usedBps,
1295
+ remainingMs: Math.max(0, this.slaMs - elapsed),
1296
+ slaMs: this.slaMs,
1297
+ elapsedMs: elapsed
1298
+ };
1299
+ }
1300
+ };
1301
+
1225
1302
  // src/middleware.ts
1226
1303
  function usageTokens(modelId, usage) {
1227
1304
  const u = usage;
@@ -1314,7 +1391,9 @@ function budgetGuardMiddleware(guard) {
1314
1391
  export {
1315
1392
  BudgetExceeded,
1316
1393
  BudgetGuard,
1394
+ DeadlineExceeded,
1317
1395
  FloeGuardError,
1396
+ LatencyBudget,
1318
1397
  UnpriceableModelError,
1319
1398
  budgetGuardMiddleware,
1320
1399
  priceTokens,