@bastani/atomic 0.8.26-alpha.5 → 0.8.26-alpha.7

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 (264) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +7 -4
  3. package/dist/builtin/intercom/CHANGELOG.md +12 -0
  4. package/dist/builtin/intercom/package.json +2 -2
  5. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  6. package/dist/builtin/mcp/package.json +3 -3
  7. package/dist/builtin/subagents/CHANGELOG.md +12 -0
  8. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
  9. package/dist/builtin/subagents/agents/debugger.md +6 -6
  10. package/dist/builtin/subagents/package.json +4 -4
  11. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
  12. package/dist/builtin/subagents/skills/browser/EXAMPLES.md +151 -0
  13. package/dist/builtin/subagents/skills/browser/LICENSE.txt +21 -0
  14. package/dist/builtin/subagents/skills/browser/REFERENCE.md +451 -0
  15. package/dist/builtin/subagents/skills/browser/SKILL.md +170 -0
  16. package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
  17. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +48 -10
  18. package/dist/builtin/subagents/src/runs/foreground/execution.ts +30 -9
  19. package/dist/builtin/subagents/src/runs/shared/final-drain.ts +34 -0
  20. package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +416 -7
  21. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  22. package/dist/builtin/web-access/package.json +2 -2
  23. package/dist/builtin/workflows/CHANGELOG.md +17 -0
  24. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +4 -1
  25. package/dist/builtin/workflows/builtin/goal.ts +127 -99
  26. package/dist/builtin/workflows/builtin/open-claude-design.ts +224 -147
  27. package/dist/builtin/workflows/builtin/ralph.ts +160 -197
  28. package/dist/builtin/workflows/package.json +2 -2
  29. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  30. package/dist/builtin/workflows/src/extension/index.ts +10 -2
  31. package/dist/builtin/workflows/src/extension/runtime.ts +35 -3
  32. package/dist/builtin/workflows/src/runs/background/status.ts +52 -6
  33. package/dist/builtin/workflows/src/runs/foreground/executor.ts +441 -15
  34. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +69 -8
  35. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +402 -8
  36. package/dist/builtin/workflows/src/shared/persistence-restore.ts +182 -6
  37. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +76 -6
  38. package/dist/builtin/workflows/src/shared/stage-prompt.ts +33 -2
  39. package/dist/builtin/workflows/src/shared/store-types.ts +31 -0
  40. package/dist/builtin/workflows/src/shared/store.ts +99 -11
  41. package/dist/builtin/workflows/src/shared/workflow-failures.ts +758 -132
  42. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +9 -0
  43. package/dist/core/agent-session.d.ts +28 -1
  44. package/dist/core/agent-session.d.ts.map +1 -1
  45. package/dist/core/agent-session.js +110 -28
  46. package/dist/core/agent-session.js.map +1 -1
  47. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  48. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  49. package/dist/core/compaction/branch-summarization.js +6 -3
  50. package/dist/core/compaction/branch-summarization.js.map +1 -1
  51. package/dist/core/compaction/compaction.d.ts.map +1 -1
  52. package/dist/core/compaction/compaction.js +23 -10
  53. package/dist/core/compaction/compaction.js.map +1 -1
  54. package/dist/core/compaction/context-compaction.d.ts +61 -0
  55. package/dist/core/compaction/context-compaction.d.ts.map +1 -0
  56. package/dist/core/compaction/context-compaction.js +602 -0
  57. package/dist/core/compaction/context-compaction.js.map +1 -0
  58. package/dist/core/compaction/index.d.ts +1 -0
  59. package/dist/core/compaction/index.d.ts.map +1 -1
  60. package/dist/core/compaction/index.js +1 -0
  61. package/dist/core/compaction/index.js.map +1 -1
  62. package/dist/core/index.d.ts +1 -1
  63. package/dist/core/index.d.ts.map +1 -1
  64. package/dist/core/index.js.map +1 -1
  65. package/dist/core/session-manager.d.ts +41 -1
  66. package/dist/core/session-manager.d.ts.map +1 -1
  67. package/dist/core/session-manager.js +146 -7
  68. package/dist/core/session-manager.js.map +1 -1
  69. package/dist/core/slash-commands.d.ts.map +1 -1
  70. package/dist/core/slash-commands.js +1 -0
  71. package/dist/core/slash-commands.js.map +1 -1
  72. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +5 -5
  73. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
  74. package/dist/core/tools/ask-user-question/tool/format-answer.js +5 -5
  75. package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
  76. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +16 -3
  77. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
  78. package/dist/core/tools/ask-user-question/tool/response-envelope.js +21 -3
  79. package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
  80. package/dist/index.d.ts +3 -3
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +2 -2
  83. package/dist/index.js.map +1 -1
  84. package/dist/modes/index.d.ts +1 -1
  85. package/dist/modes/index.d.ts.map +1 -1
  86. package/dist/modes/index.js.map +1 -1
  87. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  88. package/dist/modes/interactive/components/chat-session-host.js +17 -0
  89. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  90. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  91. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  92. package/dist/modes/interactive/interactive-mode.js +74 -0
  93. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  94. package/dist/modes/rpc/rpc-client.d.ts +12 -7
  95. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  96. package/dist/modes/rpc/rpc-client.js +8 -1
  97. package/dist/modes/rpc/rpc-client.js.map +1 -1
  98. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  99. package/dist/modes/rpc/rpc-mode.js +4 -0
  100. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  101. package/dist/modes/rpc/rpc-types.d.ts +13 -2
  102. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  103. package/dist/modes/rpc/rpc-types.js.map +1 -1
  104. package/docs/compaction.md +42 -23
  105. package/docs/custom-provider.md +11 -9
  106. package/docs/extensions.md +35 -35
  107. package/docs/index.md +1 -8
  108. package/docs/json.md +14 -11
  109. package/docs/packages.md +2 -0
  110. package/docs/providers.md +4 -1
  111. package/docs/quickstart.md +5 -12
  112. package/docs/rpc.md +44 -8
  113. package/docs/sdk.md +1 -8
  114. package/docs/session-format.md +25 -12
  115. package/docs/sessions.md +2 -1
  116. package/docs/skills.md +1 -15
  117. package/docs/termux.md +9 -10
  118. package/docs/themes.md +2 -2
  119. package/docs/tmux.md +3 -3
  120. package/docs/tui.md +19 -32
  121. package/docs/usage.md +2 -0
  122. package/docs/workflows.md +44 -2
  123. package/package.json +4 -12
  124. package/dist/builtin/subagents/skills/browser-use/SKILL.md +0 -234
  125. package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +0 -76
  126. package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +0 -92
  127. package/node_modules/@earendil-works/pi-tui/README.md +0 -779
  128. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts +0 -54
  129. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts.map +0 -1
  130. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +0 -632
  131. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js.map +0 -1
  132. package/node_modules/@earendil-works/pi-tui/dist/components/box.d.ts +0 -22
  133. package/node_modules/@earendil-works/pi-tui/dist/components/box.d.ts.map +0 -1
  134. package/node_modules/@earendil-works/pi-tui/dist/components/box.js +0 -104
  135. package/node_modules/@earendil-works/pi-tui/dist/components/box.js.map +0 -1
  136. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.d.ts +0 -22
  137. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.d.ts.map +0 -1
  138. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.js +0 -35
  139. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.js.map +0 -1
  140. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +0 -249
  141. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +0 -1
  142. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +0 -1857
  143. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +0 -1
  144. package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts +0 -28
  145. package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts.map +0 -1
  146. package/node_modules/@earendil-works/pi-tui/dist/components/image.js +0 -89
  147. package/node_modules/@earendil-works/pi-tui/dist/components/image.js.map +0 -1
  148. package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts +0 -37
  149. package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts.map +0 -1
  150. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +0 -378
  151. package/node_modules/@earendil-works/pi-tui/dist/components/input.js.map +0 -1
  152. package/node_modules/@earendil-works/pi-tui/dist/components/loader.d.ts +0 -31
  153. package/node_modules/@earendil-works/pi-tui/dist/components/loader.d.ts.map +0 -1
  154. package/node_modules/@earendil-works/pi-tui/dist/components/loader.js +0 -69
  155. package/node_modules/@earendil-works/pi-tui/dist/components/loader.js.map +0 -1
  156. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts +0 -96
  157. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts.map +0 -1
  158. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +0 -644
  159. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js.map +0 -1
  160. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.d.ts +0 -50
  161. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.d.ts.map +0 -1
  162. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.js +0 -159
  163. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.js.map +0 -1
  164. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.d.ts +0 -50
  165. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.d.ts.map +0 -1
  166. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.js +0 -185
  167. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.js.map +0 -1
  168. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.d.ts +0 -12
  169. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.d.ts.map +0 -1
  170. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.js +0 -23
  171. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.js.map +0 -1
  172. package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts +0 -19
  173. package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts.map +0 -1
  174. package/node_modules/@earendil-works/pi-tui/dist/components/text.js +0 -89
  175. package/node_modules/@earendil-works/pi-tui/dist/components/text.js.map +0 -1
  176. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.d.ts +0 -13
  177. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.d.ts.map +0 -1
  178. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js +0 -51
  179. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js.map +0 -1
  180. package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts +0 -39
  181. package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts.map +0 -1
  182. package/node_modules/@earendil-works/pi-tui/dist/editor-component.js +0 -2
  183. package/node_modules/@earendil-works/pi-tui/dist/editor-component.js.map +0 -1
  184. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts +0 -16
  185. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts.map +0 -1
  186. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js +0 -110
  187. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js.map +0 -1
  188. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +0 -23
  189. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +0 -1
  190. package/node_modules/@earendil-works/pi-tui/dist/index.js +0 -32
  191. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +0 -1
  192. package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts +0 -193
  193. package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts.map +0 -1
  194. package/node_modules/@earendil-works/pi-tui/dist/keybindings.js +0 -174
  195. package/node_modules/@earendil-works/pi-tui/dist/keybindings.js.map +0 -1
  196. package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts +0 -184
  197. package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts.map +0 -1
  198. package/node_modules/@earendil-works/pi-tui/dist/keys.js +0 -1173
  199. package/node_modules/@earendil-works/pi-tui/dist/keys.js.map +0 -1
  200. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.d.ts +0 -28
  201. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.d.ts.map +0 -1
  202. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.js +0 -44
  203. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.js.map +0 -1
  204. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts +0 -3
  205. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts.map +0 -1
  206. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +0 -53
  207. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js.map +0 -1
  208. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts +0 -50
  209. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts.map +0 -1
  210. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js +0 -361
  211. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js.map +0 -1
  212. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts +0 -90
  213. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +0 -1
  214. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +0 -366
  215. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +0 -1
  216. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +0 -113
  217. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +0 -1
  218. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +0 -472
  219. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +0 -1
  220. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +0 -227
  221. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +0 -1
  222. package/node_modules/@earendil-works/pi-tui/dist/tui.js +0 -1106
  223. package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +0 -1
  224. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.d.ts +0 -17
  225. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.d.ts.map +0 -1
  226. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.js +0 -25
  227. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.js.map +0 -1
  228. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +0 -84
  229. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +0 -1
  230. package/node_modules/@earendil-works/pi-tui/dist/utils.js +0 -1029
  231. package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +0 -1
  232. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts +0 -25
  233. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts.map +0 -1
  234. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +0 -96
  235. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js.map +0 -1
  236. package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  237. package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  238. package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  239. package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  240. package/node_modules/@earendil-works/pi-tui/package.json +0 -47
  241. package/node_modules/get-east-asian-width/index.d.ts +0 -60
  242. package/node_modules/get-east-asian-width/index.js +0 -30
  243. package/node_modules/get-east-asian-width/license +0 -9
  244. package/node_modules/get-east-asian-width/lookup-data.js +0 -21
  245. package/node_modules/get-east-asian-width/lookup.js +0 -138
  246. package/node_modules/get-east-asian-width/package.json +0 -71
  247. package/node_modules/get-east-asian-width/readme.md +0 -65
  248. package/node_modules/get-east-asian-width/utilities.js +0 -24
  249. package/node_modules/marked/LICENSE.md +0 -44
  250. package/node_modules/marked/README.md +0 -106
  251. package/node_modules/marked/bin/main.js +0 -282
  252. package/node_modules/marked/bin/marked.js +0 -15
  253. package/node_modules/marked/lib/marked.cjs +0 -2211
  254. package/node_modules/marked/lib/marked.cjs.map +0 -7
  255. package/node_modules/marked/lib/marked.d.cts +0 -728
  256. package/node_modules/marked/lib/marked.d.ts +0 -728
  257. package/node_modules/marked/lib/marked.esm.js +0 -2189
  258. package/node_modules/marked/lib/marked.esm.js.map +0 -7
  259. package/node_modules/marked/lib/marked.umd.js +0 -2213
  260. package/node_modules/marked/lib/marked.umd.js.map +0 -7
  261. package/node_modules/marked/man/marked.1 +0 -111
  262. package/node_modules/marked/man/marked.1.md +0 -92
  263. package/node_modules/marked/marked.min.js +0 -69
  264. package/node_modules/marked/package.json +0 -111
