@namzu/sdk 32.0.0 → 33.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  5. package/dist/bridge/a2a/mapper.js +13 -0
  6. package/dist/bridge/a2a/mapper.js.map +1 -1
  7. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  8. package/dist/bridge/sse/mapper.js +3 -0
  9. package/dist/bridge/sse/mapper.js.map +1 -1
  10. package/dist/connector/index.d.ts +1 -1
  11. package/dist/connector/index.d.ts.map +1 -1
  12. package/dist/connector/index.js +1 -1
  13. package/dist/connector/index.js.map +1 -1
  14. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  15. package/dist/connector/mcp/adapter.js +71 -16
  16. package/dist/connector/mcp/adapter.js.map +1 -1
  17. package/dist/connector/mcp/client.d.ts.map +1 -1
  18. package/dist/connector/mcp/client.js +3 -2
  19. package/dist/connector/mcp/client.js.map +1 -1
  20. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  21. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  22. package/dist/connector/mcp/http-redirect.js +31 -0
  23. package/dist/connector/mcp/http-redirect.js.map +1 -0
  24. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  25. package/dist/connector/mcp/http-sse.js +5 -0
  26. package/dist/connector/mcp/http-sse.js.map +1 -1
  27. package/dist/connector/mcp/image-admission.d.ts +16 -0
  28. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  29. package/dist/connector/mcp/image-admission.js +288 -0
  30. package/dist/connector/mcp/image-admission.js.map +1 -0
  31. package/dist/connector/mcp/stdio.d.ts +13 -0
  32. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  33. package/dist/connector/mcp/stdio.js +66 -23
  34. package/dist/connector/mcp/stdio.js.map +1 -1
  35. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  36. package/dist/connector/mcp/streamable-http.js +3 -0
  37. package/dist/connector/mcp/streamable-http.js.map +1 -1
  38. package/dist/constants/tools/index.js +1 -1
  39. package/dist/constants/tools/index.js.map +1 -1
  40. package/dist/execution/base.d.ts +13 -0
  41. package/dist/execution/base.d.ts.map +1 -1
  42. package/dist/execution/base.js +103 -8
  43. package/dist/execution/base.js.map +1 -1
  44. package/dist/execution/errors.d.ts +12 -0
  45. package/dist/execution/errors.d.ts.map +1 -0
  46. package/dist/execution/errors.js +21 -0
  47. package/dist/execution/errors.js.map +1 -0
  48. package/dist/execution/factory.d.ts.map +1 -1
  49. package/dist/execution/factory.js +1 -0
  50. package/dist/execution/factory.js.map +1 -1
  51. package/dist/execution/hybrid.d.ts +5 -0
  52. package/dist/execution/hybrid.d.ts.map +1 -1
  53. package/dist/execution/hybrid.js +36 -16
  54. package/dist/execution/hybrid.js.map +1 -1
  55. package/dist/execution/index.d.ts +1 -0
  56. package/dist/execution/index.d.ts.map +1 -1
  57. package/dist/execution/index.js +1 -0
  58. package/dist/execution/index.js.map +1 -1
  59. package/dist/execution/local.d.ts +13 -0
  60. package/dist/execution/local.d.ts.map +1 -1
  61. package/dist/execution/local.js +145 -19
  62. package/dist/execution/local.js.map +1 -1
  63. package/dist/execution/output.d.ts +27 -0
  64. package/dist/execution/output.d.ts.map +1 -0
  65. package/dist/execution/output.js +105 -0
  66. package/dist/execution/output.js.map +1 -0
  67. package/dist/execution/remote.d.ts +26 -2
  68. package/dist/execution/remote.d.ts.map +1 -1
  69. package/dist/execution/remote.js +86 -15
  70. package/dist/execution/remote.js.map +1 -1
  71. package/dist/plugin/lifecycle.d.ts.map +1 -1
  72. package/dist/plugin/lifecycle.js +9 -3
  73. package/dist/plugin/lifecycle.js.map +1 -1
  74. package/dist/process/kill-tree.d.ts +11 -13
  75. package/dist/process/kill-tree.d.ts.map +1 -1
  76. package/dist/process/kill-tree.js +11 -13
  77. package/dist/process/kill-tree.js.map +1 -1
  78. package/dist/provider/capabilities.d.ts +2 -0
  79. package/dist/provider/capabilities.d.ts.map +1 -1
  80. package/dist/provider/capabilities.js +6 -0
  81. package/dist/provider/capabilities.js.map +1 -1
  82. package/dist/provider/fallback.d.ts.map +1 -1
  83. package/dist/provider/fallback.js +31 -0
  84. package/dist/provider/fallback.js.map +1 -1
  85. package/dist/provider/idle-timeout.d.ts.map +1 -1
  86. package/dist/provider/idle-timeout.js +5 -0
  87. package/dist/provider/idle-timeout.js.map +1 -1
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +2 -2
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +2 -2
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/registry/tool/execute.d.ts +7 -1
  96. package/dist/registry/tool/execute.d.ts.map +1 -1
  97. package/dist/registry/tool/execute.js +201 -54
  98. package/dist/registry/tool/execute.js.map +1 -1
  99. package/dist/run/command-gate.d.ts.map +1 -1
  100. package/dist/run/command-gate.js +11 -0
  101. package/dist/run/command-gate.js.map +1 -1
  102. package/dist/run/workspace-fingerprint.d.ts +4 -4
  103. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  104. package/dist/run/workspace-fingerprint.js +6 -4
  105. package/dist/run/workspace-fingerprint.js.map +1 -1
  106. package/dist/runtime/bidi/index.d.ts +1 -1
  107. package/dist/runtime/bidi/index.d.ts.map +1 -1
  108. package/dist/runtime/bidi/index.js +1 -1
  109. package/dist/runtime/bidi/index.js.map +1 -1
  110. package/dist/runtime/bidi/session.d.ts +19 -0
  111. package/dist/runtime/bidi/session.d.ts.map +1 -1
  112. package/dist/runtime/bidi/session.js +150 -26
  113. package/dist/runtime/bidi/session.js.map +1 -1
  114. package/dist/runtime/query/executor.d.ts +43 -1
  115. package/dist/runtime/query/executor.d.ts.map +1 -1
  116. package/dist/runtime/query/executor.js +626 -161
  117. package/dist/runtime/query/executor.js.map +1 -1
  118. package/dist/runtime/query/index.d.ts +3 -1
  119. package/dist/runtime/query/index.d.ts.map +1 -1
  120. package/dist/runtime/query/index.js +48 -4
  121. package/dist/runtime/query/index.js.map +1 -1
  122. package/dist/runtime/query/iteration/index.d.ts +10 -0
  123. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  124. package/dist/runtime/query/iteration/index.js +67 -0
  125. package/dist/runtime/query/iteration/index.js.map +1 -1
  126. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  127. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  128. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  129. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  131. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  132. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  133. package/dist/runtime/query/request-rich-content.js +33 -13
  134. package/dist/runtime/query/request-rich-content.js.map +1 -1
  135. package/dist/runtime/query/result.d.ts.map +1 -1
  136. package/dist/runtime/query/result.js +25 -22
  137. package/dist/runtime/query/result.js.map +1 -1
  138. package/dist/runtime/query/resume-pending.d.ts +5 -1
  139. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  140. package/dist/runtime/query/resume-pending.js +66 -1
  141. package/dist/runtime/query/resume-pending.js.map +1 -1
  142. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  143. package/dist/runtime/query/tool-output-budget.js +45 -16
  144. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  145. package/dist/runtime/query/tooling.d.ts +6 -0
  146. package/dist/runtime/query/tooling.d.ts.map +1 -1
  147. package/dist/runtime/query/tooling.js +4 -0
  148. package/dist/runtime/query/tooling.js.map +1 -1
  149. package/dist/sandbox/provider/local.d.ts.map +1 -1
  150. package/dist/sandbox/provider/local.js +193 -41
  151. package/dist/sandbox/provider/local.js.map +1 -1
  152. package/dist/scheduler/delegating.d.ts +2 -1
  153. package/dist/scheduler/delegating.d.ts.map +1 -1
  154. package/dist/scheduler/delegating.js +6 -5
  155. package/dist/scheduler/delegating.js.map +1 -1
  156. package/dist/scheduler/local.d.ts +2 -1
  157. package/dist/scheduler/local.d.ts.map +1 -1
  158. package/dist/scheduler/local.js +2 -2
  159. package/dist/scheduler/local.js.map +1 -1
  160. package/dist/session/workspace/driver.d.ts +12 -4
  161. package/dist/session/workspace/driver.d.ts.map +1 -1
  162. package/dist/session/workspace/git-worktree.d.ts +17 -3
  163. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  164. package/dist/session/workspace/git-worktree.js +153 -34
  165. package/dist/session/workspace/git-worktree.js.map +1 -1
  166. package/dist/skills/registry.d.ts +15 -0
  167. package/dist/skills/registry.d.ts.map +1 -1
  168. package/dist/skills/registry.js +33 -0
  169. package/dist/skills/registry.js.map +1 -1
  170. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  171. package/dist/tools/builtins/computer-use.js +76 -2
  172. package/dist/tools/builtins/computer-use.js.map +1 -1
  173. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  174. package/dist/tools/builtins/run-code.js +9 -11
  175. package/dist/tools/builtins/run-code.js.map +1 -1
  176. package/dist/tools/builtins/skill.d.ts +2 -1
  177. package/dist/tools/builtins/skill.d.ts.map +1 -1
  178. package/dist/tools/builtins/skill.js +250 -6
  179. package/dist/tools/builtins/skill.js.map +1 -1
  180. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  181. package/dist/tools/coordinator/agent.js +100 -30
  182. package/dist/tools/coordinator/agent.js.map +1 -1
  183. package/dist/types/agent/manager.d.ts +1 -1
  184. package/dist/types/agent/manager.d.ts.map +1 -1
  185. package/dist/types/agent/scheduler.d.ts +10 -1
  186. package/dist/types/agent/scheduler.d.ts.map +1 -1
  187. package/dist/types/authorization/index.d.ts +12 -12
  188. package/dist/types/bidi/index.d.ts +19 -2
  189. package/dist/types/bidi/index.d.ts.map +1 -1
  190. package/dist/types/computer-use/index.d.ts +18 -0
  191. package/dist/types/computer-use/index.d.ts.map +1 -1
  192. package/dist/types/connector/execution-context.d.ts +3 -1
  193. package/dist/types/connector/execution-context.d.ts.map +1 -1
  194. package/dist/types/errors/catalog.d.ts.map +1 -1
  195. package/dist/types/errors/catalog.js +13 -6
  196. package/dist/types/errors/catalog.js.map +1 -1
  197. package/dist/types/errors/index.d.ts.map +1 -1
  198. package/dist/types/errors/index.js +21 -8
  199. package/dist/types/errors/index.js.map +1 -1
  200. package/dist/types/execution/index.d.ts +43 -1
  201. package/dist/types/execution/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.d.ts +11 -0
  203. package/dist/types/hitl/index.d.ts.map +1 -1
  204. package/dist/types/hitl/index.js.map +1 -1
  205. package/dist/types/message/index.d.ts +1 -1
  206. package/dist/types/message/index.d.ts.map +1 -1
  207. package/dist/types/message/index.js +2 -1
  208. package/dist/types/message/index.js.map +1 -1
  209. package/dist/types/plugin/index.d.ts +14 -1
  210. package/dist/types/plugin/index.d.ts.map +1 -1
  211. package/dist/types/plugin/index.js +1 -0
  212. package/dist/types/plugin/index.js.map +1 -1
  213. package/dist/types/provider/config.d.ts +9 -0
  214. package/dist/types/provider/config.d.ts.map +1 -1
  215. package/dist/types/provider/interface.d.ts +14 -0
  216. package/dist/types/provider/interface.d.ts.map +1 -1
  217. package/dist/types/run/events.d.ts +17 -0
  218. package/dist/types/run/events.d.ts.map +1 -1
  219. package/dist/types/run/events.js.map +1 -1
  220. package/dist/types/sandbox/index.d.ts +15 -10
  221. package/dist/types/sandbox/index.d.ts.map +1 -1
  222. package/dist/types/sandbox/index.js.map +1 -1
  223. package/dist/types/tool/index.d.ts +77 -6
  224. package/dist/types/tool/index.d.ts.map +1 -1
  225. package/dist/types/tool/index.js.map +1 -1
  226. package/dist/types/tool/presentation.d.ts +6 -2
  227. package/dist/types/tool/presentation.d.ts.map +1 -1
  228. package/dist/types/workspace/ref.d.ts +5 -0
  229. package/dist/types/workspace/ref.d.ts.map +1 -1
  230. package/dist/utils/abort.d.ts +8 -0
  231. package/dist/utils/abort.d.ts.map +1 -1
  232. package/dist/utils/abort.js +16 -0
  233. package/dist/utils/abort.js.map +1 -1
  234. package/package.json +4 -4
  235. package/src/authorization/gate.ts +1 -1
  236. package/src/bridge/a2a/mapper.ts +23 -4
  237. package/src/bridge/sse/mapper.ts +3 -0
  238. package/src/connector/index.ts +2 -0
  239. package/src/connector/mcp/adapter.ts +82 -21
  240. package/src/connector/mcp/client.ts +2 -1
  241. package/src/connector/mcp/http-redirect.ts +32 -0
  242. package/src/connector/mcp/http-sse.ts +5 -0
  243. package/src/connector/mcp/image-admission.ts +272 -0
  244. package/src/connector/mcp/stdio.ts +63 -21
  245. package/src/connector/mcp/streamable-http.ts +3 -0
  246. package/src/constants/tools/index.ts +1 -1
  247. package/src/execution/base.ts +124 -7
  248. package/src/execution/errors.ts +24 -0
  249. package/src/execution/factory.ts +1 -0
  250. package/src/execution/hybrid.ts +47 -16
  251. package/src/execution/index.ts +1 -0
  252. package/src/execution/local.ts +164 -19
  253. package/src/execution/output.ts +104 -0
  254. package/src/execution/remote.ts +122 -17
  255. package/src/plugin/lifecycle.ts +9 -3
  256. package/src/process/kill-tree.ts +11 -13
  257. package/src/provider/capabilities.ts +10 -0
  258. package/src/provider/fallback.ts +39 -0
  259. package/src/provider/idle-timeout.ts +8 -0
  260. package/src/provider/retry.ts +8 -0
  261. package/src/public-runtime.ts +7 -1
  262. package/src/registry/tool/execute.ts +230 -64
  263. package/src/run/command-gate.ts +12 -0
  264. package/src/run/workspace-fingerprint.ts +5 -4
  265. package/src/runtime/bidi/index.ts +1 -1
  266. package/src/runtime/bidi/session.ts +177 -28
  267. package/src/runtime/query/executor.ts +769 -163
  268. package/src/runtime/query/index.ts +58 -7
  269. package/src/runtime/query/iteration/index.ts +72 -0
  270. package/src/runtime/query/iteration/phases/context.ts +5 -0
  271. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  272. package/src/runtime/query/request-rich-content.ts +39 -14
  273. package/src/runtime/query/result.ts +25 -23
  274. package/src/runtime/query/resume-pending.ts +83 -1
  275. package/src/runtime/query/tool-output-budget.ts +52 -17
  276. package/src/runtime/query/tooling.ts +10 -0
  277. package/src/sandbox/provider/local.ts +213 -45
  278. package/src/scheduler/delegating.ts +6 -5
  279. package/src/scheduler/local.ts +3 -2
  280. package/src/session/workspace/driver.ts +12 -4
  281. package/src/session/workspace/git-worktree.ts +173 -34
  282. package/src/skills/registry.ts +47 -0
  283. package/src/tools/builtins/computer-use.ts +86 -2
  284. package/src/tools/builtins/run-code.ts +9 -11
  285. package/src/tools/builtins/skill.ts +325 -7
  286. package/src/tools/coordinator/agent.ts +108 -33
  287. package/src/types/agent/manager.ts +1 -1
  288. package/src/types/agent/scheduler.ts +10 -1
  289. package/src/types/bidi/index.ts +19 -2
  290. package/src/types/computer-use/index.ts +19 -0
  291. package/src/types/connector/execution-context.ts +3 -0
  292. package/src/types/errors/catalog.ts +14 -6
  293. package/src/types/errors/index.ts +22 -8
  294. package/src/types/execution/index.ts +46 -1
  295. package/src/types/hitl/index.ts +11 -0
  296. package/src/types/message/index.ts +3 -2
  297. package/src/types/plugin/index.ts +15 -0
  298. package/src/types/provider/config.ts +9 -0
  299. package/src/types/provider/interface.ts +18 -0
  300. package/src/types/run/events.ts +17 -3
  301. package/src/types/sandbox/index.ts +15 -10
  302. package/src/types/tool/index.ts +85 -6
  303. package/src/types/tool/presentation.ts +10 -3
  304. package/src/types/workspace/ref.ts +5 -0
  305. package/src/utils/abort.ts +18 -0
