@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
@@ -48,7 +48,12 @@ import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/c
48
48
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
49
49
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
50
50
  import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
51
- import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
51
+ import { formatModelAttemptNote, isRetryableModelFailure, modelFailureMessage } from "../shared/model-fallback.ts";
52
+ import {
53
+ assistantStopReason,
54
+ isAssistantFailureStopReason,
55
+ shouldStartSubagentFinalDrain,
56
+ } from "../shared/final-drain.ts";
52
57
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
53
58
  import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
54
59
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
@@ -230,6 +235,7 @@ interface RunPiStreamingResult {
230
235
  finalOutput: string;
231
236
  interrupted?: boolean;
232
237
  observedMutationAttempt?: boolean;
238
+ modelFailureSignal?: unknown;
233
239
  }
234
240
 
235
241
  function runPiStreaming(
@@ -270,6 +276,7 @@ function runPiStreaming(
270
276
  let model: string | undefined;
271
277
  let error: string | undefined;
272
278
  let assistantError: string | undefined;
279
+ let assistantFailureSignal: unknown;
273
280
  let interrupted = false;
274
281
  let observedMutationAttempt = false;
275
282
  const rawStdoutLines: string[] = [];
@@ -330,7 +337,6 @@ function runPiStreaming(
330
337
 
331
338
  if (event.type !== "message_end" || event.message.role !== "assistant") return;
332
339
  if (event.message.model) model = event.message.model;
333
- if (event.message.errorMessage) assistantError = event.message.errorMessage;
334
340
  const eventUsage = event.message.usage;
335
341
  if (eventUsage) {
336
342
  usage.turns++;
@@ -340,12 +346,21 @@ function runPiStreaming(
340
346
  usage.cacheWrite += eventUsage.cacheWrite ?? 0;
341
347
  usage.cost += eventUsage.cost?.total ?? 0;
342
348
  }
343
- const stopReason = (event.message as { stopReason?: string }).stopReason;
344
- const hasToolCall = Array.isArray(event.message.content)
345
- && event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
346
- if (stopReason === "stop" && !hasToolCall) {
347
- if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
348
- cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
349
+ const stopReason = assistantStopReason(event.message);
350
+ if (event.message.errorMessage) {
351
+ assistantError = event.message.errorMessage;
352
+ assistantFailureSignal = event.message;
353
+ }
354
+ if (isAssistantFailureStopReason(stopReason)) {
355
+ assistantError = modelFailureMessage(event.message);
356
+ assistantFailureSignal = event.message;
357
+ }
358
+ if (shouldStartSubagentFinalDrain(event.message)) {
359
+ if (extractTextFromContent(event.message.content).trim()) {
360
+ assistantError = undefined;
361
+ assistantFailureSignal = undefined;
362
+ }
363
+ cleanTerminalAssistantStopReceived = true;
349
364
  startFinalDrain();
350
365
  }
351
366
  }
@@ -448,6 +463,9 @@ function runPiStreaming(
448
463
  finalOutput,
449
464
  interrupted,
450
465
  observedMutationAttempt,
466
+ ...(assistantFailureSignal !== undefined && finalError === assistantError
467
+ ? { modelFailureSignal: assistantFailureSignal }
468
+ : {}),
451
469
  });
452
470
  });
453
471
 
@@ -459,7 +477,20 @@ function runPiStreaming(
459
477
  outputStream.end();
460
478
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
461
479
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
462
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
480
+ const finalError = error ?? assistantError ?? spawnErrorMessage;
481
+ resolve({
482
+ stderr,
483
+ exitCode: 1,
484
+ messages,
485
+ usage,
486
+ model,
487
+ error: finalError,
488
+ finalOutput,
489
+ observedMutationAttempt,
490
+ ...(assistantFailureSignal !== undefined && finalError === assistantError
491
+ ? { modelFailureSignal: assistantFailureSignal }
492
+ : {}),
493
+ });
463
494
  });
