@qduc/term2 0.32.2 → 0.34.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 (435) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +30 -14
  3. package/dist/agent.js.map +1 -1
  4. package/dist/build-info.json +2 -2
  5. package/dist/completion-verification-admin.d.ts +2 -0
  6. package/dist/completion-verification-admin.d.ts.map +1 -0
  7. package/dist/completion-verification-admin.js +37 -0
  8. package/dist/completion-verification-admin.js.map +1 -0
  9. package/dist/components/input/MenuSurface.d.ts.map +1 -1
  10. package/dist/components/input/MenuSurface.js +12 -3
  11. package/dist/components/input/MenuSurface.js.map +1 -1
  12. package/dist/components/input/ModelMenuSession.d.ts.map +1 -1
  13. package/dist/components/input/ModelMenuSession.js +15 -1
  14. package/dist/components/input/ModelMenuSession.js.map +1 -1
  15. package/dist/components/input/ProviderMenuSession.d.ts.map +1 -1
  16. package/dist/components/input/ProviderMenuSession.js +2 -2
  17. package/dist/components/input/ProviderMenuSession.js.map +1 -1
  18. package/dist/components/input/SettingsMenuSession.d.ts.map +1 -1
  19. package/dist/components/input/SettingsMenuSession.js +119 -82
  20. package/dist/components/input/SettingsMenuSession.js.map +1 -1
  21. package/dist/components/input/SettingsValueMenuSession.d.ts.map +1 -1
  22. package/dist/components/input/SettingsValueMenuSession.js +18 -3
  23. package/dist/components/input/SettingsValueMenuSession.js.map +1 -1
  24. package/dist/components/input/determine-active-menu.d.ts.map +1 -1
  25. package/dist/components/input/determine-active-menu.js +5 -1
  26. package/dist/components/input/determine-active-menu.js.map +1 -1
  27. package/dist/components/input/settings-intent-host.d.ts.map +1 -1
  28. package/dist/components/input/settings-intent-host.js +60 -4
  29. package/dist/components/input/settings-intent-host.js.map +1 -1
  30. package/dist/components/menu/ModelSelectionMenu.d.ts +4 -0
  31. package/dist/components/menu/ModelSelectionMenu.d.ts.map +1 -1
  32. package/dist/components/menu/ModelSelectionMenu.js +15 -16
  33. package/dist/components/menu/ModelSelectionMenu.js.map +1 -1
  34. package/dist/components/menu/ProviderSelectionMenu.d.ts +1 -0
  35. package/dist/components/menu/ProviderSelectionMenu.d.ts.map +1 -1
  36. package/dist/components/menu/ProviderSelectionMenu.js +21 -6
  37. package/dist/components/menu/ProviderSelectionMenu.js.map +1 -1
  38. package/dist/components/menu/SettingsSelectionMenu.d.ts +3 -0
  39. package/dist/components/menu/SettingsSelectionMenu.d.ts.map +1 -1
  40. package/dist/components/menu/SettingsSelectionMenu.js +78 -47
  41. package/dist/components/menu/SettingsSelectionMenu.js.map +1 -1
  42. package/dist/components/menu/SettingsValueSelectionMenu.d.ts +2 -0
  43. package/dist/components/menu/SettingsValueSelectionMenu.d.ts.map +1 -1
  44. package/dist/components/menu/SettingsValueSelectionMenu.js +17 -12
  45. package/dist/components/menu/SettingsValueSelectionMenu.js.map +1 -1
  46. package/dist/components/menu/settings-value-formatter.d.ts +8 -3
  47. package/dist/components/menu/settings-value-formatter.d.ts.map +1 -1
  48. package/dist/components/menu/settings-value-formatter.js +55 -32
  49. package/dist/components/menu/settings-value-formatter.js.map +1 -1
  50. package/dist/components/message/command-message-helpers.d.ts +5 -5
  51. package/dist/contracts/run-termination.d.ts +3 -1
  52. package/dist/contracts/run-termination.d.ts.map +1 -1
  53. package/dist/hooks/settings-completion-config.d.ts +28 -12
  54. package/dist/hooks/settings-completion-config.d.ts.map +1 -1
  55. package/dist/hooks/settings-completion-config.js +73 -76
  56. package/dist/hooks/settings-completion-config.js.map +1 -1
  57. package/dist/hooks/settings-completion-logic.d.ts +10 -1
  58. package/dist/hooks/settings-completion-logic.d.ts.map +1 -1
  59. package/dist/hooks/settings-completion-logic.js +80 -49
  60. package/dist/hooks/settings-completion-logic.js.map +1 -1
  61. package/dist/hooks/use-provider-selection.d.ts +6 -0
  62. package/dist/hooks/use-provider-selection.d.ts.map +1 -1
  63. package/dist/hooks/use-provider-selection.js +69 -10
  64. package/dist/hooks/use-provider-selection.js.map +1 -1
  65. package/dist/hooks/use-settings-completion.d.ts +14 -0
  66. package/dist/hooks/use-settings-completion.d.ts.map +1 -1
  67. package/dist/hooks/use-settings-completion.js +54 -6
  68. package/dist/hooks/use-settings-completion.js.map +1 -1
  69. package/dist/hooks/use-settings-value-completion.d.ts.map +1 -1
  70. package/dist/hooks/use-settings-value-completion.js +15 -4
  71. package/dist/hooks/use-settings-value-completion.js.map +1 -1
  72. package/dist/lib/agent-chat-service.d.ts.map +1 -1
  73. package/dist/lib/agent-chat-service.js +2 -6
  74. package/dist/lib/agent-chat-service.js.map +1 -1
  75. package/dist/lib/agent-client.d.ts +2 -1
  76. package/dist/lib/agent-client.d.ts.map +1 -1
  77. package/dist/lib/agent-client.js +99 -85
  78. package/dist/lib/agent-client.js.map +1 -1
  79. package/dist/lib/agent-factory.d.ts.map +1 -1
  80. package/dist/lib/agent-factory.js +17 -19
  81. package/dist/lib/agent-factory.js.map +1 -1
  82. package/dist/non-interactive.d.ts.map +1 -1
  83. package/dist/non-interactive.js +4 -0
  84. package/dist/non-interactive.js.map +1 -1
  85. package/dist/prompts/tool-surface-guidance.js +1 -1
  86. package/dist/prompts/tool-surface-guidance.js.map +1 -1
  87. package/dist/prompts/tool-surface-guidance.ts +1 -1
  88. package/dist/providers/ai-sdk-anthropic.provider.d.ts.map +1 -1
  89. package/dist/providers/ai-sdk-anthropic.provider.js +16 -2
  90. package/dist/providers/ai-sdk-anthropic.provider.js.map +1 -1
  91. package/dist/providers/ai-sdk-google.provider.d.ts.map +1 -1
  92. package/dist/providers/ai-sdk-google.provider.js +16 -2
  93. package/dist/providers/ai-sdk-google.provider.js.map +1 -1
  94. package/dist/providers/ai-sdk-openrouter.provider.d.ts.map +1 -1
  95. package/dist/providers/ai-sdk-openrouter.provider.js +16 -1
  96. package/dist/providers/ai-sdk-openrouter.provider.js.map +1 -1
  97. package/dist/providers/ai-sdk-provider-settings.d.ts +2 -0
  98. package/dist/providers/ai-sdk-provider-settings.d.ts.map +1 -1
  99. package/dist/providers/ai-sdk-provider-settings.js +5 -0
  100. package/dist/providers/ai-sdk-provider-settings.js.map +1 -1
  101. package/dist/providers/ai-sdk-streamed-model.d.ts +3 -3
  102. package/dist/providers/ai-sdk-streamed-model.d.ts.map +1 -1
  103. package/dist/providers/ai-sdk-streamed-model.js +5 -69
  104. package/dist/providers/ai-sdk-streamed-model.js.map +1 -1
  105. package/dist/providers/anthropic-chat-caching.d.ts +5 -0
  106. package/dist/providers/anthropic-chat-caching.d.ts.map +1 -0
  107. package/dist/providers/{common/openai-compatible-messages.js → anthropic-chat-caching.js} +19 -1
  108. package/dist/providers/anthropic-chat-caching.js.map +1 -0
  109. package/dist/providers/codex.provider.js +2 -0
  110. package/dist/providers/codex.provider.js.map +1 -1
  111. package/dist/providers/custom-provider-runtime.d.ts +18 -0
  112. package/dist/providers/custom-provider-runtime.d.ts.map +1 -0
  113. package/dist/providers/custom-provider-runtime.js +26 -0
  114. package/dist/providers/custom-provider-runtime.js.map +1 -0
  115. package/dist/providers/grok.provider.d.ts +13 -3
  116. package/dist/providers/grok.provider.d.ts.map +1 -1
  117. package/dist/providers/grok.provider.js +82 -6
  118. package/dist/providers/grok.provider.js.map +1 -1
  119. package/dist/providers/llama-cpp.provider.d.ts +3 -0
  120. package/dist/providers/llama-cpp.provider.d.ts.map +1 -1
  121. package/dist/providers/llama-cpp.provider.js +17 -0
  122. package/dist/providers/llama-cpp.provider.js.map +1 -1
  123. package/dist/providers/model-catalog/catalog.generated.d.ts +347 -351
  124. package/dist/providers/model-catalog/catalog.generated.d.ts.map +1 -1
  125. package/dist/providers/model-catalog/catalog.generated.js +362 -357
  126. package/dist/providers/model-catalog/catalog.generated.js.map +1 -1
  127. package/dist/providers/neuralwatt.provider.d.ts +6 -0
  128. package/dist/providers/neuralwatt.provider.d.ts.map +1 -0
  129. package/dist/providers/neuralwatt.provider.js +8 -0
  130. package/dist/providers/neuralwatt.provider.js.map +1 -0
  131. package/dist/providers/openai-compatible-lazy.d.ts.map +1 -1
  132. package/dist/providers/openai-compatible-lazy.js +2 -0
  133. package/dist/providers/openai-compatible-lazy.js.map +1 -1
  134. package/dist/providers/openai-compatible-middleware.d.ts +4 -9
  135. package/dist/providers/openai-compatible-middleware.d.ts.map +1 -1
  136. package/dist/providers/openai-compatible-middleware.js +25 -48
  137. package/dist/providers/openai-compatible-middleware.js.map +1 -1
  138. package/dist/providers/openai-compatible-models.d.ts +9 -1
  139. package/dist/providers/openai-compatible-models.d.ts.map +1 -1
  140. package/dist/providers/openai-compatible-models.js +10 -2
  141. package/dist/providers/openai-compatible-models.js.map +1 -1
  142. package/dist/providers/openai-compatible-response-normalizer.d.ts +1 -1
  143. package/dist/providers/openai-compatible-response-normalizer.d.ts.map +1 -1
  144. package/dist/providers/openai-compatible-response-normalizer.js +6 -6
  145. package/dist/providers/openai-compatible-response-normalizer.js.map +1 -1
  146. package/dist/providers/openai-compatible.provider.d.ts +2 -32
  147. package/dist/providers/openai-compatible.provider.d.ts.map +1 -1
  148. package/dist/providers/openai-compatible.provider.js +27 -144
  149. package/dist/providers/openai-compatible.provider.js.map +1 -1
  150. package/dist/providers/openai.provider.d.ts.map +1 -1
  151. package/dist/providers/openai.provider.js +1 -0
  152. package/dist/providers/openai.provider.js.map +1 -1
  153. package/dist/providers/opencode-middleware.d.ts +9 -0
  154. package/dist/providers/opencode-middleware.d.ts.map +1 -0
  155. package/dist/providers/opencode-middleware.js +15 -0
  156. package/dist/providers/opencode-middleware.js.map +1 -0
  157. package/dist/providers/opencode-model-provider.d.ts +19 -0
  158. package/dist/providers/opencode-model-provider.d.ts.map +1 -0
  159. package/dist/providers/opencode-model-provider.js +128 -0
  160. package/dist/providers/opencode-model-provider.js.map +1 -0
  161. package/dist/providers/opencode.provider.d.ts +2 -0
  162. package/dist/providers/opencode.provider.d.ts.map +1 -1
  163. package/dist/providers/opencode.provider.js +4 -0
  164. package/dist/providers/opencode.provider.js.map +1 -1
  165. package/dist/providers/openrouter-decision-models.d.ts +7 -0
  166. package/dist/providers/openrouter-decision-models.d.ts.map +1 -0
  167. package/dist/providers/openrouter-decision-models.js +31 -0
  168. package/dist/providers/openrouter-decision-models.js.map +1 -0
  169. package/dist/providers/openrouter.provider.js +2 -1
  170. package/dist/providers/openrouter.provider.js.map +1 -1
  171. package/dist/providers/orcarouter-models.d.ts +12 -0
  172. package/dist/providers/orcarouter-models.d.ts.map +1 -0
  173. package/dist/providers/orcarouter-models.js +38 -0
  174. package/dist/providers/orcarouter-models.js.map +1 -0
  175. package/dist/providers/provider-presets.d.ts +37 -0
  176. package/dist/providers/provider-presets.d.ts.map +1 -0
  177. package/dist/providers/provider-presets.js +33 -0
  178. package/dist/providers/provider-presets.js.map +1 -0
  179. package/dist/providers/provider-presets.json +201 -0
  180. package/dist/providers/provider-service.d.ts +3 -2
  181. package/dist/providers/provider-service.d.ts.map +1 -1
  182. package/dist/providers/provider-service.js +4 -20
  183. package/dist/providers/provider-service.js.map +1 -1
  184. package/dist/providers/registry.d.ts +7 -0
  185. package/dist/providers/registry.d.ts.map +1 -1
  186. package/dist/providers/registry.js.map +1 -1
  187. package/dist/services/agent-runtime/application-run-loop.d.ts +25 -2
  188. package/dist/services/agent-runtime/application-run-loop.d.ts.map +1 -1
  189. package/dist/services/agent-runtime/application-run-loop.js +927 -758
  190. package/dist/services/agent-runtime/application-run-loop.js.map +1 -1
  191. package/dist/services/agent-runtime/tool-call-execution.d.ts +1 -1
  192. package/dist/services/agent-runtime/tool-call-execution.d.ts.map +1 -1
  193. package/dist/services/agent-runtime/tool-call-execution.js +6 -3
  194. package/dist/services/agent-runtime/tool-call-execution.js.map +1 -1
  195. package/dist/services/approval/approval-flow-coordinator.d.ts +4 -2
  196. package/dist/services/approval/approval-flow-coordinator.d.ts.map +1 -1
  197. package/dist/services/approval/approval-flow-coordinator.js +2 -1
  198. package/dist/services/approval/approval-flow-coordinator.js.map +1 -1
  199. package/dist/services/approval/approval-state.d.ts +0 -13
  200. package/dist/services/approval/approval-state.d.ts.map +1 -1
  201. package/dist/services/approval/approval-state.js +0 -5
  202. package/dist/services/approval/approval-state.js.map +1 -1
  203. package/dist/services/approval/background-subagent-approval-controller.d.ts.map +1 -1
  204. package/dist/services/approval/background-subagent-approval-controller.js +0 -2
  205. package/dist/services/approval/background-subagent-approval-controller.js.map +1 -1
  206. package/dist/services/approval/nested-approval-owner.d.ts.map +1 -1
  207. package/dist/services/approval/nested-approval-owner.js +5 -1
  208. package/dist/services/approval/nested-approval-owner.js.map +1 -1
  209. package/dist/services/approval/shell-auto-approval-resolver.d.ts +2 -2
  210. package/dist/services/approval/shell-auto-approval-resolver.d.ts.map +1 -1
  211. package/dist/services/approval/shell-auto-approval-resolver.js +2 -2
  212. package/dist/services/approval/shell-auto-approval-resolver.js.map +1 -1
  213. package/dist/services/approval/tool-approval-batch-coordinator.d.ts +2 -2
  214. package/dist/services/approval/tool-approval-batch-coordinator.d.ts.map +1 -1
  215. package/dist/services/approval/tool-approval-batch-coordinator.js +4 -4
  216. package/dist/services/approval/tool-approval-batch-coordinator.js.map +1 -1
  217. package/dist/services/assembly/feature-selection.d.ts +48 -0
  218. package/dist/services/assembly/feature-selection.d.ts.map +1 -0
  219. package/dist/services/assembly/feature-selection.js +12 -0
  220. package/dist/services/assembly/feature-selection.js.map +1 -0
  221. package/dist/services/completion-verification/contracts.d.ts +151 -0
  222. package/dist/services/completion-verification/contracts.d.ts.map +1 -0
  223. package/dist/services/completion-verification/contracts.js +84 -0
  224. package/dist/services/completion-verification/contracts.js.map +1 -0
  225. package/dist/services/completion-verification/gate.d.ts +9 -0
  226. package/dist/services/completion-verification/gate.d.ts.map +1 -0
  227. package/dist/services/completion-verification/gate.js +60 -0
  228. package/dist/services/completion-verification/gate.js.map +1 -0
  229. package/dist/services/completion-verification/goal-stop-policy.d.ts +10 -0
  230. package/dist/services/completion-verification/goal-stop-policy.d.ts.map +1 -0
  231. package/dist/services/completion-verification/goal-stop-policy.js +53 -0
  232. package/dist/services/completion-verification/goal-stop-policy.js.map +1 -0
  233. package/dist/services/completion-verification/orchestrator.d.ts +26 -0
  234. package/dist/services/completion-verification/orchestrator.d.ts.map +1 -0
  235. package/dist/services/completion-verification/orchestrator.js +275 -0
  236. package/dist/services/completion-verification/orchestrator.js.map +1 -0
  237. package/dist/services/completion-verification/root-boundary.d.ts +21 -0
  238. package/dist/services/completion-verification/root-boundary.d.ts.map +1 -0
  239. package/dist/services/completion-verification/root-boundary.js +127 -0
  240. package/dist/services/completion-verification/root-boundary.js.map +1 -0
  241. package/dist/services/completion-verification/store.d.ts +13 -0
  242. package/dist/services/completion-verification/store.d.ts.map +1 -0
  243. package/dist/services/completion-verification/store.js +69 -0
  244. package/dist/services/completion-verification/store.js.map +1 -0
  245. package/dist/services/completion-verification/verifier.d.ts +11 -0
  246. package/dist/services/completion-verification/verifier.d.ts.map +1 -0
  247. package/dist/services/completion-verification/verifier.js +173 -0
  248. package/dist/services/completion-verification/verifier.js.map +1 -0
  249. package/dist/services/completion-verification/workspace.d.ts +17 -0
  250. package/dist/services/completion-verification/workspace.d.ts.map +1 -0
  251. package/dist/services/completion-verification/workspace.js +183 -0
  252. package/dist/services/completion-verification/workspace.js.map +1 -0
  253. package/dist/services/conversation/conversation-adapter.d.ts +17 -47
  254. package/dist/services/conversation/conversation-adapter.d.ts.map +1 -1
  255. package/dist/services/conversation/conversation-adapter.js +114 -88
  256. package/dist/services/conversation/conversation-adapter.js.map +1 -1
  257. package/dist/services/conversation/conversation-orchestrator.d.ts.map +1 -1
  258. package/dist/services/conversation/conversation-orchestrator.js +48 -121
  259. package/dist/services/conversation/conversation-orchestrator.js.map +1 -1
  260. package/dist/services/conversation/conversation-record-stream.d.ts +21 -0
  261. package/dist/services/conversation/conversation-record-stream.d.ts.map +1 -0
  262. package/dist/services/conversation/conversation-record-stream.js +186 -0
  263. package/dist/services/conversation/conversation-record-stream.js.map +1 -0
  264. package/dist/services/conversation/conversation-record.d.ts +160 -0
  265. package/dist/services/conversation/conversation-record.d.ts.map +1 -0
  266. package/dist/services/conversation/conversation-record.js +371 -0
  267. package/dist/services/conversation/conversation-record.js.map +1 -0
  268. package/dist/services/conversation/conversation-result-builder.d.ts +0 -4
  269. package/dist/services/conversation/conversation-result-builder.d.ts.map +1 -1
  270. package/dist/services/conversation/conversation-result-builder.js +0 -10
  271. package/dist/services/conversation/conversation-result-builder.js.map +1 -1
  272. package/dist/services/conversation/conversation-service.d.ts +2 -1
  273. package/dist/services/conversation/conversation-service.d.ts.map +1 -1
  274. package/dist/services/conversation/conversation-service.js +3 -0
  275. package/dist/services/conversation/conversation-service.js.map +1 -1
  276. package/dist/services/conversation/durable-goal-stop-check.js +1 -1
  277. package/dist/services/conversation/durable-goal-stop-check.js.map +1 -1
  278. package/dist/services/conversation/durable-goal.d.ts.map +1 -1
  279. package/dist/services/conversation/durable-goal.js +1 -1
  280. package/dist/services/conversation/durable-goal.js.map +1 -1
  281. package/dist/services/conversation-agent-client.d.ts +17 -7
  282. package/dist/services/conversation-agent-client.d.ts.map +1 -1
  283. package/dist/services/logging/conversation-logger.d.ts +1 -7
  284. package/dist/services/logging/conversation-logger.d.ts.map +1 -1
  285. package/dist/services/logging/conversation-logger.js +0 -18
  286. package/dist/services/logging/conversation-logger.js.map +1 -1
  287. package/dist/services/model-service.d.ts +2 -6
  288. package/dist/services/model-service.d.ts.map +1 -1
  289. package/dist/services/model-service.js +8 -21
  290. package/dist/services/model-service.js.map +1 -1
  291. package/dist/services/models/decision-model-listing.d.ts +2 -12
  292. package/dist/services/models/decision-model-listing.d.ts.map +1 -1
  293. package/dist/services/models/decision-model-listing.js +2 -29
  294. package/dist/services/models/decision-model-listing.js.map +1 -1
  295. package/dist/services/provider-continuity.d.ts +26 -0
  296. package/dist/services/provider-continuity.d.ts.map +1 -1
  297. package/dist/services/provider-continuity.js +47 -0
  298. package/dist/services/provider-continuity.js.map +1 -1
  299. package/dist/services/retry/recovery-executor.d.ts +3 -4
  300. package/dist/services/retry/recovery-executor.d.ts.map +1 -1
  301. package/dist/services/retry/recovery-executor.js +13 -75
  302. package/dist/services/retry/recovery-executor.js.map +1 -1
  303. package/dist/services/retry/retry-contracts.d.ts +2 -10
  304. package/dist/services/retry/retry-contracts.d.ts.map +1 -1
  305. package/dist/services/session/continuation-plan-applier.d.ts +5 -9
  306. package/dist/services/session/continuation-plan-applier.d.ts.map +1 -1
  307. package/dist/services/session/continuation-plan-applier.js +9 -43
  308. package/dist/services/session/continuation-plan-applier.js.map +1 -1
  309. package/dist/services/session/continuation-recovery-handler.d.ts +4 -6
  310. package/dist/services/session/continuation-recovery-handler.d.ts.map +1 -1
  311. package/dist/services/session/continuation-recovery-handler.js +3 -15
  312. package/dist/services/session/continuation-recovery-handler.js.map +1 -1
  313. package/dist/services/session/initial-input-preparer.d.ts +2 -2
  314. package/dist/services/session/initial-input-preparer.d.ts.map +1 -1
  315. package/dist/services/session/initial-input-preparer.js +3 -3
  316. package/dist/services/session/initial-input-preparer.js.map +1 -1
  317. package/dist/services/session/initial-turn-recovery-handler.d.ts +2 -4
  318. package/dist/services/session/initial-turn-recovery-handler.d.ts.map +1 -1
  319. package/dist/services/session/initial-turn-recovery-handler.js +4 -5
  320. package/dist/services/session/initial-turn-recovery-handler.js.map +1 -1
  321. package/dist/services/session/live-run.d.ts +9 -3
  322. package/dist/services/session/live-run.d.ts.map +1 -1
  323. package/dist/services/session/live-run.js +19 -4
  324. package/dist/services/session/live-run.js.map +1 -1
  325. package/dist/services/session/session-composition.d.ts +8 -12
  326. package/dist/services/session/session-composition.d.ts.map +1 -1
  327. package/dist/services/session/session-composition.js +41 -87
  328. package/dist/services/session/session-composition.js.map +1 -1
  329. package/dist/services/session/session-input-planner.d.ts +2 -2
  330. package/dist/services/session/session-input-planner.d.ts.map +1 -1
  331. package/dist/services/session/session-input-planner.js +6 -13
  332. package/dist/services/session/session-input-planner.js.map +1 -1
  333. package/dist/services/session/session-lifecycle.d.ts +73 -23
  334. package/dist/services/session/session-lifecycle.d.ts.map +1 -1
  335. package/dist/services/session/session-lifecycle.js +127 -45
  336. package/dist/services/session/session-lifecycle.js.map +1 -1
  337. package/dist/services/session/session-stream-processor.d.ts +4 -30
  338. package/dist/services/session/session-stream-processor.d.ts.map +1 -1
  339. package/dist/services/session/session-stream-processor.js +11 -300
  340. package/dist/services/session/session-stream-processor.js.map +1 -1
  341. package/dist/services/session/session-tool-tracker.d.ts +9 -144
  342. package/dist/services/session/session-tool-tracker.d.ts.map +1 -1
  343. package/dist/services/session/session-tool-tracker.js +17 -251
  344. package/dist/services/session/session-tool-tracker.js.map +1 -1
  345. package/dist/services/session/{turn-workflow.d.ts → session-turn-adapter.d.ts} +10 -43
  346. package/dist/services/session/session-turn-adapter.d.ts.map +1 -0
  347. package/dist/services/session/session-turn-adapter.js +734 -0
  348. package/dist/services/session/session-turn-adapter.js.map +1 -0
  349. package/dist/services/session/turn-attempt-factory.d.ts +2 -11
  350. package/dist/services/session/turn-attempt-factory.d.ts.map +1 -1
  351. package/dist/services/session/turn-attempt-factory.js +4 -12
  352. package/dist/services/session/turn-attempt-factory.js.map +1 -1
  353. package/dist/services/session/turn-attempt.d.ts +0 -3
  354. package/dist/services/session/turn-attempt.d.ts.map +1 -1
  355. package/dist/services/session/turn-attempt.js +0 -5
  356. package/dist/services/session/turn-attempt.js.map +1 -1
  357. package/dist/services/session/turn-coordinator.d.ts +3 -3
  358. package/dist/services/session/turn-coordinator.d.ts.map +1 -1
  359. package/dist/services/session/turn-coordinator.js +31 -26
  360. package/dist/services/session/turn-coordinator.js.map +1 -1
  361. package/dist/services/session/turn-projection-state.d.ts +45 -0
  362. package/dist/services/session/turn-projection-state.d.ts.map +1 -0
  363. package/dist/services/session/turn-projection-state.js +61 -0
  364. package/dist/services/session/turn-projection-state.js.map +1 -0
  365. package/dist/services/settings/settings-env.d.ts +5 -0
  366. package/dist/services/settings/settings-env.d.ts.map +1 -1
  367. package/dist/services/settings/settings-env.js +22 -0
  368. package/dist/services/settings/settings-env.js.map +1 -1
  369. package/dist/services/settings/settings-schema.d.ts +8 -0
  370. package/dist/services/settings/settings-schema.d.ts.map +1 -1
  371. package/dist/services/settings/settings-schema.js +17 -0
  372. package/dist/services/settings/settings-schema.js.map +1 -1
  373. package/dist/services/settings/settings-service.d.ts +6 -0
  374. package/dist/services/settings/settings-service.d.ts.map +1 -1
  375. package/dist/services/settings/settings-service.js +21 -9
  376. package/dist/services/settings/settings-service.js.map +1 -1
  377. package/dist/services/settings/settings-sources.d.ts +2 -0
  378. package/dist/services/settings/settings-sources.d.ts.map +1 -1
  379. package/dist/services/settings/settings-sources.js +2 -0
  380. package/dist/services/settings/settings-sources.js.map +1 -1
  381. package/dist/services/settings/settings-ui-metadata.d.ts +21 -0
  382. package/dist/services/settings/settings-ui-metadata.d.ts.map +1 -1
  383. package/dist/services/settings/settings-ui-metadata.js +201 -23
  384. package/dist/services/settings/settings-ui-metadata.js.map +1 -1
  385. package/dist/services/settings/test-helpers/settings-consumer-inventory.d.ts +1 -0
  386. package/dist/services/settings/test-helpers/settings-consumer-inventory.d.ts.map +1 -1
  387. package/dist/services/settings/test-helpers/settings-consumer-inventory.js +4 -0
  388. package/dist/services/settings/test-helpers/settings-consumer-inventory.js.map +1 -1
  389. package/dist/services/subagents/tool-policy.d.ts +5 -5
  390. package/dist/services/subagents/tool-policy.d.ts.map +1 -1
  391. package/dist/services/subagents/tool-policy.js +15 -9
  392. package/dist/services/subagents/tool-policy.js.map +1 -1
  393. package/dist/tools/agent/goal-check.js +2 -2
  394. package/dist/tools/agent/goal-check.js.map +1 -1
  395. package/dist/tools/agent/list-models.d.ts +71 -0
  396. package/dist/tools/agent/list-models.d.ts.map +1 -0
  397. package/dist/tools/agent/list-models.js +175 -0
  398. package/dist/tools/agent/list-models.js.map +1 -0
  399. package/dist/tools/system/run-code/run-code-runtime.d.ts.map +1 -1
  400. package/dist/tools/system/run-code/run-code-runtime.js +90 -19
  401. package/dist/tools/system/run-code/run-code-runtime.js.map +1 -1
  402. package/dist/tools/system/shell.d.ts +2 -2
  403. package/dist/tools/system/shell.d.ts.map +1 -1
  404. package/dist/tools/system/shell.js +1 -0
  405. package/dist/tools/system/shell.js.map +1 -1
  406. package/dist/tools/tool-names.d.ts +1 -1
  407. package/dist/tools/tool-names.d.ts.map +1 -1
  408. package/dist/utils/ai/token-usage.js +9 -8
  409. package/dist/utils/ai/token-usage.js.map +1 -1
  410. package/dist/utils/value-suggestions.d.ts.map +1 -1
  411. package/dist/utils/value-suggestions.js +14 -11
  412. package/dist/utils/value-suggestions.js.map +1 -1
  413. package/package.json +2 -2
  414. package/dist/providers/anthropic-middleware.d.ts +0 -25
  415. package/dist/providers/anthropic-middleware.d.ts.map +0 -1
  416. package/dist/providers/anthropic-middleware.js +0 -32
  417. package/dist/providers/anthropic-middleware.js.map +0 -1
  418. package/dist/providers/common/openai-compatible-messages.d.ts +0 -2
  419. package/dist/providers/common/openai-compatible-messages.d.ts.map +0 -1
  420. package/dist/providers/common/openai-compatible-messages.js.map +0 -1
  421. package/dist/services/session/continuation-call-id-resolver.d.ts +0 -29
  422. package/dist/services/session/continuation-call-id-resolver.d.ts.map +0 -1
  423. package/dist/services/session/continuation-call-id-resolver.js +0 -69
  424. package/dist/services/session/continuation-call-id-resolver.js.map +0 -1
  425. package/dist/services/session/continuation-state.d.ts +0 -69
  426. package/dist/services/session/continuation-state.d.ts.map +0 -1
  427. package/dist/services/session/continuation-state.js +0 -78
  428. package/dist/services/session/continuation-state.js.map +0 -1
  429. package/dist/services/session/session-manager.d.ts +0 -87
  430. package/dist/services/session/session-manager.d.ts.map +0 -1
  431. package/dist/services/session/session-manager.js +0 -146
  432. package/dist/services/session/session-manager.js.map +0 -1
  433. package/dist/services/session/turn-workflow.d.ts.map +0 -1
  434. package/dist/services/session/turn-workflow.js +0 -1055
  435. package/dist/services/session/turn-workflow.js.map +0 -1