@@ -25,6 +25,7 @@ import { EmergencySaveManager } from '../../manager/run/emergency.js'
25
25
  import type { RunPersistence } from '../../manager/run/persistence.js'
26
26
  import { resolveModelPricing } from '../../pricing/index.js'
27
27
  import { resolveProviderCapabilities } from '../../provider/capabilities.js'
28
+ import { isCallerAbortError } from '../../provider/errors.js'
28
29
  import {
29
30
  type ProviderChainMember,
30
31
  type ServingMember,
@@ -77,6 +78,7 @@ import type { AgentPersona } from '../../types/persona/index.js'
77
78
  import type { LLMProvider } from '../../types/provider/index.js'
78
79
  import type { TaskRouterConfig } from '../../types/router/index.js'
79
80
  import type { ReviewAnswer } from '../../types/run/answer-review.js'
81
+ import { cancelCauseOf } from '../../types/run/cancel-cause.js'
80
82
  import type { CheckpointStore, FencingToken } from '../../types/run/checkpoint-store.js'
81
83
  import type { RunEventCursor, RunEventReplay } from '../../types/run/event-cursor.js'
82
84
  import { resolveRunEventReplay } from '../../types/run/event-cursor.js'
@@ -758,7 +760,9 @@ export interface QueryParams {
758
760
  * attachments against a `supportsVision: false` driver), the runtime
759
761
  * warns loudly, emits a `capability_warning` run event, and degrades
760
762
  * explicitly (tool surfaces stripped from prompt + request;
761
- * attachments left unmapped by the driver). `true`: throw instead of
763
+ * attachments left unmapped by the driver). The same policy is checked
764
+ * immediately before every request for image/document blocks produced by a
765
+ * tool, because those do not exist at run setup. `true`: throw instead of
762
766
  * degrading.