464
495
  });
465
496
  }
@@ -770,7 +801,14 @@ async function runSingleStep(
770
801
  finalOutputSnapshot = outputSnapshot;
771
802
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
772
803
  if (attempt.success) break;
773
- if (!completionGuardTriggered && isRetryableModelFailure(error) && index < candidates.length - 1) {
804
+ const retrySignal = run.modelFailureSignal ?? error;
805
+ if (
806
+ !completionGuardTriggered
807
+ && structuredError === undefined
808
+ && hiddenError?.hasError !== true
809
+ && isRetryableModelFailure(retrySignal)
810
+ && index < candidates.length - 1
811
+ ) {
774
812
  pendingAttemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
775
813
  continue;
776
814
  }
@@ -54,7 +54,13 @@ import {
54
54
  buildModelCandidates,
55
55
  formatModelAttemptNote,
56
56
  isRetryableModelFailure,
57
+ modelFailureMessage,
57
58
  } from "../shared/model-fallback.ts";
59
+ import {
60
+ assistantStopReason,
61
+ isAssistantFailureStopReason,
62
+ shouldStartSubagentFinalDrain,
63
+ } from "../shared/final-drain.ts";
58
64
  import {
59
65
  createMutatingFailureState,
60
66
  didMutatingToolFail,
@@ -76,6 +82,7 @@ import { acceptanceFailureMessage, evaluateAcceptance, formatAcceptancePrompt, r
76
82
 
77
83
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
78
84
  const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
85
+ const modelFailureSignalByResult = new WeakMap<SingleResult, unknown>();
79
86
 
80
87
  function emptyUsage(): Usage {
81
88
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
@@ -293,6 +300,7 @@ async function runSingleAttempt(
293
300
  let detached = false;
294
301
  let intercomStarted = false;
295
302
  let assistantError: string | undefined;
303
+ let assistantFailureSignal: unknown;
296
304
  let removeAbortListener: (() => void) | undefined;
297
305
  let removeInterruptListener: (() => void) | undefined;
298
306
  let activityTimer: NodeJS.Timeout | undefined;
@@ -544,16 +552,25 @@ async function runSingleAttempt(
544
552
  progress.tokens = result.usage.input + result.usage.output;
545
553
  }
546
554
  if (!result.model && evt.message.model) result.model = evt.message.model;
547
- if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
548
555
  const assistantText = extractTextFromContent(evt.message.content);
549
556
  appendRecentOutput(progress, assistantText.split("\n").slice(-10));
550
- // Final assistant message: start the exit drain window.
551
- const stopReason = (evt.message as { stopReason?: string }).stopReason;
552
- const hasToolCall = Array.isArray(evt.message.content)
553
- && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
554
- if (stopReason === "stop" && !hasToolCall) {
555
- if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
556
- cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
557
+ // Clean final assistant stops start the exit drain window; provider error/aborted
558
+ // stop reasons remain failure evidence so pi-ai can auto-retry before exit.
559
+ const stopReason = assistantStopReason(evt.message);
560
+ if (evt.message.errorMessage) {
561
+ assistantError = evt.message.errorMessage;
562
+ assistantFailureSignal = evt.message;
563
+ }
564
+ if (isAssistantFailureStopReason(stopReason)) {
565
+ assistantError = modelFailureMessage(evt.message);
566
+ assistantFailureSignal = evt.message;
567
+ }
568
+ if (shouldStartSubagentFinalDrain(evt.message)) {
569
+ if (assistantText.trim()) {
570
+ assistantError = undefined;
571
+ assistantFailureSignal = undefined;
572
+ }
573
+ cleanTerminalAssistantStopReceived = true;
557
574
  startFinalDrain();
558
575
  }
559
576
  }
@@ -633,6 +650,9 @@ async function runSingleAttempt(
633
650
  processClosed = true;
634
651
  if (buf.trim()) processLine(buf);
635
652
  if (!result.error && assistantError) result.error = assistantError;
653
+ if (assistantFailureSignal !== undefined && result.error === assistantError) {
654
+ modelFailureSignalByResult.set(result, assistantFailureSignal);
655
+ }
636
656
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !result.error;
637
657
  if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
638
658
  result.error = stderrBuf.trim();
@@ -964,7 +984,8 @@ export async function runSync(
964
984
  if (attemptSucceeded) {
965
985
  break;
966
986
  }
967
- if (isRetryableModelFailure(result.error) && i < modelsToTry.length - 1) {
987
+ const retrySignal = modelFailureSignalByResult.get(result) ?? result.error;
988
+ if (isRetryableModelFailure(retrySignal) && i < modelsToTry.length - 1) {
968
989
  pendingAttemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
969
990
  continue;
970
991
  }
@@ -0,0 +1,34 @@
1
+ export interface SubagentAssistantDrainMessage {
2
+ readonly role?: unknown;
3
+ readonly content?: unknown;
4
+ readonly stopReason?: unknown;
5
+ readonly errorMessage?: unknown;
6
+ }
7
+
8
+ export function assistantStopReason(message: SubagentAssistantDrainMessage): string | undefined {
9
+ return typeof message.stopReason === "string" ? message.stopReason : undefined;
10
+ }
11
+
12
+ export function isAssistantFailureStopReason(stopReason: string | undefined): boolean {
13
+ return stopReason === "error" || stopReason === "aborted";
14
+ }
15
+
16
+ export function assistantMessageHasToolCall(message: SubagentAssistantDrainMessage): boolean {
17
+ return Array.isArray(message.content)
18
+ && message.content.some((part) => part !== null
19
+ && typeof part === "object"
20
+ && (part as { readonly type?: unknown }).type === "toolCall");
21
+ }
22
+
23
+ function assistantMessageHasError(message: SubagentAssistantDrainMessage): boolean {
24
+ const errorMessage = message.errorMessage;
25
+ if (typeof errorMessage === "string") return errorMessage.trim().length > 0;
26
+ return errorMessage !== undefined && errorMessage !== null;
27
+ }
28
+
29
+ export function shouldStartSubagentFinalDrain(message: SubagentAssistantDrainMessage): boolean {
30
+ if (message.role !== undefined && message.role !== "assistant") return false;
31
+ return assistantStopReason(message) === "stop"
32
+ && !assistantMessageHasError(message)
33
+ && !assistantMessageHasToolCall(message);
34
+ }
@@ -62,7 +62,7 @@ export function currentModelFullId(model: { provider: string; id: string } | und
62
62
  return `${String(model.provider)}/${model.id}`;
63
63
  }
64
64
 
65
- const RETRYABLE_MODEL_FAILURE_PATTERNS = [
65
+ const RETRYABLE_MODEL_FAILURE_PATTERNS: readonly RegExp[] = [
66
66
  /rate\s*limit/i,
67
67
  /too many requests/i,
68
68
  /\b429\b/,
@@ -71,6 +71,7 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
71
71
  /credit/i,
72
72
  /auth(?:entication)?/i,
73
73
  /unauthori[sz]ed/i,
74
+ /\b40[13]\b/,
74
75
  /forbidden/i,
75
76
  /api key/i,
76
77
  /token expired/i,
@@ -90,14 +91,422 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
90
91
  /upstream/i,
91
92
  /timed? out/i,
92
93
  /timeout/i,
93
- /\b502\b/,
94
- /\b503\b/,
95
- /\b504\b/,
94
+ /\b50[0-4]\b/,
96
95
  ];
97
96
 
98
- export function isRetryableModelFailure(error: string | undefined): boolean {
99
- if (!error) return false;
100
- return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
97
+ const NON_RETRYABLE_FAILURE_PATTERNS: readonly RegExp[] = [
98
+ /command failed/i,
99
+ /tests? failed/i,
100
+ /shell/i,
101
+ /missing file/i,
102
+ /no such file/i,
103
+ /completion guard/i,
104
+ /cancel/i,
105
+ /abort/i,
106
+ /interrupted/i,
107
+ ];
108
+
109
+ const CANCELLED_FAILURE_PATTERNS: readonly RegExp[] = [
110
+ /cancel/i,
111
+ /abort/i,
112
+ /interrupted/i,
113
+ ];
114
+
115
+ export type ModelFallbackFailureKind =
116
+ | "auth_on_candidate_provider"
117
+ | "rate_limit"
118
+ | "provider_unavailable"
119
+ | "network_timeout"
120
+ | "model_unavailable"
121
+ | "cancelled"
122
+ | "task_failure"
123
+ | "unknown";
124
+
125
+ export type ModelFallbackFailureSource =
126
+ | "assistant_message"
127
+ | "diagnostic"
128
+ | "throw"
129
+ | "structured"
130
+ | "string_fallback";
131
+
132
+ export interface ModelFallbackFailureSignal {
133
+ readonly kind: ModelFallbackFailureKind;
134
+ readonly message: string;
135
+ readonly source: ModelFallbackFailureSource;
136
+ readonly stopReason?: string;
137
+ readonly status?: number;
138
+ readonly code?: string | number;
139
+ readonly name?: string;
140
+ }
141
+
142
+ const FALLBACKABLE_FAILURE_KINDS: ReadonlySet<ModelFallbackFailureKind> = new Set([
143
+ "auth_on_candidate_provider",
144
+ "rate_limit",
145
+ "provider_unavailable",
146
+ "network_timeout",
147
+ "model_unavailable",
148
+ ]);
149
+
150
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
151
+ return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
152
+ }
153
+
154
+ function field(value: unknown, key: string): unknown {
155
+ return asRecord(value)?.[key];
156
+ }
157
+
158
+ function stringField(value: unknown, key: string): string | undefined {
159
+ const raw = field(value, key);
160
+ return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
161
+ }
162
+
163
+ function errorName(value: unknown): string | undefined {
164
+ return value instanceof Error ? value.name : stringField(value, "name");
165
+ }
166
+
167
+ function directMessageFrom(value: unknown): string | undefined {
168
+ return stringField(value, "errorMessage")
169
+ ?? stringField(value, "message")
170
+ ?? stringField(value, "statusText");
171
+ }
172
+
173
+ function integerFrom(value: unknown): number | undefined {
174
+ if (typeof value === "number" && Number.isInteger(value)) return value;
175
+ if (typeof value !== "string" || value.trim().length === 0) return undefined;
176
+ const parsed = Number(value.trim());
177
+ return Number.isInteger(parsed) ? parsed : undefined;
178
+ }
179
+
180
+ function statusFrom(value: unknown): number | undefined {
181
+ return integerFrom(field(value, "status"))
182
+ ?? integerFrom(field(value, "statusCode"))
183
+ ?? integerFrom(field(value, "httpStatus"));
184
+ }
185
+
186
+ function codeFrom(value: unknown): string | number | undefined {
187
+ const rawCode = field(value, "code");
188
+ return typeof rawCode === "string" || typeof rawCode === "number" ? rawCode : undefined;
189
+ }
190
+
191
+ function stopReasonFrom(value: unknown): string | undefined {
192
+ return stringField(value, "stopReason");
193
+ }
194
+
195
+ function finishReasonFrom(value: unknown): string | undefined {
196
+ return stringField(value, "finish_reason") ?? stringField(value, "finishReason");
197
+ }
198
+
199
+ function causeOf(value: unknown): unknown {
200
+ return value instanceof Error ? value.cause : field(value, "cause");
201
+ }
202
+
203
+ function diagnosticErrors(value: unknown): readonly unknown[] {
204
+ const diagnostics = field(value, "diagnostics");
205
+ if (!Array.isArray(diagnostics)) return [];
206
+ const errors: unknown[] = [];
207
+ for (const diagnostic of diagnostics) {
208
+ const diagnosticError = field(diagnostic, "error");
209
+ errors.push(diagnosticError ?? diagnostic);
210
+ }
211
+ return errors;
212
+ }
213
+
214
+ function normalizeCode(value: string | number | undefined): string | undefined {
215
+ if (value === undefined) return undefined;
216
+ const normalized = String(value)
217
+ .trim()
218
+ .toLowerCase()
219
+ .replace(/[^a-z0-9]+/g, "_")
220
+ .replace(/^_+|_+$/g, "");
221
+ return normalized.length > 0 ? normalized : undefined;
222
+ }
223
+
224
+ function kindFromStatus(status: number | undefined): ModelFallbackFailureKind | undefined {
225
+ switch (status) {
226
+ case 401:
227
+ case 403:
228
+ return "auth_on_candidate_provider";
229
+ case 408:
230
+ return "network_timeout";
231
+ case 404:
232
+ return "model_unavailable";
233
+ case 429:
234
+ return "rate_limit";
235
+ default:
236
+ if (status !== undefined && status >= 500 && status <= 599) return "provider_unavailable";
237
+ return undefined;
238
+ }
239
+ }
240
+
241
+ function refusalKindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
242
+ const normalizedCode = normalizeCode(code);
243
+ if (normalizedCode === undefined) return undefined;
244
+ if (normalizedCode.includes("content_filter") || normalizedCode.includes("contentfilter")) return "task_failure";
245
+ if (normalizedCode.includes("safety") || normalizedCode.includes("policy")) return "task_failure";
246
+ switch (normalizedCode) {
247
+ case "blocked":
248
+ case "blocked_by_provider":
249
+ case "blocked_by_safety":
250
+ case "blocked_by_policy":
251
+ case "provider_refusal":
252
+ case "refusal":
253
+ case "tool_refusal":
254
+ case "tool_call_refusal":
255
+ case "tool_use_refusal":
256
+ return "task_failure";
257
+ default:
258
+ return undefined;
259
+ }
260
+ }
261
+
262
+ function kindFromCode(code: string | number | undefined): ModelFallbackFailureKind | undefined {
263
+ const normalizedCode = normalizeCode(code);
264
+ if (normalizedCode === undefined) return undefined;
265
+ const refusalKind = refusalKindFromCode(code);
266
+ if (refusalKind !== undefined) return refusalKind;
267
+ const httpStatusKind = kindFromStatus(integerFrom(code));
268
+ if (httpStatusKind !== undefined) return httpStatusKind;
269
+
270
+ switch (normalizedCode) {
271
+ case "auth":
272
+ case "auth_required":
273
+ case "authentication_required":
274
+ case "unauthorized":
275
+ case "forbidden":
276
+ case "invalid_api_key":
277
+ case "missing_api_key":
278
+ case "invalid_key":
279
+ return "auth_on_candidate_provider";
280
+ case "etimedout":
281
+ case "econnreset":
282
+ case "econnrefused":
283
+ case "enotfound":
284
+ case "eai_again":
285
+ case "fetch_failed":
286
+ case "network_error":
287
+ case "timeout":
288
+ case "timeout_error":
289
+ case "und_err_connect_timeout":
290
+ return "network_timeout";
291
+ case "rate_limit":
292
+ case "rate_limit_exceeded":
293
+ case "too_many_requests":
294
+ case "quota_exceeded":
295
+ return "rate_limit";
296
+ case "aborterror":
297
+ case "aborted":
298
+ case "cancelled":
299
+ case "canceled":
300
+ return "cancelled";
301
+ case "model_not_found":
302
+ case "model_unavailable":
303
+ case "model_disabled":
304
+ case "unknown_model":
305
+ return "model_unavailable";
306
+ case "provider_error":
307
+ case "api_error":
308
+ case "service_unavailable":
309
+ case "temporarily_unavailable":
310
+ case "overloaded":
311
+ return "provider_unavailable";
312
+ default:
313
+ return undefined;
314
+ }
315
+ }
316
+
317
+ const PROVIDER_REFUSAL_FAILURE_PATTERNS: readonly RegExp[] = [
318
+ /\bfinish[_\s-]?reason\b[^\n]*\bcontent[_\s-]?filter\b/i,
319
+ /\bcontent[_\s-]?filter(?:ed|ing)?\b/i,
320
+ /\b(?:safety|policy)\b[^\n]*\b(?:refus(?:e|al|ed|es|ing)?|block(?:ed|ing)?|filter(?:ed|ing)?|violat(?:e|ion|ed|ing)?|disallow(?:ed|ing)?|reject(?:ed|ion|ing)?)\b/i,
321
+ /\b(?:refus(?:e|al|ed|es|ing)?|block(?:ed|ing)?|filter(?:ed|ing)?|violat(?:e|ion|ed|ing)?|disallow(?:ed|ing)?|reject(?:ed|ion|ing)?)\b[^\n]*\b(?:safety|policy)\b/i,
322
+ /\btool[_\s-]?(?:call|use)?[_\s-]?refus(?:e|al|ed|es|ing)?\b/i,
323
+ /\btool(?:\s+call|\s+use)?\b[^\n]*\brefus(?:e|al|ed|es|ing)?\b/i,
324
+ /\brefus(?:e|al|ed|es|ing)?\b[^\n]*\btool(?:\s+call|\s+use)?\b/i,
325
+ /\bprovider[_\s-]?refus(?:e|al|ed|es|ing)?\b/i,
326
+ /\bprovider\b[^\n]*\brefus(?:e|al|ed|es|ing)?\b[^\n]*\b(?:prompt|request|content|policy|safety)\b/i,
327
+ ];
328
+
329
+ function refusalKindFromMessage(message: string): ModelFallbackFailureKind | undefined {
330
+ if (CANCELLED_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "cancelled";
331
+ if (NON_RETRYABLE_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "task_failure";
332
+ if (PROVIDER_REFUSAL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return "task_failure";
333
+ return undefined;
334
+ }
335
+
336
+ function fallbackKindFromMessage(message: string, name: string | undefined): ModelFallbackFailureKind | undefined {
337
+ const refusalKind = refusalKindFromMessage(message);
338
+ if (refusalKind !== undefined) return refusalKind;
339
+ const nameKind = kindFromCode(name);
340
+ if (nameKind !== undefined) return nameKind;
341
+ if (!RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(message))) return undefined;
342
+ if (/rate\s*limit|too many requests|\b429\b|quota|billing|credit/i.test(message)) return "rate_limit";
343
+ if (/auth|unauthori[sz]ed|\b40[13]\b|api key|token expired|forbidden|invalid key/i.test(message)) return "auth_on_candidate_provider";
344
+ if (/model.*(?:unavailable|disabled|not found|unknown)|(?:unavailable|disabled|not found|unknown).*model/i.test(message)) return "model_unavailable";
345
+ if (/network|fetch failed|socket|connection refused|timeout|timed? out/i.test(message)) return "network_timeout";
346
+ return "provider_unavailable";
347
+ }
348
+
349
+ function signalSource(value: unknown, fallback: ModelFallbackFailureSource | undefined): ModelFallbackFailureSource {
350
+ if (fallback !== undefined) return fallback;
351
+ if (stopReasonFrom(value) !== undefined || diagnosticErrors(value).length > 0) return "assistant_message";
352
+ if (value instanceof Error) return "throw";
353
+ return "structured";
354
+ }
355
+
356
+ function makeSignal(
357
+ kind: ModelFallbackFailureKind,
358
+ value: unknown,
359
+ source: ModelFallbackFailureSource | undefined,
360
+ ): ModelFallbackFailureSignal {
361
+ const status = statusFrom(value);
362
+ const code = codeFrom(value);
363
+ const name = errorName(value);
364
+ const stopReason = stopReasonFrom(value);
365
+ return {
366
+ kind,
367
+ message: modelFailureMessage(value),
368
+ source: signalSource(value, source),
369
+ ...(stopReason !== undefined ? { stopReason } : {}),
370
+ ...(status !== undefined ? { status } : {}),
371
+ ...(code !== undefined ? { code } : {}),
372
+ ...(name !== undefined ? { name } : {}),
373
+ };
374
+ }
375
+
376
+ function fallbackSignalFromMessage(
377
+ value: unknown,
378
+ source: ModelFallbackFailureSource | undefined,
379
+ ): ModelFallbackFailureSignal | undefined {
380
+ const message = modelFailureMessage(value);
381
+ if (!message.trim()) return undefined;
382
+ const kind = fallbackKindFromMessage(message, errorName(value));
383
+ return kind === undefined ? undefined : makeSignal(kind, value, source);
384
+ }
385
+
386
+ function classifyAssistantRefusalSignal(
387
+ value: unknown,
388
+ source: ModelFallbackFailureSource | undefined,
389
+ ): ModelFallbackFailureSignal | undefined {
390
+ const codeRefusalKind = refusalKindFromCode(codeFrom(value))
391
+ ?? refusalKindFromCode(errorName(value))
392
+ ?? refusalKindFromCode(finishReasonFrom(value));
393
+ if (codeRefusalKind !== undefined) return makeSignal(codeRefusalKind, value, source);
394
+
395
+ const messageRefusalKind = refusalKindFromMessage(directMessageFrom(value) ?? "");
396
+ return messageRefusalKind === undefined ? undefined : makeSignal(messageRefusalKind, value, source);
397
+ }
398
+
399
+ function isRefusalSignal(signal: ModelFallbackFailureSignal): boolean {
400
+ return signal.kind === "cancelled" || signal.kind === "task_failure";
401
+ }
402
+
403
+ function structuredSignal(
404
+ value: unknown,
405
+ seen: Set<unknown>,
406
+ source?: ModelFallbackFailureSource,
407
+ ): ModelFallbackFailureSignal | undefined {
408
+ if (value === undefined || value === null || seen.has(value)) return undefined;
409
+ if (typeof value === "object") seen.add(value);
410
+
411
+ const stopReason = stopReasonFrom(value)?.toLowerCase();
412
+ if (stopReason === "aborted") return makeSignal("cancelled", value, source);
413
+
414
+ const directRefusalSignal = classifyAssistantRefusalSignal(value, source);
415
+ if (directRefusalSignal !== undefined) return directRefusalSignal;
416
+
417
+ const codeKind = kindFromCode(codeFrom(value));
418
+ const nameKind = kindFromCode(errorName(value));
419
+ if (codeKind === "cancelled" || nameKind === "cancelled") return makeSignal("cancelled", value, source);
420
+
421
+ let firstNestedFallbackSignal: ModelFallbackFailureSignal | undefined;
422
+ const nestedSeen = new Set(seen);
423
+ for (const diagnosticError of diagnosticErrors(value)) {
424
+ const diagnosticSignal = structuredSignal(diagnosticError, nestedSeen, "diagnostic")
425
+ ?? fallbackSignalFromMessage(diagnosticError, "diagnostic");
426
+ if (diagnosticSignal === undefined) continue;
427
+ if (isRefusalSignal(diagnosticSignal)) return diagnosticSignal;
428
+ firstNestedFallbackSignal ??= diagnosticSignal;
429
+ }
430
+
431
+ const cause = causeOf(value);
432
+ const causeSignal = structuredSignal(cause, nestedSeen, source)
433
+ ?? fallbackSignalFromMessage(cause, source);
434
+ if (causeSignal !== undefined) {
435
+ if (isRefusalSignal(causeSignal)) return causeSignal;
436
+ firstNestedFallbackSignal ??= causeSignal;
437
+ }
438
+
439
+ const statusKind = kindFromStatus(statusFrom(value));
440
+ if (statusKind !== undefined) return makeSignal(statusKind, value, source);
441
+ if (codeKind !== undefined) return makeSignal(codeKind, value, source);
442
+ if (nameKind !== undefined) return makeSignal(nameKind, value, source);
443
+
444
+ if (firstNestedFallbackSignal !== undefined) return firstNestedFallbackSignal;
445
+
446
+ if (stopReason === "error") return makeSignal("provider_unavailable", value, source);
447
+
448
+ return undefined;
449
+ }
450
+
451
+ function messageFromUnknown(value: unknown, seen: Set<unknown>): string | undefined {
452
+ if (value === undefined || value === null || seen.has(value)) return undefined;
453
+ if (typeof value === "string") return value.trim().length > 0 ? value : undefined;
454
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
455
+ if (typeof value === "symbol" || typeof value === "function") return undefined;
456
+ seen.add(value);
457
+
458
+ if (value instanceof Error && value.message.trim().length > 0) return value.message;
459
+ const directMessage = directMessageFrom(value);
460
+ if (directMessage !== undefined) return directMessage;
461
+
462
+ for (const diagnosticError of diagnosticErrors(value)) {
463
+ const diagnosticMessage = messageFromUnknown(diagnosticError, seen);
464
+ if (diagnosticMessage !== undefined) return diagnosticMessage;
465
+ }
466
+
467
+ const causeMessage = messageFromUnknown(causeOf(value), seen);
468
+ if (causeMessage !== undefined) return causeMessage;
469
+
470
+ const stopReason = stopReasonFrom(value);
471
+ if (stopReason !== undefined) return `Assistant message ended with stopReason:${stopReason}`;
472
+ const finishReason = finishReasonFrom(value);
473
+ if (finishReason !== undefined) return `Model request finished with finish_reason:${finishReason}`;
474
+ const status = statusFrom(value);
475
+ if (status !== undefined) return `Model request failed with status ${status}`;
476
+ const code = codeFrom(value);
477
+ if (code !== undefined) return `Model request failed with code ${String(code)}`;
478
+
479
+ return undefined;
480
+ }
481
+
482
+ export function modelFailureMessage(error: unknown): string {
483
+ const structuredMessage = messageFromUnknown(error, new Set());
484
+ if (structuredMessage !== undefined) return structuredMessage;
485
+ const rendered = String(error);
486
+ return rendered === "[object Object]" ? "Model request failed" : rendered;
487
+ }
488
+
489
+ export function normalizeModelFailureSignal(error: unknown): ModelFallbackFailureSignal {
490
+ const structured = structuredSignal(error, new Set());
491
+ if (structured !== undefined) return structured;
492
+
493
+ const message = modelFailureMessage(error);
494
+ const name = errorName(error);
495
+ const fallbackKind = message.trim().length > 0
496
+ ? fallbackKindFromMessage(message, name)
497
+ : undefined;
498
+ return {
499
+ kind: fallbackKind ?? "unknown",
500
+ message,
501
+ source: "string_fallback",
502
+ ...(name !== undefined ? { name } : {}),
503
+ };
504
+ }
505
+
506
+ export function isRetryableModelFailure(error: unknown): boolean {
507
+ if (error === undefined) return false;
508
+ const signal = normalizeModelFailureSignal(error);
509
+ return FALLBACKABLE_FAILURE_KINDS.has(signal.kind);
101
510
  }
102
511
 
103
512
  export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {