@bastani/atomic 0.9.4-alpha.6 → 0.9.4-alpha.8

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 (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/builtin/cursor/package.json +3 -3
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +2 -2
  5. package/dist/builtin/subagents/CHANGELOG.md +14 -0
  6. package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
  7. package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
  8. package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
  9. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
  10. package/dist/builtin/subagents/agents/debugger.md +1 -1
  11. package/dist/builtin/subagents/agents/worker.md +1 -1
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +11 -1
  14. package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +11 -1
  15. package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +2 -0
  16. package/dist/builtin/subagents/src/runs/background/subagent-runner-step.ts +27 -4
  17. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +25 -0
  18. package/dist/builtin/subagents/src/runs/foreground/chain-execution-dynamic-step.ts +1 -0
  19. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +1 -0
  20. package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-step.ts +1 -0
  21. package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +1 -0
  22. package/dist/builtin/subagents/src/runs/foreground/chain-execution-types.ts +2 -0
  23. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +3 -1
  24. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +22 -0
  25. package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +15 -5
  26. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +5 -1
  27. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +2 -0
  28. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +4 -1
  29. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +4 -1
  30. package/dist/builtin/subagents/src/runs/shared/attempt-watchdog.ts +126 -0
  31. package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +53 -0
  32. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +52 -53
  33. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +2 -0
  34. package/dist/builtin/subagents/src/shared/model-info.ts +16 -0
  35. package/dist/builtin/subagents/src/shared/types-config.ts +2 -0
  36. package/dist/builtin/web-access/package.json +1 -1
  37. package/dist/builtin/workflows/CHANGELOG.md +16 -0
  38. package/dist/builtin/workflows/builtin/deep-research-codebase-utils.ts +8 -1
  39. package/dist/builtin/workflows/builtin/goal-runner.ts +14 -1
  40. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +5 -2
  41. package/dist/builtin/workflows/builtin/ralph-models.ts +44 -9
  42. package/dist/builtin/workflows/package.json +1 -1
  43. package/dist/builtin/workflows/src/runs/shared/model-fallback-failures.ts +34 -4
  44. package/dist/core/agent-session-extension-bindings.d.ts +2 -0
  45. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  46. package/dist/core/agent-session-extension-bindings.js +10 -2
  47. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  48. package/dist/core/agent-session-methods.d.ts +2 -1
  49. package/dist/core/agent-session-methods.d.ts.map +1 -1
  50. package/dist/core/agent-session-methods.js.map +1 -1
  51. package/dist/core/copilot-model-catalog.d.ts +25 -74
  52. package/dist/core/copilot-model-catalog.d.ts.map +1 -1
  53. package/dist/core/copilot-model-catalog.js +118 -68
  54. package/dist/core/copilot-model-catalog.js.map +1 -1
  55. package/dist/core/copilot-model-synthesis.d.ts +10 -0
  56. package/dist/core/copilot-model-synthesis.d.ts.map +1 -0
  57. package/dist/core/copilot-model-synthesis.js +89 -0
  58. package/dist/core/copilot-model-synthesis.js.map +1 -0
  59. package/dist/core/model-registry-builtins.d.ts.map +1 -1
  60. package/dist/core/model-registry-builtins.js +21 -3
  61. package/dist/core/model-registry-builtins.js.map +1 -1
  62. package/dist/core/tools/read-selectors.d.ts.map +1 -1
  63. package/dist/core/tools/read-selectors.js +19 -1
  64. package/dist/core/tools/read-selectors.js.map +1 -1
  65. package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
  66. package/dist/modes/interactive/interactive-model-routing.js +1 -0
  67. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  68. package/docs/models.md +3 -3
  69. package/docs/providers.md +1 -1
  70. package/docs/rpc.md +2 -2
  71. package/docs/sdk.md +1 -1
  72. package/docs/settings.md +2 -1
  73. package/docs/subagents.md +6 -0
  74. package/docs/workflows.md +3 -1
  75. package/npm-shrinkwrap.json +27 -27
  76. package/package.json +6 -6
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { APP_NAME } from "@bastani/atomic";
3
3
  import { currentModelFullId, resolveModelCandidate } from "../shared/model-fallback.ts";
4
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
4
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
5
  import { normalizeSkillInput } from "../../agents/skills.ts";
6
6
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
7
7
  import {
@@ -76,6 +76,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
76
76
  currentModel: currentModelFullId(ctx.model),
77
77
  };
78
78
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
79
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
79
80
  const depthPolicy = resolveSubagentDepthPolicy(ctx, deps.config.maxSubagentDepth);
80
81
  const currentMaxSubagentDepth = depthPolicy.maxSubagentDepth;
81
82
  const workflowStageSubagentGuard = depthPolicy.workflowStageSubagentGuard;
@@ -110,6 +111,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
110
111
  agents,
111
112
  ctx: asyncCtx,
112
113
  availableModels,
114
+ knownModelProviders,
113
115
  cwd: effectiveCwd,
114
116
  maxOutput: params.maxOutput,
115
117
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -139,6 +141,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
139
141
  agents,
140
142
  ctx: asyncCtx,
141
143
  availableModels,
144
+ knownModelProviders,
142
145
  cwd: effectiveCwd,
143
146
  maxOutput: params.maxOutput,
144
147
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -181,6 +184,7 @@ export function runAsyncPath(data: ExecutionContextData, deps: ResolvedExecutorD
181
184
  agentConfig: a,
182
185
  ctx: asyncCtx,
183
186
  availableModels,
187
+ knownModelProviders,
184
188
  cwd: effectiveCwd,
185
189
  maxOutput: params.maxOutput,
186
190
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -38,6 +38,7 @@ interface ForegroundParallelRunInput {
38
38
  maxSubagentDepths: number[];
39
39
  workflowStageSubagentGuard?: boolean;
40
40
  availableModels: ModelInfo[];
41
+ knownModelProviders: string[];
41
42
  modelOverrides: (string | undefined)[];
42
43
  behaviors: ResolvedStepBehavior[];
43
44
  firstProgressIndex: number;
@@ -110,6 +111,7 @@ export async function runForegroundParallelTasks(input: ForegroundParallelRunInp
110
111
  nestedRoute: input.foregroundControl?.nestedRoute,
111
112
  modelOverride: input.modelOverrides[index],
112
113
  availableModels: input.availableModels,
114
+ knownModelProviders: input.knownModelProviders,
113
115
  preferredModelProvider: input.ctx.model?.provider,
114
116
  currentModel: currentModelFullId(input.ctx.model),
115
117
  skills: effectiveSkills === false ? [] : effectiveSkills,
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { APP_NAME } from "@bastani/atomic";
3
3
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
4
4
  import { currentModelFullId, resolveModelCandidate } from "../shared/model-fallback.ts";
5
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
6
6
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
7
7
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
8
8
  import { recordRun } from "../shared/run-history.ts";
@@ -108,6 +108,7 @@ export async function runParallelPath(data: ExecutionContextData, deps: Resolved
108
108
 
109
109
  const currentProvider = ctx.model?.provider;
110
110
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
111
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
111
112
  let taskTexts = tasks.map((t) => t.task);
112
113
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
113
114
  normalizeSkillInput(t.skill),
@@ -205,6 +206,7 @@ export async function runParallelPath(data: ExecutionContextData, deps: Resolved
205
206
  agents,
206
207
  ctx: asyncCtx,
207
208
  availableModels,
209
+ knownModelProviders,
208
210
  cwd: effectiveCwd,
209
211
  maxOutput: params.maxOutput,
210
212
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -281,6 +283,7 @@ export async function runParallelPath(data: ExecutionContextData, deps: Resolved
281
283
  paramsCwd: effectiveCwd,
282
284
  workflowStageSubagentGuard,
283
285
  availableModels,
286
+ knownModelProviders,
284
287
  modelOverrides,
285
288
  behaviors,
286
289
  firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { APP_NAME } from "@bastani/atomic";
3
3
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
4
4
  import { currentModelFullId, resolveModelCandidate } from "../shared/model-fallback.ts";
5
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
5
+ import { collectKnownModelProviders, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
6
6
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
7
7
  import { recordRun } from "../shared/run-history.ts";
8
8
  import { getSingleResultOutput, compactForegroundDetails } from "../../shared/utils.ts";
@@ -76,6 +76,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
76
76
 
77
77
  const currentProvider = ctx.model?.provider;
78
78
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
79
+ const knownModelProviders = collectKnownModelProviders(ctx.modelRegistry);
79
80
  let task = params.task ?? "";
80
81
  let modelOverride: string | undefined = resolveModelCandidate(
81
82
  (params.model as string | undefined) ?? agentConfig.model,
@@ -145,6 +146,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
145
146
  agentConfig,
146
147
  ctx: asyncCtx,
147
148
  availableModels,
149
+ knownModelProviders,
148
150
  cwd: effectiveCwd,
149
151
  maxOutput: params.maxOutput,
150
152
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
@@ -246,6 +248,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
246
248
  index: 0,
247
249
  modelOverride,
248
250
  availableModels,
251
+ knownModelProviders,
249
252
  preferredModelProvider: currentProvider,
250
253
  currentModel: currentModelFullId(ctx.model),
251
254
  skills: effectiveSkills,
@@ -0,0 +1,126 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import { trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
3
+
4
+ const DEFAULT_IDLE_MS = 5 * 60_000;
5
+ const DEFAULT_WALL_MS = 60 * 60_000;
6
+ const DEFAULT_KILL_GRACE_MS = 3_000;
7
+
8
+ export interface AttemptTimeoutConfig {
9
+ idleMs: number;
10
+ wallMs: number;
11
+ killGraceMs: number;
12
+ }
13
+
14
+ export interface AttemptWatchdog {
15
+ activity(): void;
16
+ clear(): void;
17
+ }
18
+
19
+ /** Parse a millisecond override from the environment. Non-numeric values are
20
+ * silently ignored (the default applies). Zero or negative values are clamped
21
+ * to `0`, which callers treat as "timeout disabled". */
22
+ function envMs(name: string): number | undefined {
23
+ const raw = process.env[name];
24
+ if (!raw) return undefined;
25
+ const value = Number(raw);
26
+ if (!Number.isFinite(value)) return undefined;
27
+ return value > 0 ? Math.floor(value) : 0;
28
+ }
29
+
30
+ function positiveEnvMs(name: string): number | undefined {
31
+ const value = envMs(name);
32
+ return value !== undefined && value > 0 ? value : undefined;
33
+ }
34
+
35
+ export function resolveAttemptTimeoutConfig(): AttemptTimeoutConfig {
36
+ return {
37
+ // `0` (or a negative value) disables the corresponding timeout entirely.
38
+ idleMs: envMs("ATOMIC_SUBAGENT_ATTEMPT_IDLE_TIMEOUT_MS") ?? DEFAULT_IDLE_MS,
39
+ wallMs: envMs("ATOMIC_SUBAGENT_ATTEMPT_TIMEOUT_MS") ?? DEFAULT_WALL_MS,
40
+ // The SIGTERM→SIGKILL grace period intentionally cannot be disabled: once a
41
+ // watchdog trips, escalation must always be bounded. `0`, negative, or
42
+ // non-numeric values fall back to the default.
43
+ killGraceMs: positiveEnvMs("ATOMIC_SUBAGENT_ATTEMPT_KILL_GRACE_MS") ?? DEFAULT_KILL_GRACE_MS,
44
+ };
45
+ }
46
+
47
+ export function idleTimeoutMessage(idleMs: number): string {
48
+ return `Subagent model attempt timed out after ${idleMs}ms without child activity.`;
49
+ }
50
+
51
+ export function wallTimeoutMessage(wallMs: number): string {
52
+ return `Subagent model attempt timed out after ${wallMs}ms.`;
53
+ }
54
+
55
+ export function createAttemptWatchdog(params: {
56
+ child: ChildProcess;
57
+ config?: Partial<AttemptTimeoutConfig>;
58
+ onTimeout: (message: string) => void;
59
+ isSettled: () => boolean;
60
+ /** Reports whether a tool call is currently executing in the child. A slow tool
61
+ * (long build, large test suite) can legitimately stay silent past the idle
62
+ * window, so an in-flight tool execution counts as activity and defers the idle
63
+ * trip. The wall-clock cap still bounds the whole attempt. */
64
+ isToolActive?: () => boolean;
65
+ }): AttemptWatchdog {
66
+ const config = { ...resolveAttemptTimeoutConfig(), ...(params.config ?? {}) };
67
+ let idleTimer: NodeJS.Timeout | undefined;
68
+ let wallTimer: NodeJS.Timeout | undefined;
69
+ let killTimer: NodeJS.Timeout | undefined;
70
+ let tripped = false;
71
+
72
+ const clearIdle = () => {
73
+ if (!idleTimer) return;
74
+ clearTimeout(idleTimer);
75
+ idleTimer = undefined;
76
+ };
77
+ const clearAll = () => {
78
+ clearIdle();
79
+ if (wallTimer) {
80
+ clearTimeout(wallTimer);
81
+ wallTimer = undefined;
82
+ }
83
+ if (killTimer) {
84
+ clearTimeout(killTimer);
85
+ killTimer = undefined;
86
+ }
87
+ };
88
+ const trip = (message: string) => {
89
+ if (tripped || params.isSettled()) return;
90
+ tripped = true;
91
+ clearIdle();
92
+ params.onTimeout(message);
93
+ trySignalChild(params.child, "SIGTERM");
94
+ killTimer = setTimeout(() => {
95
+ if (!params.isSettled()) trySignalChild(params.child, "SIGKILL");
96
+ }, config.killGraceMs);
97
+ killTimer.unref?.();
98
+ };
99
+ const scheduleIdle = () => {
100
+ clearIdle();
101
+ if (config.idleMs <= 0) return; // idle watchdog disabled
102
+ idleTimer = setTimeout(() => {
103
+ if (params.isToolActive?.()) {
104
+ // Do not kill a healthy attempt that is busy inside a slow tool call;
105
+ // re-arm the idle window and let the wall-clock cap bound the attempt.
106
+ scheduleIdle();
107
+ return;
108
+ }
109
+ trip(idleTimeoutMessage(config.idleMs));
110
+ }, config.idleMs);
111
+ idleTimer.unref?.();
112
+ };
113
+
114
+ scheduleIdle();
115
+ if (config.wallMs > 0) {
116
+ wallTimer = setTimeout(() => trip(wallTimeoutMessage(config.wallMs)), config.wallMs);
117
+ wallTimer.unref?.();
118
+ }
119
+
120
+ return {
121
+ activity() {
122
+ if (!tripped && !params.isSettled()) scheduleIdle();
123
+ },
124
+ clear: clearAll,
125
+ };
126
+ }
@@ -0,0 +1,53 @@
1
+ import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
2
+ import type { ModelAttempt } from "../../shared/types.ts";
3
+ import type { AvailableModelInfo } from "./model-fallback.ts";
4
+
5
+ export interface FilteredModelCandidates {
6
+ candidates: string[];
7
+ skippedAttempts: ModelAttempt[];
8
+ }
9
+
10
+ function providerFromModel(model: string | undefined): string | undefined {
11
+ if (!model) return undefined;
12
+ const { baseModel } = splitKnownThinkingSuffix(model);
13
+ const slash = baseModel.indexOf("/");
14
+ return slash > 0 ? baseModel.slice(0, slash) : undefined;
15
+ }
16
+
17
+ export function skippedModelAttempt(model: string, reason: string): ModelAttempt {
18
+ return {
19
+ model,
20
+ success: false,
21
+ exitCode: null,
22
+ error: reason,
23
+ };
24
+ }
25
+
26
+ export function filterSpawnableModelCandidates(params: {
27
+ candidates: string[];
28
+ availableModels?: AvailableModelInfo[];
29
+ knownModelProviders?: string[];
30
+ currentModel?: string;
31
+ }): FilteredModelCandidates {
32
+ const providersWithAvailableAuth = new Set((params.availableModels ?? []).map((model) => model.provider));
33
+ const knownProviders = new Set(params.knownModelProviders ?? []);
34
+ const currentModel = params.currentModel;
35
+ const filtered: string[] = [];
36
+ const skippedAttempts: ModelAttempt[] = [];
37
+ for (const candidate of params.candidates) {
38
+ if (currentModel && candidate === currentModel) {
39
+ filtered.push(candidate);
40
+ continue;
41
+ }
42
+ const provider = providerFromModel(candidate);
43
+ const shouldSkip = provider !== undefined
44
+ && knownProviders.has(provider)
45
+ && !providersWithAvailableAuth.has(provider);
46
+ if (!shouldSkip) {
47
+ filtered.push(candidate);
48
+ continue;
49
+ }
50
+ skippedAttempts.push(skippedModelAttempt(candidate, `Skipped ${candidate}: provider '${provider}' has no configured API key/auth in the current session.`));
51
+ }
52
+ return { candidates: filtered, skippedAttempts };
53
+ }
@@ -117,6 +117,7 @@ export type ModelFallbackFailureKind =
117
117
  | "provider_unavailable"
118
118
  | "network_timeout"
119
119
  | "model_unavailable"
120
+ | "request_incompatible"
120
121
  | "cancelled"
121
122
  | "task_failure"
122
123
  | "unknown";
@@ -144,6 +145,7 @@ const FALLBACKABLE_FAILURE_KINDS: ReadonlySet<ModelFallbackFailureKind> = new Se
144
145
  "provider_unavailable",
145
146
  "network_timeout",
146
147
  "model_unavailable",
148
+ "request_incompatible",
147
149
  ]);
148
150
 
149
151
  function asRecord(value: unknown): Record<string, unknown> | undefined {
@@ -222,6 +224,9 @@ function normalizeCode(value: string | number | undefined): string | undefined {
222
224
 
223
225
  function kindFromStatus(status: number | undefined): ModelFallbackFailureKind | undefined {
224
226
  switch (status) {
227
+ case 400:
228
+ case 413: case 422:
229
+ return "request_incompatible";
225
230
  case 401:
226
231
  case 403:
227
232
  return "auth_on_candidate_provider";
@@ -237,6 +242,15 @@ function kindFromStatus(status: number | undefined): ModelFallbackFailureKind |
237
242
  }
238
243
  }
239
244
 
245
+ const REQUEST_INCOMPATIBLE_CODES: ReadonlySet<string> = new Set([
246
+ "invalid_request", "invalid_request_error", "bad_request", "context_length_exceeded",
247
+ "request_too_large", "too_large", "request_entity_too_large", "max_tokens",
248
+ "max_context_length", "context_window_exceeded",
249
+ ]);
250
+ function requestIncompatibleKindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
251
+ const normalizedCode = normalizeCode(code);
252
+ return normalizedCode !== undefined && REQUEST_INCOMPATIBLE_CODES.has(normalizedCode) ? "request_incompatible" : undefined;
253
+ }
240
254
  function refusalKindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
241
255
  const normalizedCode = normalizeCode(code);
242
256
  if (normalizedCode === undefined) return undefined;
@@ -258,6 +272,14 @@ function refusalKindFromCode(code: string | number | undefined): ModelFallbackFa
258
272
  }
259
273
  }
260
274
 
275
+ const CODE_KINDS_BY_KIND: ReadonlyArray<readonly [ModelFallbackFailureKind, ReadonlySet<string>]> = [
276
+ ["auth_on_candidate_provider", new Set(["auth", "auth_required", "authentication_required", "unauthorized", "forbidden", "invalid_api_key", "missing_api_key", "invalid_key"])],
277
+ ["network_timeout", new Set(["etimedout", "econnreset", "econnrefused", "enotfound", "eai_again", "fetch_failed", "network_error", "timeout", "timeout_error", "und_err_connect_timeout"])],
278
+ ["rate_limit", new Set(["rate_limit", "rate_limit_exceeded", "too_many_requests", "quota_exceeded"])],
279
+ ["cancelled", new Set(["aborterror", "aborted", "cancelled", "canceled"])],
280
+ ["model_unavailable", new Set(["model_not_found", "model_unavailable", "model_disabled", "unknown_model"])],
281
+ ["provider_unavailable", new Set(["provider_error", "api_error", "service_unavailable", "temporarily_unavailable", "overloaded"])],
282
+ ];
261
283
  function kindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
262
284
  const normalizedCode = normalizeCode(code);
263
285
  if (normalizedCode === undefined) return undefined;
@@ -265,54 +287,19 @@ function kindFromCode(code: string | number | undefined): ModelFallbackFailureKi
265
287
  if (refusalKind !== undefined) return refusalKind;
266
288
  const httpStatusKind = kindFromStatus(integerFrom(code));
267
289
  if (httpStatusKind !== undefined) return httpStatusKind;
268
-
269
- switch (normalizedCode) {
270
- case "auth":
271
- case "auth_required":
272
- case "authentication_required":
273
- case "unauthorized":
274
- case "forbidden":
275
- case "invalid_api_key":
276
- case "missing_api_key":
277
- case "invalid_key":
278
- return "auth_on_candidate_provider";
279
- case "etimedout":
280
- case "econnreset":
281
- case "econnrefused":
282
- case "enotfound":
283
- case "eai_again":
284
- case "fetch_failed":
285
- case "network_error":
286
- case "timeout":
287
- case "timeout_error":
288
- case "und_err_connect_timeout":
289
- return "network_timeout";
290
- case "rate_limit":
291
- case "rate_limit_exceeded":
292
- case "too_many_requests":
293
- case "quota_exceeded":
294
- return "rate_limit";
295
- case "aborterror":
296
- case "aborted":
297
- case "cancelled":
298
- case "canceled":
299
- return "cancelled";
300
- case "model_not_found":
301
- case "model_unavailable":
302
- case "model_disabled":
303
- case "unknown_model":
304
- return "model_unavailable";
305
- case "provider_error":
306
- case "api_error":
307
- case "service_unavailable":
308
- case "temporarily_unavailable":
309
- case "overloaded":
310
- return "provider_unavailable";
311
- default:
312
- return undefined;
313
- }
314
- }
315
-
290
+ const requestIncompatibleKind = requestIncompatibleKindFromCode(code);
291
+ if (requestIncompatibleKind !== undefined) return requestIncompatibleKind;
292
+ return CODE_KINDS_BY_KIND.find(([_, codes]) => codes.has(normalizedCode))?.[0];
293
+ }
294
+
295
+ const REQUEST_INCOMPATIBLE_FAILURE_PATTERNS: readonly RegExp[] = [
296
+ /\bcontext[_\s-]?(?:length|window)(?:[_\s-]?exceeded)?\b/i,
297
+ /\bmax[_\s-]?(?:context|tokens?)\b/i,
298
+ /\b(?:request(?:[_\s-]?entity)?[_\s-]?too|too)[_\s-]?large\b/i,
299
+ /\b(?:unsupported|unknown|invalid)\s+(?:tool|parameter|function)\b/i,
300
+ /\b(?:tool|parameter|function)\s+(?:not\s+(?:supported|found|allowed)|unknown|invalid)\b/i,
301
+ /\b(?:invalid[_\s-]?request(?:[_\s-]?error)?|bad[_\s-]?request)\b/i,
302
+ ];
316
303
  const PROVIDER_REFUSAL_FAILURE_PATTERNS: readonly RegExp[] = [
317
304
  /\bfinish[_\s-]?reason\b[^\n]*\bcontent[_\s-]?filter\b/i,
318
305
  /\bcontent[_\s-]?filter(?:ed|ing)?\b/i,
@@ -335,6 +322,7 @@ function refusalKindFromMessage(message: string): ModelFallbackFailureKind | und
335
322
  function fallbackKindFromMessage(message: string, name: string | undefined): ModelFallbackFailureKind | undefined {
336
323
  const refusalKind = refusalKindFromMessage(message);
337
324
  if (refusalKind !== undefined) return refusalKind;
325
+ if (REQUEST_INCOMPATIBLE_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "request_incompatible";
338
326
  const nameKind = kindFromCode(name);
339
327
  if (nameKind !== undefined) return nameKind;
340
328
  if (!RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return undefined;
@@ -372,15 +360,21 @@ function makeSignal(
372
360
  };
373
361
  }
374
362
 
375
- function fallbackSignalFromMessage(
363
+ function fallbackSignalForMessage(
364
+ message: string | undefined,
376
365
  value: unknown,
377
366
  source: ModelFallbackFailureSource | undefined,
378
367
  ): ModelFallbackFailureSignal | undefined {
379
- const message = modelFailureMessage(value);
380
- if (!message.trim()) return undefined;
368
+ if (message === undefined || message.trim().length === 0) return undefined;
381
369
  const kind = fallbackKindFromMessage(message, errorName(value));
382
370
  return kind === undefined ? undefined : makeSignal(kind, value, source);
383
371
  }
372
+ function fallbackSignalFromMessage(value: unknown, source: ModelFallbackFailureSource | undefined): ModelFallbackFailureSignal | undefined {
373
+ return fallbackSignalForMessage(modelFailureMessage(value), value, source);
374
+ }
375
+ function fallbackSignalFromDirectMessage(value: unknown, source: ModelFallbackFailureSource | undefined): ModelFallbackFailureSignal | undefined {
376
+ return fallbackSignalForMessage(directMessageFrom(value), value, source);
377
+ }
384
378
 
385
379
  function classifyAssistantRefusalSignal(
386
380
  value: unknown,
@@ -433,6 +427,12 @@ function structuredSignal(
433
427
  if (isRefusalSignal(causeSignal)) return causeSignal;
434
428
  firstNestedFallbackSignal ??= causeSignal;
435
429
  }
430
+ // Direct-message classification runs after nested traversal so a generic wrapper
431
+ // ("invalid request"/"400 bad request") cannot mask a non-retryable nested signal.
432
+ // Kept in the same position as the workflows classifier so the two copies stay
433
+ // behaviorally parallel (see test/unit/model-fallback-classifier-conformance.test.ts).
434
+ const directMessageSignal = fallbackSignalFromDirectMessage(value, source);
435
+ if (directMessageSignal !== undefined) return directMessageSignal;
436
436
  const statusKind = kindFromStatus(statusFrom(value));
437
437
  if (statusKind !== undefined) return makeSignal(statusKind, value, source);
438
438
  if (codeKind !== undefined) return makeSignal(codeKind, value, source);
@@ -463,8 +463,7 @@ function messageFromUnknown(value: unknown, seen: Set<unknown>): string | undefi
463
463
  const status = statusFrom(value);
464
464
  if (status !== undefined) return `Model request failed with status ${status}`;
465
465
  const code = codeFrom(value);
466
- if (code !== undefined) return `Model request failed with code ${String(code)}`;
467
- return undefined;
466
+ return code !== undefined ? `Model request failed with code ${String(code)}` : undefined;
468
467
  }
469
468
  export function modelFailureMessage(error: unknown): string {
470
469
  const structuredMessage = messageFromUnknown(error, new Set());
@@ -1,4 +1,5 @@
1
1
  import type { CodexFastModeResolvedSettings, CodexFastModeScope } from "@bastani/atomic";
2
+ import type { ModelAttempt } from "../../shared/types.ts";
2
3
 
3
4
  export interface RunnerSubagentStep {
4
5
  agent: string;
@@ -13,6 +14,7 @@ export interface RunnerSubagentStep {
13
14
  fastMode?: boolean;
14
15
  modelFastModes?: Record<string, boolean>;
15
16
  modelCandidates?: string[];
17
+ modelAttempts?: ModelAttempt[];
16
18
  codexFastModeSettings?: CodexFastModeResolvedSettings;
17
19
  codexFastModeScope?: CodexFastModeScope;
18
20
  tools?: string[];
@@ -27,6 +27,22 @@ export function toModelInfo(model: RegistryModelLike): ModelInfo {
27
27
  };
28
28
  }
29
29
 
30
+ interface KnownProviderRegistryLike {
31
+ getAvailable(): ReadonlyArray<{ provider: string }>;
32
+ /** Older hosts may not expose `getAll()`; callers probe at runtime and fall back
33
+ * to `getAvailable()`. */
34
+ getAll?: () => ReadonlyArray<{ provider: string }>;
35
+ }
36
+
37
+ /** Collect the distinct providers of every model the registry knows about — including
38
+ * providers without configured auth — falling back to `getAvailable()` when the host
39
+ * registry does not expose `getAll()`. Used to pre-filter spawn candidates whose
40
+ * provider is known but keyless. */
41
+ export function collectKnownModelProviders(registry: KnownProviderRegistryLike): string[] {
42
+ const models = typeof registry.getAll === "function" ? registry.getAll() : registry.getAvailable();
43
+ return [...new Set(models.map((model) => model.provider))];
44
+ }
45
+
30
46
  /** Resolve the effective thinking level from a model string (which may contain a known suffix like `:high`)
31
47
  * and an explicit thinking config value. Returns `undefined` when no thinking is applicable
32
48
  * (e.g. no model was specified, or the model has no suffix and no config was provided). */
@@ -80,6 +80,8 @@ export interface RunSyncOptions {
80
80
  modelOverride?: string;
81
81
  /** Registry models available for heuristic bare-model resolution */
82
82
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
83
+ /** Providers known to the registry before auth filtering */
84
+ knownModelProviders?: string[];
83
85
  /** Current parent-session provider to prefer for ambiguous bare model ids */
84
86
  preferredModelProvider?: string;
85
87
  /** Current parent-session model to try after configured fallback models */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.4-alpha.6",
3
+ "version": "0.9.4-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.4-alpha.8] - 2026-07-02
10
+
11
+ ### Changed
12
+
13
+ - Changed the Claude Fable 5 reasoning level from `xhigh` to `high` across all builtin workflow model chains (`ralph` prompt-engineer/reviewer-a/reviewer-b/reviewer-c, `goal` reviewer, `deep-research-codebase` planner, and `open-claude-design`), covering both the native `anthropic/claude-fable-5` entries and their OpenRouter mirrors.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed the builtin `goal` reviewer and `deep-research-codebase` planner model fallback chains missing the OpenRouter mirror of their primary model: `openrouter/anthropic/claude-fable-5` is now the first OpenRouter fallback candidate in both chains, matching the ordering already used by the `ralph` prompt-engineer/reviewer and `open-claude-design` chains.
18
+
19
+ ## [0.9.4-alpha.7] - 2026-07-02
20
+
21
+ ### Fixed
22
+
23
+ - Fixed workflow stage model fallback so request/context incompatibility failures (HTTP 400/413/422 bad/unprocessable/payload-too-large request, unsupported tool/parameter, context-length/context-window overflow, `invalid_request`/`bad_request`/`too_large` errors) advance the chain to the next candidate instead of stopping. When none of the configured candidates can serve the request, the stage now falls back to the current user-selected model. Refusals, content-filter/safety blocks, cancellations, and task failures still stop the chain ([#1580](https://github.com/bastani-inc/atomic/issues/1580)).
24
+
9
25
  ## [0.9.4-alpha.6] - 2026-07-01
10
26
 
11
27
  ### Changed
@@ -32,7 +32,7 @@ export interface DeepResearchCodebaseResult {
32
32
  export const FILE_ONLY_OUTPUT = "file-only" satisfies WorkflowOutputMode;
33
33
 
34
34
  export const PLANNER_MODEL_CONFIG = {
35
- model: "anthropic/claude-fable-5:xhigh",
35
+ model: "anthropic/claude-fable-5:high",
36
36
  fallbackModels: [
37
37
  "openai-codex/gpt-5.5:xhigh",
38
38
  "github-copilot/gpt-5.5:xhigh",
@@ -41,15 +41,22 @@ export const PLANNER_MODEL_CONFIG = {
41
41
  "anthropic/claude-opus-4-8:xhigh",
42
42
  "zai/glm-5.2:xhigh",
43
43
  "zai-coding-cn/glm-5.2:xhigh",
44
+ "github-copilot/claude-sonnet-5 (1m):high",
45
+ "anthropic/claude-sonnet-5:high",
46
+ "github-copilot/claude-sonnet-4.6 (1m):high",
47
+ "anthropic/claude-sonnet-4-6:high",
44
48
  "github-copilot/gemini-3.5-flash (1m):high",
45
49
  "google/gemini-3.5-flash:high",
46
50
  "google-vertex/gemini-3.5-flash:high",
47
51
  "github-copilot/gemini-3.1-pro-preview (1m):high",
48
52
  "google/gemini-3.1-pro-preview:high",
49
53
  "google-vertex/gemini-3.1-pro-preview:high",
54
+ "openrouter/anthropic/claude-fable-5:high",
50
55
  "openrouter/openai/gpt-5.5:xhigh",
51
56
  "openrouter/anthropic/claude-opus-4-8:xhigh",
52
57
  "openrouter/z-ai/glm-5.2:xhigh",
58
+ "openrouter/anthropic/claude-sonnet-5:high",
59
+ "openrouter/anthropic/claude-sonnet-4-6:high",
53
60
  "openrouter/google/gemini-3.5-flash:high",
54
61
  "openrouter/google/gemini-3.1-pro-preview:high"
55
62
  ],
@@ -101,6 +101,10 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
101
101
  "anthropic/claude-opus-4-8:medium",
102
102
  "zai/glm-5.2:medium",
103
103
  "zai-coding-cn/glm-5.2:medium",
104
+ "github-copilot/claude-sonnet-5 (1m):medium",
105
+ "anthropic/claude-sonnet-5:medium",
106
+ "github-copilot/claude-sonnet-4.6 (1m):medium",
107
+ "anthropic/claude-sonnet-4-6:medium",
104
108
  "github-copilot/gemini-3.5-flash (1m):medium",
105
109
  "google/gemini-3.5-flash:medium",
106
110
  "google-vertex/gemini-3.5-flash:medium",
@@ -110,6 +114,8 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
110
114
  "openrouter/openai/gpt-5.5:medium",
111
115
  "openrouter/anthropic/claude-opus-4-8:medium",
112
116
  "openrouter/z-ai/glm-5.2:medium",
117
+ "openrouter/anthropic/claude-sonnet-5:medium",
118
+ "openrouter/anthropic/claude-sonnet-4-6:medium",
113
119
  "openrouter/google/gemini-3.5-flash:medium",
114
120
  "openrouter/google/gemini-3.1-pro-preview:medium"
115
121
  ],
@@ -117,7 +123,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
117
123
  };
118
124
 
119
125
  const reviewerModelConfig = {
120
- model: "anthropic/claude-fable-5:xhigh",
126
+ model: "anthropic/claude-fable-5:high",
121
127
  fallbackModels: [
122
128
  "openai-codex/gpt-5.5:xhigh",
123
129
  "github-copilot/gpt-5.5:xhigh",
@@ -126,15 +132,22 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
126
132
  "anthropic/claude-opus-4-8:xhigh",
127
133
  "zai/glm-5.2:xhigh",
128
134
  "zai-coding-cn/glm-5.2:xhigh",
135
+ "github-copilot/claude-sonnet-5 (1m):high",
136
+ "anthropic/claude-sonnet-5:high",
137
+ "github-copilot/claude-sonnet-4.6 (1m):high",
138
+ "anthropic/claude-sonnet-4-6:high",
129
139
  "github-copilot/gemini-3.5-flash (1m):high",
130
140
  "google/gemini-3.5-flash:high",
131
141
  "google-vertex/gemini-3.5-flash:high",
132
142
  "github-copilot/gemini-3.1-pro-preview (1m):high",
133
143
  "google/gemini-3.1-pro-preview:high",
134
144
  "google-vertex/gemini-3.1-pro-preview:high",
145
+ "openrouter/anthropic/claude-fable-5:high",
135
146
  "openrouter/openai/gpt-5.5:xhigh",
136
147
  "openrouter/anthropic/claude-opus-4-8:xhigh",
137
148
  "openrouter/z-ai/glm-5.2:xhigh",
149
+ "openrouter/anthropic/claude-sonnet-5:high",
150
+ "openrouter/anthropic/claude-sonnet-4-6:high",
138
151
  "openrouter/google/gemini-3.5-flash:high",
139
152
  "openrouter/google/gemini-3.1-pro-preview:high"
140
153
  ],