763
767
  */
764
768
  strictCapabilities?: boolean
@@ -1367,6 +1371,40 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1367
1371
  eventTranslator.wireActivityStore(ctx.activityStore, ctx.runId)
1368
1372
  eventTranslator.wirePlanManager(ctx.planManager, ctx.runId)
1369
1373
  eventTranslator.setGeneration(params.claimFence)
1374
+ let interruptHooksStarted = false
1375
+ const executeUserInterruptHooks = async (terminalError: unknown): Promise<void> => {
1376
+ if (
1377
+ interruptHooksStarted ||
1378
+ !params.pluginManager ||
1379
+ !isCallerAbortError(terminalError, ctx.abortController.signal) ||
1380
+ params.parentRunId !== undefined ||
1381
+ (params.depth ?? 0) !== 0 ||
1382
+ cancelCauseOf(ctx.abortController.signal.reason) !== 'user'
1383
+ ) {
1384
+ return
1385
+ }
1386
+
1387
+ interruptHooksStarted = true
1388
+ try {
1389
+ // Deliberately omit the already-aborted run signal. The lifecycle
1390
+ // manager still supplies each handler its own deadline signal, while
1391
+ // `run_interrupt`'s observational fan-out prevents one result from
1392
+ // suppressing the cleanup hooks that follow it.
1393
+ await params.pluginManager.executeHooks(
1394
+ 'run_interrupt',
1395
+ { runId: ctx.runId, cancelCause: 'user' },
1396
+ eventTranslator.emitEvent,
1397
+ )
1398
+ } catch (error) {
1399
+ // Cancellation is the terminal authority. A hook event sink or an
1400
+ // unexpected manager failure is reported, but cannot turn Stop into a
1401
+ // failed run or prevent the durable cancellation verdict.
1402
+ ctx.log.error('Run interrupt hooks did not settle cleanly', {
1403
+ [NAMZU.RUN_ID]: ctx.runId,
1404
+ ...errorAttributes(error),
1405
+ })
1406
+ }
1407
+ }
1370
1408
 
