floe-guard 0.6.0 → 0.8.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
@@ -142,6 +142,22 @@ interface BudgetAdvisory {
142
142
  spentUsd: number;
143
143
  /** Hosted reports the tightest cap across all scopes; local is always "local". */
144
144
  scope: "local";
145
+ /**
146
+ * The guard's own next-call estimate (the costlier of the last LLM and last
147
+ * tool call — the same value the default reservation uses). 0 until the first
148
+ * call is recorded, so a planner can't divide by a cold estimate.
149
+ *
150
+ * Optional so adding it stays a non-breaking, additive change for any code
151
+ * that constructs a `BudgetAdvisory` literal; `advisory()` always sets it.
152
+ */
153
+ expectedCost?: number;
154
+ /**
155
+ * How many more calls the remaining budget buys at expectedCost:
156
+ * floor(remainingUsd / expectedCost). null when expectedCost is 0 (no call
157
+ * recorded yet) — unknown, not zero. Optional for the same additive reason as
158
+ * expectedCost; `advisory()` always sets it.
159
+ */
160
+ estCallsRemaining?: number | null;
145
161
  }
146
162
  declare class BudgetGuard {
147
163
  readonly limitUsd: number;
@@ -514,4 +530,33 @@ interface BudgetGuardMiddleware {
514
530
  */
515
531
  declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
516
532
 
517
- 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 };
533
+ interface RetryPlan<T> {
534
+ /** Operation to run for this retry attempt. */
535
+ call: () => T | Promise<T>;
536
+ /**
537
+ * Estimated retry cost passed to `guard.check()` before the retry runs.
538
+ * Leave undefined to use the guard's default last-call estimate.
539
+ */
540
+ estimatedCost?: number;
541
+ }
542
+ interface BudgetRetryOptions<T> {
543
+ /** Estimated cost for retrying the original call. */
544
+ estimatedCost?: number;
545
+ /** Total attempts, including the first call. Default: 2. */
546
+ maxAttempts?: number;
547
+ /** Choose a cheaper retry plan when `guard.advisory().nearLimit` is true. */
548
+ onDegrade?: (error: unknown, advisory: BudgetAdvisory) => RetryPlan<T> | Promise<RetryPlan<T> | undefined> | undefined;
549
+ /** Decide whether an error is retryable. Defaults to all non-budget errors. */
550
+ retryIf?: (error: unknown) => boolean;
551
+ }
552
+ /**
553
+ * Run `call` with budget-aware retries.
554
+ *
555
+ * The first attempt runs unchanged. If it fails with a retryable error, the
556
+ * helper retries as-is with ample budget, asks `onDegrade` for a cheaper plan
557
+ * when near the limit, and always calls `guard.check(estimatedCost)` before a
558
+ * retry so an over-budget retry is blocked before it runs.
559
+ */
560
+ declare function withBudgetRetry<T>(guard: BudgetGuard, call: () => T | Promise<T>, options?: BudgetRetryOptions<T>): Promise<T>;
561
+
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 };
package/dist/index.d.ts CHANGED
@@ -142,6 +142,22 @@ interface BudgetAdvisory {
142
142
  spentUsd: number;
143
143
  /** Hosted reports the tightest cap across all scopes; local is always "local". */
144
144
  scope: "local";
145
+ /**
146
+ * The guard's own next-call estimate (the costlier of the last LLM and last
147
+ * tool call — the same value the default reservation uses). 0 until the first
148
+ * call is recorded, so a planner can't divide by a cold estimate.
149
+ *
150
+ * Optional so adding it stays a non-breaking, additive change for any code
151
+ * that constructs a `BudgetAdvisory` literal; `advisory()` always sets it.
152
+ */
153
+ expectedCost?: number;
154
+ /**
155
+ * How many more calls the remaining budget buys at expectedCost:
156
+ * floor(remainingUsd / expectedCost). null when expectedCost is 0 (no call
157
+ * recorded yet) — unknown, not zero. Optional for the same additive reason as
158
+ * expectedCost; `advisory()` always sets it.
159
+ */
160
+ estCallsRemaining?: number | null;
145
161
  }
