@nathapp/nax 0.82.0-canary.4 → 0.82.0-canary.5

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.
Files changed (2) hide show
  1. package/dist/nax.js +599 -364
  2. package/package.json +2 -2
package/dist/nax.js CHANGED
@@ -23676,8 +23676,9 @@ var init_client = __esm(() => {
23676
23676
  });
23677
23677
 
23678
23678
  // src/agents/native/errors.ts
23679
- function toAdapterFailure(kind) {
23680
- return FAILURES[kind] ?? UNKNOWN;
23679
+ function toAdapterFailure(protocolError) {
23680
+ const base = FAILURES[protocolError.kind] ?? UNKNOWN;
23681
+ return protocolError.retryAfter === undefined ? base : { ...base, retryAfterSeconds: protocolError.retryAfter };
23681
23682
  }
23682
23683
  var FAILURES, UNKNOWN;
23683
23684
  var init_errors4 = __esm(() => {
@@ -24270,6 +24271,9 @@ function turnRetryDelayMs(err, retryIndex, config2, random = Math.random, remain
24270
24271
  const delayMs = retryAfter !== undefined ? retryAfter * 1000 : backoffMs(retryIndex, config2.baseDelayMs, random);
24271
24272
  return remainingMs === undefined ? delayMs : Math.min(delayMs, remainingMs);
24272
24273
  }
24274
+ function turnRetryFitsBudget(delayMs, remainingMs) {
24275
+ return remainingMs === undefined || delayMs <= remainingMs;
24276
+ }
24273
24277
  function abortError(signal) {
24274
24278
  return signal.reason ?? new DOMException("Aborted", "AbortError");
24275
24279
  }
@@ -24303,7 +24307,10 @@ async function retryTransportFault(firstError, deps) {
24303
24307
  if (deps.deadline?.expired() === true || deps.signal?.aborted === true)
24304
24308
  throw err;
24305
24309
  const fault = err;
24306
- const delayMs = turnRetryDelayMs(fault, retryIndex, deps.config, random, deps.deadline?.remainingMs());
24310
+ const remainingMs = deps.deadline?.remainingMs();
24311
+ const delayMs = turnRetryDelayMs(fault, retryIndex, deps.config, random);
24312
+ if (!turnRetryFitsBudget(delayMs, remainingMs))
24313
+ throw fault;
24307
24314
  deps.onRetry?.(retryIndex + 1, delayMs, fault);
24308
24315
  try {
24309
24316
  await abortableSleep(delayMs, sleep2, deps.signal);
@@ -24320,7 +24327,7 @@ async function retryTransportFault(firstError, deps) {
24320
24327
  }
24321
24328
  var RETRYABLE_KINDS;
24322
24329
  var init_turn_retry = __esm(() => {
24323
- RETRYABLE_KINDS = new Set(["transport", "overloaded"]);
24330
+ RETRYABLE_KINDS = new Set(["transport", "overloaded", "rate-limit"]);
24324
24331
  });
24325
24332
 
24326
24333
  // src/agents/native/session/turn-loop.ts
@@ -24682,7 +24689,7 @@ class NativeAgentAdapter {
24682
24689
  output: "",
24683
24690
  tokenUsage: { inputTokens: 0, outputTokens: 0 },
24684
24691
  estimatedCostUsd: 0,
24685
- adapterFailure: toAdapterFailure(err.protocolError.kind)
24692
+ adapterFailure: toAdapterFailure(err.protocolError)
24686
24693
  };
24687
24694
  }
24688
24695
  throw err;
@@ -24789,7 +24796,7 @@ class NativeAgentAdapter {
24789
24796
  });
24790
24797
  markNativeTurnOutcome(handle.id, true);
24791
24798
  if (isProtocolStreamError(err)) {
24792
- const adapterFailure = toAdapterFailure(err.protocolError.kind);
24799
+ const adapterFailure = toAdapterFailure(err.protocolError);
24793
24800
  const usage = readNativeTurnFailureUsage(err);
24794
24801
  throw new SessionTurnError(err.protocolError.message, false, adapterFailure.retriable, usage?.tokenUsage, usage?.costUsd, undefined, adapterFailure);
24795
24802
  }
@@ -29606,6 +29613,100 @@ var init_idle_watchdog = __esm(() => {
29606
29613
  };
29607
29614
  });
29608
29615
 
29616
+ // src/agents/retry/failure-policy.ts
29617
+ function failurePolicyFor(outcome) {
29618
+ return POLICIES[outcome];
29619
+ }
29620
+ function resolveCooldownExpiry(failure, now) {
29621
+ const { cooldown } = failurePolicyFor(failure.outcome);
29622
+ if (cooldown === "none")
29623
+ return null;
29624
+ if (cooldown === "run")
29625
+ return "run";
29626
+ const retryAfter = failure.retryAfterSeconds;
29627
+ const fromProvider = retryAfter !== undefined && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter * 1000 : undefined;
29628
+ return now + (fromProvider ?? cooldown.ms);
29629
+ }
29630
+ var TRANSIENT_COOLDOWN_MS = 60000, POLICIES;
29631
+ var init_failure_policy = __esm(() => {
29632
+ POLICIES = Object.freeze({
29633
+ "fail-auth": { sameAgentRetry: "none", swap: "immediate", cooldown: "run", terminalBackoff: false },
29634
+ "fail-quota": { sameAgentRetry: "none", swap: "immediate", cooldown: "run", terminalBackoff: false },
29635
+ "fail-rate-limit": {
29636
+ sameAgentRetry: "none",
29637
+ swap: "immediate",
29638
+ cooldown: { ms: TRANSIENT_COOLDOWN_MS },
29639
+ terminalBackoff: true
29640
+ },
29641
+ "fail-service-down": {
29642
+ sameAgentRetry: "adapter-error",
29643
+ swap: "immediate",
29644
+ cooldown: { ms: TRANSIENT_COOLDOWN_MS },
29645
+ terminalBackoff: true
29646
+ },
29647
+ "fail-stale": {
29648
+ sameAgentRetry: "stale",
29649
+ swap: "immediate",
29650
+ cooldown: { ms: TRANSIENT_COOLDOWN_MS },
29651
+ terminalBackoff: true
29652
+ },
29653
+ "fail-timeout": { sameAgentRetry: "timeout", swap: "after-retry-lane", cooldown: "none", terminalBackoff: false },
29654
+ "fail-adapter-error": {
29655
+ sameAgentRetry: "adapter-error",
29656
+ swap: "quality-gated",
29657
+ cooldown: "none",
29658
+ terminalBackoff: false
29659
+ },
29660
+ "fail-quality": { sameAgentRetry: "none", swap: "quality-gated", cooldown: "none", terminalBackoff: false },
29661
+ "fail-unknown": { sameAgentRetry: "none", swap: "quality-gated", cooldown: "none", terminalBackoff: false },
29662
+ "fail-aborted": { sameAgentRetry: "none", swap: "never", cooldown: "none", terminalBackoff: false }
29663
+ });
29664
+ });
29665
+
29666
+ // src/agents/cooldown-store.ts
29667
+ class CooldownStore {
29668
+ _now;
29669
+ _entries = new Map;
29670
+ constructor(_now) {
29671
+ this._now = _now;
29672
+ }
29673
+ mark(agent, failure) {
29674
+ const expiresAt = resolveCooldownExpiry(failure, this._now());
29675
+ if (expiresAt === null)
29676
+ return;
29677
+ this._entries.set(agent, { failure, expiresAt });
29678
+ }
29679
+ isCooling(agent) {
29680
+ return this._live(agent) !== undefined;
29681
+ }
29682
+ failureFor(agent) {
29683
+ return this._live(agent)?.failure;
29684
+ }
29685
+ sweepTransient() {
29686
+ for (const [agent, entry] of this._entries) {
29687
+ if (entry.expiresAt !== "run")
29688
+ this._entries.delete(agent);
29689
+ }
29690
+ }
29691
+ clear() {
29692
+ this._entries.clear();
29693
+ }
29694
+ _live(agent) {
29695
+ const entry = this._entries.get(agent);
29696
+ if (!entry)
29697
+ return;
29698
+ if (entry.expiresAt === "run")
29699
+ return entry;
29700
+ if (entry.expiresAt > this._now())
29701
+ return entry;
29702
+ this._entries.delete(agent);
29703
+ return;
29704
+ }
29705
+ }
29706
+ var init_cooldown_store = __esm(() => {
29707
+ init_failure_policy();
29708
+ });
29709
+
29609
29710
  // src/agents/hop-budget.ts
29610
29711
  function resolveStartAgent(source, primary, fallbackEnabled, storyId, logger) {
29611
29712
  if (!fallbackEnabled || !source.isUnavailable(primary))
@@ -29806,95 +29907,56 @@ var init_manager_dispatch = __esm(() => {
29806
29907
  init_session_types();
29807
29908
  });
29808
29909
 
29809
- // src/agents/registry.ts
29810
- function adapterFor(name) {
29811
- return name === NATIVE_AGENT ? new NativeAgentAdapter : new AcpAgentAdapter(name);
29812
- }
29813
- function buildAdapterList() {
29814
- return [...Array.from(_registryTestAdapters.values()), ...KNOWN_AGENT_NAMES.map(adapterFor)];
29815
- }
29816
- function getAllAgents() {
29817
- return buildAdapterList();
29818
- }
29819
- async function getInstalledAgents() {
29820
- const allAdapters = buildAdapterList();
29821
- const results = await Promise.all(allAdapters.map(async (agent) => ({ agent, installed: await agent.isInstalled() })));
29822
- return results.filter((r) => r.installed).map((r) => r.agent);
29823
- }
29824
- function createAgentRegistry(config2) {
29825
- const logger = getLogger();
29826
- const adapterCache = new Map;
29827
- const protocol = config2.agent?.protocol ?? "acp";
29828
- logger?.info("agents", `Agent protocol: ${protocol}`, { protocol, hasConfig: !!config2.agent });
29829
- function cachedAdapter(name) {
29830
- let adapter = adapterCache.get(name);
29831
- if (adapter === undefined) {
29832
- adapter = name === NATIVE_AGENT ? new NativeAgentAdapter : new AcpAgentAdapter(name);
29833
- adapterCache.set(name, adapter);
29834
- logger?.debug("agents", `Created ${adapter.constructor.name} for ${name}`, { name });
29835
- }
29836
- return adapter;
29837
- }
29838
- function getAgent(name) {
29839
- if (_registryTestAdapters.has(name))
29840
- return _registryTestAdapters.get(name);
29841
- if (!KNOWN_AGENT_NAMES.includes(name))
29842
- return;
29843
- return cachedAdapter(name);
29844
- }
29845
- async function getInstalledAgents2() {
29846
- const testAdapters = Array.from(_registryTestAdapters.values());
29847
- const adapters = KNOWN_AGENT_NAMES.map(cachedAdapter);
29848
- const allAdapters = [...testAdapters, ...adapters];
29849
- const results = await Promise.all(allAdapters.map(async (agent) => ({ agent, installed: await agent.isInstalled() })));
29850
- return results.filter((r) => r.installed).map((r) => r.agent);
29851
- }
29852
- async function checkAgentHealth() {
29853
- const testAdapters = Array.from(_registryTestAdapters.values());
29854
- const adapters = KNOWN_AGENT_NAMES.map(cachedAdapter);
29855
- const allAdapters = [...testAdapters, ...adapters];
29856
- return Promise.all(allAdapters.map(async (agent) => ({
29857
- name: agent.name,
29858
- displayName: agent.displayName,
29859
- installed: await agent.isInstalled()
29860
- })));
29861
- }
29862
- return { getAgent, getInstalledAgents: getInstalledAgents2, checkAgentHealth, protocol };
29910
+ // src/agents/retry/resolve-exhaustion.ts
29911
+ async function resolveExhaustion(input) {
29912
+ const { failure, retryStrategy, retryCtx, signal, sleep: sleep2, onExhausted } = input;
29913
+ if (signal?.aborted)
29914
+ return "cancelled";
29915
+ if (failure && failurePolicyFor(failure.outcome).terminalBackoff) {
29916
+ const decision = retryStrategy.shouldRetry(failure, input.attempt, retryCtx);
29917
+ if (decision.retry) {
29918
+ await sleep2(decision.delayMs, signal);
29919
+ return signal?.aborted ? "cancelled" : "retry";
29920
+ }
29921
+ }
29922
+ if (input.swapWasPossible)
29923
+ onExhausted(input.hopsSoFar);
29924
+ return "exhausted";
29925
+ }
29926
+ var init_resolve_exhaustion = __esm(() => {
29927
+ init_failure_policy();
29928
+ });
29929
+
29930
+ // src/agents/manager-exhaustion.ts
29931
+ function resolveManagerExhaustion(options) {
29932
+ return resolveExhaustion({
29933
+ failure: options.failure,
29934
+ attempt: options.attempt,
29935
+ hopsSoFar: options.hopsSoFar,
29936
+ swapWasPossible: options.swapWasPossible,
29937
+ retryStrategy: options.retryStrategy,
29938
+ retryCtx: {
29939
+ site: options.site,
29940
+ agentName: options.agent,
29941
+ stage: options.stage,
29942
+ storyId: options.storyId
29943
+ },
29944
+ signal: options.signal,
29945
+ sleep: options.sleep,
29946
+ onExhausted: options.onExhausted
29947
+ });
29863
29948
  }
29864
- var KNOWN_AGENT_NAMES, _registryTestAdapters;
29865
- var init_registry2 = __esm(() => {
29866
- init_logger2();
29867
- init_adapter2();
29868
- init_native();
29869
- KNOWN_AGENT_NAMES = ["claude", "codex", "opencode", "gemini", "aider", "pi", NATIVE_AGENT];
29870
- _registryTestAdapters = new Map;
29871
- });
29872
-
29873
- // src/agents/retry/default-strategy.ts
29874
- var MAX_RETRIES = 3, defaultRetryStrategy;
29875
- var init_default_strategy = __esm(() => {
29876
- defaultRetryStrategy = {
29877
- shouldRetry(failure, attempt, _ctx) {
29878
- if (attempt >= MAX_RETRIES)
29879
- return { retry: false };
29880
- if (failure instanceof Error)
29881
- return { retry: false };
29882
- const af = failure;
29883
- if (af.retriable === false)
29884
- return { retry: false };
29885
- if (af.outcome !== "fail-rate-limit" && af.outcome !== "fail-stale")
29886
- return { retry: false };
29887
- const delayMs = 2 ** (attempt + 1) * 1000;
29888
- return { retry: true, delayMs };
29889
- }
29890
- };
29949
+ var init_manager_exhaustion = __esm(() => {
29950
+ init_resolve_exhaustion();
29891
29951
  });
29892
29952
 
29893
29953
  // src/agents/retry/hop-retry-policy.ts
29894
29954
  function trySameAgentRetry(result, state, deps) {
29895
29955
  const { staleRetryAttempts, timeoutRetryAttempts, adapterErrorRetries, currentRunOptions, tier } = state;
29896
29956
  const { config: config2, requestRunOptions, signal } = deps;
29897
- const isFailStale = result.adapterFailure?.outcome === "fail-stale";
29957
+ const outcome = result.adapterFailure?.outcome;
29958
+ const lane = outcome ? failurePolicyFor(outcome).sameAgentRetry : "none";
29959
+ const isFailStale = lane === "stale";
29898
29960
  const maxStaleRetries = resolveIdleWatchdogSettings(config2.agent?.idleWatchdog).maxRetryAttempts;
29899
29961
  if (isFailStale && result.adapterFailure?.retriable && staleRetryAttempts < maxStaleRetries) {
29900
29962
  const newAttempts = staleRetryAttempts + 1;
@@ -29910,7 +29972,7 @@ function trySameAgentRetry(result, state, deps) {
29910
29972
  }
29911
29973
  };
29912
29974
  }
29913
- const isFailTimeout = result.adapterFailure?.outcome === "fail-timeout";
29975
+ const isFailTimeout = lane === "timeout";
29914
29976
  if (isFailTimeout && result.adapterFailure?.retriable) {
29915
29977
  const timeoutConfig = extractTimeoutRetryConfig(config2);
29916
29978
  if (timeoutRetryShouldRetry(timeoutRetryAttempts, timeoutConfig)) {
@@ -29929,7 +29991,7 @@ function trySameAgentRetry(result, state, deps) {
29929
29991
  };
29930
29992
  }
29931
29993
  }
29932
- const isFailAdapterError = result.adapterFailure?.outcome === "fail-adapter-error";
29994
+ const isFailAdapterError = lane === "adapter-error";
29933
29995
  if (isFailAdapterError && !signal?.aborted) {
29934
29996
  const runConfig = requestRunOptions.config ?? config2;
29935
29997
  const maxAdapterRetries = result.adapterFailure?.retriable ? runConfig.execution?.sessionErrorRetryableMaxRetries ?? 3 : runConfig.execution?.sessionErrorMaxRetries ?? 1;
@@ -29992,23 +30054,25 @@ function describeRetryLogEvent(retryDecision, storyId, agent) {
29992
30054
  var init_hop_retry_policy = __esm(() => {
29993
30055
  init_config();
29994
30056
  init_idle_watchdog();
30057
+ init_failure_policy();
29995
30058
  });
29996
30059
 
29997
30060
  // src/agents/swap-decision.ts
29998
30061
  function decideSwap(failure, hopsSoFar, fallback) {
29999
30062
  if (!failure)
30000
30063
  return { swap: false, reason: "no-failure" };
30001
- if (failure.outcome === "fail-aborted" || failure.outcome === "fail-timeout") {
30064
+ const policy = failurePolicyFor(failure.outcome);
30065
+ if (policy.swap === "never")
30002
30066
  return { swap: false, reason: "outcome-refused" };
30003
- }
30004
30067
  if (!fallback?.enabled)
30005
30068
  return { swap: false, reason: "fallback-disabled" };
30006
30069
  if (hopsSoFar >= (fallback.maxHopsPerStory ?? DEFAULT_MAX_HOPS)) {
30007
30070
  return { swap: false, reason: "hop-cap-reached" };
30008
30071
  }
30009
- if (failure.category === "availability")
30010
- return { swap: true };
30011
- return fallback.onQualityFailure ? { swap: true } : { swap: false, reason: "quality-failure-declined" };
30072
+ if (policy.swap === "quality-gated") {
30073
+ return fallback.onQualityFailure ? { swap: true } : { swap: false, reason: "quality-failure-declined" };
30074
+ }
30075
+ return { swap: true };
30012
30076
  }
30013
30077
  function normaliseFallbackTarget(value) {
30014
30078
  return typeof value === "string" ? { agent: value } : { agent: value.agent, tier: value.tier };
@@ -30036,6 +30100,290 @@ function logSwapDecline(logger, reason, input) {
30036
30100
  });
30037
30101
  }
30038
30102
  var DEFAULT_MAX_HOPS = 2;
30103
+ var init_swap_decision = __esm(() => {
30104
+ init_failure_policy();
30105
+ });
30106
+
30107
+ // src/agents/manager-run-fallback.ts
30108
+ async function runWithFallback(input) {
30109
+ const { request, config: config2, budget, logger } = input;
30110
+ const fallbacks = [];
30111
+ const primaryAgent = input.primaryAgentOverride ?? input.getDefault();
30112
+ const storyId = request.runOptions.storyId;
30113
+ const start = resolveStartAgent(input, primaryAgent, config2.agent?.fallback?.enabled, storyId, logger);
30114
+ let currentAgent = start.agent;
30115
+ let currentHopKind = "tier" in start ? { kind: "primary", tier: start.tier } : { kind: "primary" };
30116
+ let hopsSoFar = budget.spent(storyId);
30117
+ let rateLimitRetry = 0;
30118
+ let staleRetryAttempts = 0;
30119
+ let timeoutRetryAttempts = 0;
30120
+ let adapterErrorRetries = 0;
30121
+ let currentBundle = request.bundle;
30122
+ let currentRunOptions = request.runOptions;
30123
+ let finalPrompt;
30124
+ const startedAt = Date.now();
30125
+ const agentChain = [primaryAgent];
30126
+ let finalStatus = "error";
30127
+ let totalCostUsd = 0;
30128
+ try {
30129
+ while (true) {
30130
+ const hop = await executeHop(input, currentAgent, currentBundle, currentHopKind, currentRunOptions);
30131
+ const { result } = hop;
30132
+ const updatedBundle = hop.bundle ?? currentBundle;
30133
+ finalPrompt = hop.prompt ?? finalPrompt;
30134
+ totalCostUsd += result.estimatedCostUsd ?? 0;
30135
+ if (result.success) {
30136
+ finalStatus = "ok";
30137
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30138
+ }
30139
+ const retry = trySameAgentRetry(result, { staleRetryAttempts, timeoutRetryAttempts, adapterErrorRetries, currentRunOptions, tier: currentHopKind.tier }, { config: config2, requestRunOptions: request.runOptions, signal: request.signal });
30140
+ if (retry) {
30141
+ ({ staleRetryAttempts, timeoutRetryAttempts, adapterErrorRetries, currentRunOptions } = applyRetry(retry, {
30142
+ staleRetryAttempts,
30143
+ timeoutRetryAttempts,
30144
+ adapterErrorRetries,
30145
+ currentRunOptions
30146
+ }));
30147
+ recordRetry(input, fallbacks, retry, currentAgent, storyId);
30148
+ currentHopKind = retry.kind;
30149
+ continue;
30150
+ }
30151
+ if (request.noFallback) {
30152
+ finalStatus = "error";
30153
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30154
+ }
30155
+ const swap = decideSwap(result.adapterFailure, hopsSoFar, config2.agent?.fallback);
30156
+ if (!swap.swap) {
30157
+ logSwapDecline(logger, swap.reason, {
30158
+ storyId,
30159
+ agent: currentAgent,
30160
+ hopsSoFar,
30161
+ failure: result.adapterFailure
30162
+ });
30163
+ if (result.adapterFailure?.outcome === "fail-stale") {
30164
+ logger?.warn("agent-manager", "fail-stale: no swap candidate, returning terminal failure", { storyId });
30165
+ finalStatus = "error";
30166
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30167
+ }
30168
+ const outcome = await input.resolveExhaustion({
30169
+ failure: result.adapterFailure,
30170
+ hopsSoFar,
30171
+ attempt: rateLimitRetry,
30172
+ swapWasPossible: swap.reason === "hop-cap-reached",
30173
+ agent: currentAgent,
30174
+ site: "run",
30175
+ storyId,
30176
+ stage: request.runOptions.pipelineStage ?? "run",
30177
+ signal: request.signal
30178
+ });
30179
+ if (outcome === "retry") {
30180
+ rateLimitRetry += 1;
30181
+ continue;
30182
+ }
30183
+ finalStatus = outcome === "cancelled" ? "cancelled" : hopsSoFar > 0 ? "exhausted" : "error";
30184
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30185
+ }
30186
+ const failure = result.adapterFailure ?? unknownFailure();
30187
+ input.markUnavailable(currentAgent, failure);
30188
+ const next = input.nextCandidate(primaryAgent, hopsSoFar, currentAgent);
30189
+ if (!next) {
30190
+ const outcome = await input.resolveExhaustion({
30191
+ failure,
30192
+ hopsSoFar,
30193
+ attempt: rateLimitRetry,
30194
+ swapWasPossible: true,
30195
+ agent: currentAgent,
30196
+ site: "run",
30197
+ storyId,
30198
+ stage: request.runOptions.pipelineStage ?? "run",
30199
+ signal: request.signal
30200
+ });
30201
+ if (outcome === "retry") {
30202
+ rateLimitRetry += 1;
30203
+ continue;
30204
+ }
30205
+ finalStatus = outcome === "cancelled" ? "cancelled" : "exhausted";
30206
+ return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30207
+ }
30208
+ hopsSoFar = budget.spend(storyId, hopsSoFar);
30209
+ rateLimitRetry = 0;
30210
+ currentBundle = updatedBundle;
30211
+ currentHopKind = { kind: "swap", failure, ...next.tier ? { tier: next.tier } : {} };
30212
+ const fallback = buildFallbackRecord({
30213
+ storyId,
30214
+ priorAgent: currentAgent,
30215
+ newAgent: next.agent,
30216
+ hop: hopsSoFar,
30217
+ failure,
30218
+ costUsd: result.estimatedCostUsd ?? 0
30219
+ });
30220
+ fallbacks.push(fallback);
30221
+ input.emitSwapAttempt(fallback);
30222
+ logger?.info("agent-manager", "Agent swap triggered", {
30223
+ storyId,
30224
+ fromAgent: currentAgent,
30225
+ toAgent: next.agent,
30226
+ hop: hopsSoFar
30227
+ });
30228
+ agentChain.push(next.agent);
30229
+ currentAgent = next.agent;
30230
+ }
30231
+ } finally {
30232
+ input.dispatchEvents.emitOperationCompleted({
30233
+ kind: "operation-completed",
30234
+ operation: "run-with-fallback",
30235
+ agentChain,
30236
+ hopCount: hopsSoFar,
30237
+ fallbackTriggered: fallbacks.length > 0,
30238
+ totalElapsedMs: Date.now() - startedAt,
30239
+ totalCostUsd,
30240
+ finalStatus,
30241
+ storyId,
30242
+ stage: request.runOptions.pipelineStage ?? "run",
30243
+ timestamp: Date.now(),
30244
+ ...request.runOptions.callId !== undefined ? { callId: request.runOptions.callId } : {},
30245
+ ...request.runOptions.scopeId !== undefined ? { scopeId: request.runOptions.scopeId } : {}
30246
+ });
30247
+ }
30248
+ }
30249
+ async function executeHop(input, agent, bundle, kind, options) {
30250
+ if (input.request.executeHop)
30251
+ return input.request.executeHop(agent, bundle, kind, options);
30252
+ if (!input.runHop)
30253
+ return { result: unboundResult(agent), bundle };
30254
+ const raw = await input.runHop(agent, options);
30255
+ const hop = "result" in raw && raw.result != null ? raw : { result: raw };
30256
+ return { ...hop, bundle };
30257
+ }
30258
+ function unboundResult(agent) {
30259
+ return {
30260
+ success: false,
30261
+ exitCode: 1,
30262
+ output: `AgentManager run hop is not wired for agent "${agent}"`,
30263
+ rateLimited: false,
30264
+ durationMs: 0,
30265
+ estimatedCostUsd: 0
30266
+ };
30267
+ }
30268
+ function unknownFailure() {
30269
+ return { category: "quality", outcome: "fail-unknown", retriable: false, message: "" };
30270
+ }
30271
+ function applyRetry(retry, state) {
30272
+ return {
30273
+ staleRetryAttempts: retry.outcome === "stale-retry" ? retry.staleRetryAttempts : state.staleRetryAttempts,
30274
+ timeoutRetryAttempts: retry.outcome === "timeout-retry" ? retry.timeoutRetryAttempts : state.timeoutRetryAttempts,
30275
+ adapterErrorRetries: retry.outcome === "adapter-error" ? retry.adapterErrorRetries : state.adapterErrorRetries,
30276
+ currentRunOptions: retry.outcome === "timeout-retry" ? retry.currentRunOptions : state.currentRunOptions,
30277
+ tier: state.tier
30278
+ };
30279
+ }
30280
+ function recordRetry(input, fallbacks, retry, agent, storyId) {
30281
+ const fallback = buildFallbackRecord({
30282
+ storyId,
30283
+ priorAgent: agent,
30284
+ newAgent: agent,
30285
+ hop: retry.kind.attempt,
30286
+ failure: retry.fallbackRecord,
30287
+ costUsd: retry.fallbackRecord.costUsd
30288
+ });
30289
+ const event = describeRetryLogEvent(retry, storyId, agent);
30290
+ if (event.recordFallback) {
30291
+ fallbacks.push(fallback);
30292
+ input.emitSwapAttempt(fallback);
30293
+ }
30294
+ input.logger?.[event.level]("agent-manager", event.message, event.fields);
30295
+ }
30296
+ var init_manager_run_fallback = __esm(() => {
30297
+ init_manager_dispatch();
30298
+ init_hop_retry_policy();
30299
+ init_swap_decision();
30300
+ });
30301
+
30302
+ // src/agents/registry.ts
30303
+ function adapterFor(name) {
30304
+ return name === NATIVE_AGENT ? new NativeAgentAdapter : new AcpAgentAdapter(name);
30305
+ }
30306
+ function buildAdapterList() {
30307
+ return [...Array.from(_registryTestAdapters.values()), ...KNOWN_AGENT_NAMES.map(adapterFor)];
30308
+ }
30309
+ function getAllAgents() {
30310
+ return buildAdapterList();
30311
+ }
30312
+ async function getInstalledAgents() {
30313
+ const allAdapters = buildAdapterList();
30314
+ const results = await Promise.all(allAdapters.map(async (agent) => ({ agent, installed: await agent.isInstalled() })));
30315
+ return results.filter((r) => r.installed).map((r) => r.agent);
30316
+ }
30317
+ function createAgentRegistry(config2) {
30318
+ const logger = getLogger();
30319
+ const adapterCache = new Map;
30320
+ const protocol = config2.agent?.protocol ?? "acp";
30321
+ logger?.info("agents", `Agent protocol: ${protocol}`, { protocol, hasConfig: !!config2.agent });
30322
+ function cachedAdapter(name) {
30323
+ let adapter = adapterCache.get(name);
30324
+ if (adapter === undefined) {
30325
+ adapter = name === NATIVE_AGENT ? new NativeAgentAdapter : new AcpAgentAdapter(name);
30326
+ adapterCache.set(name, adapter);
30327
+ logger?.debug("agents", `Created ${adapter.constructor.name} for ${name}`, { name });
30328
+ }
30329
+ return adapter;
30330
+ }
30331
+ function getAgent(name) {
30332
+ if (_registryTestAdapters.has(name))
30333
+ return _registryTestAdapters.get(name);
30334
+ if (!KNOWN_AGENT_NAMES.includes(name))
30335
+ return;
30336
+ return cachedAdapter(name);
30337
+ }
30338
+ async function getInstalledAgents2() {
30339
+ const testAdapters = Array.from(_registryTestAdapters.values());
30340
+ const adapters = KNOWN_AGENT_NAMES.map(cachedAdapter);
30341
+ const allAdapters = [...testAdapters, ...adapters];
30342
+ const results = await Promise.all(allAdapters.map(async (agent) => ({ agent, installed: await agent.isInstalled() })));
30343
+ return results.filter((r) => r.installed).map((r) => r.agent);
30344
+ }
30345
+ async function checkAgentHealth() {
30346
+ const testAdapters = Array.from(_registryTestAdapters.values());
30347
+ const adapters = KNOWN_AGENT_NAMES.map(cachedAdapter);
30348
+ const allAdapters = [...testAdapters, ...adapters];
30349
+ return Promise.all(allAdapters.map(async (agent) => ({
30350
+ name: agent.name,
30351
+ displayName: agent.displayName,
30352
+ installed: await agent.isInstalled()
30353
+ })));
30354
+ }
30355
+ return { getAgent, getInstalledAgents: getInstalledAgents2, checkAgentHealth, protocol };
30356
+ }
30357
+ var KNOWN_AGENT_NAMES, _registryTestAdapters;
30358
+ var init_registry2 = __esm(() => {
30359
+ init_logger2();
30360
+ init_adapter2();
30361
+ init_native();
30362
+ KNOWN_AGENT_NAMES = ["claude", "codex", "opencode", "gemini", "aider", "pi", NATIVE_AGENT];
30363
+ _registryTestAdapters = new Map;
30364
+ });
30365
+
30366
+ // src/agents/retry/default-strategy.ts
30367
+ var MAX_RETRIES = 3, defaultRetryStrategy;
30368
+ var init_default_strategy = __esm(() => {
30369
+ init_failure_policy();
30370
+ defaultRetryStrategy = {
30371
+ shouldRetry(failure, attempt, _ctx) {
30372
+ if (attempt >= MAX_RETRIES)
30373
+ return { retry: false };
30374
+ if (failure instanceof Error)
30375
+ return { retry: false };
30376
+ const af = failure;
30377
+ if (af.retriable === false)
30378
+ return { retry: false };
30379
+ if (!failurePolicyFor(af.outcome).terminalBackoff)
30380
+ return { retry: false };
30381
+ const retryAfterSeconds = af.retryAfterSeconds;
30382
+ const delayMs = retryAfterSeconds !== undefined && Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0 ? retryAfterSeconds * 1000 : 2 ** (attempt + 1) * 1000;
30383
+ return { retry: true, delayMs };
30384
+ }
30385
+ };
30386
+ });
30039
30387
 
30040
30388
  // src/agents/manager.ts
30041
30389
  import { EventEmitter } from "events";
@@ -30043,7 +30391,7 @@ import { EventEmitter } from "events";
30043
30391
  class AgentManager {
30044
30392
  _config;
30045
30393
  _registry;
30046
- _unavailable = new Map;
30394
+ _cooldowns = new CooldownStore(() => _agentManagerDeps.now());
30047
30395
  _prunedFallback = new Set;
30048
30396
  _budget = new StoryHopBudget;
30049
30397
  _emitter = (() => {
@@ -30099,22 +30447,19 @@ class AgentManager {
30099
30447
  return "claude";
30100
30448
  }
30101
30449
  isUnavailable(agent) {
30102
- return this._unavailable.has(agent);
30450
+ return this._cooldowns.isCooling(agent);
30103
30451
  }
30104
30452
  markUnavailable(agent, reason) {
30105
- this._unavailable.set(agent, reason);
30453
+ this._cooldowns.mark(agent, reason);
30106
30454
  this._emitter.emit("onAgentUnavailable", { agent, failure: reason });
30107
30455
  }
30108
30456
  reset() {
30109
- this._unavailable.clear();
30457
+ this._cooldowns.clear();
30110
30458
  this._prunedFallback.clear();
30111
30459
  this._budget.clear();
30112
30460
  }
30113
30461
  resetTransientUnavailable() {
30114
- for (const [agent, failure] of this._unavailable) {
30115
- if (failure.outcome !== "fail-auth" && failure.outcome !== "fail-quota")
30116
- this._unavailable.delete(agent);
30117
- }
30462
+ this._cooldowns.sweepTransient();
30118
30463
  }
30119
30464
  async validateCredentials() {
30120
30465
  const primary = this.getDefault();
@@ -30145,211 +30490,26 @@ class AgentManager {
30145
30490
  shouldSwap(failure, hopsSoFar) {
30146
30491
  return decideSwap(failure, hopsSoFar, this._config.agent?.fallback).swap;
30147
30492
  }
30148
- nextCandidate(current, _hopsSoFar) {
30149
- return availableCandidates(this._config.agent?.fallback?.map, current, this._isExcluded)[0] ?? null;
30493
+ nextCandidate(current, _hopsSoFar, exclude) {
30494
+ const excluded = (candidate) => candidate === exclude || this._isExcluded(candidate);
30495
+ return availableCandidates(this._config.agent?.fallback?.map, current, excluded)[0] ?? null;
30150
30496
  }
30151
30497
  async runWithFallback(request, primaryAgentOverride) {
30152
- const logger = this._loggerOverride ?? getSafeLogger();
30153
- const fallbacks = [];
30154
- const primaryAgent = primaryAgentOverride ?? this.getDefault();
30155
- const storyId = request.runOptions.storyId;
30156
- const start = resolveStartAgent(this, primaryAgent, this._config.agent?.fallback?.enabled, storyId, logger);
30157
- let currentAgent = start.agent;
30158
- let currentHopKind = "tier" in start ? { kind: "primary", tier: start.tier } : { kind: "primary" };
30159
- let hopsSoFar = this._budget.spent(storyId);
30160
- let rateLimitRetry = 0;
30161
- let staleRetryAttempts = 0;
30162
- let timeoutRetryAttempts = 0;
30163
- let adapterErrorRetries = 0;
30164
- let currentBundle = request.bundle;
30165
- let currentRunOptions = request.runOptions;
30166
- let finalPrompt;
30167
- const _opStartMs = Date.now();
30168
- const _agentChain = [primaryAgent];
30169
- let _finalStatus = "error";
30170
- let _totalCostUsd = 0;
30171
- try {
30172
- while (true) {
30173
- let result;
30174
- let updatedBundle = currentBundle;
30175
- if (request.executeHop) {
30176
- const hopOut = await request.executeHop(currentAgent, currentBundle, currentHopKind, currentRunOptions);
30177
- result = hopOut.result;
30178
- updatedBundle = hopOut.bundle ?? currentBundle;
30179
- finalPrompt = hopOut.prompt ?? finalPrompt;
30180
- } else {
30181
- if (!this._runHop) {
30182
- const unboundResult = {
30183
- success: false,
30184
- exitCode: 1,
30185
- output: `AgentManager run hop is not wired for agent "${currentAgent}"`,
30186
- rateLimited: false,
30187
- durationMs: 0,
30188
- estimatedCostUsd: 0
30189
- };
30190
- _finalStatus = "error";
30191
- return { result: unboundResult, fallbacks, finalBundle: currentBundle, finalPrompt };
30192
- }
30193
- const rawHopOut = await this._runHop(currentAgent, currentRunOptions);
30194
- const hopOut = "result" in rawHopOut && rawHopOut.result != null ? rawHopOut : { result: rawHopOut, prompt: undefined };
30195
- result = hopOut.result;
30196
- finalPrompt = hopOut.prompt ?? finalPrompt;
30197
- }
30198
- _totalCostUsd += result.estimatedCostUsd ?? 0;
30199
- if (result.success) {
30200
- _finalStatus = "ok";
30201
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30202
- }
30203
- const isFailStale = result.adapterFailure?.outcome === "fail-stale";
30204
- const retryState = {
30205
- staleRetryAttempts,
30206
- timeoutRetryAttempts,
30207
- adapterErrorRetries,
30208
- currentRunOptions,
30209
- tier: "tier" in currentHopKind ? currentHopKind.tier : undefined
30210
- };
30211
- const retryDecision = trySameAgentRetry(result, retryState, {
30212
- config: this._config,
30213
- requestRunOptions: request.runOptions,
30214
- signal: request.signal
30215
- });
30216
- if (retryDecision) {
30217
- staleRetryAttempts = retryDecision.outcome === "stale-retry" ? retryDecision.staleRetryAttempts : staleRetryAttempts;
30218
- timeoutRetryAttempts = retryDecision.outcome === "timeout-retry" ? retryDecision.timeoutRetryAttempts : timeoutRetryAttempts;
30219
- adapterErrorRetries = retryDecision.outcome === "adapter-error" ? retryDecision.adapterErrorRetries : adapterErrorRetries;
30220
- currentRunOptions = retryDecision.outcome === "timeout-retry" ? retryDecision.currentRunOptions : currentRunOptions;
30221
- const retryHop = buildFallbackRecord({
30222
- storyId: request.runOptions.storyId,
30223
- priorAgent: currentAgent,
30224
- newAgent: currentAgent,
30225
- hop: retryDecision.kind.attempt,
30226
- failure: retryDecision.fallbackRecord,
30227
- costUsd: retryDecision.fallbackRecord.costUsd
30228
- });
30229
- const logEvent = describeRetryLogEvent(retryDecision, request.runOptions.storyId, currentAgent);
30230
- if (logEvent.recordFallback) {
30231
- fallbacks.push(retryHop);
30232
- this._emitter.emit("onSwapAttempt", retryHop);
30233
- }
30234
- if (logEvent.level === "warn") {
30235
- logger?.warn("agent-manager", logEvent.message, logEvent.fields);
30236
- } else {
30237
- logger?.info("agent-manager", logEvent.message, logEvent.fields);
30238
- }
30239
- currentHopKind = retryDecision.kind;
30240
- continue;
30241
- }
30242
- if (request.noFallback) {
30243
- _finalStatus = "error";
30244
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30245
- }
30246
- const fb = this._config.agent?.fallback;
30247
- const swapDecision = decideSwap(result.adapterFailure, hopsSoFar, fb);
30248
- if (!swapDecision.swap) {
30249
- logSwapDecline(logger, swapDecision.reason, {
30250
- storyId: request.runOptions.storyId,
30251
- agent: currentAgent,
30252
- hopsSoFar,
30253
- failure: result.adapterFailure
30254
- });
30255
- if (isFailStale) {
30256
- logger?.warn("agent-manager", "fail-stale: no swap candidate, returning terminal failure", {
30257
- storyId: request.runOptions.storyId
30258
- });
30259
- _finalStatus = "error";
30260
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30261
- }
30262
- if (result.adapterFailure) {
30263
- const retryCtx = {
30264
- site: "run",
30265
- agentName: currentAgent,
30266
- stage: request.runOptions.pipelineStage ?? "run",
30267
- storyId: request.runOptions.storyId
30268
- };
30269
- const decision = this._retryStrategy.shouldRetry(result.adapterFailure, rateLimitRetry, retryCtx);
30270
- if (decision.retry) {
30271
- if (request.signal?.aborted) {
30272
- logger?.info("agent-manager", "Rate-limited backoff aborted \u2014 shutdown in progress", {
30273
- storyId: request.runOptions.storyId
30274
- });
30275
- _finalStatus = "cancelled";
30276
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30277
- }
30278
- rateLimitRetry += 1;
30279
- logger?.info("agent-manager", "Rate-limited with no swap candidate \u2014 backing off", {
30280
- storyId: request.runOptions.storyId,
30281
- attempt: rateLimitRetry,
30282
- backoffMs: decision.delayMs
30283
- });
30284
- await _agentManagerDeps.sleep(decision.delayMs, request.signal);
30285
- if (request.signal?.aborted) {
30286
- _finalStatus = "cancelled";
30287
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30288
- }
30289
- continue;
30290
- }
30291
- }
30292
- if (hopsSoFar > 0) {
30293
- this._emitter.emit("onSwapExhausted", { storyId: request.runOptions.storyId, hops: hopsSoFar });
30294
- _finalStatus = "exhausted";
30295
- } else {
30296
- _finalStatus = "error";
30297
- }
30298
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30299
- }
30300
- const adapterFailure = result.adapterFailure ?? {
30301
- category: "quality",
30302
- outcome: "fail-unknown",
30303
- retriable: false,
30304
- message: ""
30305
- };
30306
- this.markUnavailable(currentAgent, adapterFailure);
30307
- const next = this.nextCandidate(primaryAgent, hopsSoFar);
30308
- if (!next) {
30309
- this._emitter.emit("onSwapExhausted", { storyId: request.runOptions.storyId, hops: hopsSoFar });
30310
- _finalStatus = "exhausted";
30311
- return { result, fallbacks, finalBundle: updatedBundle, finalPrompt, finalAgent: currentAgent };
30312
- }
30313
- hopsSoFar = this._budget.spend(storyId, hopsSoFar);
30314
- rateLimitRetry = 0;
30315
- currentBundle = updatedBundle;
30316
- currentHopKind = { kind: "swap", failure: adapterFailure, ..."tier" in next ? { tier: next.tier } : {} };
30317
- const hop = buildFallbackRecord({
30318
- storyId: request.runOptions.storyId,
30319
- priorAgent: currentAgent,
30320
- newAgent: next.agent,
30321
- hop: hopsSoFar,
30322
- failure: adapterFailure,
30323
- costUsd: result.estimatedCostUsd ?? 0
30324
- });
30325
- fallbacks.push(hop);
30326
- this._emitter.emit("onSwapAttempt", hop);
30327
- logger?.info("agent-manager", "Agent swap triggered", {
30328
- storyId: request.runOptions.storyId,
30329
- fromAgent: currentAgent,
30330
- toAgent: next.agent,
30331
- hop: hopsSoFar
30332
- });
30333
- _agentChain.push(next.agent);
30334
- currentAgent = next.agent;
30335
- }
30336
- } finally {
30337
- this._dispatchEvents.emitOperationCompleted({
30338
- kind: "operation-completed",
30339
- operation: "run-with-fallback",
30340
- agentChain: _agentChain,
30341
- hopCount: hopsSoFar,
30342
- fallbackTriggered: fallbacks.length > 0,
30343
- totalElapsedMs: Date.now() - _opStartMs,
30344
- totalCostUsd: _totalCostUsd,
30345
- finalStatus: _finalStatus,
30346
- storyId: request.runOptions.storyId,
30347
- stage: request.runOptions.pipelineStage ?? "run",
30348
- timestamp: Date.now(),
30349
- ...request.runOptions.callId !== undefined ? { callId: request.runOptions.callId } : {},
30350
- ...request.runOptions.scopeId !== undefined ? { scopeId: request.runOptions.scopeId } : {}
30351
- });
30352
- }
30498
+ return runWithFallback({
30499
+ request,
30500
+ primaryAgentOverride,
30501
+ config: this._config,
30502
+ budget: this._budget,
30503
+ runHop: this._runHop,
30504
+ dispatchEvents: this._dispatchEvents,
30505
+ logger: this._loggerOverride ?? getSafeLogger(),
30506
+ getDefault: () => this.getDefault(),
30507
+ isUnavailable: (agent) => this.isUnavailable(agent),
30508
+ markUnavailable: (agent, failure) => this.markUnavailable(agent, failure),
30509
+ nextCandidate: (current, hops, exclude) => this.nextCandidate(current, hops, exclude),
30510
+ resolveExhaustion: (options) => this._resolveExhaustion(options),
30511
+ emitSwapAttempt: (fallback) => this._emitter.emit("onSwapAttempt", fallback)
30512
+ });
30353
30513
  }
30354
30514
  async completeWithFallback(prompt, options, primaryAgentOverride) {
30355
30515
  const logger = this._loggerOverride ?? getSafeLogger();
@@ -30359,6 +30519,7 @@ class AgentManager {
30359
30519
  let currentTier;
30360
30520
  let hopsSoFar = this._budget.spent(options.storyId);
30361
30521
  let staleRetryAttempts = 0;
30522
+ let rateLimitRetry = 0;
30362
30523
  const maxStaleRetries = resolveIdleWatchdogSettings(this._config.agent?.idleWatchdog).maxRetryAttempts;
30363
30524
  const _opStartMs = Date.now();
30364
30525
  const _agentChain = [primaryAgent];
@@ -30437,12 +30598,50 @@ class AgentManager {
30437
30598
  hopsSoFar,
30438
30599
  failure: result.adapterFailure
30439
30600
  });
30601
+ const outcome = await this._resolveExhaustion({
30602
+ failure: result.adapterFailure,
30603
+ hopsSoFar,
30604
+ attempt: rateLimitRetry,
30605
+ swapWasPossible: dec.reason === "hop-cap-reached",
30606
+ agent: currentAgent,
30607
+ site: "complete",
30608
+ storyId: options.storyId,
30609
+ stage: options.pipelineStage ?? "run",
30610
+ signal: options.signal
30611
+ });
30612
+ if (outcome === "cancelled") {
30613
+ _finalStatus = "cancelled";
30614
+ return { result, fallbacks, ...currentTier !== undefined ? { finalTier: currentTier } : {} };
30615
+ }
30616
+ if (outcome === "retry") {
30617
+ rateLimitRetry += 1;
30618
+ continue;
30619
+ }
30440
30620
  _finalStatus = hopsSoFar > 0 ? "exhausted" : "error";
30441
30621
  return { result, fallbacks, ...currentTier !== undefined ? { finalTier: currentTier } : {} };
30442
30622
  }
30443
30623
  this.markUnavailable(currentAgent, result.adapterFailure);
30444
- const next = this.nextCandidate(primaryAgent, hopsSoFar);
30624
+ const next = this.nextCandidate(primaryAgent, hopsSoFar, currentAgent);
30445
30625
  if (!next) {
30626
+ const outcome = await this._resolveExhaustion({
30627
+ failure: result.adapterFailure,
30628
+ hopsSoFar,
30629
+ attempt: rateLimitRetry,
30630
+ swapWasPossible: true,
30631
+ agent: currentAgent,
30632
+ site: "complete",
30633
+ storyId: options.storyId,
30634
+ stage: options.pipelineStage ?? "run",
30635
+ signal: options.signal
30636
+ });
30637
+ if (outcome === "cancelled") {
30638
+ _finalStatus = "cancelled";
30639
+ return { result, fallbacks, ...currentTier !== undefined ? { finalTier: currentTier } : {} };
30640
+ }
30641
+ if (outcome === "retry") {
30642
+ rateLimitRetry += 1;
30643
+ continue;
30644
+ }
30446
30645
  _finalStatus = "exhausted";
30447
30646
  return { result, fallbacks, ...currentTier !== undefined ? { finalTier: currentTier } : {} };
30448
30647
  }
@@ -30610,6 +30809,14 @@ class AgentManager {
30610
30809
  async completeAs(agentName, prompt, options) {
30611
30810
  return (await this.completeAsWithFallback(agentName, prompt, options)).result;
30612
30811
  }
30812
+ _resolveExhaustion(options) {
30813
+ return resolveManagerExhaustion({
30814
+ ...options,
30815
+ retryStrategy: this._retryStrategy,
30816
+ sleep: _agentManagerDeps.sleep,
30817
+ onExhausted: (hops) => this._emitter.emit("onSwapExhausted", { storyId: options.storyId, hops })
30818
+ });
30819
+ }
30613
30820
  close() {
30614
30821
  this._emitter.removeAllListeners();
30615
30822
  }
@@ -30630,12 +30837,16 @@ var init_manager = __esm(() => {
30630
30837
  init_idle_watchdog();
30631
30838
  init_bun_deps();
30632
30839
  init_complete_exception_classifier();
30840
+ init_cooldown_store();
30633
30841
  init_manager_dispatch();
30842
+ init_manager_exhaustion();
30843
+ init_manager_run_fallback();
30634
30844
  init_registry2();
30635
30845
  init_default_strategy();
30636
- init_hop_retry_policy();
30846
+ init_swap_decision();
30637
30847
  _agentManagerDeps = {
30638
- sleep: (ms, signal) => cancellableDelay(ms, signal)
30848
+ sleep: (ms, signal) => cancellableDelay(ms, signal),
30849
+ now: () => Date.now()
30639
30850
  };
30640
30851
  });
30641
30852
 
@@ -48637,7 +48848,7 @@ function turnResultToAgentResult(r) {
48637
48848
  success: !r.adapterFailure,
48638
48849
  exitCode: r.adapterFailure ? 1 : 0,
48639
48850
  output: r.output,
48640
- rateLimited: false,
48851
+ rateLimited: r.adapterFailure?.outcome === "fail-rate-limit",
48641
48852
  durationMs: 0,
48642
48853
  estimatedCostUsd: r.estimatedCostUsd ?? 0,
48643
48854
  exactCostUsd: r.exactCostUsd,
@@ -49068,6 +49279,15 @@ function recordAgentFallbacks(ctx, fallbacks) {
49068
49279
  }
49069
49280
  store.set(ctx.storyId, [...fallbacks]);
49070
49281
  }
49282
+ function recordAdapterFailure(ctx, failure) {
49283
+ if (!ctx.storyId)
49284
+ return;
49285
+ if (failure) {
49286
+ ctx.runtime.lastAdapterFailure.set(ctx.storyId, failure);
49287
+ return;
49288
+ }
49289
+ ctx.runtime.lastAdapterFailure.delete(ctx.storyId);
49290
+ }
49071
49291
  var MAX_COMPLETE_RETRY_ATTEMPTS = 20, correlationSequence = 0;
49072
49292
  var init_call_resolvers = __esm(() => {
49073
49293
  init_retry();
@@ -49417,7 +49637,7 @@ async function callOp(ctx, op, input) {
49417
49637
  }
49418
49638
  return sendWithParseRetry(initialPrompt, bodyCtx);
49419
49639
  };
49420
- const executeHop = _callOpDeps.buildHopCallback({
49640
+ const executeHop2 = _callOpDeps.buildHopCallback({
49421
49641
  ...hopCtx,
49422
49642
  hopBody: effectiveHopBody,
49423
49643
  hopBodyInput: input
@@ -49425,12 +49645,13 @@ async function callOp(ctx, op, input) {
49425
49645
  const rawOutcome = await ctx.runtime.agentManager.runWithFallback({
49426
49646
  runOptions,
49427
49647
  signal: abortSignal,
49428
- executeHop,
49648
+ executeHop: executeHop2,
49429
49649
  noFallback: runOp.noFallback,
49430
49650
  bundle: ctx.contextBundle
49431
49651
  }, dispatchAgent);
49432
49652
  const outcome = normalizeRunOutcome(rawOutcome);
49433
49653
  recordAgentFallbacks(ctx, outcome.fallbacks);
49654
+ recordAdapterFailure(ctx, outcome.result.adapterFailure);
49434
49655
  if (abortSignal?.aborted) {
49435
49656
  throw new NaxError(`callOp[${op.name}]: aborted`, "CALL_OP_ABORTED", { stage: op.stage, storyId: ctx.storyId });
49436
49657
  }
@@ -60907,7 +61128,7 @@ var package_default;
60907
61128
  var init_package = __esm(() => {
60908
61129
  package_default = {
60909
61130
  name: "@nathapp/nax",
60910
- version: "0.82.0-canary.4",
61131
+ version: "0.82.0-canary.5",
60911
61132
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
60912
61133
  type: "module",
60913
61134
  bin: {
@@ -60975,7 +61196,7 @@ var init_package = __esm(() => {
60975
61196
  "spec:lint": "bun run scripts/spec-lint.ts"
60976
61197
  },
60977
61198
  dependencies: {
60978
- "@nathapp/nax-ai": "0.1.9",
61199
+ "@nathapp/nax-ai": "0.1.10",
60979
61200
  "@types/react": "^19.2.14",
60980
61201
  chalk: "^5.6.2",
60981
61202
  commander: "^13.1.0",
@@ -61028,8 +61249,8 @@ var init_version = __esm(() => {
61028
61249
  NAX_VERSION = package_default.version;
61029
61250
  NAX_COMMIT = (() => {
61030
61251
  try {
61031
- if (/^[0-9a-f]{6,10}$/.test("a125d17a"))
61032
- return "a125d17a";
61252
+ if (/^[0-9a-f]{6,10}$/.test("363f5424"))
61253
+ return "363f5424";
61033
61254
  } catch {}
61034
61255
  try {
61035
61256
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -62971,6 +63192,7 @@ function createRuntime(config2, workdir, opts) {
62971
63192
  const rectificationOscillations = new Map;
62972
63193
  const reviewFindingRecurrences = new Map;
62973
63194
  const agentFallbacks = new Map;
63195
+ const lastAdapterFailure = new Map;
62974
63196
  const runtimeCrashRetries = new Map;
62975
63197
  const storyFixHistory = createStoryFixHistory();
62976
63198
  const mutationSummaries = new Map;
@@ -63002,6 +63224,7 @@ function createRuntime(config2, workdir, opts) {
63002
63224
  rectificationOscillations,
63003
63225
  reviewFindingRecurrences,
63004
63226
  agentFallbacks,
63227
+ lastAdapterFailure,
63005
63228
  runtimeCrashRetries,
63006
63229
  storyFixHistory,
63007
63230
  mutationSummaries,
@@ -85345,6 +85568,30 @@ var init_execution_helpers = __esm(() => {
85345
85568
  init_errors();
85346
85569
  });
85347
85570
 
85571
+ // src/execution/post-run-notifications.ts
85572
+ async function sendPostRunNotification(ctx, notification) {
85573
+ if (!ctx.interaction)
85574
+ return;
85575
+ try {
85576
+ await ctx.interaction.send({
85577
+ id: `${notification.idPrefix}-${ctx.story.id}-${Date.now()}`,
85578
+ type: "notify",
85579
+ featureName: ctx.featureDir ? ctx.featureDir.split("/").pop() ?? "unknown" : "unknown",
85580
+ storyId: ctx.story.id,
85581
+ stage: "execution",
85582
+ summary: notification.summary,
85583
+ detail: notification.detail,
85584
+ fallback: "continue",
85585
+ createdAt: Date.now()
85586
+ });
85587
+ } catch (err) {
85588
+ getLogger().warn("execution", notification.failureMessage, { storyId: ctx.story.id, error: errorMessage(err) });
85589
+ }
85590
+ }
85591
+ var init_post_run_notifications = __esm(() => {
85592
+ init_logger2();
85593
+ });
85594
+
85348
85595
  // src/execution/post-run-review-summary.ts
85349
85596
  function applyReviewsFailedOpen(ctx, phaseOutputs) {
85350
85597
  const count = [phaseOutputs[semanticReviewOp.name], phaseOutputs[adversarialReviewOp.name]].filter((output) => output?.failOpen === true).length;
@@ -85504,10 +85751,12 @@ async function applyPostRunInspection(ctx, planResult, opts) {
85504
85751
  const { capturedTokenUsage, capturedResponse, capturedCostUsd } = opts;
85505
85752
  const isTdd = opts.tddMode !== null;
85506
85753
  const implementerOutput = planResult.phaseOutputs[implementerOp.name];
85754
+ const lastFailure = ctx.runtime.lastAdapterFailure.get(ctx.story.id);
85755
+ const providerUnavailable = lastFailure?.outcome === "fail-rate-limit" || lastFailure?.outcome === "fail-quota" || lastFailure?.outcome === "fail-service-down";
85507
85756
  const agentResult = {
85508
85757
  success: implementerOutput?.success ?? false,
85509
85758
  estimatedCostUsd: capturedCostUsd || planResult.phaseCosts[implementerOp.name] || 0,
85510
- rateLimited: false,
85759
+ rateLimited: lastFailure?.outcome === "fail-rate-limit",
85511
85760
  output: capturedResponse,
85512
85761
  exitCode: implementerOutput?.success ? 0 : 1,
85513
85762
  durationMs: implementerOutput?.durationMs ?? planResult.durationMs,
@@ -85619,7 +85868,7 @@ async function applyPostRunInspection(ctx, planResult, opts) {
85619
85868
  if (Object.keys(tddIsolations).length > 0) {
85620
85869
  ctx.tddIsolations = tddIsolations;
85621
85870
  }
85622
- const needsHumanReview = failureCategory === "session-failure";
85871
+ const needsHumanReview = failureCategory === "session-failure" && !providerUnavailable;
85623
85872
  const combinedOutput = (agentResult.output ?? "") + (agentResult.stderr ?? "");
85624
85873
  if (isTdd) {
85625
85874
  const { cleanupVerdict: cleanupVerdict2 } = await Promise.resolve().then(() => (init_verdict(), exports_verdict));
@@ -85629,14 +85878,30 @@ async function applyPostRunInspection(ctx, planResult, opts) {
85629
85878
  }
85630
85879
  const rectOut = planResult.phaseOutputs.rectification;
85631
85880
  ctx.rectifyAttempt = rectOut?.iterationCount ?? 0;
85632
- return { agentResult, selfVerificationFailed, pauseReason, failureCategory, needsHumanReview, combinedOutput };
85881
+ return {
85882
+ agentResult,
85883
+ selfVerificationFailed,
85884
+ pauseReason,
85885
+ failureCategory,
85886
+ needsHumanReview,
85887
+ providerUnavailable,
85888
+ combinedOutput
85889
+ };
85633
85890
  }
85634
85891
  async function decideStageAction(ctx, planResult, inspection, opts) {
85635
85892
  const logger = getLogger();
85636
85893
  const isTdd = opts.tddMode !== null;
85637
85894
  const isLiteMode = opts.tddMode?.isLite ?? false;
85638
85895
  const shouldRollback = shouldRollbackTddFailure(opts.tddMode, inspection.failureCategory);
85639
- const { agentResult, selfVerificationFailed, pauseReason, failureCategory, needsHumanReview, combinedOutput } = inspection;
85896
+ const {
85897
+ agentResult,
85898
+ selfVerificationFailed,
85899
+ pauseReason,
85900
+ failureCategory,
85901
+ needsHumanReview,
85902
+ providerUnavailable,
85903
+ combinedOutput
85904
+ } = inspection;
85640
85905
  if (isTdd && !planResult.success) {
85641
85906
  ctx.tddFailureCategory = failureCategory;
85642
85907
  }
@@ -85716,27 +85981,13 @@ Reason: ${breaker.reason}`,
85716
85981
  }
85717
85982
  if (pauseReason) {
85718
85983
  logger.warn("execution", "Plan run produced pauseReason", { storyId: ctx.story.id, pauseReason });
85719
- if (ctx.interaction) {
85720
- try {
85721
- await ctx.interaction.send({
85722
- id: `pause-${ctx.story.id}-${Date.now()}`,
85723
- type: "notify",
85724
- featureName: ctx.featureDir ? ctx.featureDir.split("/").pop() ?? "unknown" : "unknown",
85725
- storyId: ctx.story.id,
85726
- stage: "execution",
85727
- summary: `Execution paused: ${ctx.story.id}`,
85728
- detail: `Story: ${ctx.story.title}
85984
+ await sendPostRunNotification(ctx, {
85985
+ idPrefix: "pause",
85986
+ summary: `Execution paused: ${ctx.story.id}`,
85987
+ detail: `Story: ${ctx.story.title}
85729
85988
  Reason: ${pauseReason}`,
85730
- fallback: "continue",
85731
- createdAt: Date.now()
85732
- });
85733
- } catch (notifyErr) {
85734
- logger.warn("execution", "Failed to send pause notification", {
85735
- storyId: ctx.story.id,
85736
- error: String(notifyErr)
85737
- });
85738
- }
85739
- }
85989
+ failureMessage: "Failed to send pause notification"
85990
+ });
85740
85991
  return { action: "pause", reason: pauseReason };
85741
85992
  }
85742
85993
  if (isTdd && !planResult.success) {
@@ -85754,30 +86005,16 @@ Reason: ${pauseReason}`,
85754
86005
  });
85755
86006
  }
85756
86007
  }
85757
- if (needsHumanReview) {
86008
+ if (needsHumanReview && !providerUnavailable) {
85758
86009
  logger.warn("execution", "Human review needed", { storyId: ctx.story.id, failureCategory });
85759
- if (ctx.interaction) {
85760
- try {
85761
- await ctx.interaction.send({
85762
- id: `human-review-${ctx.story.id}-${Date.now()}`,
85763
- type: "notify",
85764
- featureName: ctx.featureDir ? ctx.featureDir.split("/").pop() ?? "unknown" : "unknown",
85765
- storyId: ctx.story.id,
85766
- stage: "execution",
85767
- summary: `Human review needed: ${ctx.story.id}`,
85768
- detail: `Story: ${ctx.story.title}
86010
+ await sendPostRunNotification(ctx, {
86011
+ idPrefix: "human-review",
86012
+ summary: `Human review needed: ${ctx.story.id}`,
86013
+ detail: `Story: ${ctx.story.title}
85769
86014
  Reason: Human review needed
85770
86015
  Category: ${failureCategory ?? "unknown"}`,
85771
- fallback: "continue",
85772
- createdAt: Date.now()
85773
- });
85774
- } catch (notifyErr) {
85775
- logger.warn("execution", "Failed to send human review notification", {
85776
- storyId: ctx.story.id,
85777
- error: String(notifyErr)
85778
- });
85779
- }
85780
- }
86016
+ failureMessage: "Failed to send human review notification"
86017
+ });
85781
86018
  return { action: "pause", reason: `Human review needed: ${failureCategory ?? "unknown"}` };
85782
86019
  }
85783
86020
  return routeTddFailure(failureCategory, isLiteMode, ctx);
@@ -85815,9 +86052,6 @@ Category: ${failureCategory ?? "unknown"}`,
85815
86052
  stderrTail: stderrTail || undefined,
85816
86053
  outputTail: outputTail || undefined
85817
86054
  });
85818
- if (agentResult.rateLimited) {
85819
- logger.warn("execution", "Rate limited \u2014 will retry", { storyId: ctx.story.id });
85820
- }
85821
86055
  await cleanupSessionOnFailure(ctx);
85822
86056
  const failedPhaseNames = Object.keys(failedPhases);
85823
86057
  const reasonParts = [];
@@ -85852,6 +86086,7 @@ var init_post_run = __esm(() => {
85852
86086
  init_rollback();
85853
86087
  init_git();
85854
86088
  init_oscillation_breaker();
86089
+ init_post_run_notifications();
85855
86090
  init_post_run_review_summary();
85856
86091
  init_recurrence_pause();
85857
86092
  init_tdd_failure_category();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.82.0-canary.4",
3
+ "version": "0.82.0-canary.5",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,7 +68,7 @@
68
68
  "spec:lint": "bun run scripts/spec-lint.ts"
69
69
  },
70
70
  "dependencies": {
71
- "@nathapp/nax-ai": "0.1.9",
71
+ "@nathapp/nax-ai": "0.1.10",
72
72
  "@types/react": "^19.2.14",
73
73
  "chalk": "^5.6.2",
74
74
  "commander": "^13.1.0",