1371
1409
  if (attachmentResolutionCancelled) {
1372
1410
  // Attachment materialization happens before RunContext exists. Once it
@@ -1375,6 +1413,9 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1375
1413
  // and settle through the ordinary cancellation classifier. Prompt
1376
1414
  // contributions/cache, host callbacks, tools, plugins, sandbox, guardrails,
1377
1415
  // advisors, and providers are all authority-bearing work and stay out.
1416
+ // The dedicated root interrupt notification is the sole plugin exception:
1417
+ // it runs after cancellation under its own deadline and cannot regain model
1418
+ // or tool authority.
1378
1419
  if (params.resumeFromCheckpoint && !selectedResumeState) {
1379
1420
  // The canonical resume surface hands query the checkpoint state it
1380
1421
  // already selected. A raw resume query has no such snapshot; after
@@ -1460,6 +1501,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1460
1501
  const terminalError = ctx.abortController.signal.aborted
1461
1502
  ? ctx.abortController.signal.reason
1462
1503
  : error
1504
+ await executeUserInterruptHooks(terminalError)
1505
+ yield* eventTranslator.drainPending()
1463
1506
  yield* cancelledAssembler.handleError(terminalError, rootSpan)
1464
1507
  } finally {
1465
1508
  rootSpan.end()
@@ -1620,6 +1663,14 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1620
1663
  const questionParks = params.questionParks ?? new QuestionParkBinding()
1621
1664
  const pendingAnswers = params.pendingAnswers ?? new PendingAnswers()
1622
1665
 
1666
+ // One gate instance owns both model-issued calls and calls dispatched by
1667
+ // another tool. Constructing it only inside the iteration review left the
1668
+ // nested registry path outside the operator's policy entirely.
1669
+ const gateConfig = params.authorizationGate
1670
+ const verificationGate = gateConfig?.enabled
1671
+ ? new AuthorizationGate(gateConfig, ctx.log)
1672
+ : undefined
1673
+
1623
1674
  // is null only when the run has no disk layout (tests,
1624
1675
  // in-memory hosts); the budget then degrades to middle-elision.
1625
1676
  const runDirForTools = ctx.runMgr.getRunDir()
@@ -1660,6 +1711,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1660
1711
  // `read`/`grep` without a new affordance.
1661
1712
  ...(toolOutputDir ? { toolOutputDir } : {}),
1662
1713
  ...(params.repairToolCall ? { repairToolCall: params.repairToolCall } : {}),
1714
+ ...(verificationGate ? { authorizationGate: verificationGate } : {}),
1715
+ recordAudit: (input) => ctx.runMgr.recordAudit(input),
1663
1716
  // The durable pause, reachable from any tool rather than from the
1664
1717
  // four kernel-owned points that used to own it. Built here from
1665
1718
  // the machinery the run already holds; the recorder binds a few
@@ -1806,14 +1859,10 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1806
1859
  }
1807
1860
  }
1808
1861
 