@@ -46,11 +46,16 @@ export class MaxTurnsExceededError extends Error {
46
46
  }
47
47
  }
48
48
  class EventQueue {
49
+ acceptBeforePublish;
50
+ constructor(acceptBeforePublish) {
51
+ this.acceptBeforePublish = acceptBeforePublish;
52
+ }
49
53
  #items = [];
50
54
  #waiters = [];
51
55
  #closed = false;
52
56
  #failure;
53
57
  push(item) {
58
+ this.acceptBeforePublish?.(item);
54
59
  const waiter = this.#waiters.shift();
55
60
  if (waiter)
56
61
  waiter.resolve({ done: false, value: item });
@@ -98,6 +103,7 @@ export class ApplicationRunLoop {
98
103
  #contextCompactionSessionState;
99
104
  #turnInputMailbox;
100
105
  #activeAbortController = null;
106
+ #activeTurnController = null;
101
107
  #segmentGeneration = 0;
102
108
  /** The run whose budget an out-of-band grant applies to, held across a pause. */
103
109
  #activeRunBudgetState;
@@ -143,6 +149,7 @@ export class ApplicationRunLoop {
143
149
  this.#turnInputMailbox.closeTurn();
144
150
  }
145
151
  abort() {
152
+ this.#activeTurnController?.abort();
146
153
  this.abortSegment();
147
154
  // An aborted turn will not resume, so nothing may keep waiting on it.
148
155
  this.#turnInputMailbox.abortTurn();
@@ -192,7 +199,6 @@ export class ApplicationRunLoop {
192
199
  #createToolCallExecution() {
193
200
  return new ToolCallExecution({
194
201
  toolLifecycle: this.#deps.toolLifecycle,
195
- getOnToolDispatch: this.#deps.getOnToolDispatch,
196
202
  resolveMaxParallelToolCalls: this.#deps.resolveMaxParallelToolCalls,
197
203
  isMaxTurnsExceeded: (error) => error instanceof MaxTurnsExceededError,
198
204
  logDiagnostic: this.#deps.logDiagnostic,
@@ -256,12 +262,9 @@ export class ApplicationRunLoop {
256
262
  * the tool results of the round that was in flight when they typed.
257
263
  */
258
264
  startStream(agent, input, options = {}) {
259
- // Without a declared turn scope, a fresh stream is the only evidence this
260
- // loop gets that a new turn has begun: anything still waiting belonged to
261
- // the previous one and must not leak into work the user did not aim it at.
262
- // With one, this call may equally be the same turn restarting after a
263
- // retry, which the loop cannot tell apart — so `openTurn`/`closeTurn` own
264
- // the decision instead.
265
+ return this.#run(this.#createState(agent, input, options), options);
266
+ }
267
+ #createState(agent, input, options) {
265
268
  this.#turnInputMailbox.startStream();
266
269
  const state = {
267
270
  agent,
@@ -293,6 +296,7 @@ export class ApplicationRunLoop {
293
296
  approvals: options.approvals ?? new ApprovalLedger(),
294
297
  turnCount: 0,
295
298
  maxTurns: options.maxTurns,
299
+ completionRepairDeadline: options.completionRepairDeadline,
296
300
  ...(options.runBudget
297
301
  ? {
298
302
  runBudget: new RunBudget(options.runBudget),
@@ -329,24 +333,31 @@ export class ApplicationRunLoop {
329
333
  state.approvalDecisionInterruption = interruption;
330
334
  state.approvalMessage = approvalOptions?.message;
331
335
  };
332
- return this.#run(state, options);
336
+ return state;
337
+ }
338
+ /** Root turns use the same loop; session operations never dispatch a run. */
339
+ async runTurn(session) {
340
+ const controller = new AbortController();
341
+ this.#activeTurnController = controller;
342
+ try {
343
+ await this.#runTurn(undefined, session, controller.signal);
344
+ }
345
+ finally {
346
+ if (this.#activeTurnController === controller)
347
+ this.#activeTurnController = null;
348
+ }
333
349
  }
334
350
  continueRunStream(handle, options = {}) {
335
351
  const state = unwrapContinuationHandle(handle);
336
352
  if (!state || typeof state !== 'object' || !('agent' in state) || !('input' in state)) {
337
353
  throw new Error('Invalid application continuation state');
338
354
  }
339
- // Handles created before the ledger existed cannot resume meaningfully;
340
- // give them a fresh ledger rather than crashing on `approvals` access.
341
- if (!state.approvals)
342
- state.approvals = new ApprovalLedger();
343
- // A continuation handle from before approval batching carried only the
344
- // current pending item. Normalize it into the queue used by current runs.
345
- if (!state.toolExecution)
346
- state.toolExecution = this.#createToolCallExecution();
347
- // Handles created before cost accounting existed have no record list.
348
- if (!state.costRecords)
349
- state.costRecords = [];
355
+ if (!state.live?.resume)
356
+ throw new Error('Application turn is not waiting for continuation');
357
+ this.#refreshContinuation(state, options);
358
+ return this.#run(state, options);
359
+ }
360
+ #refreshContinuation(state, options) {
350
361
  if (!state.runBudget && options.runBudget) {
351
362
  state.runBudget = new RunBudget(options.runBudget);
352
363
  }
@@ -382,13 +393,7 @@ export class ApplicationRunLoop {
382
393
  state.turnCount = 0;
383
394
  if (state.maxTurns === undefined)
384
395
  state.maxTurns = options.maxTurns;
385
- if (typeof state.supportsConversationChaining !== 'boolean') {
386
- state.supportsConversationChaining = options.supportsConversationChaining === true;
387
- }
388
- else {
389
- // AgentClient supplies the current provider capability on every resume.
390
- state.supportsConversationChaining = options.supportsConversationChaining === true;
391
- }
396
+ state.supportsConversationChaining = options.supportsConversationChaining === true;
392
397
  // A continuation receives a fresh immutable snapshot from the session;
393
398
  // refresh the preparation closure when supplied, while preserving the
394
399
  // root closure for callers that do not provide one again.
@@ -409,12 +414,12 @@ export class ApplicationRunLoop {
409
414
  state.turnId = options.turnId;
410
415
  if (options.hookScope !== undefined)
411
416
  state.hookScope = options.hookScope;
412
- return this.#run(state, options);
413
417
  }
414
- #run(state, options) {
415
- const queue = new EventQueue();
418
+ #createView(state, options, acceptEvent) {
419
+ const queue = new EventQueue(acceptEvent);
416
420
  const segmentGeneration = ++this.#segmentGeneration;
417
- this.abortSegment();
421
+ if (!state.live)
422
+ this.abortSegment();
418
423
  const controller = new AbortController();
419
424
  this.#activeAbortController = controller;
420
425
  this.#turnInputMailbox.startSegment();
@@ -465,46 +470,137 @@ export class ApplicationRunLoop {
465
470
  return state.costRecords ?? [];
466
471
  },
467
472
  });
468
- let exitError;
469
- stream.completed = this.#execute(state, stream, queue, effectiveOptions, toolContext, controller)
470
- .catch((error) => {
471
- exitError = error;
472
- stream.cancelled = error instanceof Error && error.name === 'AbortError';
473
+ let resolveSegment;
474
+ let rejectSegment;
475
+ stream.completed = new Promise((resolve, reject) => {
476
+ resolveSegment = resolve;
477
+ rejectSegment = reject;
478
+ });
479
+ // The iterator may report a failure before the caller observes completed.
480
+ void stream.completed.catch(() => undefined);
481
+ const current = {
482
+ state,
483
+ stream,
484
+ queue,
485
+ options: effectiveOptions,
486
+ toolContext,
487
+ requestAbortController: controller,
488
+ generation: segmentGeneration,
489
+ resolveSegment,
490
+ rejectSegment,
491
+ };
492
+ this.#activeRunBudgetState = state.runBudget ? state : undefined;
493
+ return current;
494
+ }
495
+ #run(state, options) {
496
+ const current = this.#createView(state, options);
497
+ if (state.live) {
498
+ const resume = state.live.resume;
499
+ Object.assign(state.live, current, { resume: undefined });
500
+ resume();
501
+ }
502
+ else {
503
+ state.live = current;
504
+ void this.#runTurn(current).then((value) => this.#settleSegment(current, value), (error) => this.#settleSegment(current, undefined, error));
505
+ }
506
+ return current.stream;
507
+ }
508
+ #observe(context) {
509
+ context.session.beginSegment(context.stream);
510
+ context.observation = context.session.consume(context.stream);
511
+ void context.observation.catch(() => undefined);
512
+ }
513
+ async #completeTurn(context) {
514
+ const value = finish(context.stream, context.state, context.queue);
515
+ if (context.session) {
516
+ this.#settleSegment(context, value);
517
+ await context.observation;
518
+ await context.session.completed(context.stream);
519
+ }
520
+ return value;
521
+ }
522
+ /** Complete an event view; a pause leaves the owning async task alive. */
523
+ #settleSegment(context, value, error, waiting = false) {
524
+ const { state, stream, queue, generation } = context;
525
+ if (error === undefined)
526
+ context.session?.acceptSegment(stream);
527
+ state.runBudget?.pause();
528
+ const cancelled = error instanceof Error && error.name === 'AbortError';
529
+ const paused = (waiting || context.resume !== undefined) && !cancelled && error === undefined;
530
+ if (error !== undefined) {
531
+ stream.cancelled = cancelled;
473
532
  queue.close(error);
474
- throw error;
475
- })
476
- .finally(() => {
477
- state.runBudget?.pause();
478
- if (segmentGeneration !== this.#segmentGeneration)
479
- return;
480
- if (this.#activeAbortController === controller)
481
- this.#activeAbortController = null;
482
- // A segment that ends holding approvals has paused the turn, not ended
483
- // it: the caller resumes through continueRunStream, which offers
484
- // another request boundary. Injections wait for it. Only a segment
485
- // that ends with nothing outstanding has truly finished the turn, and
486
- // then anything still waiting has to be sent as its own turn instead.
487
- const pendingApprovals = state.toolExecution.pendingApprovals.length;
488
- const pendingBudgetInteraction = state.pendingRunBudgetInteraction !== undefined;
489
- const cancelled = stream.cancelled === true;
490
- const turnPaused = (pendingApprovals > 0 || pendingBudgetInteraction) && !cancelled && exitError === undefined;
491
- this.#turnInputMailbox.settleSegment({
492
- paused: turnPaused,
493
- reason: {
494
- pendingApprovals,
495
- pendingBudgetInteraction,
496
- cancelled,
497
- error: exitError instanceof Error ? exitError.name : exitError ? String(exitError) : undefined,
498
- turnCount: state.turnCount,
499
- },
500
- });
501
- if (turnPaused)
533
+ context.rejectSegment(error);
534
+ }
535
+ else {
536
+ context.resolveSegment(value);
537
+ }
538
+ if (generation !== this.#segmentGeneration)
539
+ return;
540
+ if (!paused && this.#activeAbortController === context.requestAbortController)
541
+ this.#activeAbortController = null;
542
+ this.#turnInputMailbox.settleSegment({
543
+ paused,
544
+ reason: {
545
+ pendingApprovals: state.toolExecution.pendingApprovals.length,
546
+ pendingBudgetInteraction: state.pendingRunBudgetInteraction !== undefined,
547
+ cancelled,
548
+ error: error instanceof Error ? error.name : error ? String(error) : undefined,
549
+ turnCount: state.turnCount,
550
+ },
551
+ });
552
+ if (!paused && this.#activeRunBudgetState === state)
553
+ this.#activeRunBudgetState = undefined;
554
+ }
555
+ /** Publish a paused view and suspend here until its owner reconnects or cancels. */
556
+ async #waitForDecision(context) {
557
+ if (context.session) {
558
+ this.#settleSegment(context, finish(context.stream, context.state, context.queue), undefined, true);
559
+ await context.observation;
560
+ const options = await context.session.interrupted(context.stream);
561
+ this.#assertLive(context);
562
+ this.#refreshContinuation(context.state, options);
563
+ Object.assign(context, this.#createView(context.state, { ...options, signal: context.turnSignal }, (event) => context.session.acceptEvent(event)));
564
+ this.#observe(context);
565
+ return;
566
+ }
567
+ const { state, stream, queue, options } = context;
568
+ await new Promise((resolve, reject) => {
569
+ const signal = options.signal;
570
+ const abort = () => {
571
+ context.resume = undefined;
572
+ reject(Object.assign(new Error('Operation aborted'), { name: 'AbortError' }));
573
+ };
574
+ context.resume = () => {
575
+ signal?.removeEventListener('abort', abort);
576
+ resolve();
577
+ };
578
+ signal?.addEventListener('abort', abort, { once: true });
579
+ if (signal?.aborted) {
580
+ abort();
502
581
  return;
503
- if (this.#activeRunBudgetState === state)
504
- this.#activeRunBudgetState = undefined;
582
+ }
583
+ this.#settleSegment(context, finish(stream, state, queue));
505
584
  });
506
- this.#activeRunBudgetState = state.runBudget ? state : undefined;
507
- return stream;
585
+ }
586
+ /** A budget decision resumes this exact boundary without recounting it. */
587
+ async #waitForBudget(context) {
588
+ const { state } = context;
589
+ while (state.pendingRunBudgetInteraction) {
590
+ if (state.runBudgetInteractionDecision === 'stop') {
591
+ state.pendingRunBudgetInteraction = undefined;
592
+ state.runBudgetInteractionDecision = undefined;
593
+ return true;
594
+ }
595
+ if (state.runBudgetInteractionDecision === 'continue') {
596
+ state.pendingRunBudgetInteraction = undefined;
597
+ state.runBudgetInteractionDecision = undefined;
598
+ return false;
599
+ }
600
+ context.stream.interruptions = [state.pendingRunBudgetInteraction];
601
+ await this.#waitForDecision(context);
602
+ }
603
+ return false;
508
604
  }
509
605
  /** Route boundary advice through the same admission lane as mode notices. */
510
606
  #queuePendingSystemNotice(text) {
@@ -512,746 +608,825 @@ export class ApplicationRunLoop {
512
608
  }
513
609
  #admitPendingSteers(state, stream, queue) {
514
610
  this.#turnInputMailbox.admitAtRequestBoundary((item) => {
611
+ const text = typeof item.content === 'string'
612
+ ? item.content
613
+ : Array.isArray(item.content)
614
+ ? item.content.map((part) => (typeof part?.text === 'string' ? part.text : '')).join('')
615
+ : '';
616
+ if (item.role === 'user' && !text.startsWith('[Mode Notice]'))
617
+ state.completionRepairDeadline = undefined;
515
618
  state.history.push(item);
516
619
  state.input.push(...normalizeApplicationInput([item]));
517
620
  outputPush(stream, queue, { type: 'item', item });
518
621
  }, state.turnCount);
519
622
  }
520
- async #execute(state, stream, queue, options, toolContext, requestAbortController) {
623
+ /** The single model → tools → model cycle, shared by root and nested turns. */
624
+ async #runTurn(context, session, turnSignal) {
521
625
  while (true) {
522
- if (options.signal?.aborted)
523
- throw Object.assign(new Error('Operation aborted'), { name: 'AbortError' });
524
- if (state.pendingRunBudgetInteraction) {
525
- if (state.runBudgetInteractionDecision === 'stop') {
526
- state.pendingRunBudgetInteraction = undefined;
527
- state.runBudgetInteractionDecision = undefined;
528
- return finish(stream, state, queue);
529
- }
530
- if (state.runBudgetInteractionDecision === 'continue') {
531
- state.pendingRunBudgetInteraction = undefined;
532
- state.runBudgetInteractionDecision = undefined;
533
- }
534
- else {
535
- return this.#pauseForRunBudgetInteraction(state, stream, queue);
626
+ try {
627
+ turnSignal?.throwIfAborted();
628
+ if (!context) {
629
+ const start = await session.prepare(turnSignal);
630
+ turnSignal?.throwIfAborted();
631
+ if (!start)
632
+ return;
633
+ const options = { ...start.options, signal: turnSignal };
634
+ const state = this.#createState(start.agent, start.input, options);
635
+ this.#refreshContinuation(state, options);
636
+ context = this.#createView(state, options, (event) => session.acceptEvent(event));
637
+ context.session = session;
638
+ context.turnSignal = turnSignal;
639
+ state.live = context;
640
+ this.#observe(context);
536
641
  }
642
+ this.#assertLive(context);
643
+ // New explicit user authority supersedes a harness repair continuation before its deadline is applied.
644
+ this.#admitPendingSteers(context.state, context.stream, context.queue);
645
+ if (await this.#withinRepairDeadline(context, () => this.#prepareRequest(context)))
646
+ return await this.#completeTurn(context);
647
+ const response = await this.#withinRepairDeadline(context, () => this.#askModel(context));
648
+ this.#assertLive(context);
649
+ const calls = this.#recordResponse(context, response);
650
+ if (await this.#withinRepairDeadline(context, () => this.#executeTools(context, response, calls)))
651
+ return await this.#completeTurn(context);
652
+ if (calls.length === 0 && (await this.#finishTurn(context)))
653
+ return await this.#completeTurn(context);
537
654
  }
538
- if (state.toolExecution.pendingApprovals.length > 0 && state.approvalDecision) {
539
- state.toolExecution.resolveApproval({ callId: state.approvalDecisionCallId, interruption: state.approvalDecisionInterruption }, state.approvalDecision, state.approvalMessage, state.approvals);
540
- state.approvalDecision = undefined;
541
- state.approvalDecisionCallId = undefined;
542
- state.approvalDecisionInterruption = undefined;
543
- state.approvalMessage = undefined;
544
- await state.toolExecution.settle(this.#toolCallContext(state, stream, queue, toolContext));
545
- stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
546
- if (state.toolExecution.pendingApprovals.length > 0) {
547
- return finish(stream, state, queue);
655
+ catch (error) {
656
+ if (context?.completionRepairExpired) {
657
+ context.state.terminalCause = 'completion_verification_blocked';
658
+ this.#appendStopNotice(context, 'Completion verification blocked: the repair task deadline expired. The durable goal remains active.');
659
+ return await this.#completeTurn(context);
548
660
  }
549
- // Nothing is outstanding, so this segment is terminal — see
550
- // `stopAfterApprovalResolution`. Stopping here rather than at the
551
- // request boundary below is what keeps the just-recorded tool result
552
- // in the committed history.
553
- if (options.stopAfterApprovalResolution)
554
- return finish(stream, state, queue);
555
- }
556
- // A budget interaction can park a response after its tool calls are
557
- // recorded but before any body runs. Resume those retained calls only
558
- // after processing an ordinary approval decision for that same plan.
559
- if (state.toolExecution.hasPlan) {
560
- await state.toolExecution.settle(this.#toolCallContext(state, stream, queue, toolContext));
561
- if (state.toolExecution.pendingApprovals.length) {
562
- stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
563
- return finish(stream, state, queue);
661
+ if (!session)
662
+ throw error;
663
+ if (context) {
664
+ this.#settleSegment(context, undefined, error);
665
+ await context.observation?.catch(() => undefined);
564
666
  }
667
+ turnSignal?.throwIfAborted();
668
+ if (!(await session.recover(error)))
669
+ return;
670
+ context = undefined;
565
671
  }
566
- // The request boundary: every tool result of the previous round is in
567
- // history, and the next request has not been built. A user message
568
- // admitted here reaches the model in sequence, mid-turn.
569
- if (!state.requestBoundaryReady) {
570
- this.#admitPendingSteers(state, stream, queue);
571
- this.#evaluateRunBudget(state, stream, queue);
572
- if (state.pendingRunBudgetInteraction)
573
- return this.#pauseForRunBudgetInteraction(state, stream, queue);
574
- const compactionHistoryResult = state.usesDeltaHistory
575
- ? state.compactionHistory
576
- ? mergeChainedCompactionHistory(state.compactionHistory, state.history)
577
- : { kind: 'skipped', reason: 'missing_snapshot' }
578
- : { kind: 'ready', history: state.history };
579
- const boundaryDecision = options.onRequestBoundary?.(compactionHistoryResult.kind === 'ready' ? compactionHistoryResult.history : state.history, (text) => this.#queuePendingSystemNotice(text), {
580
- ...(state.lastCompletedInputTokens !== undefined
581
- ? { lastCompletedInputTokens: state.lastCompletedInputTokens }
582
- : {}),
583
- ...(compactionHistoryResult.kind === 'skipped'
584
- ? { compactionSkipReason: compactionHistoryResult.reason }
585
- : {}),
586
- });
587
- const compactionHistory = compactionHistoryResult.kind === 'ready' ? compactionHistoryResult.history : undefined;
588
- if (options.boundaryCompaction && compactionHistory && !boundaryDecision?.deferCompaction) {
589
- const compactionStartedAt = Date.now();
590
- let costsRecordedLive = false;
591
- const compaction = await options.boundaryCompaction.compact({
592
- history: compactionHistory,
593
- instructions: state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions,
594
- tools: toModelTools(state.agent.tools),
595
- recordCostRecords: (records) => {
596
- if (records.length) {
597
- costsRecordedLive = true;
598
- state.costRecords ??= [];
599
- state.costRecords.push(...records);
600
- for (const record of records)
601
- queue.push({ type: 'cost_update', record });
602
- this.#evaluateRunBudget(state, stream, queue);
603
- }
604
- return !state.pendingRunBudgetInteraction && !state.criticalWrapUpPending;
605
- },
606
- automaticCompactionsThisRun: state.automaticCompactionsThisRun ?? 0,
607
- lastCompletedInputTokens: state.lastCompletedInputTokens,
608
- signal: options.signal,
609
- onStarted: (provider) => outputPush(stream, queue, { type: 'context_compaction_started', provider, strategy: 'local' }),
610
- });
611
- if (compaction.kind !== 'unchanged' && !costsRecordedLive && compaction.costRecords?.length) {
672
+ }
673
+ }
674
+ async #withinRepairDeadline(context, operation) {
675
+ const deadline = context.state.completionRepairDeadline;
676
+ if (deadline === undefined)
677
+ return operation();
678
+ let timer;
679
+ try {
680
+ return await Promise.race([
681
+ new Promise((_resolve, reject) => {
682
+ const expire = () => {
683
+ if (context.state.completionRepairDeadline !== deadline)
684
+ return;
685
+ context.completionRepairExpired = true;
686
+ context.requestAbortController.abort();
687
+ reject(new Error('Completion repair deadline exceeded'));
688
+ };
689
+ if (Date.now() >= deadline)
690
+ expire();
691
+ else
692
+ timer = setTimeout(expire, deadline - Date.now());
693
+ }),
694
+ Promise.resolve().then(() => {
695
+ if (context.completionRepairExpired)
696
+ throw new Error('Completion repair deadline exceeded');
697
+ return operation();
698
+ }),
699
+ ]);
700
+ }
701
+ finally {
702
+ if (timer)
703
+ clearTimeout(timer);
704
+ }
705
+ }
706
+ #appendStopNotice(context, notice) {
707
+ const text = `\n\n${notice}`;
708
+ context.stream.finalOutput = (context.stream.finalOutput ?? '') + text;
709
+ context.queue.push({ type: 'text_delta', text });
710
+ }
711
+ #assertLive(context) {
712
+ if (context.options.signal?.aborted)
713
+ throw Object.assign(new Error('Operation aborted'), { name: 'AbortError' });
714
+ }
715
+ /** Admit input and prepare context once for the next logical model round. */
716
+ async #prepareRequest(context) {
717
+ let { state, stream, queue, options } = context;
718
+ // The request boundary: every tool result of the previous round is in
719
+ // history, and the next request has not been built. A user message
720
+ // admitted here reaches the model in sequence, mid-turn.
721
+ this.#admitPendingSteers(state, stream, queue);
722
+ this.#evaluateRunBudget(state, stream, queue);
723
+ if (await this.#waitForBudget(context))
724
+ return true;
725
+ ({ state, stream, queue, options } = context);
726
+ const compactionHistoryResult = state.usesDeltaHistory
727
+ ? state.compactionHistory
728
+ ? mergeChainedCompactionHistory(state.compactionHistory, state.history)
729
+ : { kind: 'skipped', reason: 'missing_snapshot' }
730
+ : { kind: 'ready', history: state.history };
731
+ const boundaryDecision = options.onRequestBoundary?.(compactionHistoryResult.kind === 'ready' ? compactionHistoryResult.history : state.history, (text) => this.#queuePendingSystemNotice(text), {
732
+ ...(state.lastCompletedInputTokens !== undefined
733
+ ? { lastCompletedInputTokens: state.lastCompletedInputTokens }
734
+ : {}),
735
+ ...(compactionHistoryResult.kind === 'skipped' ? { compactionSkipReason: compactionHistoryResult.reason } : {}),
736
+ });
737
+ const compactionHistory = compactionHistoryResult.kind === 'ready' ? compactionHistoryResult.history : undefined;
738
+ if (options.boundaryCompaction && compactionHistory && !boundaryDecision?.deferCompaction) {
739
+ const compactionStartedAt = Date.now();
740
+ let costsRecordedLive = false;
741
+ const compaction = await options.boundaryCompaction.compact({
742
+ history: compactionHistory,
743
+ instructions: state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions,
744
+ tools: toModelTools(state.agent.tools),
745
+ recordCostRecords: (records) => {
746
+ if (records.length) {
747
+ costsRecordedLive = true;
612
748
  state.costRecords ??= [];
613
- state.costRecords.push(...compaction.costRecords);
614
- }
615
- if (compaction.kind === 'compacted') {
616
- state.inputUsageInvalidatedByCompaction = true;
617
- state.lastCompletedInputTokens = undefined;
618
- state.compactedDuringRun = true;
619
- state.history.splice(0, state.history.length, ...compaction.history);
620
- state.input.splice(0, state.input.length, ...normalizeApplicationInput(compaction.modelInput));
621
- state.responseId = undefined;
622
- state.responseProviderId = undefined;
623
- state.usesDeltaHistory = false;
624
- state.automaticCompactionsThisRun = (state.automaticCompactionsThisRun ?? 0) + 1;
625
- outputPush(stream, queue, {
626
- type: 'context_compaction_completed',
627
- provider: state.currentProviderId ?? 'unknown',
628
- strategy: 'local',
629
- durationMs: Math.max(0, Date.now() - compactionStartedAt),
630
- });
631
- }
632
- else if (compaction.kind === 'failed') {
633
- outputPush(stream, queue, {
634
- type: 'context_compaction_failed',
635
- provider: compaction.provider,
636
- strategy: 'local',
637
- durationMs: Math.max(0, Date.now() - compactionStartedAt),
638
- });
639
- if (compaction.error)
640
- throw compaction.error;
641
- }
642
- else if (compaction.notice && compaction.notice !== state.compactionNotice) {
643
- state.compactionNotice = compaction.notice;
644
- this.#queuePendingSystemNotice(compaction.notice);
749
+ state.costRecords.push(...records);
750
+ for (const record of records)
751
+ queue.push({ type: 'cost_update', record });
752
+ this.#evaluateRunBudget(state, stream, queue);
645
753
  }
646
- // Summarization is an asynchronous part of this same request
647
- // boundary. Admit anything that arrived while it was in flight after
648
- // applying a replacement, so the steer cannot be overwritten by the
649
- // checkpoint or miss a terminal model request.
650
- this.#admitPendingSteers(state, stream, queue);
651
- this.#evaluateRunBudget(state, stream, queue);
652
- if (state.pendingRunBudgetInteraction)
653
- return this.#pauseForRunBudgetInteraction(state, stream, queue);
654
- }
655
- this.#admitPendingSteers(state, stream, queue);
656
- state.turnCount += 1;
657
- this.#evaluateRunBudget(state, stream, queue);
658
- if (state.pendingRunBudgetInteraction) {
659
- state.requestBoundaryReady = true;
660
- return this.#pauseForRunBudgetInteraction(state, stream, queue);
661
- }
662
- // Retain the legacy error only for callers that have not adopted staged
663
- // budgets. A configured run budget turns the same count into critical
664
- // evidence and lets the decision surface choose what happens next.
665
- if (!state.runBudget && state.maxTurns !== undefined && state.turnCount > state.maxTurns) {
666
- throw new MaxTurnsExceededError(state.maxTurns);
667
- }
754
+ return !state.pendingRunBudgetInteraction && !state.criticalWrapUpPending;
755
+ },
756
+ automaticCompactionsThisRun: state.automaticCompactionsThisRun ?? 0,
757
+ lastCompletedInputTokens: state.lastCompletedInputTokens,
758
+ signal: options.signal,
759
+ onStarted: (provider) => outputPush(stream, queue, { type: 'context_compaction_started', provider, strategy: 'local' }),
760
+ });
761
+ if (compaction.kind !== 'unchanged' && !costsRecordedLive && compaction.costRecords?.length) {
762
+ state.costRecords ??= [];
763
+ state.costRecords.push(...compaction.costRecords);
668
764
  }
669
- else {
670
- state.requestBoundaryReady = undefined;
765
+ if (compaction.kind === 'compacted') {
766
+ state.inputUsageInvalidatedByCompaction = true;
767
+ state.lastCompletedInputTokens = undefined;
768
+ state.compactedDuringRun = true;
769
+ state.history.splice(0, state.history.length, ...compaction.history);
770
+ state.input.splice(0, state.input.length, ...normalizeApplicationInput(compaction.modelInput));
771
+ state.responseId = undefined;
772
+ state.responseProviderId = undefined;
773
+ state.usesDeltaHistory = false;
774
+ state.automaticCompactionsThisRun = (state.automaticCompactionsThisRun ?? 0) + 1;
775
+ outputPush(stream, queue, {
776
+ type: 'context_compaction_completed',
777
+ provider: state.currentProviderId ?? 'unknown',
778
+ strategy: 'local',
779
+ durationMs: Math.max(0, Date.now() - compactionStartedAt),
780
+ });
671
781
  }
672
- const model = await this.#deps.resolveModel(state.agent.model);
673
- const maxRetries = state.agent.modelSettings?.retry?.maxRetries ?? 0;
674
- let attempt = 0;
675
- let streamedToolCalls = [];
676
- let completion;
677
- let sawToolCall = false;
678
- let activeRequestId = '';
679
- let activeRequest;
680
- let pendingNativeReasoning;
681
- let deferredNativeReasoning = [];
682
- let criticalWrapUp = false;
683
- while (true) {
684
- streamedToolCalls = [];
685
- completion = undefined;
686
- sawToolCall = false;
687
- pendingNativeReasoning = undefined;
688
- deferredNativeReasoning = [];
689
- // Stable process-unique request id, allocated immediately before dispatch
690
- // so a cost record can be settled exactly once (success or failure).
691
- activeRequestId = this.#nextRequestId();
692
- const requestId = activeRequestId;
693
- const outputLengthBefore = stream.output.length;
694
- const newItemsLengthBefore = stream.newItems.length;
695
- const inputLengthBefore = state.input.length;
696
- const historyLengthBefore = state.history.length;
697
- const criticalWrapUpDispatchedBefore = state.criticalWrapUpDispatched;
698
- let provisionalTextCharacters = 0;
699
- let provisionalReasoningCharacters = 0;
700
- let provisionalTextDeltas = 0;
701
- let provisionalReasoningDeltas = 0;
702
- let lastTextPartId;
703
- criticalWrapUp = state.criticalWrapUpPending === true && state.criticalWrapUpDispatched !== true;
704
- if (criticalWrapUp)
705
- state.criticalWrapUpDispatched = true;
706
- const disableChaining = state.disableChainingForAttempt === true;
707
- if (disableChaining) {
708
- state.disableChainingForAttempt = false;
709
- state.responseId = undefined;
710
- state.responseProviderId = undefined;
711
- }
712
- // A per-request signal lets a recoverable guard trip cancel only this
713
- // provider request; the segment signal still cancels everything.
714
- const requestSignal = new AbortController();
715
- const abortRequest = () => requestSignal.abort();
716
- if (options.signal?.aborted)
717
- requestSignal.abort();
718
- else
719
- options.signal?.addEventListener('abort', abortRequest, { once: true });
720
- const requestInstructions = state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions;
721
- // Same synchronous step as the instructions above, so the snapshot is
722
- // exactly the state the model is shown in this request.
723
- state.requestSnapshot = state.agent.resolveRequestSnapshot?.();
724
- const requestCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
725
- const effectiveOutput = resolveRequestOutput(requestCatalog, state.agent.modelSettings?.maxTokens);
726
- const request = {
727
- instructions: criticalWrapUp
728
- ? `${requestInstructions}\n\nBudget containment is terminal. Do not call tools. In this one final response, summarize what you completed, the evidence you have, and what remains.`
729
- : requestInstructions,
730
- ...(state.supportsConversationChaining &&
731
- !disableChaining &&
732
- state.responseId &&
733
- state.responseProviderId !== undefined &&
734
- state.responseProviderId === state.currentProviderId
735
- ? { previousResponseId: state.responseId }
736
- : {}),
737
- ...(disableChaining ? { disableChaining: true } : {}),
738
- input: projectModelRequestHistory(state.input),
739
- tools: toModelTools(criticalWrapUp ? [] : state.agent.tools),
740
- applicationTools: criticalWrapUp ? [] : state.agent.tools,
741
- ...(state.agent.modelSettings?.temperature !== undefined
742
- ? { temperature: state.agent.modelSettings.temperature }
743
- : {}),
744
- ...(state.agent.modelSettings?.reasoning ? { reasoning: state.agent.modelSettings.reasoning } : {}),
745
- ...(effectiveOutput !== undefined ? { maxTokens: effectiveOutput } : {}),
746
- ...(state.agent.outputType !== undefined ? { outputType: state.agent.outputType } : {}),
747
- ...(state.agent.modelSettings?.codex ? { codex: state.agent.modelSettings.codex } : {}),
748
- ...(state.agent.modelSettings?.providerData
749
- ? {
750
- providerOptions: this.#contextCompactionSessionState.disabled
751
- ? Object.fromEntries(Object.entries(state.agent.modelSettings.providerData).filter(([key]) => key !== 'contextCompaction'))
752
- : state.agent.modelSettings.providerData,
753
- }
754
- : {}),
755
- signal: requestSignal.signal,
756
- ...(state.recoveryBudget ? { recoveryBudget: state.recoveryBudget } : {}),
757
- };
758
- activeRequest = request;
759
- const generationGuard = new GenerationGuard({
760
- maxOutputCharacters: state.agent.modelSettings?.maxStreamOutputChars,
761
- maxTextCharacters: state.agent.modelSettings?.maxStreamOutputChars,
762
- maxReasoningCharacters: state.agent.modelSettings?.maxStreamOutputChars,
763
- maxToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
764
- maxCumulativeToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
765
- requestDeadlineMs: state.agent.modelSettings?.maxModelRequestDurationMs,
766
- maxStreamIdleMs: state.agent.modelSettings?.maxModelStreamIdleMs,
767
- ...options.generationGuard,
782
+ else if (compaction.kind === 'failed') {
783
+ outputPush(stream, queue, {
784
+ type: 'context_compaction_failed',
785
+ provider: compaction.provider,
786
+ strategy: 'local',
787
+ durationMs: Math.max(0, Date.now() - compactionStartedAt),
768
788
  });
769
- const consume = async () => {
770
- const deadline = new GenerationStreamDeadlines({ totalMs: generationGuard.requestDeadlineMs, idleMs: generationGuard.maxStreamIdleMs }, () => requestAbortController.abort(), () => generationGuard.progress);
771
- const toolArgumentRunaway = isLunaChainedRequest(state, request)
772
- ? new ToolArgumentRunawayGuard({
773
- ...DEFAULT_TOOL_ARGUMENT_RUNAWAY_SIGNATURE,
774
- timeoutMs: generationGuard.toolArgumentRunawayMs,
775
- }, () => requestAbortController.abort())
776
- : undefined;
777
- let iterator;
778
- let iteratorFinished = false;
779
- try {
780
- iterator = model.stream(request)[Symbol.asyncIterator]();
781
- while (true) {
782
- const nextOperation = iterator.next();
783
- const next = await deadline.wait(toolArgumentRunaway ? toolArgumentRunaway.wait(nextOperation) : nextOperation);
784
- if (next.done) {
785
- iteratorFinished = true;
786
- return;
787
- }
788
- // Any streamed event is transport activity: re-arm the inactivity
789
- // window so long legitimate reasoning is never mistaken for a stall.
790
- deadline.recordActivity();
791
- const event = next.value;
792
- if (event.type === 'completion') {
793
- toolArgumentRunaway?.observeToolCallCompleted();
794
- generationGuard.observeCompletion(event.output);
795
- completion = event;
796
- continue;
797
- }
798
- if (event.type === 'text_delta') {
799
- const startsNewPart = event.partId !== undefined &&
800
- lastTextPartId !== undefined &&
801
- event.partId !== lastTextPartId &&
802
- event.text.length > 0;
803
- if (event.partId !== undefined && event.text.length > 0)
804
- lastTextPartId = event.partId;
805
- const text = startsNewPart ? `${ASSISTANT_TEXT_PART_SEPARATOR}${event.text}` : event.text;
806
- toolArgumentRunaway?.observeText();
807
- generationGuard.observeText(text);
808
- provisionalTextCharacters += text.length;
809
- provisionalTextDeltas++;
810
- outputPush(stream, queue, { type: 'text_delta', text });
811
- continue;
812
- }
813
- if (event.type === 'codex_rate_limits') {
814
- outputPush(stream, queue, { type: 'codex_rate_limits', rateLimits: event.rateLimits });
815
- continue;
816
- }
817
- if (event.type === 'reasoning_delta') {
818
- const accepted = generationGuard.observeReasoning(event.text);
819
- if (!accepted)
820
- continue;
821
- const forwarded = accepted === event.text ? event : { ...event, text: accepted };
822
- pendingNativeReasoning = appendNativeReasoning(pendingNativeReasoning, forwarded);
823
- provisionalReasoningCharacters += accepted.length;
824
- provisionalReasoningDeltas++;
825
- outputPush(stream, queue, { type: 'reasoning_delta', text: accepted });
826
- continue;
827
- }
828
- if (event.type === 'tool_call_streaming_delta') {
829
- toolArgumentRunaway?.observeToolArgumentProgress(event.argumentCharCount);
830
- generationGuard.observeToolArgumentProgress(event.argumentCharCount);
831
- outputPush(stream, queue, event);
832
- continue;
833
- }
834
- if (event.type === 'context_compaction_started' || event.type === 'context_compaction_completed') {
835
- outputPush(stream, queue, event);
836
- continue;
837
- }
838
- if (event.type === 'tool_call') {
839
- toolArgumentRunaway?.observeToolCallCompleted();
840
- generationGuard.observeToolCall(event.arguments);
841
- // The authoritative completion may introduce a native
842
- // replacement boundary before this call's reasoning.
843
- if (pendingNativeReasoning)
844
- deferredNativeReasoning.push(pendingNativeReasoning);
845
- pendingNativeReasoning = undefined;
846
- sawToolCall = true;
847
- streamedToolCalls.push(event);
848
- }
849
- }
850
- }
851
- finally {
852
- deadline.dispose();
853
- toolArgumentRunaway?.dispose();
854
- if (iterator && !iteratorFinished)
855
- void iterator.return?.().catch(() => undefined);
789
+ if (compaction.error)
790
+ throw compaction.error;
791
+ }
792
+ else if (compaction.notice && compaction.notice !== state.compactionNotice) {
793
+ state.compactionNotice = compaction.notice;
794
+ this.#queuePendingSystemNotice(compaction.notice);
795
+ }
796
+ // Summarization is an asynchronous part of this same request
797
+ // boundary. Admit anything that arrived while it was in flight after
798
+ // applying a replacement, so the steer cannot be overwritten by the
799
+ // checkpoint or miss a terminal model request.
800
+ this.#admitPendingSteers(state, stream, queue);
801
+ this.#evaluateRunBudget(state, stream, queue);
802
+ if (await this.#waitForBudget(context))
803
+ return true;
804
+ ({ state, stream, queue, options } = context);
805
+ }
806
+ this.#admitPendingSteers(state, stream, queue);
807
+ state.turnCount += 1;
808
+ this.#evaluateRunBudget(state, stream, queue);
809
+ if (await this.#waitForBudget(context))
810
+ return true;
811
+ ({ state, stream, queue, options } = context);
812
+ // Retain the legacy error only for callers that have not adopted staged
813
+ // budgets. A configured run budget turns the same count into critical
814
+ // evidence and lets the decision surface choose what happens next.
815
+ if (!state.runBudget && state.maxTurns !== undefined && state.turnCount > state.maxTurns) {
816
+ throw new MaxTurnsExceededError(state.maxTurns);
817
+ }
818
+ return false;
819
+ }
820
+ /** Own streaming and safe request attempts; never dispatch tools here. */
821
+ async #askModel(context) {
822
+ const { state, stream, queue, options, requestAbortController } = context;
823
+ const model = await this.#deps.resolveModel(state.agent.model);
824
+ const maxRetries = state.agent.modelSettings?.retry?.maxRetries ?? 0;
825
+ let attempt = 0;
826
+ let streamedToolCalls = [];
827
+ let completion;
828
+ let sawToolCall = false;
829
+ let activeRequestId = '';
830
+ let activeRequest;
831
+ let pendingNativeReasoning;
832
+ let deferredNativeReasoning = [];
833
+ let criticalWrapUp = false;
834
+ while (true) {
835
+ streamedToolCalls = [];
836
+ completion = undefined;
837
+ sawToolCall = false;
838
+ pendingNativeReasoning = undefined;
839
+ deferredNativeReasoning = [];
840
+ // Stable process-unique request id, allocated immediately before dispatch
841
+ // so a cost record can be settled exactly once (success or failure).
842
+ activeRequestId = this.#nextRequestId();
843
+ const requestId = activeRequestId;
844
+ const outputLengthBefore = stream.output.length;
845
+ const newItemsLengthBefore = stream.newItems.length;
846
+ const inputLengthBefore = state.input.length;
847
+ const historyLengthBefore = state.history.length;
848
+ const criticalWrapUpDispatchedBefore = state.criticalWrapUpDispatched;
849
+ let provisionalTextCharacters = 0;
850
+ let provisionalReasoningCharacters = 0;
851
+ let provisionalTextDeltas = 0;
852
+ let provisionalReasoningDeltas = 0;
853
+ let lastTextPartId;
854
+ criticalWrapUp = state.criticalWrapUpPending === true && state.criticalWrapUpDispatched !== true;
855
+ if (criticalWrapUp)
856
+ state.criticalWrapUpDispatched = true;
857
+ const disableChaining = state.disableChainingForAttempt === true;
858
+ if (disableChaining) {
859
+ state.disableChainingForAttempt = false;
860
+ state.responseId = undefined;
861
+ state.responseProviderId = undefined;
862
+ }
863
+ // A per-request signal lets a recoverable guard trip cancel only this
864
+ // provider request; the segment signal still cancels everything.
865
+ const requestSignal = new AbortController();
866
+ const abortRequest = () => requestSignal.abort();
867
+ if (options.signal?.aborted)
868
+ requestSignal.abort();
869
+ else
870
+ options.signal?.addEventListener('abort', abortRequest, { once: true });
871
+ const requestInstructions = state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions;
872
+ // Same synchronous step as the instructions above, so the snapshot is
873
+ // exactly the state the model is shown in this request.
874
+ state.requestSnapshot = state.agent.resolveRequestSnapshot?.();
875
+ const requestCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
876
+ const effectiveOutput = resolveRequestOutput(requestCatalog, state.agent.modelSettings?.maxTokens);
877
+ const request = {
878
+ instructions: criticalWrapUp
879
+ ? `${requestInstructions}\n\nBudget containment is terminal. Do not call tools. In this one final response, summarize what you completed, the evidence you have, and what remains.`
880
+ : requestInstructions,
881
+ ...(state.supportsConversationChaining &&
882
+ !disableChaining &&
883
+ state.responseId &&
884
+ state.responseProviderId !== undefined &&
885
+ state.responseProviderId === state.currentProviderId
886
+ ? { previousResponseId: state.responseId }
887
+ : {}),
888
+ ...(disableChaining ? { disableChaining: true } : {}),
889
+ input: projectModelRequestHistory(state.input),
890
+ tools: toModelTools(criticalWrapUp ? [] : state.agent.tools),
891
+ applicationTools: criticalWrapUp ? [] : state.agent.tools,
892
+ ...(state.agent.modelSettings?.temperature !== undefined
893
+ ? { temperature: state.agent.modelSettings.temperature }
894
+ : {}),
895
+ ...(state.agent.modelSettings?.reasoning ? { reasoning: state.agent.modelSettings.reasoning } : {}),
896
+ ...(effectiveOutput !== undefined ? { maxTokens: effectiveOutput } : {}),
897
+ ...(state.agent.outputType !== undefined ? { outputType: state.agent.outputType } : {}),
898
+ ...(state.agent.modelSettings?.codex ? { codex: state.agent.modelSettings.codex } : {}),
899
+ ...(state.agent.modelSettings?.providerData
900
+ ? {
901
+ providerOptions: this.#contextCompactionSessionState.disabled
902
+ ? Object.fromEntries(Object.entries(state.agent.modelSettings.providerData).filter(([key]) => key !== 'contextCompaction'))
903
+ : state.agent.modelSettings.providerData,
856
904
  }
857
- };
858
- const dispatch = async () => {
859
- state.requestPreparation?.prepare(request);
860
- // A response ID can be established after a self-contained request.
861
- // It does not make this run's full history a delta needing a snapshot.
862
- const needsChainedSnapshot = Boolean(request.previousResponseId && state.usesDeltaHistory);
863
- const fullContext = needsChainedSnapshot && state.compactionHistory
864
- ? mergeChainedCompactionHistory(state.compactionHistory, state.history)
865
- : undefined;
866
- const contextCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
867
- enforceRequestInputLimit({
868
- contextWindow: contextCatalog?.contextWindow,
869
- maxOutputTokens: request.maxTokens,
870
- limit: state.requestInputLimit
871
- ? state.requestInputLimit()
872
- : state.agent.modelSettings?.maxRequestInputTokens,
873
- history: fullContext?.kind === 'ready'
874
- ? fullContext.history
875
- : needsChainedSnapshot
876
- ? [...(state.compactionHistory ?? []), ...request.input]
877
- : request.input,
878
- instructions: request.instructions ?? '',
879
- tools: request.tools,
880
- lastCompletedInputTokens: state.inputUsageInvalidatedByCompaction
881
- ? undefined
882
- : state.lastCompletedInputTokens,
883
- chainedContextKnown: !needsChainedSnapshot || fullContext?.kind === 'ready',
884
- });
885
- await consume();
886
- };
905
+ : {}),
906
+ signal: requestSignal.signal,
907
+ ...(state.recoveryBudget ? { recoveryBudget: state.recoveryBudget } : {}),
908
+ };
909
+ activeRequest = request;
910
+ const generationGuard = new GenerationGuard({
911
+ maxOutputCharacters: state.agent.modelSettings?.maxStreamOutputChars,
912
+ maxTextCharacters: state.agent.modelSettings?.maxStreamOutputChars,
913
+ maxReasoningCharacters: state.agent.modelSettings?.maxStreamOutputChars,
914
+ maxToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
915
+ maxCumulativeToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
916
+ requestDeadlineMs: state.agent.modelSettings?.maxModelRequestDurationMs,
917
+ maxStreamIdleMs: state.agent.modelSettings?.maxModelStreamIdleMs,
918
+ ...options.generationGuard,
919
+ });
920
+ const consume = async () => {
921
+ const deadline = new GenerationStreamDeadlines({ totalMs: generationGuard.requestDeadlineMs, idleMs: generationGuard.maxStreamIdleMs }, () => requestAbortController.abort(), () => generationGuard.progress);
922
+ const toolArgumentRunaway = isLunaChainedRequest(state, request)
923
+ ? new ToolArgumentRunawayGuard({
924
+ ...DEFAULT_TOOL_ARGUMENT_RUNAWAY_SIGNATURE,
925
+ timeoutMs: generationGuard.toolArgumentRunawayMs,
926
+ }, () => requestAbortController.abort())
927
+ : undefined;
928
+ let iterator;
929
+ let iteratorFinished = false;
887
930
  try {
888
- if (state.requestPreparation)
889
- await state.requestPreparation.run(dispatch);
890
- else
891
- await dispatch();
892
- break;
893
- }
894
- catch (error) {
895
- if (error instanceof RequestInputLimitError) {
896
- this.#deps.logDiagnostic?.('Request input admission refused', {
897
- code: error.code,
898
- action: error.action,
899
- reason: error.reason,
900
- guardClass: 'admission',
901
- setting: 'agent.maxRequestInputTokens',
902
- executionScope: state.hookScope ?? 'root',
903
- recovery: 'retained_work',
904
- limit: error.limit,
905
- estimatedTokens: error.estimatedTokens,
906
- observedTokens: error.observedTokens,
907
- capacity: error.capacity,
908
- outputReserve: error.outputReserve,
909
- estimationReserve: error.estimationReserve,
910
- });
911
- throw error;
912
- }
913
- if (isProviderContextOverflow(error)) {
914
- this.#observeTerminalFailure(error, {
915
- requestId: activeRequestId,
916
- provider: state.currentProviderId,
917
- model: state.agent.model,
918
- tier: resolveServiceTier(request),
919
- });
920
- throw new ProviderContextOverflowError(error);
921
- }
922
- const recoverOversizedToolArgument = isOversizedToolArgumentTrip(error) &&
923
- (state.oversizedToolArgumentRecoveries ?? 0) < MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES &&
924
- !options.signal?.aborted;
925
- if (recoverOversizedToolArgument)
926
- requestSignal.abort();
927
- else if (error instanceof GenerationGuardError)
928
- requestAbortController.abort();
929
- const decision = classifyInLoopModelRetry(error, attempt, maxRetries, Math.random, {
930
- previousResponseId: activeRequest?.previousResponseId,
931
- });
932
- const rollbackProvisionalAttempt = () => {
933
- stream.output.splice(outputLengthBefore);
934
- if (stream.newItems !== stream.output) {
935
- stream.newItems.splice(newItemsLengthBefore);
931
+ iterator = model.stream(request)[Symbol.asyncIterator]();
932
+ while (true) {
933
+ const nextOperation = iterator.next();
934
+ const next = await deadline.wait(toolArgumentRunaway ? toolArgumentRunaway.wait(nextOperation) : nextOperation);
935
+ if (next.done) {
936
+ iteratorFinished = true;
937
+ return;
936
938
  }
937
- if (provisionalTextDeltas > 0 || provisionalReasoningDeltas > 0) {
938
- queue.push({
939
- type: 'model_attempt_rollback',
940
- textCharacters: provisionalTextCharacters,
941
- reasoningCharacters: provisionalReasoningCharacters,
942
- textDeltas: provisionalTextDeltas,
943
- reasoningDeltas: provisionalReasoningDeltas,
944
- });
939
+ // Any streamed event is transport activity: re-arm the inactivity
940
+ // window so long legitimate reasoning is never mistaken for a stall.
941
+ deadline.recordActivity();
942
+ const event = next.value;
943
+ if (event.type === 'completion') {
944
+ toolArgumentRunaway?.observeToolCallCompleted();
945
+ generationGuard.observeCompletion(event.output);
946
+ completion = event;
947
+ continue;
945
948
  }
946
- };
947
- if (recoverOversizedToolArgument) {
948
- // No tool ran: a call is dispatched only after its arguments
949
- // complete. Discard the partial response and ask for smaller calls.
950
- state.oversizedToolArgumentRecoveries = (state.oversizedToolArgumentRecoveries ?? 0) + 1;
951
- state.input.splice(inputLengthBefore);
952
- state.history.splice(historyLengthBefore);
953
- state.criticalWrapUpDispatched = criticalWrapUpDispatchedBefore;
954
- rollbackProvisionalAttempt();
955
- this.#deps.logDiagnostic?.('Recovering oversized tool argument in run loop', {
956
- code: error.code,
957
- recovery: state.oversizedToolArgumentRecoveries,
958
- maxRecoveries: MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES,
959
- });
960
- this.#queuePendingSystemNotice(oversizedToolArgumentNotice(error.message));
961
- this.#admitPendingSteers(state, stream, queue);
962
- continue;
963
- }
964
- if (decision.retryable && !options.signal?.aborted) {
965
- attempt++;
966
- state.input.splice(inputLengthBefore);
967
- state.history.splice(historyLengthBefore);
968
- state.criticalWrapUpDispatched = criticalWrapUpDispatchedBefore;
969
- rollbackProvisionalAttempt();
970
- if (decision.kind === 'chain_recovery') {
971
- state.disableChainingForAttempt = true;
972
- state.responseId = undefined;
973
- state.responseProviderId = undefined;
949
+ if (event.type === 'text_delta') {
950
+ const startsNewPart = event.partId !== undefined &&
951
+ lastTextPartId !== undefined &&
952
+ event.partId !== lastTextPartId &&
953
+ event.text.length > 0;
954
+ if (event.partId !== undefined && event.text.length > 0)
955
+ lastTextPartId = event.partId;
956
+ const text = startsNewPart ? `${ASSISTANT_TEXT_PART_SEPARATOR}${event.text}` : event.text;
957
+ toolArgumentRunaway?.observeText();
958
+ generationGuard.observeText(text);
959
+ provisionalTextCharacters += text.length;
960
+ provisionalTextDeltas++;
961
+ outputPush(stream, queue, { type: 'text_delta', text });
962
+ continue;
963
+ }
964
+ if (event.type === 'codex_rate_limits') {
965
+ outputPush(stream, queue, { type: 'codex_rate_limits', rateLimits: event.rateLimits });
966
+ continue;
967
+ }
968
+ if (event.type === 'reasoning_delta') {
969
+ const accepted = generationGuard.observeReasoning(event.text);
970
+ if (!accepted)
971
+ continue;
972
+ const forwarded = accepted === event.text ? event : { ...event, text: accepted };
973
+ pendingNativeReasoning = appendNativeReasoning(pendingNativeReasoning, forwarded);
974
+ provisionalReasoningCharacters += accepted.length;
975
+ provisionalReasoningDeltas++;
976
+ outputPush(stream, queue, { type: 'reasoning_delta', text: accepted });
977
+ continue;
978
+ }
979
+ if (event.type === 'tool_call_streaming_delta') {
980
+ toolArgumentRunaway?.observeToolArgumentProgress(event.argumentCharCount);
981
+ generationGuard.observeToolArgumentProgress(event.argumentCharCount);
982
+ outputPush(stream, queue, event);
983
+ continue;
984
+ }
985
+ if (event.type === 'context_compaction_started' || event.type === 'context_compaction_completed') {
986
+ outputPush(stream, queue, event);
987
+ continue;
988
+ }
989
+ if (event.type === 'tool_call') {
990
+ toolArgumentRunaway?.observeToolCallCompleted();
991
+ generationGuard.observeToolCall(event.arguments);
992
+ // The authoritative completion may introduce a native
993
+ // replacement boundary before this call's reasoning.
994
+ if (pendingNativeReasoning)
995
+ deferredNativeReasoning.push(pendingNativeReasoning);
996
+ pendingNativeReasoning = undefined;
997
+ sawToolCall = true;
998
+ streamedToolCalls.push(event);
974
999
  }
975
- this.#deps.logDiagnostic?.('Retrying model request in run loop', {
976
- attempt,
977
- maxRetries,
978
- kind: decision.kind,
979
- delayMs: decision.delayMs,
980
- error: describeError(error),
981
- });
982
- await (this.#deps.waitBeforeModelRetry ?? sleepWithAbort)(decision.delayMs, options.signal);
983
- continue;
984
- }
985
- // Session recovery owns full-history rebuild for a chained delta.
986
- // Retract this cycle's provisional tokens so a later chain_recovery
987
- // does not append a second answer after a half-sentence.
988
- if (!decision.retryable && decision.reason === 'chained_delta_not_self_contained') {
989
- rollbackProvisionalAttempt();
990
1000
  }
991
- // Dispatch began, but no terminal completion was accepted. Record an
992
- // unpriced marker so the summary stays honest (partial) rather than
993
- // appearing exact. Observational only: the error still propagates.
994
- const cancelled = error instanceof Error && error.name === 'AbortError';
995
- const record = this.#appendCostRecord(state, {
996
- requestId,
997
- provider: state.currentProviderId,
998
- model: state.agent.model,
999
- tier: resolveServiceTier(request),
1000
- outcome: cancelled ? 'cancelled' : 'failed',
1001
- });
1002
- // Only the live queue sees cost records: `output`/`newItems` feed
1003
- // persistence and replay, and a cost event must never become a
1004
- // restored history item.
1005
- queue.push({ type: 'cost_update', record });
1006
- this.#evaluateRunBudget(state, stream, queue);
1007
- this.#observeTerminalFailure(error, {
1008
- requestId,
1009
- provider: state.currentProviderId,
1010
- model: state.agent.model,
1011
- tier: resolveServiceTier(request),
1012
- });
1013
- throw error;
1014
1001
  }
1015
1002
  finally {
1016
- options.signal?.removeEventListener('abort', abortRequest);
1003
+ deadline.dispose();
1004
+ toolArgumentRunaway?.dispose();
1005
+ if (iterator && !iteratorFinished)
1006
+ void iterator.return?.().catch(() => undefined);
1017
1007
  }
1008
+ };
1009
+ const dispatch = async () => {
1010
+ state.requestPreparation?.prepare(request);
1011
+ // A response ID can be established after a self-contained request.
1012
+ // It does not make this run's full history a delta needing a snapshot.
1013
+ const needsChainedSnapshot = Boolean(request.previousResponseId && state.usesDeltaHistory);
1014
+ const fullContext = needsChainedSnapshot && state.compactionHistory
1015
+ ? mergeChainedCompactionHistory(state.compactionHistory, state.history)
1016
+ : undefined;
1017
+ const contextCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
1018
+ enforceRequestInputLimit({
1019
+ contextWindow: contextCatalog?.contextWindow,
1020
+ maxOutputTokens: request.maxTokens,
1021
+ limit: state.requestInputLimit ? state.requestInputLimit() : state.agent.modelSettings?.maxRequestInputTokens,
1022
+ history: fullContext?.kind === 'ready'
1023
+ ? fullContext.history
1024
+ : needsChainedSnapshot
1025
+ ? [...(state.compactionHistory ?? []), ...request.input]
1026
+ : request.input,
1027
+ instructions: request.instructions ?? '',
1028
+ tools: request.tools,
1029
+ lastCompletedInputTokens: state.inputUsageInvalidatedByCompaction
1030
+ ? undefined
1031
+ : state.lastCompletedInputTokens,
1032
+ chainedContextKnown: !needsChainedSnapshot || fullContext?.kind === 'ready',
1033
+ });
1034
+ await consume();
1035
+ };
1036
+ try {
1037
+ if (state.requestPreparation)
1038
+ await state.requestPreparation.run(dispatch);
1039
+ else
1040
+ await dispatch();
1041
+ break;
1018
1042
  }
1019
- if (!completion) {
1020
- // A few application-owned model fixtures (and legacy adapters) end a
1021
- // tool-call stream immediately after the call and use the next request
1022
- // as the continuation boundary. Preserve that contract while routing
1023
- // the calls through the same ordered dispatcher. An empty stream still
1024
- // fails closed as an incomplete model turn.
1025
- if (streamedToolCalls.length === 0) {
1026
- const error = new Error('Application model turn ended without completion');
1043
+ catch (error) {
1044
+ if (error instanceof RequestInputLimitError) {
1045
+ this.#deps.logDiagnostic?.('Request input admission refused', {
1046
+ code: error.code,
1047
+ action: error.action,
1048
+ reason: error.reason,
1049
+ guardClass: 'admission',
1050
+ setting: 'agent.maxRequestInputTokens',
1051
+ executionScope: state.hookScope ?? 'root',
1052
+ recovery: 'retained_work',
1053
+ limit: error.limit,
1054
+ estimatedTokens: error.estimatedTokens,
1055
+ observedTokens: error.observedTokens,
1056
+ capacity: error.capacity,
1057
+ outputReserve: error.outputReserve,
1058
+ estimationReserve: error.estimationReserve,
1059
+ });
1060
+ throw error;
1061
+ }
1062
+ if (isProviderContextOverflow(error)) {
1027
1063
  this.#observeTerminalFailure(error, {
1028
1064
  requestId: activeRequestId,
1029
1065
  provider: state.currentProviderId,
1030
1066
  model: state.agent.model,
1031
- tier: activeRequest ? resolveServiceTier(activeRequest) : 'standard',
1067
+ tier: resolveServiceTier(request),
1032
1068
  });
1033
- throw error;
1034
- }
1035
- if (criticalWrapUp) {
1036
- return finish(stream, state, queue);
1069
+ throw new ProviderContextOverflowError(error);
1037
1070
  }
1038
- for (const reasoning of deferredNativeReasoning)
1039
- commitPendingNativeReasoning(state, stream, queue, reasoning);
1040
- pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1041
- await this.#dispatchToolCalls(state, stream, queue, streamedToolCalls, toolContext);
1042
- if (state.toolExecution.terminateAfterExecution)
1043
- return finish(stream, state, queue);
1044
- if (state.toolExecution.pendingApprovals.length > 0) {
1045
- stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
1046
- return finish(stream, state, queue);
1047
- }
1048
- continue;
1049
- }
1050
- const finalCompletion = completion;
1051
- // A terminal response ends recovery, unlike partial tokens or tool
1052
- // activity. Later failures must not inherit an old episode's deadline.
1053
- state.recoveryBudget?.noteModelResponseCompleted();
1054
- // Commit the authoritative terminal state before handling terminal-only
1055
- // tool calls. Approval may pause immediately after this point, and the
1056
- // continuation must still carry the response that produced the calls.
1057
- // Response IDs are provider-chain state, not generic run metadata. Keep
1058
- // them out of continuation handles for providers that do not support
1059
- // chaining, while still exposing the completed provider response on the
1060
- // current stream for diagnostics.
1061
- if (state.supportsConversationChaining && state.currentProviderId !== undefined) {
1062
- state.responseId = finalCompletion.responseId;
1063
- state.responseProviderId = state.currentProviderId;
1064
- }
1065
- else {
1066
- state.responseId = undefined;
1067
- state.responseProviderId = undefined;
1068
- }
1069
- let normalizedCompletionUsage;
1070
- const upstreamProvider = extractAiSdkUpstreamProvider(finalCompletion.providerMetadata);
1071
- if (finalCompletion.usage !== undefined) {
1072
- normalizedCompletionUsage = normalizeModelUsage(finalCompletion.usage);
1073
- if (upstreamProvider) {
1074
- if (normalizedCompletionUsage) {
1075
- normalizedCompletionUsage.upstream_provider = upstreamProvider;
1071
+ const recoverOversizedToolArgument = isOversizedToolArgumentTrip(error) &&
1072
+ (state.oversizedToolArgumentRecoveries ?? 0) < MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES &&
1073
+ !options.signal?.aborted;
1074
+ if (recoverOversizedToolArgument)
1075
+ requestSignal.abort();
1076
+ else if (error instanceof GenerationGuardError)
1077
+ requestAbortController.abort();
1078
+ const decision = classifyInLoopModelRetry(error, attempt, maxRetries, Math.random, {
1079
+ previousResponseId: activeRequest?.previousResponseId,
1080
+ });
1081
+ const rollbackProvisionalAttempt = () => {
1082
+ stream.output.splice(outputLengthBefore);
1083
+ if (stream.newItems !== stream.output) {
1084
+ stream.newItems.splice(newItemsLengthBefore);
1076
1085
  }
1077
- else {
1078
- normalizedCompletionUsage = { upstream_provider: upstreamProvider };
1086
+ if (provisionalTextDeltas > 0 || provisionalReasoningDeltas > 0) {
1087
+ queue.push({
1088
+ type: 'model_attempt_rollback',
1089
+ textCharacters: provisionalTextCharacters,
1090
+ reasoningCharacters: provisionalReasoningCharacters,
1091
+ textDeltas: provisionalTextDeltas,
1092
+ reasoningDeltas: provisionalReasoningDeltas,
1093
+ });
1079
1094
  }
1095
+ };
1096
+ if (recoverOversizedToolArgument) {
1097
+ // No tool ran: a call is dispatched only after its arguments
1098
+ // complete. Discard the partial response and ask for smaller calls.
1099
+ state.oversizedToolArgumentRecoveries = (state.oversizedToolArgumentRecoveries ?? 0) + 1;
1100
+ state.input.splice(inputLengthBefore);
1101
+ state.history.splice(historyLengthBefore);
1102
+ state.criticalWrapUpDispatched = criticalWrapUpDispatchedBefore;
1103
+ rollbackProvisionalAttempt();
1104
+ this.#deps.logDiagnostic?.('Recovering oversized tool argument in run loop', {
1105
+ code: error.code,
1106
+ recovery: state.oversizedToolArgumentRecoveries,
1107
+ maxRecoveries: MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES,
1108
+ });
1109
+ this.#queuePendingSystemNotice(oversizedToolArgumentNotice(error.message));
1110
+ this.#admitPendingSteers(state, stream, queue);
1111
+ continue;
1080
1112
  }
1081
- if (normalizedCompletionUsage) {
1082
- if (normalizedCompletionUsage.prompt_tokens !== undefined) {
1083
- state.lastCompletedInputTokens = normalizedCompletionUsage.prompt_tokens;
1084
- state.inputUsageInvalidatedByCompaction = false;
1113
+ if (decision.retryable && !options.signal?.aborted) {
1114
+ attempt++;
1115
+ state.input.splice(inputLengthBefore);
1116
+ state.history.splice(historyLengthBefore);
1117
+ state.criticalWrapUpDispatched = criticalWrapUpDispatchedBefore;
1118
+ rollbackProvisionalAttempt();
1119
+ if (decision.kind === 'chain_recovery') {
1120
+ state.disableChainingForAttempt = true;
1121
+ state.responseId = undefined;
1122
+ state.responseProviderId = undefined;
1085
1123
  }
1086
- const accumulated = addTokenUsage(normalizeModelUsage(state.usage), normalizedCompletionUsage);
1087
- state.usage = {
1088
- ...(accumulated.prompt_tokens !== undefined ? { inputTokens: accumulated.prompt_tokens } : {}),
1089
- ...(accumulated.completion_tokens !== undefined ? { outputTokens: accumulated.completion_tokens } : {}),
1090
- // `total_tokens` is the billable normalized total, including cache
1091
- // creation tokens. Keep it alongside the application aliases so the
1092
- // authoritative run accumulator never reconstructs a smaller total.
1093
- ...(accumulated.total_tokens !== undefined ? { totalTokens: accumulated.total_tokens } : {}),
1094
- ...(accumulated.cache_read_tokens !== undefined
1095
- ? { cachedInputTokens: accumulated.cache_read_tokens }
1096
- : {}),
1097
- ...(accumulated.cache_creation_tokens !== undefined
1098
- ? { cacheWriteTokens: accumulated.cache_creation_tokens }
1099
- : {}),
1100
- };
1124
+ this.#deps.logDiagnostic?.('Retrying model request in run loop', {
1125
+ attempt,
1126
+ maxRetries,
1127
+ kind: decision.kind,
1128
+ delayMs: decision.delayMs,
1129
+ error: describeError(error),
1130
+ });
1131
+ await (this.#deps.waitBeforeModelRetry ?? sleepWithAbort)(decision.delayMs, options.signal);
1132
+ continue;
1101
1133
  }
1102
- else {
1103
- // Providers may carry a future/opaque usage shape. Preserve the old
1104
- // pass-through behavior when normalization cannot recognize it.
1105
- state.usage = finalCompletion.usage;
1134
+ // Session recovery owns full-history rebuild for a chained delta.
1135
+ // Retract this cycle's provisional tokens so a later chain_recovery
1136
+ // does not append a second answer after a half-sentence.
1137
+ if (!decision.retryable && decision.reason === 'chained_delta_not_self_contained') {
1138
+ rollbackProvisionalAttempt();
1106
1139
  }
1140
+ // Dispatch began, but no terminal completion was accepted. Record an
1141
+ // unpriced marker so the summary stays honest (partial) rather than
1142
+ // appearing exact. Observational only: the error still propagates.
1143
+ const cancelled = error instanceof Error && error.name === 'AbortError';
1144
+ const record = this.#appendCostRecord(state, {
1145
+ requestId,
1146
+ provider: state.currentProviderId,
1147
+ model: state.agent.model,
1148
+ tier: resolveServiceTier(request),
1149
+ outcome: cancelled ? 'cancelled' : 'failed',
1150
+ });
1151
+ // Only the live queue sees cost records: `output`/`newItems` feed
1152
+ // persistence and replay, and a cost event must never become a
1153
+ // restored history item.
1154
+ queue.push({ type: 'cost_update', record });
1155
+ this.#evaluateRunBudget(state, stream, queue);
1156
+ this.#observeTerminalFailure(error, {
1157
+ requestId,
1158
+ provider: state.currentProviderId,
1159
+ model: state.agent.model,
1160
+ tier: resolveServiceTier(request),
1161
+ });
1162
+ throw error;
1107
1163
  }
1108
- else if (upstreamProvider) {
1109
- normalizedCompletionUsage = { upstream_provider: upstreamProvider };
1110
- }
1111
- else {
1112
- normalizedCompletionUsage = undefined;
1164
+ finally {
1165
+ options.signal?.removeEventListener('abort', abortRequest);
1113
1166
  }
1114
- // Attribute cost while the dispatched request's identity is still known.
1115
- // Emit it immediately so the UI can show this request's cost before the
1116
- // run ends; the terminal result re-delivers the cumulative record list
1117
- // and the session accumulator ignores the duplicates by request id.
1118
- // Only the live queue sees the event: `output`/`newItems` feed
1119
- // persistence and replay, and a cost event must never become a restored
1120
- // history item.
1121
- const record = this.#appendCostRecord(state, {
1122
- requestId: activeRequestId,
1123
- provider: state.currentProviderId,
1124
- model: state.agent.model,
1125
- tier: activeRequest ? resolveServiceTier(activeRequest) : 'standard',
1126
- outcome: 'completed',
1127
- usage: normalizedCompletionUsage,
1128
- providerUsd: finalCompletion.costUsd,
1129
- });
1130
- queue.push({ type: 'cost_update', record });
1131
- // Cost and usage arrive in the same completion metadata, so surface both
1132
- // live. The footer is a per-request indicator: emit this request's own
1133
- // usage, never the run accumulator in `state.usage`.
1134
- if (normalizedCompletionUsage)
1135
- queue.push({ type: 'usage_update', usage: normalizedCompletionUsage });
1136
- this.#evaluateRunBudget(state, stream, queue);
1137
- stream.lastResponseId = finalCompletion.responseId;
1138
- stream.rawResponses?.push(finalCompletion);
1139
- // Some provider adapters report function calls only in the terminal
1140
- // completion rather than as separate stream events. Their reasoning may
1141
- // likewise be terminal-only, so associate it before replaying calls.
1142
- const toolCalls = [...streamedToolCalls];
1143
- const hasTerminalReasoning = finalCompletion.output.some((item) => item.type === 'reasoning' && appendNativeReasoning(undefined, item) !== undefined);
1144
- if (hasTerminalReasoning) {
1145
- // Terminal output supplies native ordering and authoritative metadata.
1146
- // Streamed fragments have already been displayed, but are not history.
1147
- pendingNativeReasoning = undefined;
1167
+ }
1168
+ return {
1169
+ streamedToolCalls,
1170
+ completion,
1171
+ sawToolCall,
1172
+ activeRequestId,
1173
+ activeRequest,
1174
+ pendingNativeReasoning,
1175
+ deferredNativeReasoning,
1176
+ criticalWrapUp,
1177
+ };
1178
+ }
1179
+ /** Accept response facts before any tool from that response can execute. */
1180
+ #recordResponse(context, response) {
1181
+ const { state, stream, queue } = context;
1182
+ const { streamedToolCalls, completion, sawToolCall, activeRequestId, activeRequest, deferredNativeReasoning, criticalWrapUp, } = response;
1183
+ let { pendingNativeReasoning } = response;
1184
+ if (!completion) {
1185
+ if (streamedToolCalls.length === 0) {
1186
+ const error = new Error('Application model turn ended without completion');
1187
+ this.#observeTerminalFailure(error, {
1188
+ requestId: activeRequestId,
1189
+ provider: state.currentProviderId,
1190
+ model: state.agent.model,
1191
+ tier: activeRequest ? resolveServiceTier(activeRequest) : 'standard',
1192
+ });
1193
+ throw error;
1148
1194
  }
1149
- else {
1195
+ if (!criticalWrapUp) {
1150
1196
  for (const reasoning of deferredNativeReasoning)
1151
1197
  commitPendingNativeReasoning(state, stream, queue, reasoning);
1198
+ commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1152
1199
  }
1153
- for (const item of finalCompletion.output) {
1154
- if (item.type === 'reasoning') {
1155
- // Each terminal native item is independently replayable; encrypted
1156
- // reasoning cannot be concatenated or overwritten by its sibling.
1157
- pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1158
- pendingNativeReasoning = appendNativeReasoning(undefined, item);
1159
- continue;
1160
- }
1161
- if (item.type === 'tool_call') {
1162
- pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1163
- if (!sawToolCall)
1164
- toolCalls.push(item);
1165
- continue;
1200
+ return criticalWrapUp ? [] : streamedToolCalls;
1201
+ }
1202
+ const finalCompletion = completion;
1203
+ // A terminal response ends recovery, unlike partial tokens or tool
1204
+ // activity. Later failures must not inherit an old episode's deadline.
1205
+ state.recoveryBudget?.noteModelResponseCompleted();
1206
+ // Commit the authoritative terminal state before handling terminal-only
1207
+ // tool calls. Approval may pause immediately after this point, and the
1208
+ // continuation must still carry the response that produced the calls.
1209
+ // Response IDs are provider-chain state, not generic run metadata. Keep
1210
+ // them out of continuation handles for providers that do not support
1211
+ // chaining, while still exposing the completed provider response on the
1212
+ // current stream for diagnostics.
1213
+ if (state.supportsConversationChaining && state.currentProviderId !== undefined) {
1214
+ state.responseId = finalCompletion.responseId;
1215
+ state.responseProviderId = state.currentProviderId;
1216
+ }
1217
+ else {
1218
+ state.responseId = undefined;
1219
+ state.responseProviderId = undefined;
1220
+ }
1221
+ let normalizedCompletionUsage;
1222
+ const upstreamProvider = extractAiSdkUpstreamProvider(finalCompletion.providerMetadata);
1223
+ if (finalCompletion.usage !== undefined) {
1224
+ normalizedCompletionUsage = normalizeModelUsage(finalCompletion.usage);
1225
+ if (upstreamProvider) {
1226
+ if (normalizedCompletionUsage) {
1227
+ normalizedCompletionUsage.upstream_provider = upstreamProvider;
1166
1228
  }
1167
- if (item.type !== 'provider_opaque')
1168
- continue;
1169
- pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1170
- const historyItem = {
1171
- ...item.item,
1172
- providerOpaque: {
1173
- provider: item.provider,
1174
- ...(item.sourceProvider ? { sourceProvider: item.sourceProvider } : {}),
1175
- },
1176
- };
1177
- state.history.push(historyItem);
1178
- state.input.push(item);
1179
- if (isOpenAICompaction(item)) {
1180
- // Completion usage describes the request before native replacement.
1181
- // Retain billed usage/costs; invalidate only the next admission hint.
1182
- state.lastCompletedInputTokens = undefined;
1183
- state.inputUsageInvalidatedByCompaction = true;
1229
+ else {
1230
+ normalizedCompletionUsage = { upstream_provider: upstreamProvider };
1184
1231
  }
1185
- outputPush(stream, queue, { type: 'item', item });
1186
1232
  }
1187
- sawToolCall = toolCalls.length > 0;
1188
- pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1189
- const assistantText = finalCompletion.output
1190
- .filter((item) => item.type === 'message')
1191
- .flatMap((item) => item.content)
1192
- // Whitespace-only output (e.g. a background-notification acknowledgment)
1193
- // must not enter the canonical history: the truthy check used to let
1194
- // "\n\n" through, accumulating junk in model context.
1195
- .map((part) => part.text)
1196
- .filter((text) => text.length > 0)
1197
- .join(ASSISTANT_TEXT_PART_SEPARATOR)
1198
- .trim();
1199
- if (assistantText) {
1200
- stream.finalOutput = assistantText;
1201
- const item = {
1202
- type: 'message',
1203
- role: 'assistant',
1204
- content: [{ type: 'output_text', text: assistantText }],
1233
+ if (normalizedCompletionUsage) {
1234
+ if (normalizedCompletionUsage.prompt_tokens !== undefined) {
1235
+ state.lastCompletedInputTokens = normalizedCompletionUsage.prompt_tokens;
1236
+ state.inputUsageInvalidatedByCompaction = false;
1237
+ }
1238
+ const accumulated = addTokenUsage(normalizeModelUsage(state.usage), normalizedCompletionUsage);
1239
+ state.usage = {
1240
+ ...(accumulated.prompt_tokens !== undefined ? { inputTokens: accumulated.prompt_tokens } : {}),
1241
+ ...(accumulated.completion_tokens !== undefined ? { outputTokens: accumulated.completion_tokens } : {}),
1242
+ // `total_tokens` is the billable normalized total, including cache
1243
+ // creation tokens. Keep it alongside the application aliases so the
1244
+ // authoritative run accumulator never reconstructs a smaller total.
1245
+ ...(accumulated.total_tokens !== undefined ? { totalTokens: accumulated.total_tokens } : {}),
1246
+ ...(accumulated.cache_read_tokens !== undefined ? { cachedInputTokens: accumulated.cache_read_tokens } : {}),
1247
+ ...(accumulated.cache_creation_tokens !== undefined
1248
+ ? { cacheWriteTokens: accumulated.cache_creation_tokens }
1249
+ : {}),
1205
1250
  };
1206
- state.history.push(item);
1207
- outputPush(stream, queue, { type: 'item', item });
1208
- state.input.push({ type: 'message', role: 'assistant', content: [{ type: 'text', text: assistantText }] });
1209
1251
  }
1210
- // Tool calls are dispatched only after the reasoning, provider-opaque,
1211
- // and assistant-text items from the same completion are committed. A
1212
- // response that carries both prose and a tool call must serialize as
1213
- // text -> function_call -> function_call_result; dispatching first left
1214
- // the text stranded after its own tool result, where the assistant
1215
- // message merger glued it onto the *next* turn's tool call and every
1216
- // request ended with a bare assistant message. Models read that as a
1217
- // turn truncated mid-sentence and restart instead of progressing.
1218
- if (!state.criticalWrapUpPending && toolCalls.length > 0) {
1219
- await this.#dispatchToolCalls(state, stream, queue, toolCalls, toolContext);
1252
+ else {
1253
+ // Providers may carry a future/opaque usage shape. Preserve the old
1254
+ // pass-through behavior when normalization cannot recognize it.
1255
+ state.usage = finalCompletion.usage;
1220
1256
  }
1221
- if (state.toolExecution.terminateAfterExecution)
1222
- return finish(stream, state, queue);
1223
- // A critical subagent gets exactly this final tool-free model call.
1224
- if (criticalWrapUp)
1225
- return finish(stream, state, queue);
1226
- if (state.pendingRunBudgetInteraction && toolCalls.length > 0) {
1227
- return this.#pauseForRunBudgetInteraction(state, stream, queue);
1257
+ }
1258
+ else if (upstreamProvider) {
1259
+ normalizedCompletionUsage = { upstream_provider: upstreamProvider };
1260
+ }
1261
+ else {
1262
+ normalizedCompletionUsage = undefined;
1263
+ }
1264
+ // Attribute cost while the dispatched request's identity is still known.
1265
+ // Emit it immediately so the UI can show this request's cost before the
1266
+ // run ends; the terminal result re-delivers the cumulative record list
1267
+ // and the session accumulator ignores the duplicates by request id.
1268
+ // Only the live queue sees the event: `output`/`newItems` feed
1269
+ // persistence and replay, and a cost event must never become a restored
1270
+ // history item.
1271
+ const record = this.#appendCostRecord(state, {
1272
+ requestId: activeRequestId,
1273
+ provider: state.currentProviderId,
1274
+ model: state.agent.model,
1275
+ tier: activeRequest ? resolveServiceTier(activeRequest) : 'standard',
1276
+ outcome: 'completed',
1277
+ usage: normalizedCompletionUsage,
1278
+ providerUsd: finalCompletion.costUsd,
1279
+ });
1280
+ queue.push({ type: 'cost_update', record });
1281
+ // Cost and usage arrive in the same completion metadata, so surface both
1282
+ // live. The footer is a per-request indicator: emit this request's own
1283
+ // usage, never the run accumulator in `state.usage`.
1284
+ if (normalizedCompletionUsage)
1285
+ queue.push({ type: 'usage_update', usage: normalizedCompletionUsage });
1286
+ this.#evaluateRunBudget(state, stream, queue);
1287
+ stream.lastResponseId = finalCompletion.responseId;
1288
+ stream.rawResponses?.push(finalCompletion);
1289
+ // Some provider adapters report function calls only in the terminal
1290
+ // completion rather than as separate stream events. Their reasoning may
1291
+ // likewise be terminal-only, so associate it before replaying calls.
1292
+ const toolCalls = [...streamedToolCalls];
1293
+ const hasTerminalReasoning = finalCompletion.output.some((item) => item.type === 'reasoning' && appendNativeReasoning(undefined, item) !== undefined);
1294
+ if (hasTerminalReasoning) {
1295
+ // Terminal output supplies native ordering and authoritative metadata.
1296
+ // Streamed fragments have already been displayed, but are not history.
1297
+ pendingNativeReasoning = undefined;
1298
+ }
1299
+ else {
1300
+ for (const reasoning of deferredNativeReasoning)
1301
+ commitPendingNativeReasoning(state, stream, queue, reasoning);
1302
+ }
1303
+ for (const item of finalCompletion.output) {
1304
+ if (item.type === 'reasoning') {
1305
+ // Each terminal native item is independently replayable; encrypted
1306
+ // reasoning cannot be concatenated or overwritten by its sibling.
1307
+ pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1308
+ pendingNativeReasoning = appendNativeReasoning(undefined, item);
1309
+ continue;
1228
1310
  }
1311
+ if (item.type === 'tool_call') {
1312
+ pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1313
+ if (!sawToolCall)
1314
+ toolCalls.push(item);
1315
+ continue;
1316
+ }
1317
+ if (item.type !== 'provider_opaque')
1318
+ continue;
1319
+ pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1320
+ const historyItem = {
1321
+ ...item.item,
1322
+ providerOpaque: {
1323
+ provider: item.provider,
1324
+ ...(item.sourceProvider ? { sourceProvider: item.sourceProvider } : {}),
1325
+ },
1326
+ };
1327
+ state.history.push(historyItem);
1328
+ state.input.push(item);
1329
+ if (isOpenAICompaction(item)) {
1330
+ // Completion usage describes the request before native replacement.
1331
+ // Retain billed usage/costs; invalidate only the next admission hint.
1332
+ state.lastCompletedInputTokens = undefined;
1333
+ state.inputUsageInvalidatedByCompaction = true;
1334
+ }
1335
+ outputPush(stream, queue, { type: 'item', item });
1336
+ }
1337
+ pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
1338
+ const assistantText = finalCompletion.output
1339
+ .filter((item) => item.type === 'message')
1340
+ .flatMap((item) => item.content)
1341
+ // Whitespace-only output (e.g. a background-notification acknowledgment)
1342
+ // must not enter the canonical history: the truthy check used to let
1343
+ // "\n\n" through, accumulating junk in model context.
1344
+ .map((part) => part.text)
1345
+ .filter((text) => text.length > 0)
1346
+ .join(ASSISTANT_TEXT_PART_SEPARATOR)
1347
+ .trim();
1348
+ if (assistantText) {
1349
+ stream.finalOutput = assistantText;
1350
+ const item = {
1351
+ type: 'message',
1352
+ role: 'assistant',
1353
+ content: [{ type: 'output_text', text: assistantText }],
1354
+ };
1355
+ state.history.push(item);
1356
+ outputPush(stream, queue, { type: 'item', item });
1357
+ state.input.push({ type: 'message', role: 'assistant', content: [{ type: 'text', text: assistantText }] });
1358
+ }
1359
+ return toolCalls;
1360
+ }
1361
+ /** Execute and settle one tool plan; approvals suspend this operation in place. */
1362
+ async #executeTools(context, response, calls) {
1363
+ let { state, stream, queue, options, toolContext } = context;
1364
+ if (response.criticalWrapUp)
1365
+ return true;
1366
+ if (!state.criticalWrapUpPending && calls.length > 0) {
1367
+ await this.#dispatchToolCalls(state, stream, queue, calls, toolContext);
1368
+ }
1369
+ if (state.toolExecution.terminateAfterExecution)
1370
+ return true;
1371
+ if (calls.length === 0)
1372
+ return false;
1373
+ while (state.pendingRunBudgetInteraction || state.toolExecution.pendingApprovals.length > 0) {
1374
+ if (await this.#waitForBudget(context))
1375
+ return true;
1376
+ ({ state, stream, queue, options, toolContext } = context);
1377
+ let answeredApproval = false;
1229
1378
  if (state.toolExecution.pendingApprovals.length > 0) {
1230
1379
  stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
1231
- return finish(stream, state, queue);
1232
- }
1233
- if (!sawToolCall) {
1234
- // The normal stop seam. A stop policy may keep the same run going; the
1235
- // reminder is admitted at the next request boundary, where pending
1236
- // budget evidence pauses first and turn accounting applies as usual.
1237
- const stopDecision = options.onNormalStop && !options.signal?.aborted && !state.criticalWrapUpPending
1238
- ? options.onNormalStop(state.history, state.agent)
1239
- : undefined;
1240
- if (stopDecision) {
1241
- this.#deps.logDiagnostic?.('Normal stop policy decided', { action: stopDecision.action, turnCount: state.turnCount, ...stopDecision.diagnostics }, { severity: 'debug', eventType: 'run_loop.normal_stop_policy' });
1380
+ await this.#waitForDecision(context);
1381
+ ({ state, stream, queue, options, toolContext } = context);
1382
+ if (state.approvalDecision) {
1383
+ state.toolExecution.resolveApproval({ callId: state.approvalDecisionCallId, interruption: state.approvalDecisionInterruption }, state.approvalDecision, state.approvalMessage, state.approvals);
1384
+ state.approvalDecision = undefined;
1385
+ state.approvalDecisionCallId = undefined;
1386
+ state.approvalDecisionInterruption = undefined;
1387
+ state.approvalMessage = undefined;
1388
+ answeredApproval = true;
1242
1389
  }
1243
- if (stopDecision?.action === 'continue') {
1244
- this.#queuePendingSystemNotice(stopDecision.reminder);
1245
- continue;
1246
- }
1247
- if (stopDecision?.terminalCause)
1248
- state.terminalCause = stopDecision.terminalCause;
1249
- // Evidence discovered when a response has already completed needs no
1250
- // human boundary: there is no later request or tool to block.
1251
- state.pendingRunBudgetInteraction = undefined;
1252
- return finish(stream, state, queue);
1253
1390
  }
1391
+ this.#assertLive(context);
1392
+ if (state.toolExecution.hasPlan) {
1393
+ await state.toolExecution.settle(this.#toolCallContext(state, stream, queue, toolContext));
1394
+ }
1395
+ stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
1396
+ if (answeredApproval && options.stopAfterApprovalResolution && stream.interruptions.length === 0)
1397
+ return true;
1398
+ if (state.toolExecution.terminateAfterExecution)
1399
+ return true;
1400
+ }
1401
+ return false;
1402
+ }
1403
+ /** Normal no-tool completion; only an explicit stop-policy continuation repeats. */
1404
+ async #finishTurn(context) {
1405
+ const { state, options } = context;
1406
+ // The normal stop seam. A stop policy may keep the same run going; the
1407
+ // reminder is admitted at the next request boundary, where pending
1408
+ // budget evidence pauses first and turn accounting applies as usual.
1409
+ const stopDecision = options.onNormalStop && !options.signal?.aborted && !state.criticalWrapUpPending
1410
+ ? await options.onNormalStop(state.history, state.agent, context.requestAbortController.signal)
1411
+ : undefined;
1412
+ if (stopDecision) {
1413
+ this.#deps.logDiagnostic?.('Normal stop policy decided', { action: stopDecision.action, turnCount: state.turnCount, ...stopDecision.diagnostics }, { severity: 'debug', eventType: 'run_loop.normal_stop_policy' });
1414
+ }
1415
+ if (stopDecision?.action === 'continue') {
1416
+ if (stopDecision.repairDeadline !== undefined)
1417
+ state.completionRepairDeadline = stopDecision.repairDeadline;
1418
+ this.#queuePendingSystemNotice(stopDecision.reminder);
1419
+ return false;
1254
1420
  }
1421
+ if (stopDecision?.terminalCause)
1422
+ state.terminalCause = stopDecision.terminalCause;
1423
+ if (stopDecision?.action === 'stop' && stopDecision.notice) {
1424
+ this.#appendStopNotice(context, stopDecision.notice);
1425
+ }
1426
+ // Evidence discovered when a response has already completed needs no
1427
+ // human boundary: there is no later request or tool to block.
1428
+ state.pendingRunBudgetInteraction = undefined;
1429
+ return true;
1255
1430
  }
1256
1431
  async #dispatchToolCalls(state, stream, queue, events, toolContext) {
1257
1432
  const context = this.#toolCallContext(state, stream, queue, toolContext);
@@ -1288,6 +1463,7 @@ export class ApplicationRunLoop {
1288
1463
  });
1289
1464
  outputPush(stream, queue, { type: 'item', item: callItem });
1290
1465
  },
1466
+ onExecutionStarted: (callId) => state.live?.session?.acceptToolDispatch(callId),
1291
1467
  onDispatch: (entry) => {
1292
1468
  outputPush(stream, queue, {
1293
1469
  type: 'tool_call_dispatched',
@@ -1388,13 +1564,6 @@ export class ApplicationRunLoop {
1388
1564
  #requiresHumanBudgetDecision(event) {
1389
1565
  return event.type === 'tool_stall' || (event.type === 'budget_stage' && event.stage !== 'soft');
1390
1566
  }
1391
- #pauseForRunBudgetInteraction(state, stream, queue) {
1392
- const interaction = state.pendingRunBudgetInteraction;
1393
- if (!interaction)
1394
- return finish(stream, state, queue);
1395
- stream.interruptions = [interaction];
1396
- return finish(stream, state, queue);
1397
- }
1398
1567
  }
1399
1568
  function isLunaChainedRequest(state, request) {
1400
1569
  return (state.currentProviderId === 'codex' &&