@rulvar/core 1.27.0 → 1.29.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.ts CHANGED
@@ -2465,11 +2465,16 @@ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
2465
2465
  */
2466
2466
  declare function retryClassOf(error: WireError): RetryClass | undefined;
2467
2467
  /**
2468
- * The delay before retry number `retryIndex` (0-based: the delay after
2469
- * the first failed attempt has index 0). A provider-supplied
2470
- * retryAfterMs REPLACES the computed delay (Appendix A). Jitter is
2471
- * equal-jitter: half the backoff is deterministic, half random, so a
2472
- * jittered delay never collapses to zero.
2468
+ * The delay before retry number `retryIndex` (zero based: the delay
2469
+ * after the first failed attempt has index 0). A VALID provider
2470
+ * supplied retryAfterMs (finite and nonnegative) REPLACES the
2471
+ * computed delay (Appendix A); anything else (NaN, Infinity, a
2472
+ * negative) is ignored as adapter noise and the policy backoff
2473
+ * applies, so this boundary stays defensive against custom adapters
2474
+ * (v1.28.0 review P2). Jitter is equal jitter: half the backoff is
2475
+ * deterministic, half random, so a jittered delay never collapses to
2476
+ * zero. The result is always a finite nonnegative integer clamped to
2477
+ * the Node timer maximum (2147483647 ms).
2473
2478
  */
2474
2479
  declare function retryDelayMs(policy: RetryPolicy, retryIndex: number, retryAfterMs?: number, random?: () => number): number;
2475
2480
  //#endregion
package/dist/index.js CHANGED
@@ -7033,18 +7033,35 @@ function retryClassOf(error) {
7033
7033
  return "transport";
7034
7034
  }
7035
7035
  /**
7036
- * The delay before retry number `retryIndex` (0-based: the delay after
7037
- * the first failed attempt has index 0). A provider-supplied
7038
- * retryAfterMs REPLACES the computed delay (Appendix A). Jitter is
7039
- * equal-jitter: half the backoff is deterministic, half random, so a
7040
- * jittered delay never collapses to zero.
7036
+ * The largest delay a Node timer represents exactly (2^31 above that
7037
+ * a timer overflows and fires almost immediately); every returned
7038
+ * delay is clamped to it so a huge provider value can never turn
7039
+ * into an instant retry storm.
7040
+ */
7041
+ const MAX_TIMER_DELAY_MS = 2147483647;
7042
+ /** Bounds a delay to a finite nonnegative integer a Node timer can honor. */
7043
+ function timerSafe(ms) {
7044
+ if (!Number.isFinite(ms) || ms <= 0) return 0;
7045
+ return Math.min(Math.round(ms), MAX_TIMER_DELAY_MS);
7046
+ }
7047
+ /**
7048
+ * The delay before retry number `retryIndex` (zero based: the delay
7049
+ * after the first failed attempt has index 0). A VALID provider
7050
+ * supplied retryAfterMs (finite and nonnegative) REPLACES the
7051
+ * computed delay (Appendix A); anything else (NaN, Infinity, a
7052
+ * negative) is ignored as adapter noise and the policy backoff
7053
+ * applies, so this boundary stays defensive against custom adapters
7054
+ * (v1.28.0 review P2). Jitter is equal jitter: half the backoff is
7055
+ * deterministic, half random, so a jittered delay never collapses to
7056
+ * zero. The result is always a finite nonnegative integer clamped to
7057
+ * the Node timer maximum (2147483647 ms).
7041
7058
  */
7042
7059
  function retryDelayMs(policy, retryIndex, retryAfterMs, random = nativeRandom) {
7043
- if (retryAfterMs !== void 0) return retryAfterMs;
7060
+ if (retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) return timerSafe(retryAfterMs);
7044
7061
  const { initialMs, factor, maxMs, jitter } = policy.backoff;
7045
7062
  const base = Math.min(maxMs, initialMs * factor ** retryIndex);
7046
- if (jitter !== true) return base;
7047
- return base / 2 + random() * (base / 2);
7063
+ if (jitter !== true) return timerSafe(base);
7064
+ return timerSafe(base / 2 + random() * (base / 2));
7048
7065
  }
7049
7066
  //#endregion
7050
7067
  //#region src/model/roles.ts
@@ -8026,10 +8043,6 @@ async function streamTurn(adapter, req, options) {
8026
8043
  break;
8027
8044
  }