146
162
  declare class BudgetGuard {
147
163
  readonly limitUsd: number;
@@ -514,4 +530,33 @@ interface BudgetGuardMiddleware {
514
530
  */
515
531
  declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
516
532
 
517
- 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 };
533
+ interface RetryPlan<T> {
534
+ /** Operation to run for this retry attempt. */
535
+ call: () => T | Promise<T>;
536
+ /**
537
+ * Estimated retry cost passed to `guard.check()` before the retry runs.
538
+ * Leave undefined to use the guard's default last-call estimate.
539
+ */
540
+ estimatedCost?: number;
541
+ }
542
+ interface BudgetRetryOptions<T> {
543
+ /** Estimated cost for retrying the original call. */
544
+ estimatedCost?: number;
545
+ /** Total attempts, including the first call. Default: 2. */
546
+ maxAttempts?: number;
547
+ /** Choose a cheaper retry plan when `guard.advisory().nearLimit` is true. */
548
+ onDegrade?: (error: unknown, advisory: BudgetAdvisory) => RetryPlan<T> | Promise<RetryPlan<T> | undefined> | undefined;
549
+ /** Decide whether an error is retryable. Defaults to all non-budget errors. */
550
+ retryIf?: (error: unknown) => boolean;
551
+ }
552
+ /**
553
+ * Run `call` with budget-aware retries.
554
+ *
555
+ * The first attempt runs unchanged. If it fails with a retryable error, the
556
+ * helper retries as-is with ample budget, asks `onDegrade` for a cheaper plan
557
+ * when near the limit, and always calls `guard.check(estimatedCost)` before a
558
+ * retry so an over-budget retry is blocked before it runs.
559
+ */
560
+ declare function withBudgetRetry<T>(guard: BudgetGuard, call: () => T | Promise<T>, options?: BudgetRetryOptions<T>): Promise<T>;
561
+
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 };
package/dist/index.js CHANGED
@@ -1451,6 +1451,9 @@ var BudgetGuard = class {
1451
1451
  */
1452
1452
  advisory() {
1453
1453
  const usedBps = this.limitUsd <= 0 ? 1e4 : Math.max(0, Math.min(1e4, Math.floor(this.spentUsd / this.limitUsd * 1e4 + 1e-9)));
1454
+ const remainingUsd = Math.max(0, this.limitUsd - this.spentUsd);
1455
+ const expectedCost = Math.max(this.lastLlmCost, this.lastToolCost);
1456
+ const estCallsRemaining = expectedCost > 0 ? Math.floor(remainingUsd / expectedCost + 1e-9) : null;
1454
1457
  return {
1455
1458
  nearLimit: usedBps >= this.nearLimitBps,
1456
1459
  usedBps,
@@ -1458,10 +1461,12 @@ var BudgetGuard = class {
1458
1461
  // in-flight reservations. Unlike the remainingUsd getter (which subtracts
1459
1462
  // `reserved`), the advisory is a soft utilization signal about money already
1460
1463
  // spent, while the getter reports what a new call can still claim.
1461
- remainingUsd: Math.max(0, this.limitUsd - this.spentUsd),
1464
+ remainingUsd,
1462
1465
  limitUsd: this.limitUsd,
1463
1466
  spentUsd: this.spentUsd,
1464
- scope: "local"
1467
+ scope: "local",
1468
+ expectedCost,
1469
+ estCallsRemaining
1465
1470
  };
1466
1471
  }
1467
1472
  };
@@ -1624,6 +1629,42 @@ function budgetGuardMiddleware(guard) {
1624
1629
  }
1625
1630
  };
1626
1631
  }
1632
+
1633
+ // src/retry.ts
1634
+ function defaultRetryIf(error) {
1635
+ return !(error instanceof BudgetExceeded);
1636
+ }
1637
+ async function withBudgetRetry(guard, call, options = {}) {
1638
+ const maxAttempts = options.maxAttempts ?? 2;
1639
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
1640
+ throw new RangeError(`maxAttempts must be an integer >= 1, got ${maxAttempts}`);
1641
+ }
1642
+ const retryIf = options.retryIf ?? defaultRetryIf;
1643
+ let plan = { call, estimatedCost: options.estimatedCost };
1644
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1645
+ try {
1646
+ return await plan.call();
1647
+ } catch (error) {
1648
+ if (attempt >= maxAttempts || !retryIf(error)) {
1649
+ throw error;
1650
+ }
1651
+ plan = await nextPlan(guard, error, plan, options.onDegrade);
1652
+ }
1653
+ }
1654
+ throw new Error("unreachable");
1655
+ }
1656
+ async function nextPlan(guard, error, current, onDegrade) {
1657
+ const advisory = guard.advisory();
1658
+ if (advisory.nearLimit && onDegrade !== void 0) {
1659
+ const degraded = await onDegrade(error, advisory);
1660
+ if (degraded !== void 0) {
1661
+ guard.check(degraded.estimatedCost);
1662
+ return degraded;
1663
+ }
1664
+ }
1665
+ guard.check(current.estimatedCost);
1666
+ return current;
1667
+ }
1627
1668
  export {
1628
1669
  BudgetExceeded,
1629
1670
  BudgetGuard,
@@ -1634,6 +1675,7 @@ export {
1634
1675
  budgetGuardMiddleware,
1635
1676
  priceTokens,
1636
1677
  pricing_exports as pricing,
1637
- resolvePrice
1678
+ resolvePrice,
1679
+ withBudgetRetry
1638
1680
  };
1639
1681
  //# sourceMappingURL=index.js.map