1809
- const gateConfig = params.authorizationGate
1810
-
1811
- const verificationGate = gateConfig?.enabled
1812
- ? new AuthorizationGate(gateConfig, ctx.log)
1813
- : undefined
1814
-
1815
1862
  const iterationOrchestrator = new IterationOrchestrator({
1816
1863
  provider: resilientProvider,
1864
+ providerCapabilities: capabilities,
1865
+ strictCapabilities: params.strictCapabilities === true,
1817
1866
  servingMember: () => serving.current,
1818
1867
  runConfig,
1819
1868
  ...(params.stopWhen ? { stopWhen: params.stopWhen } : {}),
@@ -2581,6 +2630,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
2581
2630
  } catch (err) {
2582
2631
  // A failed run still spent its steps; report them.
2583
2632
  ctx.runMgr.setSteps(iterationOrchestrator.getSteps())
2633
+ await executeUserInterruptHooks(err)
2634
+ yield* eventTranslator.drainPending()
2584
2635
  yield* resultAssembler.handleError(err, rootSpan)
2585
2636
  } finally {
2586
2637
  // Release the process's termination path as soon as this run is
@@ -7,6 +7,7 @@ import {
7
7
  STRUCTURED_OUTPUT_REPROMPT,
8
8
  } from '../../../constants/tools/index.js'
9
9
  import { renderSkillsSection } from '../../../persona/assembler.js'
10
+ import { resolveProviderCapabilities } from '../../../provider/capabilities.js'
10
11
  import { collectChatCompletion } from '../../../provider/collect-chat-completion.js'
11
12
  import { formatCompletionNotification } from '../../../scheduler/completion-inbox.js'
12
13
  import {
@@ -19,8 +20,10 @@ import { getTracer } from '../../../telemetry/runtime-accessors.js'
19
20
  import { STRUCTURED_OUTPUT_TOOL_NAME } from '../../../tools/builtins/structuredOutput.js'
20
21
  import { DELEGATION_TIMEOUT_MS } from '../../../tools/coordinator/index.js'
21
22
  import type { CostInfo, TokenUsage } from '../../../types/common/index.js'
23
+ import { NamzuError } from '../../../types/errors/index.js'
22
24
  import type { MessageId } from '../../../types/ids/index.js'
23
25
  import {
26
+ type Message,
24
27
  createAssistantMessage,
25
28
  createRuntimeContextMessage,
26
29
  createSystemMessage,
@@ -148,6 +151,8 @@ export class IterationOrchestrator {
148
151
  * one process must not suppress each other's first envelope.
149
152
  */
150
153
  private lastEnvelopeKey: string | undefined
154
+ /** Rich tool blocks already reported; durable history is scanned every turn. */
155
+ private readonly warnedRichToolResults = new Set<string>()
151
156
  /**
152
157
  * The previous iteration held a `stopWhen` decision open for a worker.
153
158
  *
@@ -168,6 +173,70 @@ export class IterationOrchestrator {
168
173
  this.ctx = ctx
169
174
  }
170
175
 
176
+ /**
177
+ * Check the exact post-budget request for tool-result shapes the active driver
178
+ * cannot carry. Initial capability negotiation cannot see results produced by
179
+ * a later tool turn, so this boundary runs immediately before every provider
180
+ * call. Keys are durable call/block coordinates, which prevents old history
181
+ * from warning again on every subsequent iteration.
182
+ */
183
+ private async reportUnsupportedToolResults(messages: readonly Message[]): Promise<void> {
184
+ const capabilities =
185
+ this.ctx.providerCapabilities ?? resolveProviderCapabilities(this.ctx.provider)
186
+ const images: string[] = []
187
+ const documents: string[] = []
188
+ for (const message of messages) {
189
+ if (message.role !== 'tool' || !Array.isArray(message.content)) continue
190
+ for (const [index, block] of message.content.entries()) {
191
+ const key = `${message.toolCallId}:${index}:${block.type}`
192
+ if (this.warnedRichToolResults.has(key)) continue
193
+ if (block.type === 'image' && !capabilities.supportsToolResultImages) {
194
+ images.push(key)
195
+ }
196
+ if (block.type === 'document' && !capabilities.supportsToolResultDocuments) {
197
+ documents.push(key)
198
+ }
199
+ }
200
+ }
201
+
202
+ const report = async (
203
+ keys: readonly string[],
204
+ capability: 'vision' | 'documents',
205
+ label: 'image' | 'document',
206
+ ): Promise<void> => {
207
+ if (keys.length === 0) return
208
+ const message = `Provider '${this.ctx.provider.id}' declares it cannot map ${label} tool results, but this request carries ${keys.length} new ${label} block(s). The model will receive the driver's explicit text fallback instead of that content.`
209
+ if (this.ctx.strictCapabilities) {
210
+ throw new NamzuError({
211
+ code: 'capability_unavailable',
212
+ message,
213
+ details: {
214
+ providerId: this.ctx.provider.id,
215
+ capability,
216
+ blockCount: keys.length,
217
+ },
218
+ })
219
+ }
220
+ for (const key of keys) this.warnedRichToolResults.add(key)
221
+ this.ctx.log.warn('Capability mismatch: the provider cannot map rich tool results', {
222
+ 'namzu.capability.detail': message,
223
+ [GENAI.SYSTEM]: this.ctx.provider.id,
224
+ 'namzu.runtime.rich_tool_result_count': keys.length,
225
+ })
226
+ await this.ctx.emitEvent({
227
+ type: 'capability_warning',
228
+ runId: this.ctx.runMgr.id,
229
+ capability,
230
+ contentSource: 'tool-result',
231
+ providerId: this.ctx.provider.id,
232
+ message,
233
+ })
234
+ }
235
+
236
+ await report(images, 'vision', 'image')
237
+ await report(documents, 'documents', 'document')
238
+ }
239
+
171
240
  /**
172
241
  * Adopt the run's span after construction.
173
242
  *
@@ -459,6 +528,8 @@ export class IterationOrchestrator {
459
528
  requestHistory,
460
529
  this.ctx.runConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
461
530
  )
531
+ await this.reportUnsupportedToolResults(messages)
532
+ yield* this.ctx.drainPending()
462
533
 
463
534
  // What the model is about to be ASKED, recorded when it
464
535
  // changed. `run_started` carries one system prompt and tool
@@ -1986,6 +2057,7 @@ export class IterationOrchestrator {
1986
2057
  finalHistory,
1987
2058
  this.ctx.runConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
1988
2059
  )
2060
+ await this.reportUnsupportedToolResults(finalMessages)
1989
2061
 
1990
2062
  // Same cache discipline as the forced-final iteration: keep the
1991
2063
  // tools param identical to prior iterations (cache prefix intact,
@@ -7,6 +7,7 @@ import { NAMZU } from '../../../../constants/telemetry/index.js'
7
7
  import type { PlanManager } from '../../../../manager/plan/lifecycle.js'
8
8
  import type { RunPersistence } from '../../../../manager/run/persistence.js'
9
9
  import type { PromptContributionRegistry } from '../../../../prompt/contributions.js'
10
+ import type { ResolvedProviderCapabilities } from '../../../../provider/capabilities.js'
10
11
  import type { ServingMember } from '../../../../provider/fallback.js'
11
12
  import type { CompletionInbox } from '../../../../scheduler/completion-inbox.js'
12
13
  import type { ActivityStore } from '../../../../store/activity/memory.js'
@@ -44,6 +45,10 @@ import type { ToolGrantSet } from '../../tool-grants.js'
44
45
 
45
46
  export interface IterationContext {
46
47
  readonly provider: LLMProvider
48
+ /** Driver-level request shapes negotiated for this run. */
49
+ readonly providerCapabilities?: ResolvedProviderCapabilities
50
+ /** Refuse a capability mismatch instead of emitting a warning and degrading. */
51
+ readonly strictCapabilities?: boolean
47
52
  /**
48
53
  * Which chain member `provider` will route the NEXT request to.
49
54
  *
@@ -1,7 +1,8 @@
1
1
  import type { AuthorizationGate } from '../../../../authorization/index.js'
2
+ import type { ToolCallSummary } from '../../../../types/hitl/index.js'
2
3
  import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
3
4
  import type { RunEvent } from '../../../../types/run/index.js'
4
- import type { ToolCallDenials } from '../../executor.js'
5
+ import type { PreparedToolBatch, ToolCallDenials } from '../../executor.js'
5
6
  import {
6
7
  awaitProjectInstructionCallback,
7
8
  replaceProjectInstructionSnapshot,
@@ -64,28 +65,42 @@ export async function* runToolReview(
64
65
  return finish('executed')
65
66
  }
66
67
 
67
- const toolCallSummaries = toolCalls.map((tc) => {
68
- let input: unknown
69
- try {
70
- input = JSON.parse(tc.function.arguments)
71
- } catch {
72
- input = tc.function.arguments
73
- }
74
- const tool = ctx.tools.get(tc.function.name)
75
- const isDestructive = tool?.isDestructive ? tool.isDestructive(input) : false
76
-
77
- return {
78
- id: tc.id,
79
- name: tc.function.name,
80
- input,
81
- isDestructive,
82
- }
83
- })
68
+ const prepareForReview = async (): Promise<PreparedToolBatch | undefined> => {
69
+ const prepare = ctx.toolExecutor.prepareBatchForReview
70
+ return typeof prepare === 'function' ? prepare.call(ctx.toolExecutor, response) : undefined
71
+ }
72
+ let preparedBatch = await prepareForReview()
73
+ const summariesFor = (prepared: PreparedToolBatch | undefined): ToolCallSummary[] => {
74
+ const calls =
75
+ prepared?.reviewCalls ??
76
+ toolCalls.map((tc) => {
77
+ let input: unknown
78
+ try {
79
+ input = JSON.parse(tc.function.arguments)
80
+ } catch {
81
+ input = tc.function.arguments
82
+ }
83
+ return { id: tc.id, name: tc.function.name, input }
84
+ })
85
+ return calls.map((tc) => {
86
+ const tool = ctx.tools.get(tc.name)
87
+ const isDestructive = tool?.isDestructive ? tool.isDestructive(tc.input) : false
88
+
89
+ return {
90
+ id: tc.id,
91
+ name: tc.name,
92
+ input: tc.input,
93
+ isDestructive,
94
+ authorization: { decision: 'review' },
95
+ }
96
+ })
97
+ }
98
+ let toolCallSummaries = summariesFor(preparedBatch)
84
99
 
85
100
  /** Executes the batch, answering every call, and appends the results. */
86
101
  const settle = async (denials?: ToolCallDenials): Promise<void> => {
87
102
  const startedAt = Date.now()
88
- const batch = await ctx.toolExecutor.executeBatch(response, denials)
103
+ const batch = await ctx.toolExecutor.executeBatch(response, denials, undefined, preparedBatch)
89
104
  toolMs += Date.now() - startedAt
90
105
  executed = batch.results
91
106
  // Recorded AFTER execution and never before it: this advises, it does
@@ -130,6 +145,12 @@ export async function* runToolReview(
130
145
  }
131
146
  }
132
147
 
148
+ if (toolCallSummaries.length === 0) {
149
+ await settle()
150
+ yield* ctx.drainPending()
151
+ return finish('executed')
152
+ }
153
+
133
154
  /** Every call denied for the same reason (human rejection, gate stop). */
134
155
  const denyAll = (reason: string): ToolCallDenials =>
135
156
  new Map(toolCalls.map((tc) => [tc.id, reason]))
@@ -159,10 +180,16 @@ export async function* runToolReview(
159
180
  toolDef: ctx.tools.get(tc.name),
160
181
  }),
161
182
  }))
183
+ for (const { toolCall, gateResult } of gateResults) {
184
+ toolCall.authorization = {
185
+ decision: gateResult.decision,
186
+ ...(gateResult.reason ? { reason: gateResult.reason } : {}),
187
+ }
188
+ }
162
189
 
163
190
  for (const gr of gateResults) {
164
191
  if (gr.gateResult.decision === 'deny') {
165
- const reason = `Blocked by the verification gate: ${gr.gateResult.reason}`
192
+ const reason = `Blocked by the authorization gate: ${gr.gateResult.reason}`
166
193
  gateDenied.set(gr.toolCall.id, reason)
167
194
  // A gate denial is a refusal — first-class in the audit trail, never
168
195
  // an absent record (LOG-14, design §5). Written here, once per
@@ -180,7 +207,7 @@ export async function* runToolReview(
180
207
  const allDenied = gateResults.every((gr) => gr.gateResult.decision === 'deny')
181
208
 
182
209
  if (allAllowed) {
183
- ctx.log.debug('Verification gate: all tool calls pre-approved', {
210
+ ctx.log.debug('Authorization gate: all tool calls pre-approved', {
184
211
  'namzu.tool.names': gateResults.map((gr) => gr.toolCall.name),
185
212
  })
186
213
  await settle()
@@ -189,7 +216,7 @@ export async function* runToolReview(
189
216
  }
190
217
 
191
218
  if (allDenied) {
192
- ctx.log.debug('Verification gate: all tool calls denied', {
219
+ ctx.log.debug('Authorization gate: all tool calls denied', {
193
220
  'namzu.tool.names': gateResults.map((gr) => gr.toolCall.name),
194
221
  })
195
222
  await settle(gateDenied)
@@ -197,7 +224,7 @@ export async function* runToolReview(
197
224
  return finish('rejected')
198
225
  }
199
226
 
200
- ctx.log.debug('Verification gate: mixed decisions, proceeding to review', {
227
+ ctx.log.debug('Authorization gate: mixed decisions, proceeding to review', {
201
228
  'namzu.runtime.decisions': gateResults.map((gr) => ({
202
229
  tool: gr.toolCall.name,
203
230
  decision: gr.gateResult.decision,
@@ -267,6 +294,7 @@ export async function* runToolReview(
267
294
 
268
295
  // Gate denials are the floor; per-call human denials add to them.
269
296
  const denials = new Map(gateDenied)
297
+ const modifiedCallIds = new Set<string>()
270
298
 
271
299
  for (const mod of reviewDecision.modifications) {
272
300
  if (mod.action === 'modify' && mod.modifiedInput !== undefined) {
@@ -274,6 +302,7 @@ export async function* runToolReview(
274
302
  // A modification cannot resurrect a gate-denied call.
275
303
  if (tc && !denials.has(tc.id)) {
276
304
  tc.function.arguments = JSON.stringify(mod.modifiedInput)
305
+ modifiedCallIds.add(tc.id)
277
306
  }
278
307
  }
279
308
  if (mod.action === 'deny' && !denials.has(mod.toolCallId)) {
@@ -281,6 +310,41 @@ export async function* runToolReview(
281
310
  }
282
311
  }
283
312
 
313
+ // A human modification changes the raw call after the first preparation.
314
+ // Decode it once again, then require policy to explicitly allow the new
315
+ // executable value. A second nested review would be ambiguous: the human
316
+ // edited raw JSON, not an unseen schema transform of it.
317
+ if (modifiedCallIds.size > 0) {
318
+ const reprepare = ctx.toolExecutor.reprepareBatchForReview
319
+ preparedBatch =
320
+ preparedBatch && typeof reprepare === 'function'
321
+ ? await reprepare.call(ctx.toolExecutor, response, preparedBatch, modifiedCallIds)
322
+ : await prepareForReview()
323
+ toolCallSummaries = summariesFor(preparedBatch)
324
+ }
325
+ if (ctx.verificationGate && modifiedCallIds.size > 0) {
326
+ for (const summary of toolCallSummaries) {
327
+ if (!modifiedCallIds.has(summary.id)) continue
328
+ if (denials.has(summary.id)) continue
329
+ const gateResult = ctx.verificationGate.evaluate({
330
+ toolName: summary.name,
331
+ toolInput: summary.input,
332
+ toolDef: ctx.tools.get(summary.name),
333
+ })
334
+ if (gateResult.decision === 'allow') continue
335
+ const reason =
336
+ gateResult.decision === 'deny'
337
+ ? `Blocked by the authorization gate after the tool input was modified: ${gateResult.reason}`
338
+ : `Blocked by the authorization gate after the tool input was modified: the prepared value requires a new explicit approval. ${gateResult.reason}`
339
+ denials.set(summary.id, reason)
340
+ await ctx.runMgr.recordAudit({
341
+ what: { action: 'tool_call', tool: summary.name },
342
+ outcome: 'refused',
343
+ reason,
344
+ })
345
+ }
346
+ }
347
+
284
348
  const everythingDenied = denials.size === toolCalls.length
285
349
  await settle(denials)
286
350
  yield* ctx.drainPending()
@@ -34,7 +34,7 @@ interface RichOccurrence {
34
34
  readonly source: 'user' | 'tool'
35
35
  readonly kind: RichKind
36
36
  readonly bytes: number
37
- readonly reason: 'budget' | 'provider-rejected'
37
+ readonly reason: 'budget' | 'provider-rejected' | 'invalid-image'
38
38
  }
39
39
 
40
40
  export interface RequestImageIdentity {
@@ -54,6 +54,12 @@ const rejectedUserImageMarker = (): string =>
54
54
  const rejectedToolImageMarker = (): string =>
55
55
  '[image omitted from this model request because the provider rejected this image; call the producing tool again after correcting its image source if it is still needed.]'
56
56
 
57
+ const invalidUserImageMarker = (): string =>
58
+ '[image omitted from this model request because its encoded bytes are not a complete supported raster matching the declared media type; attach a corrected image in a new message if it is still needed.]'
59
+
60
+ const invalidToolImageMarker = (): string =>
61
+ '[image omitted from this model request because the producing tool returned encoded bytes that are not a complete supported raster matching the declared media type; call the tool again after correcting its image source if it is still needed.]'
62
+
57
63
  function assertInlineAttachment(
58
64
  attachment: MessageAttachment,
59
65
  ): asserts attachment is Exclude<MessageAttachment, { readonly type: 'stored' }> {
@@ -78,10 +84,12 @@ function collectRichOccurrences(messages: readonly Message[]): RichOccurrence[]
78
84
  kind: attachment.type === 'document' ? 'document' : 'image',
79
85
  bytes: attachment.data.length,
80
86
  reason:
81
- attachment.type !== 'document' &&
82
- attachment.modelOmission?.reason === 'provider-rejected'
83
- ? 'provider-rejected'
84
- : 'budget',
87
+ attachment.type !== 'document' && attachment.modelOmission?.reason === 'invalid-image'
88
+ ? 'invalid-image'
89
+ : attachment.type !== 'document' &&
90
+ attachment.modelOmission?.reason === 'provider-rejected'
91
+ ? 'provider-rejected'
92
+ : 'budget',
85
93
  })
86
94
  }
87
95
  continue
@@ -97,9 +105,11 @@ function collectRichOccurrences(messages: readonly Message[]): RichOccurrence[]
97
105
  kind: block.type,
98
106
  bytes: block.data.length,
99
107
  reason:
100
- block.type === 'image' && block.modelOmission?.reason === 'provider-rejected'
101
- ? 'provider-rejected'
102
- : 'budget',
108
+ block.type === 'image' && block.modelOmission?.reason === 'invalid-image'
109
+ ? 'invalid-image'
110
+ : block.type === 'image' && block.modelOmission?.reason === 'provider-rejected'
111
+ ? 'provider-rejected'
112
+ : 'budget',
103
113
  })
104
114
  }
105
115
  }
@@ -117,7 +127,9 @@ function appendMarkers(content: string, occurrences: readonly RichOccurrence[]):
117
127
  const markers = occurrences.map((occurrence) =>
118
128
  occurrence.reason === 'provider-rejected'
119
129
  ? rejectedUserImageMarker()
120
- : userMarker(occurrence.kind),
130
+ : occurrence.reason === 'invalid-image'
131
+ ? invalidUserImageMarker()
132
+ : userMarker(occurrence.kind),
121
133
  )
122
134
  return appendTextMarkers(content, markers)
123
135
  }
@@ -146,7 +158,9 @@ export function projectRequestRichContent(messages: Message[], maxBytes: number)
146
158
  }
147
159
 
148
160
  for (const occurrence of occurrences) {
149
- if (occurrence.reason === 'provider-rejected') omit(occurrence)
161
+ if (occurrence.reason === 'provider-rejected' || occurrence.reason === 'invalid-image') {
162
+ omit(occurrence)
163
+ }
150
164
  }
151
165
 
152
166
  let total = occurrences.reduce(
@@ -189,7 +203,9 @@ export function projectRequestRichContent(messages: Message[], maxBytes: number)
189
203
  text:
190
204
  occurrence.reason === 'provider-rejected'
191
205
  ? rejectedToolImageMarker()
192
- : toolMarker(occurrence.kind),
206
+ : occurrence.reason === 'invalid-image'
207
+ ? invalidToolImageMarker()
208
+ : toolMarker(occurrence.kind),
193
209
  }
194
210
  : block
195
211
  })
@@ -220,7 +236,10 @@ export function findSingleRequestImage(messages: readonly Message[]): RequestIma
220
236
  }
221
237
  if (attachment.type === 'document') continue
222
238
  if (candidate === null) {
223
- candidate = { data: attachment.data, mediaType: attachment.mediaType }
239
+ candidate = {
240
+ data: attachment.data,
241
+ mediaType: attachment.mediaType,
242
+ }
224
243
  } else if (!sameImage(attachment, candidate)) {
225
244
  return null
226
245
  }
@@ -302,7 +321,10 @@ export function markProviderRejectedImage(
302
321
  }
303
322
  changed = true
304
323
  count += 1
305
- return { ...attachment, modelOmission: { reason: 'provider-rejected' as const } }
324
+ return {
325
+ ...attachment,
326
+ modelOmission: { reason: 'provider-rejected' as const },
327
+ }
306
328
  })
307
329
  return changed ? { ...message, attachments } : message
308
330
  }
@@ -318,7 +340,10 @@ export function markProviderRejectedImage(
318
340
  }
319
341
  changed = true
320
342
  count += 1
321
- return { ...block, modelOmission: { reason: 'provider-rejected' as const } }
343
+ return {
344
+ ...block,
345
+ modelOmission: { reason: 'provider-rejected' as const },
346
+ }
322
347
  })
323
348
  return changed ? { ...message, content } : message
324
349
  })
@@ -140,6 +140,28 @@ export class ResultAssembler {
140
140
  // later, discarding every field of it. `toPlatformError` is the
141
141
  // projection that was written for exactly this and had no callers.
142
142
  const failure = toPlatformError(err)
143
+ // The driver's classification and the operator explanation describe the
144
+ // throwable, not the terminal verdict. Compute them before choosing paused
145
+ // versus failed so a recoverable run does not become the one path that
146
+ // discards the reason and remedy a host needs in order to recover it.
147
+ const providerError = isProviderRequestError(err)
148
+ ? {
149
+ kind: err.kind,
150
+ providerId: err.providerId,
151
+ ...(err.providerCode !== undefined ? { providerCode: err.providerCode } : {}),
152
+ ...(err.status !== undefined ? { status: err.status } : {}),
153
+ ...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
154
+ // The provider's own sentence, already truncated and scrubbed
155
+ // by the driver. Without it a host rendering this metadata
156
+ // knows a request was rejected but not which field, and has to
157
+ // re-parse prose to find out.
158
+ ...(err.detail !== undefined ? { detail: err.detail } : {}),
159
+ }
160
+ : undefined
161
+ // Classification is structural; remediation is editorial. The catalog is
162
+ // optional because inventing advice for an uncharacterised failure is worse
163
+ // than presenting the reason alone.
164
+ const explanation = explainError(err) ?? undefined
143
165
 
144
166
  // A transient failure that survived every in-turn recovery is not the
145
167
  // same thing as a bad API key, and settling both as `failed` gave the
@@ -157,6 +179,9 @@ export class ResultAssembler {
157
179
  runId: runMgr.id,
158
180
  checkpointId: resumeFrom,
159
181
  reason: errorMessage,
182
+ failure,
183
+ ...(providerError ? { providerError } : {}),
184
+ ...(explanation ? { explanation } : {}),
160
185
  })
161
186
  yield* drainPending()
162
187
 
@@ -177,35 +202,12 @@ export class ResultAssembler {
177
202
  return
178
203
  }
179
204
 
180
- // The driver's classification, carried onto the run so a host can
181
- // branch on WHAT failed without re-parsing a sentence.
182
- const providerError = isProviderRequestError(err)
183
- ? {
184
- kind: err.kind,
185
- providerId: err.providerId,
186
- ...(err.providerCode !== undefined ? { providerCode: err.providerCode } : {}),
187
- ...(err.status !== undefined ? { status: err.status } : {}),
188
- ...(err.retryAfterMs !== undefined ? { retryAfterMs: err.retryAfterMs } : {}),
189
- // The provider's own sentence, already truncated and scrubbed
190
- // by the driver. Without it a host rendering this metadata
191
- // knows a request was rejected but not which field, and has to
192
- // go re-parse `error` to find out — which is exactly the
193
- // re-parsing the line above says this exists to avoid.
194
- ...(err.detail !== undefined ? { detail: err.detail } : {}),
195
- }
196
- : undefined
197
205
  runMgr.markFailed(errorMessage, providerError)
198
206
 
199
207
  if (planManager.isActive) {
200
208
  planManager.failPlan(errorMessage)
201
209
  }
202
210
 
203
- // The classification says what kind of failure it is; the catalog
204
- // says what a person should do about it. Keeping them separate is the
205
- // point — classification is structural and belongs at the boundary,
206
- // remediation is editorial and belongs in a list a human appends to.
207
- const explanation = explainError(err) ?? undefined
208
-
209
211
  // Same terminal-verdict recording as the success path in completeRun —
210
212
  // see LOG-14, design §5. Placed AFTER the early `resumeFrom !== undefined`
211
213
  // return above, so a paused/resumable run is never audited as 'failure'.