8028
8045
  case "usage": {
8029
- if (sawFinish) {
8030
- usageViolation ??= "a usage event arrived after the finish event";
8031
- break;
8032
- }
8033
8046
  const cleaned = {};
8034
8047
  for (const field of [
8035
8048
  "inputTokens",
@@ -8062,10 +8075,6 @@ async function streamTurn(adapter, req, options) {
8062
8075
  break;
8063
8076
  }
8064
8077
  case "finish":
8065
- if (sawFinish) {
8066
- usageViolation ??= "a second finish event arrived on one stream";
8067
- break;
8068
- }
8069
8078
  sawFinish = true;
8070
8079
  finish = event.finish;
8071
8080
  usage = event.usage;
@@ -8075,7 +8084,7 @@ async function streamTurn(adapter, req, options) {
8075
8084
  wireError = event.error;
8076
8085
  break;
8077
8086
  }
8078
- if (wireError !== void 0) break;
8087
+ if (sawFinish || wireError !== void 0) break;
8079
8088
  }
8080
8089
  } catch (thrown) {
8081
8090
  if (!combined.aborted) wireError = {
@@ -8100,6 +8109,12 @@ async function streamTurn(adapter, req, options) {
8100
8109
  if (usageViolation !== void 0) outcome.usageViolation = usageViolation;
8101
8110
  return outcome;
8102
8111
  }
8112
+ if (!sawFinish && wireError === void 0) wireError = {
8113
+ code: "agent",
8114
+ message: `adapter '${adapter.id}' stream ended without a terminal finish or error event; the adapter contract requires exactly one per stream, so the partial turn is discarded as a retryable transport fault`,
8115
+ retryable: true,
8116
+ data: { kind: "transport" }
8117
+ };
8103
8118
  const outcome = {
8104
8119
  turn,
8105
8120
  usage,
@@ -8625,14 +8640,70 @@ async function runAgent(options) {
8625
8640
  };
8626
8641
  const retryPolicy = options.retry?.policy ?? DEFAULT_RETRY_POLICY;
8627
8642
  const retryOn = retryPolicy.retryOn ?? DEFAULT_RETRY_POLICY.retryOn ?? [];
8628
- const retrySleep = options.retry?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
8643
+ const injectedSleep = options.retry?.sleep;
8629
8644
  const retryRandom = options.retry?.random ?? Math.random;
8645
+ const abortKind = () => options.budget?.signal?.aborted === true ? "budget" : options.signal?.aborted === true ? "external" : void 0;
8646
+ const abortedOutcome = (aborted) => ({
8647
+ turn: {
8648
+ text: "",
8649
+ toolCalls: []
8650
+ },
8651
+ usage: ZERO_USAGE$1,
8652
+ reported: ZERO_USAGE$1,
8653
+ usageApprox: true,
8654
+ aborted
8655
+ });
8656
+ const backoffWait = async (ms) => {
8657
+ const signals = [];
8658
+ if (options.signal !== void 0) signals.push(options.signal);
8659
+ if (options.budget?.signal !== void 0) signals.push(options.budget.signal);
8660
+ const combined = signals.length === 0 ? void 0 : AbortSignal.any(signals);
8661
+ if (combined?.aborted === true) return;
8662
+ if (injectedSleep === void 0) {
8663
+ await new Promise((resolve) => {
8664
+ let unhook = () => {};
8665
+ const timer = setTimeout(() => {
8666
+ unhook();
8667
+ resolve();
8668
+ }, ms);
8669
+ if (combined !== void 0) {
8670
+ const onAbort = () => {
8671
+ clearTimeout(timer);
8672
+ resolve();
8673
+ };
8674
+ combined.addEventListener("abort", onAbort, { once: true });
8675
+ unhook = () => combined.removeEventListener("abort", onAbort);
8676
+ }
8677
+ });
8678
+ return;
8679
+ }
8680
+ const sleep = Promise.resolve(injectedSleep(ms));
8681
+ if (combined === void 0) {
8682
+ await sleep;
8683
+ return;
8684
+ }
8685
+ let unhook;
8686
+ const wake = new Promise((resolve) => {
8687
+ const onAbort = () => resolve();
8688
+ combined.addEventListener("abort", onAbort, { once: true });
8689
+ unhook = () => combined.removeEventListener("abort", onAbort);
8690
+ });
8691
+ try {
8692
+ await Promise.race([sleep, wake]);
8693
+ } finally {
8694
+ unhook?.();
8695
+ sleep.catch(() => void 0);
8696
+ }
8697
+ };
8630
8698
  const dispatchPhase = async (site) => {
8631
8699
  for (;;) {
8632
8700
  const target = site.chain[site.cursor.index] ?? site.chain[0];
8633
8701
  let tries = 0;
8634
8702
  inner: for (;;) {
8635
- const dispatch = () => streamTurn(target.adapter, site.requestFor(target), site.streamOptionsFor(target));
8703
+ const dispatch = () => {
8704
+ const aborted = abortKind();
8705
+ return aborted === void 0 ? streamTurn(target.adapter, site.requestFor(target), site.streamOptionsFor(target)) : Promise.resolve(abortedOutcome(aborted));
8706
+ };
8636
8707
  const outcome = await (options.providerSlot === void 0 ? dispatch() : options.providerSlot(target.adapter.id, dispatch));
8637
8708
  recordUsage(outcome.usage, outcome.reported, target.adapter.id, target.resolved.ref, site.role, outcome.usageViolation);
8638
8709
  tries += 1;
@@ -8643,6 +8714,11 @@ async function runAgent(options) {
8643
8714
  };
8644
8715
  usageApprox = usageApprox || outcome.usageApprox;
8645
8716
  if (retryOn.includes(retryClass) && tries < retryPolicy.attempts) {
8717
+ const abortedBefore = abortKind();
8718
+ if (abortedBefore !== void 0) return {
8719
+ outcome: abortedOutcome(abortedBefore),
8720
+ target
8721
+ };
8646
8722
  const retryAfter = (outcome.wireError?.data)?.retryAfterMs;
8647
8723
  if (outcome.wireError !== void 0) events?.emit({
8648
8724
  type: "agent:error",
@@ -8651,7 +8727,12 @@ async function runAgent(options) {
8651
8727
  error: outcome.wireError,
8652
8728
  willRetry: true
8653
8729
  });
8654
- await retrySleep(retryDelayMs(retryPolicy, tries - 1, typeof retryAfter === "number" ? retryAfter : void 0, retryRandom));
8730
+ await backoffWait(retryDelayMs(retryPolicy, tries - 1, typeof retryAfter === "number" ? retryAfter : void 0, retryRandom));
8731
+ const abortedAfter = abortKind();
8732
+ if (abortedAfter !== void 0) return {
8733
+ outcome: abortedOutcome(abortedAfter),
8734
+ target
8735
+ };
8655
8736
  continue inner;
8656
8737
  }
8657
8738
  const trigger = failoverTriggerOf(retryClass);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.27.0",
3
+ "version": "1.29.0",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",