@@ -1,19 +1,41 @@
1
- import type { WorkflowFailureKind } from "./store-types.js";
1
+ import type {
2
+ WorkflowFailureCode,
3
+ WorkflowFailureDisposition,
4
+ WorkflowFailureKind,
5
+ WorkflowFailureRecoverability,
6
+ } from "./store-types.js";
2
7
 
3
8
  export interface WorkflowFailure {
4
9
  readonly kind: WorkflowFailureKind;
5
- /** Original error text, preserved for diagnostics. */
10
+ /** Specific additive reason within the existing broad failure kind. */
11
+ readonly code?: WorkflowFailureCode;
12
+ /** Redacted diagnostic text safe for snapshots and persistence. */
6
13
  readonly message: string;
7
14
  /** Sanitized workflow-facing text shown on run/stage snapshots. */
8
15
  readonly userMessage: string;
9
16
  readonly retryable: boolean;
10
17
  readonly resumable: boolean;
18
+ readonly recoverability: WorkflowFailureRecoverability;
19
+ readonly disposition: WorkflowFailureDisposition;
20
+ readonly retryAfterMs?: number;
11
21
  readonly cause?: unknown;
12
22
  }
13
23
 
14
24
  export const WORKFLOW_AUTH_FAILURE_MESSAGE =
15
25
  "You must be logged in to run workflows. Run /login and try again.";
16
26
 
27
+ export const WORKFLOW_MISSING_API_KEY_FAILURE_MESSAGE =
28
+ "A required model provider API key is missing. Configure the provider credentials and resume the workflow.";
29
+
30
+ export const WORKFLOW_INVALID_PROVIDER_CREDENTIALS_MESSAGE =
31
+ "The configured model provider credentials are invalid. Update the provider API key, then start a new workflow run.";
32
+
33
+ export const WORKFLOW_FORBIDDEN_MODEL_CONFIG_MESSAGE =
34
+ "The configured model provider or model is not available with the current credentials. Update the model configuration, then start a new workflow run.";
35
+
36
+ export const WORKFLOW_UNKNOWN_MODEL_MESSAGE =
37
+ "The configured model is not available. Update the workflow model configuration, then start a new workflow run.";
38
+
17
39
  const WORKFLOW_FAILURE_KINDS: ReadonlySet<WorkflowFailureKind> = new Set([
18
40
  "auth",
19
41
  "rate_limit",
@@ -26,22 +48,56 @@ export function isWorkflowFailureKind(kind: string): kind is WorkflowFailureKind
26
48
  return WORKFLOW_FAILURE_KINDS.has(kind as WorkflowFailureKind);
27
49
  }
28
50
 
51
+ export function isWorkflowFailureCode(code: string): code is WorkflowFailureCode {
52
+ switch (code) {
53
+ case "login_required":
54
+ case "missing_api_key":
55
+ case "invalid_api_key":
56
+ case "forbidden_config":
57
+ case "unknown_model":
58
+ case "rate_limited":
59
+ case "quota_limited":
60
+ case "provider_unavailable":
61
+ case "cancelled":
62
+ case "unknown":
63
+ return true;
64
+ default:
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export function isWorkflowFailureRecoverability(value: string): value is WorkflowFailureRecoverability {
70
+ return value === "recoverable" || value === "non_recoverable" || value === "unknown";
71
+ }
72
+
73
+ export function isWorkflowFailureDisposition(value: string): value is WorkflowFailureDisposition {
74
+ return value === "active_blocked" || value === "terminal_killed" || value === "terminal_failed";
75
+ }
76
+
29
77
  function makeWorkflowFailure(
30
78
  kind: WorkflowFailureKind,
31
79
  message: string,
32
80
  opts: {
33
81
  readonly retryable: boolean;
34
82
  readonly resumable: boolean;
83
+ readonly recoverability: WorkflowFailureRecoverability;
84
+ readonly disposition: WorkflowFailureDisposition;
35
85
  readonly cause: unknown;
86
+ readonly code?: WorkflowFailureCode;
87
+ readonly retryAfterMs?: number;
36
88
  readonly userMessage?: string;
37
89
  },
38
90
  ): WorkflowFailure {
39
91
  return {
40
92
  kind,
93
+ ...(opts.code !== undefined ? { code: opts.code } : {}),
41
94
  message,
42
- userMessage: opts.userMessage ?? message,
95
+ userMessage: opts.userMessage ?? redactSensitiveText(message),
43
96
  retryable: opts.retryable,
44
97
  resumable: opts.resumable,
98
+ recoverability: opts.recoverability,
99
+ disposition: opts.disposition,
100
+ ...(opts.retryAfterMs !== undefined ? { retryAfterMs: opts.retryAfterMs } : {}),
45
101
  cause: opts.cause,
46
102
  };
47
103
  }
@@ -82,6 +138,35 @@ type StructuredSignal = {
82
138
  readonly code?: string | number;
83
139
  readonly name?: string;
84
140
  readonly stopReason?: string;
141
+ readonly message?: string;
142
+ readonly retryAfterMs?: number;
143
+ };
144
+
145
+ type WorkflowFailureDecision = {
146
+ readonly kind: WorkflowFailureKind;
147
+ readonly code: WorkflowFailureCode;
148
+ readonly retryable: boolean;
149
+ readonly resumable: boolean;
150
+ readonly recoverability: WorkflowFailureRecoverability;
151
+ readonly disposition: WorkflowFailureDisposition;
152
+ readonly userMessage?: string;
153
+ readonly retryAfterMs?: number;
154
+ };
155
+
156
+ type WorkflowFailureClassificationSource =
157
+ | "top_level"
158
+ | "diagnostic"
159
+ | "nested"
160
+ | "cause"
161
+ | "aggregate";
162
+
163
+ type WorkflowFailureEvidence = "strong_signal" | "weak_signal" | "message" | "status";
164
+
165
+ type WorkflowFailureClassification = {
166
+ readonly decision: WorkflowFailureDecision;
167
+ readonly source: WorkflowFailureClassificationSource;
168
+ readonly evidence: WorkflowFailureEvidence;
169
+ readonly message?: string;
85
170
  };
86
171
 
87
172
  function integerFrom(value: unknown): number | undefined {
@@ -91,17 +176,61 @@ function integerFrom(value: unknown): number | undefined {
91
176
  return Number.isInteger(parsed) ? parsed : undefined;
92
177
  }
93
178
 
179
+ function numberFrom(value: unknown): number | undefined {
180
+ if (typeof value === "number" && Number.isFinite(value)) return value;
181
+ if (typeof value !== "string" || value.trim().length === 0) return undefined;
182
+ const parsed = Number(value.trim());
183
+ return Number.isFinite(parsed) ? parsed : undefined;
184
+ }
185
+
186
+ function retryAfterHeaderMs(value: unknown): number | undefined {
187
+ const numeric = numberFrom(value);
188
+ if (numeric !== undefined && numeric >= 0) return Math.round(numeric * 1000);
189
+ if (typeof value !== "string" || value.trim().length === 0) return undefined;
190
+ const dateMs = Date.parse(value);
191
+ if (!Number.isFinite(dateMs)) return undefined;
192
+ return Math.max(0, dateMs - Date.now());
193
+ }
194
+
195
+ function retryAfterMsFrom(error: unknown): number | undefined {
196
+ const directMs = numberFrom(field(error, "retryAfterMs"));
197
+ if (directMs !== undefined && directMs >= 0) return Math.round(directMs);
198
+
199
+ const seconds = numberFrom(field(error, "retryAfterSeconds"));
200
+ if (seconds !== undefined && seconds >= 0) return Math.round(seconds * 1000);
201
+
202
+ // Provider SDKs commonly mirror the HTTP Retry-After header as retryAfter,
203
+ // so the ambiguous bare field follows header semantics (seconds/date). Use
204
+ // retryAfterMs for explicit millisecond values.
205
+ const retryAfter = retryAfterHeaderMs(field(error, "retryAfter"));
206
+ if (retryAfter !== undefined) return retryAfter;
207
+
208
+ const retryAfterHeader = retryAfterHeaderMs(field(error, "retry-after"));
209
+ if (retryAfterHeader !== undefined) return retryAfterHeader;
210
+
211
+ const headers = field(error, "headers");
212
+ const headerRecord = asRecord(headers);
213
+ const headerValue = headerRecord?.["retry-after"] ?? headerRecord?.["Retry-After"];
214
+ return retryAfterHeaderMs(headerValue);
215
+ }
216
+
94
217
  function structuredSignal(error: unknown): StructuredSignal {
95
218
  const status = integerFrom(field(error, "status"))
96
219
  ?? integerFrom(field(error, "statusCode"))
97
220
  ?? integerFrom(field(error, "httpStatus"));
98
221
  const rawCode = field(error, "code");
99
222
  const code = typeof rawCode === "string" || typeof rawCode === "number" ? rawCode : undefined;
223
+ const name = errorName(error);
224
+ const stopReason = stringField(error, "stopReason");
225
+ const message = structuredErrorMessage(error);
226
+ const retryAfterMs = retryAfterMsFrom(error);
100
227
  return {
101
228
  ...(status !== undefined ? { status } : {}),
102
229
  ...(code !== undefined ? { code } : {}),
103
- ...(errorName(error) !== undefined ? { name: errorName(error)! } : {}),
104
- ...(stringField(error, "stopReason") !== undefined ? { stopReason: stringField(error, "stopReason")! } : {}),
230
+ ...(name !== undefined ? { name } : {}),
231
+ ...(stopReason !== undefined ? { stopReason } : {}),
232
+ ...(message !== undefined ? { message } : {}),
233
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
105
234
  };
106
235
  }
107
236
 
@@ -121,121 +250,38 @@ function diagnosticErrors(error: unknown): readonly unknown[] {
121
250
  return errors;
122
251
  }
123
252
 
253
+ function nestedProviderError(error: unknown): unknown {
254
+ return field(error, "error") ?? field(error, "response") ?? field(error, "body");
255
+ }
256
+
124
257
  function normalizeCode(value: string | number | undefined): string | undefined {
125
258
  if (value === undefined) return undefined;
126
- return String(value).trim().toLowerCase().replaceAll("-", "_");
259
+ return String(value).trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
127
260
  }
128
261
 
129
- function kindFromStatus(status: number | undefined): WorkflowFailureKind | undefined {
130
- switch (status) {
131
- case 401:
132
- case 403:
133
- return "auth";
134
- case 429:
135
- return "rate_limit";
136
- case 500:
137
- case 502:
138
- case 503:
139
- case 504:
140
- return "provider";
141
- default:
142
- return undefined;
143
- }
144
- }
262
+ type StructuredCodeEvidence =
263
+ | { readonly kind: "semantic_code"; readonly normalized: string }
264
+ | { readonly kind: "wrapper_http_status"; readonly status: number };
145
265
 
146
- function kindFromCode(code: string | number | undefined): WorkflowFailureKind | undefined {
147
- const normalized = normalizeCode(code);
148
- switch (normalized) {
149
- case undefined:
150
- return undefined;
151
- case "401":
152
- case "403":
153
- case "auth":
154
- case "auth_required":
155
- case "authentication_required":
156
- case "unauthorized":
157
- case "forbidden":
158
- case "invalid_api_key":
159
- case "missing_api_key":
160
- return "auth";
161
- case "429":
162
- case "rate_limit":
163
- case "rate_limit_exceeded":
164
- case "too_many_requests":
165
- case "quota_exceeded":
166
- return "rate_limit";
167
- case "aborterror":
168
- case "aborted":
169
- case "cancelled":
170
- case "canceled":
171
- return "cancelled";
172
- case "500":
173
- case "502":
174
- case "503":
175
- case "504":
176
- case "provider_error":
177
- case "service_unavailable":
178
- case "temporarily_unavailable":
179
- case "overloaded":
180
- return "provider";
181
- default:
182
- return undefined;
266
+ function httpStatusFromCode(value: string | number | undefined): number | undefined {
267
+ if (value === undefined) return undefined;
268
+ if (typeof value === "number") {
269
+ return Number.isInteger(value) && value >= 100 && value <= 599 ? value : undefined;
183
270
  }
271
+ const trimmed = value.trim();
272
+ if (!/^\d{3}$/.test(trimmed)) return undefined;
273
+ const parsed = Number(trimmed);
274
+ return parsed >= 100 && parsed <= 599 ? parsed : undefined;
184
275
  }
185
276
 
186
- function structuredKind(error: unknown, seen = new Set<unknown>()): WorkflowFailureKind | undefined {
187
- if (error === undefined || error === null || seen.has(error)) return undefined;
188
- if (typeof error === "object") seen.add(error);
189
-
190
- const signal = structuredSignal(error);
191
- if (signal.stopReason?.toLowerCase() === "aborted") return "cancelled";
192
- const statusKind = kindFromStatus(signal.status);
193
- if (statusKind !== undefined) return statusKind;
194
- const codeKind = kindFromCode(signal.code) ?? kindFromCode(signal.name);
195
- if (codeKind !== undefined) return codeKind;
196
-
197
- for (const diagnosticError of diagnosticErrors(error)) {
198
- const diagnosticKind = structuredKind(diagnosticError, seen);
199
- if (diagnosticKind !== undefined) return diagnosticKind;
200
- }
277
+ function codeEvidenceFrom(value: string | number | undefined): StructuredCodeEvidence | undefined {
278
+ const status = httpStatusFromCode(value);
279
+ if (status !== undefined) return { kind: "wrapper_http_status", status };
201
280
 
202
- return structuredKind(causeOf(error), seen);
203
- }
204
-
205
- function failureForKind(kind: WorkflowFailureKind, message: string, cause: unknown): WorkflowFailure {
206
- switch (kind) {
207
- case "auth":
208
- return makeWorkflowFailure("auth", message, {
209
- userMessage: WORKFLOW_AUTH_FAILURE_MESSAGE,
210
- retryable: true,
211
- resumable: true,
212
- cause,
213
- });
214
- case "rate_limit":
215
- return makeWorkflowFailure("rate_limit", message, {
216
- retryable: true,
217
- resumable: true,
218
- cause,
219
- });
220
- case "cancelled":
221
- return makeWorkflowFailure("cancelled", message, {
222
- retryable: false,
223
- resumable: false,
224
- cause,
225
- });
226
- case "provider":
227
- return makeWorkflowFailure("provider", message, {
228
- retryable: true,
229
- resumable: true,
230
- cause,
231
- });
232
- case "unknown":
233
- return makeWorkflowFailure("unknown", message, {
234
- retryable: false,
235
- resumable: true,
236
- cause,
237
- });
238
- }
281
+ const normalized = normalizeCode(value);
282
+ return normalized !== undefined && normalized.length > 0
283
+ ? { kind: "semantic_code", normalized }
284
+ : undefined;
239
285
  }
240
286
 
241
287
  type TokenMatch = readonly string[];
@@ -286,24 +332,141 @@ function tokenNearAny(tokens: readonly string[], anchor: string, candidates: Rea
286
332
  return false;
287
333
  }
288
334
 
289
- const AUTH_PHRASES: readonly TokenMatch[] = [
290
- ["no", "api", "key"],
291
- ["api", "key", "not", "found"],
292
- ["missing", "api", "key"],
293
- ["no", "model", "selected"],
294
- ["no", "models", "available"],
335
+ const INVALID_API_KEY_CODES = new Set([
336
+ "401",
337
+ "invalid_api_key",
338
+ "incorrect_api_key",
339
+ "invalid_api_key_error",
340
+ "invalid_credentials",
341
+ "bad_credentials",
342
+ "authentication_error",
343
+ ]);
344
+
345
+ const LOGIN_REQUIRED_CODES = new Set([
346
+ "auth",
347
+ "auth_required",
348
+ "authentication_required",
349
+ "login_required",
350
+ "not_logged_in",
351
+ ]);
352
+
353
+ const MISSING_API_KEY_CODES = new Set([
354
+ "missing_api_key",
355
+ "api_key_missing",
356
+ "no_api_key",
357
+ ]);
358
+
359
+ const RATE_LIMIT_CODES = new Set([
360
+ "429",
361
+ "rate_limit",
362
+ "rate_limited",
363
+ "rate_limit_exceeded",
364
+ "too_many_requests",
365
+ ]);
366
+
367
+ const QUOTA_LIMIT_CODES = new Set([
368
+ "quota",
369
+ "quota_exceeded",
370
+ "insufficient_quota",
371
+ "usage_limit",
372
+ "usage_limit_exceeded",
373
+ ]);
374
+
375
+ const CANCELLED_CODES = new Set([
376
+ "aborterror",
377
+ "aborted",
378
+ "cancelled",
379
+ "canceled",
380
+ ]);
381
+
382
+ const PROVIDER_UNAVAILABLE_CODES = new Set([
383
+ "500",
384
+ "502",
385
+ "503",
386
+ "504",
387
+ "provider_error",
388
+ "service_unavailable",
389
+ "temporarily_unavailable",
390
+ "overloaded",
391
+ "timeout",
392
+ "network_error",
393
+ ]);
394
+
395
+ const UNKNOWN_MODEL_CODES = new Set([
396
+ "unknown_model",
397
+ "model_not_found",
398
+ "model_not_available",
399
+ "unsupported_model",
400
+ ]);
401
+
402
+ const FORBIDDEN_CONFIG_CODES = new Set([
403
+ "403",
404
+ "forbidden",
405
+ "permission_denied",
406
+ "access_denied",
407
+ "forbidden_config",
408
+ "invalid_model_config",
409
+ "model_access_denied",
410
+ ]);
411
+
412
+ const LOGIN_REQUIRED_PHRASES: readonly TokenMatch[] = [
295
413
  ["not", "logged", "in"],
296
414
  ["log", "in"],
297
415
  ["login", "required"],
298
416
  ["authentication", "required"],
417
+ ["please", "login"],
418
+ ["please", "log", "in"],
299
419
  ["unauthorized"],
300
420
  ];
301
421
 
422
+ const LOCAL_LOGIN_REQUIRED_PHRASES: readonly TokenMatch[] = [
423
+ ["not", "logged", "in"],
424
+ ["login", "required"],
425
+ ["please", "login"],
426
+ ["please", "log", "in"],
427
+ ["log", "in", "to", "continue"],
428
+ ];
429
+
430
+ const PROVIDER_AUTH_FALLBACK_PHRASES: readonly TokenMatch[] = [
431
+ ["unauthorized"],
432
+ ["authentication", "required"],
433
+ ];
434
+
435
+ const MISSING_API_KEY_PHRASES: readonly TokenMatch[] = [
436
+ ["no", "api", "key"],
437
+ ["api", "key", "not", "found"],
438
+ ["missing", "api", "key"],
439
+ ["api", "key", "missing"],
440
+ ["no", "model", "selected"],
441
+ ["no", "models", "available"],
442
+ ];
443
+
444
+ const INVALID_API_KEY_PHRASES: readonly TokenMatch[] = [
445
+ ["incorrect", "api", "key"],
446
+ ["invalid", "api", "key"],
447
+ ["api", "key", "invalid"],
448
+ ["api", "key", "incorrect"],
449
+ ["invalid", "credentials"],
450
+ ["invalid", "credential"],
451
+ ];
452
+
453
+ const INVALID_API_KEY_CONTEXT = new Set(["invalid", "incorrect"]);
454
+
455
+ const HTTP_RATE_LIMIT_PHRASES: readonly TokenMatch[] = [
456
+ ["429"],
457
+ ["too", "many", "requests"],
458
+ ];
459
+
302
460
  const RATE_LIMIT_PHRASES: readonly TokenMatch[] = [
303
461
  ["rate", "limit"],
304
- ["429"],
462
+ ["rate", "limited"],
463
+ ];
464
+
465
+ const QUOTA_LIMIT_PHRASES: readonly TokenMatch[] = [
305
466
  ["quota"],
306
- ["too", "many", "requests"],
467
+ ["quota", "exceeded"],
468
+ ["insufficient", "quota"],
469
+ ["usage", "limit"],
307
470
  ];
308
471
 
309
472
  const CANCELLED_PHRASES: readonly TokenMatch[] = [
@@ -312,11 +475,30 @@ const CANCELLED_PHRASES: readonly TokenMatch[] = [
312
475
  ["canceled"],
313
476
  ];
314
477
 
315
- const PROVIDER_PHRASES: readonly TokenMatch[] = [
478
+ const UNKNOWN_MODEL_PHRASES: readonly TokenMatch[] = [
316
479
  ["model", "not", "found"],
480
+ ["unknown", "model"],
481
+ ["unsupported", "model"],
482
+ ["model", "does", "not", "exist"],
483
+ ["model", "not", "available"],
484
+ ];
485
+
486
+ const FORBIDDEN_CONFIG_PHRASES: readonly TokenMatch[] = [
487
+ ["forbidden", "config"],
488
+ ["forbidden", "configuration"],
489
+ ["permission", "denied"],
490
+ ["access", "denied"],
491
+ ["not", "allowed", "to", "access", "model"],
492
+ ["does", "not", "have", "access", "to", "model"],
493
+ ];
494
+
495
+ const PROVIDER_UNAVAILABLE_PHRASES: readonly TokenMatch[] = [
317
496
  ["overloaded"],
318
497
  ["temporarily", "unavailable"],
319
498
  ["service", "unavailable"],
499
+ ["provider", "unavailable"],
500
+ ["provider", "error"],
501
+ ["model", "unavailable"],
320
502
  ["503"],
321
503
  ];
322
504
 
@@ -350,26 +532,470 @@ const PROVIDER_CONTEXT = new Set([
350
532
  "service",
351
533
  ]);
352
534
 
353
- function fallbackKindFromMessage(message: string, name: string | undefined): WorkflowFailureKind | undefined {
354
- const tokens = tokenize(message);
355
- if (hasAnyPhrase(tokens, AUTH_PHRASES) || tokenNearAny(tokens, "oauth", AUTH_CONTEXT, 8)) return "auth";
356
- if (hasAnyPhrase(tokens, RATE_LIMIT_PHRASES)) return "rate_limit";
357
- if (name?.toLowerCase() === "aborterror" || hasAnyPhrase(tokens, CANCELLED_PHRASES)) return "cancelled";
535
+ function redactedSecretReplacement(prefix: string): string {
536
+ return `${prefix}[redacted]`;
537
+ }
538
+
539
+ function redactSensitiveText(value: string): string {
540
+ return value
541
+ .replace(/(sk-[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+/g, redactedSecretReplacement("$1"))
542
+ .replace(/\b(authorization\s*:\s*bearer\s+)[^\s,;]+/gi, "$1[redacted]")
543
+ .replace(/\b(bearer\s+)[A-Za-z0-9._~+/-]{8,}=*/gi, "$1[redacted]")
544
+ .replace(/((?:api[_-]?key|token|credential|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[redacted]");
545
+ }
546
+
547
+ function authDecision(code: WorkflowFailureCode): WorkflowFailureDecision {
548
+ if (code === "invalid_api_key") {
549
+ return {
550
+ kind: "auth",
551
+ code,
552
+ retryable: false,
553
+ resumable: false,
554
+ recoverability: "non_recoverable",
555
+ disposition: "terminal_killed",
556
+ userMessage: WORKFLOW_INVALID_PROVIDER_CREDENTIALS_MESSAGE,
557
+ };
558
+ }
559
+ if (code === "missing_api_key") {
560
+ return {
561
+ kind: "auth",
562
+ code,
563
+ retryable: true,
564
+ resumable: true,
565
+ recoverability: "recoverable",
566
+ disposition: "active_blocked",
567
+ userMessage: WORKFLOW_MISSING_API_KEY_FAILURE_MESSAGE,
568
+ };
569
+ }
570
+ return {
571
+ kind: "auth",
572
+ code: "login_required",
573
+ retryable: true,
574
+ resumable: true,
575
+ recoverability: "recoverable",
576
+ disposition: "active_blocked",
577
+ userMessage: WORKFLOW_AUTH_FAILURE_MESSAGE,
578
+ };
579
+ }
580
+
581
+ function rateLimitDecision(
582
+ code: "rate_limited" | "quota_limited",
583
+ retryAfterMs?: number,
584
+ ): WorkflowFailureDecision {
585
+ return {
586
+ kind: "rate_limit",
587
+ code,
588
+ retryable: true,
589
+ resumable: true,
590
+ recoverability: "recoverable",
591
+ disposition: "active_blocked",
592
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
593
+ };
594
+ }
595
+
596
+ function providerUnavailableDecision(retryAfterMs?: number): WorkflowFailureDecision {
597
+ return {
598
+ kind: "provider",
599
+ code: "provider_unavailable",
600
+ retryable: true,
601
+ resumable: true,
602
+ recoverability: "recoverable",
603
+ disposition: "active_blocked",
604
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
605
+ };
606
+ }
607
+
608
+ function terminalProviderConfigDecision(code: "forbidden_config" | "unknown_model"): WorkflowFailureDecision {
609
+ return {
610
+ kind: "provider",
611
+ code,
612
+ retryable: false,
613
+ resumable: false,
614
+ recoverability: "non_recoverable",
615
+ disposition: "terminal_killed",
616
+ userMessage: code === "unknown_model" ? WORKFLOW_UNKNOWN_MODEL_MESSAGE : WORKFLOW_FORBIDDEN_MODEL_CONFIG_MESSAGE,
617
+ };
618
+ }
619
+
620
+ function cancelledDecision(): WorkflowFailureDecision {
621
+ return {
622
+ kind: "cancelled",
623
+ code: "cancelled",
624
+ retryable: false,
625
+ resumable: false,
626
+ recoverability: "non_recoverable",
627
+ disposition: "terminal_killed",
628
+ };
629
+ }
630
+
631
+ function unknownDecision(): WorkflowFailureDecision {
632
+ return {
633
+ kind: "unknown",
634
+ code: "unknown",
635
+ retryable: false,
636
+ resumable: true,
637
+ recoverability: "unknown",
638
+ disposition: "terminal_failed",
639
+ };
640
+ }
641
+
642
+ function strongDecisionFromNormalizedCode(normalized: string | undefined, retryAfterMs?: number): WorkflowFailureDecision | undefined {
643
+ if (normalized === undefined) return undefined;
644
+ if (CANCELLED_CODES.has(normalized)) return cancelledDecision();
645
+ if (INVALID_API_KEY_CODES.has(normalized)) return authDecision("invalid_api_key");
646
+ if (MISSING_API_KEY_CODES.has(normalized)) return authDecision("missing_api_key");
647
+ if (RATE_LIMIT_CODES.has(normalized)) return rateLimitDecision("rate_limited", retryAfterMs);
648
+ if (QUOTA_LIMIT_CODES.has(normalized)) return rateLimitDecision("quota_limited", retryAfterMs);
649
+ if (UNKNOWN_MODEL_CODES.has(normalized)) return terminalProviderConfigDecision("unknown_model");
650
+ if (FORBIDDEN_CONFIG_CODES.has(normalized)) return terminalProviderConfigDecision("forbidden_config");
651
+ if (PROVIDER_UNAVAILABLE_CODES.has(normalized)) return providerUnavailableDecision(retryAfterMs);
652
+ return undefined;
653
+ }
654
+
655
+ function weakLoginDecisionFromNormalizedCode(normalized: string | undefined): WorkflowFailureDecision | undefined {
656
+ return normalized !== undefined && LOGIN_REQUIRED_CODES.has(normalized)
657
+ ? authDecision("login_required")
658
+ : undefined;
659
+ }
660
+
661
+ function classificationForDecision(
662
+ decision: WorkflowFailureDecision,
663
+ source: WorkflowFailureClassificationSource,
664
+ message: string | undefined,
665
+ evidence: WorkflowFailureEvidence = "message",
666
+ ): WorkflowFailureClassification {
667
+ return {
668
+ decision,
669
+ source,
670
+ evidence,
671
+ ...(message !== undefined ? { message } : {}),
672
+ };
673
+ }
674
+
675
+ function hasInvalidApiKeyMessage(tokens: readonly string[]): boolean {
676
+ return hasAnyPhrase(tokens, INVALID_API_KEY_PHRASES)
677
+ || (hasPhrase(tokens, ["api", "key"]) && tokenNearAny(tokens, "key", INVALID_API_KEY_CONTEXT, 6));
678
+ }
679
+
680
+ function decisionFromMessageTokens(tokens: readonly string[], name: string | undefined, retryAfterMs?: number): WorkflowFailureDecision | undefined {
681
+ if (name?.toLowerCase() === "aborterror" || hasAnyPhrase(tokens, CANCELLED_PHRASES)) return cancelledDecision();
682
+ if (hasAnyPhrase(tokens, HTTP_RATE_LIMIT_PHRASES)) return rateLimitDecision("rate_limited", retryAfterMs);
683
+ if (hasAnyPhrase(tokens, QUOTA_LIMIT_PHRASES)) return rateLimitDecision("quota_limited", retryAfterMs);
684
+ if (hasAnyPhrase(tokens, RATE_LIMIT_PHRASES)) return rateLimitDecision("rate_limited", retryAfterMs);
685
+ if (hasInvalidApiKeyMessage(tokens)) return authDecision("invalid_api_key");
686
+ if (hasAnyPhrase(tokens, MISSING_API_KEY_PHRASES)) return authDecision("missing_api_key");
687
+ if (hasAnyPhrase(tokens, LOGIN_REQUIRED_PHRASES) || tokenNearAny(tokens, "oauth", AUTH_CONTEXT, 8)) return authDecision("login_required");
688
+ if (hasAnyPhrase(tokens, UNKNOWN_MODEL_PHRASES)) return terminalProviderConfigDecision("unknown_model");
689
+ if (hasAnyPhrase(tokens, FORBIDDEN_CONFIG_PHRASES)) return terminalProviderConfigDecision("forbidden_config");
358
690
  if (
359
- hasAnyPhrase(tokens, PROVIDER_PHRASES)
691
+ hasAnyPhrase(tokens, PROVIDER_UNAVAILABLE_PHRASES)
360
692
  || tokenNearAny(tokens, "model", MODEL_PROVIDER_CONTEXT, 8)
361
693
  || tokenNearAny(tokens, "provider", PROVIDER_CONTEXT, 8)
362
- ) return "provider";
694
+ ) return providerUnavailableDecision(retryAfterMs);
363
695
  return undefined;
364
696
  }
365
697
 
698
+ function decisionFromStatus(status: number | undefined, retryAfterMs: number | undefined): WorkflowFailureDecision | undefined {
699
+ switch (status) {
700
+ case 401:
701
+ return authDecision("invalid_api_key");
702
+ case 403:
703
+ return terminalProviderConfigDecision("forbidden_config");
704
+ case 429:
705
+ return rateLimitDecision("rate_limited", retryAfterMs);
706
+ case 500:
707
+ case 502:
708
+ case 503:
709
+ case 504:
710
+ return providerUnavailableDecision(retryAfterMs);
711
+ default:
712
+ return undefined;
713
+ }
714
+ }
715
+
716
+ const STATUS_MESSAGE_REFINEMENT_CODES: ReadonlySet<WorkflowFailureCode> = new Set([
717
+ "invalid_api_key",
718
+ "missing_api_key",
719
+ "unknown_model",
720
+ "forbidden_config",
721
+ ]);
722
+
723
+ const BROAD_AUTH_MESSAGE_REFINEMENT_CODES: ReadonlySet<WorkflowFailureCode> = new Set([
724
+ "invalid_api_key",
725
+ "missing_api_key",
726
+ ]);
727
+
728
+ const STATUS_RELATED_MESSAGE_REFINEMENT_CODES: ReadonlySet<WorkflowFailureCode> = new Set([
729
+ "invalid_api_key",
730
+ "missing_api_key",
731
+ "unknown_model",
732
+ "forbidden_config",
733
+ "rate_limited",
734
+ "quota_limited",
735
+ "cancelled",
736
+ ]);
737
+
738
+ function isRecoverableActiveBlocked(classification: WorkflowFailureClassification): boolean {
739
+ return classification.decision.disposition === "active_blocked"
740
+ && classification.decision.recoverability === "recoverable";
741
+ }
742
+
743
+ function canUseRelatedClassificationBeforeStatus(classification: WorkflowFailureClassification): boolean {
744
+ if (classification.evidence === "weak_signal") return false;
745
+ if (classification.evidence === "message") {
746
+ return STATUS_RELATED_MESSAGE_REFINEMENT_CODES.has(classification.decision.code);
747
+ }
748
+ return classification.decision.code !== "login_required";
749
+ }
750
+
751
+ function isClearLocalLoginMessage(message: string, tokens: readonly string[] = tokenize(message)): boolean {
752
+ if (message.toLowerCase().includes("/login")) return true;
753
+ return hasAnyPhrase(tokens, LOCAL_LOGIN_REQUIRED_PHRASES);
754
+ }
755
+
756
+ function hasFallbackApiError401(tokens: readonly string[]): boolean {
757
+ return hasPhrase(tokens, ["401"]) && hasPhrase(tokens, ["api", "error"]);
758
+ }
759
+
760
+ function classifyFallbackProviderAuthMessage(message: string, tokens: readonly string[]): WorkflowFailureDecision | undefined {
761
+ if (isClearLocalLoginMessage(message, tokens)) return undefined;
762
+ return hasAnyPhrase(tokens, PROVIDER_AUTH_FALLBACK_PHRASES) || hasFallbackApiError401(tokens)
763
+ ? authDecision("invalid_api_key")
764
+ : undefined;
765
+ }
766
+
767
+ function canUseLoginClassificationBeforeWrapper401(
768
+ classification: WorkflowFailureClassification | undefined,
769
+ ): classification is WorkflowFailureClassification {
770
+ if (classification === undefined || classification.decision.code !== "login_required") return false;
771
+ return classification.evidence === "weak_signal"
772
+ || classification.evidence === "strong_signal"
773
+ || (classification.message !== undefined && isClearLocalLoginMessage(classification.message));
774
+ }
775
+
776
+ function classificationFromNormalizedCode(
777
+ normalized: string | undefined,
778
+ retryAfterMs: number | undefined,
779
+ source: WorkflowFailureClassificationSource,
780
+ message: string | undefined,
781
+ ): { readonly strong?: WorkflowFailureClassification; readonly weak?: WorkflowFailureClassification } {
782
+ const strong = strongDecisionFromNormalizedCode(normalized, retryAfterMs);
783
+ if (strong !== undefined) {
784
+ return { strong: classificationForDecision(strong, source, message, "strong_signal") };
785
+ }
786
+ const weak = weakLoginDecisionFromNormalizedCode(normalized);
787
+ return weak !== undefined
788
+ ? { weak: classificationForDecision(weak, source, message, "weak_signal") }
789
+ : {};
790
+ }
791
+
792
+ function aggregateErrorItems(error: unknown): readonly unknown[] {
793
+ const nativeErrors = error instanceof AggregateError ? error.errors as unknown : undefined;
794
+ const errors = nativeErrors ?? field(error, "errors");
795
+ return Array.isArray(errors) ? errors : [];
796
+ }
797
+
798
+ function fallbackAggregateClassification(innerError: unknown): WorkflowFailureClassification {
799
+ const message = errorMessage(innerError);
800
+ const fallback = fallbackDecisionFromMessage(message, errorName(innerError));
801
+ return classificationForDecision(fallback ?? unknownDecision(), "aggregate", message);
802
+ }
803
+
804
+ function recoverableBlockedClassification(classifications: readonly WorkflowFailureClassification[]): WorkflowFailureClassification {
805
+ return classifications.find((classification) => classification.decision.retryAfterMs !== undefined)
806
+ ?? classifications[0]!;
807
+ }
808
+
809
+ function aggregateClassification(error: unknown, seen: Set<unknown>): WorkflowFailureClassification | undefined {
810
+ const innerErrors = aggregateErrorItems(error);
811
+ if (innerErrors.length === 0) return undefined;
812
+
813
+ const classifications = innerErrors.map((innerError) => {
814
+ const branchSeen = new Set(seen);
815
+ return structuredClassification(innerError, "aggregate", branchSeen) ?? fallbackAggregateClassification(innerError);
816
+ });
817
+
818
+ const terminalKilled = classifications.find(
819
+ (classification) => classification.decision.disposition === "terminal_killed",
820
+ );
821
+ if (terminalKilled !== undefined) return terminalKilled;
822
+
823
+ const allRecoverableBlocked = classifications.every(isRecoverableActiveBlocked);
824
+ if (allRecoverableBlocked) return recoverableBlockedClassification(classifications);
825
+
826
+ return classificationForDecision(unknownDecision(), "aggregate", errorMessage(error));
827
+ }
828
+
829
+ function selectDiagnosticFailureClassification(
830
+ diagnostics: readonly unknown[],
831
+ seen: ReadonlySet<unknown>,
832
+ ): WorkflowFailureClassification | undefined {
833
+ const classifications: WorkflowFailureClassification[] = [];
834
+ for (const diagnosticError of diagnostics) {
835
+ const diagnosticSeen = new Set(seen);
836
+ const diagnosticClassification = structuredClassification(diagnosticError, "diagnostic", diagnosticSeen);
837
+ if (diagnosticClassification !== undefined) classifications.push(diagnosticClassification);
838
+ }
839
+ if (classifications.length === 0) return undefined;
840
+
841
+ const terminalKilled = classifications.find(
842
+ (classification) => classification.decision.disposition === "terminal_killed",
843
+ );
844
+ if (terminalKilled !== undefined) return terminalKilled;
845
+
846
+ const terminalFailed = classifications.find(
847
+ (classification) => classification.decision.disposition === "terminal_failed",
848
+ );
849
+ if (terminalFailed !== undefined) return terminalFailed;
850
+
851
+ const allRecoverableBlocked = classifications.every(isRecoverableActiveBlocked);
852
+ if (allRecoverableBlocked) return recoverableBlockedClassification(classifications);
853
+
854
+ return classifications[0]!;
855
+ }
856
+
857
+ function relatedStructuredClassification(error: unknown, seen: Set<unknown>): WorkflowFailureClassification | undefined {
858
+ const diagnosticClassification = selectDiagnosticFailureClassification(diagnosticErrors(error), seen);
859
+ if (diagnosticClassification !== undefined) return diagnosticClassification;
860
+
861
+ const nested = nestedProviderError(error);
862
+ if (nested !== undefined && nested !== error) {
863
+ const nestedClassification = structuredClassification(nested, "nested", seen);
864
+ if (nestedClassification !== undefined) return nestedClassification;
865
+ }
866
+
867
+ const causeClassification = structuredClassification(causeOf(error), "cause", seen);
868
+ if (causeClassification !== undefined) return causeClassification;
869
+
870
+ return aggregateClassification(error, seen);
871
+ }
872
+
873
+ function structuredClassification(
874
+ error: unknown,
875
+ source: WorkflowFailureClassificationSource = "top_level",
876
+ seen = new Set<unknown>(),
877
+ ): WorkflowFailureClassification | undefined {
878
+ if (error === undefined || error === null || seen.has(error)) return undefined;
879
+ if (typeof error === "object") seen.add(error);
880
+
881
+ const signal = structuredSignal(error);
882
+ const signalMessage = signal.message ?? (typeof error === "string" ? error : undefined);
883
+ if (signal.stopReason?.toLowerCase() === "aborted") {
884
+ return classificationForDecision(cancelledDecision(), source, signalMessage, "strong_signal");
885
+ }
886
+
887
+ const retryAfterMs = signal.retryAfterMs;
888
+ let weakClassification: WorkflowFailureClassification | undefined;
889
+
890
+ const codeEvidence = codeEvidenceFrom(signal.code);
891
+ if (codeEvidence?.kind === "semantic_code") {
892
+ const codeClassification = classificationFromNormalizedCode(codeEvidence.normalized, retryAfterMs, source, signalMessage);
893
+ if (codeClassification.strong !== undefined) return codeClassification.strong;
894
+ weakClassification = codeClassification.weak ?? weakClassification;
895
+ }
896
+
897
+ const nameClassification = classificationFromNormalizedCode(normalizeCode(signal.name), retryAfterMs, source, signalMessage);
898
+ if (nameClassification.strong !== undefined) return nameClassification.strong;
899
+ weakClassification = nameClassification.weak ?? weakClassification;
900
+
901
+ const messageTokens = signalMessage !== undefined ? tokenize(signalMessage) : undefined;
902
+ const messageDecision = messageTokens !== undefined
903
+ ? decisionFromMessageTokens(messageTokens, signal.name, retryAfterMs)
904
+ : undefined;
905
+ const providerAuthMessageDecision = signalMessage !== undefined && messageTokens !== undefined
906
+ ? classifyFallbackProviderAuthMessage(signalMessage, messageTokens)
907
+ : undefined;
908
+ if (
909
+ weakClassification !== undefined &&
910
+ messageDecision !== undefined &&
911
+ BROAD_AUTH_MESSAGE_REFINEMENT_CODES.has(messageDecision.code)
912
+ ) {
913
+ return classificationForDecision(messageDecision, source, signalMessage);
914
+ }
915
+
916
+ const relatedClassification = relatedStructuredClassification(error, seen);
917
+ const effectiveStatus = signal.status ?? (codeEvidence?.kind === "wrapper_http_status" ? codeEvidence.status : undefined);
918
+ const statusDecision = decisionFromStatus(effectiveStatus, retryAfterMs);
919
+ if (statusDecision !== undefined) {
920
+ if (relatedClassification !== undefined && canUseRelatedClassificationBeforeStatus(relatedClassification)) {
921
+ return relatedClassification;
922
+ }
923
+ if (
924
+ signalMessage !== undefined &&
925
+ (effectiveStatus === 401 || effectiveStatus === 403) &&
926
+ messageDecision !== undefined &&
927
+ STATUS_MESSAGE_REFINEMENT_CODES.has(messageDecision.code)
928
+ ) {
929
+ return classificationForDecision(messageDecision, source, signalMessage);
930
+ }
931
+ if (effectiveStatus === 401) {
932
+ if (canUseLoginClassificationBeforeWrapper401(relatedClassification)) {
933
+ return relatedClassification;
934
+ }
935
+ if (canUseLoginClassificationBeforeWrapper401(weakClassification)) {
936
+ return weakClassification;
937
+ }
938
+ if (signalMessage !== undefined && isClearLocalLoginMessage(signalMessage)) {
939
+ return classificationForDecision(authDecision("login_required"), source, signalMessage);
940
+ }
941
+ }
942
+ return classificationForDecision(statusDecision, source, signalMessage, "status");
943
+ }
944
+
945
+ if (source !== "top_level") {
946
+ if (
947
+ providerAuthMessageDecision !== undefined &&
948
+ (messageDecision === undefined || messageDecision.code === "login_required")
949
+ ) {
950
+ return classificationForDecision(providerAuthMessageDecision, source, signalMessage);
951
+ }
952
+ if (messageDecision !== undefined) {
953
+ return classificationForDecision(messageDecision, source, signalMessage);
954
+ }
955
+ }
956
+
957
+ if (relatedClassification !== undefined) return relatedClassification;
958
+
959
+ return weakClassification;
960
+ }
961
+
962
+ function fallbackDecisionFromMessage(message: string, name: string | undefined): WorkflowFailureDecision | undefined {
963
+ const tokens = tokenize(message);
964
+ const decision = decisionFromMessageTokens(tokens, name);
965
+ const providerAuthDecision = classifyFallbackProviderAuthMessage(message, tokens);
966
+ if (providerAuthDecision !== undefined && (decision === undefined || decision.code === "login_required")) {
967
+ return providerAuthDecision;
968
+ }
969
+ if (decision === undefined && isClearLocalLoginMessage(message, tokens)) {
970
+ return authDecision("login_required");
971
+ }
972
+ return decision;
973
+ }
974
+
975
+ function failureForDecision(decision: WorkflowFailureDecision, message: string, cause: unknown): WorkflowFailure {
976
+ const safeMessage = redactSensitiveText(message);
977
+ return makeWorkflowFailure(decision.kind, safeMessage, {
978
+ code: decision.code,
979
+ retryable: decision.retryable,
980
+ resumable: decision.resumable,
981
+ recoverability: decision.recoverability,
982
+ disposition: decision.disposition,
983
+ cause,
984
+ ...(decision.userMessage !== undefined ? { userMessage: decision.userMessage } : {}),
985
+ ...(decision.retryAfterMs !== undefined ? { retryAfterMs: decision.retryAfterMs } : {}),
986
+ });
987
+ }
988
+
366
989
  export function classifyWorkflowFailure(error: unknown): WorkflowFailure {
367
990
  const message = errorMessage(error);
368
- const structured = structuredKind(error);
369
- if (structured !== undefined) return failureForKind(structured, message, error);
991
+ const structured = structuredClassification(error);
992
+ if (structured !== undefined) {
993
+ const structuredMessage = structured.message ?? message;
994
+ return failureForDecision(structured.decision, structuredMessage, error);
995
+ }
370
996
 
371
- const fallback = fallbackKindFromMessage(message, errorName(error));
372
- if (fallback !== undefined) return failureForKind(fallback, message, error);
997
+ const fallback = fallbackDecisionFromMessage(message, errorName(error));
998
+ if (fallback !== undefined) return failureForDecision(fallback, message, error);
373
999
 
374
- return failureForKind("unknown", message, error);
1000
+ return failureForDecision(unknownDecision(), message, error);
375
1001
  }