@nodetool-ai/websocket 0.7.0-rc.23 → 0.8.1

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 (1020) hide show
  1. package/README.md +50 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/agent/run-node-tool.d.ts +39 -0
  4. package/dist/agent/run-node-tool.d.ts.map +1 -0
  5. package/dist/agent/run-node-tool.js +55 -0
  6. package/dist/agent/run-node-tool.js.map +1 -0
  7. package/dist/chat-prediction-forwarder.d.ts +13 -0
  8. package/dist/chat-prediction-forwarder.d.ts.map +1 -0
  9. package/dist/chat-prediction-forwarder.js +22 -0
  10. package/dist/chat-prediction-forwarder.js.map +1 -0
  11. package/dist/chat-tool-call-repair.d.ts +45 -0
  12. package/dist/chat-tool-call-repair.d.ts.map +1 -0
  13. package/dist/chat-tool-call-repair.js +100 -0
  14. package/dist/chat-tool-call-repair.js.map +1 -0
  15. package/dist/chat-turn-registry.d.ts +169 -0
  16. package/dist/chat-turn-registry.d.ts.map +1 -0
  17. package/dist/chat-turn-registry.js +326 -0
  18. package/dist/chat-turn-registry.js.map +1 -0
  19. package/dist/collection-api.d.ts +2 -2
  20. package/dist/collection-api.d.ts.map +1 -1
  21. package/dist/collection-api.js +44 -13
  22. package/dist/collection-api.js.map +1 -1
  23. package/dist/configured-providers.d.ts +52 -0
  24. package/dist/configured-providers.d.ts.map +1 -0
  25. package/dist/configured-providers.js +85 -0
  26. package/dist/configured-providers.js.map +1 -0
  27. package/dist/cors.d.ts +22 -0
  28. package/dist/cors.d.ts.map +1 -0
  29. package/dist/cors.js +91 -0
  30. package/dist/cors.js.map +1 -0
  31. package/dist/credit-gate.d.ts +36 -0
  32. package/dist/credit-gate.d.ts.map +1 -0
  33. package/dist/credit-gate.js +76 -0
  34. package/dist/credit-gate.js.map +1 -0
  35. package/dist/custom-providers.d.ts +40 -0
  36. package/dist/custom-providers.d.ts.map +1 -0
  37. package/dist/custom-providers.js +134 -0
  38. package/dist/custom-providers.js.map +1 -0
  39. package/dist/debug-sessions.d.ts +10 -0
  40. package/dist/debug-sessions.d.ts.map +1 -0
  41. package/dist/debug-sessions.js +9 -0
  42. package/dist/debug-sessions.js.map +1 -0
  43. package/dist/drain.d.ts +34 -0
  44. package/dist/drain.d.ts.map +1 -0
  45. package/dist/drain.js +68 -0
  46. package/dist/drain.js.map +1 -0
  47. package/dist/e2e-server.d.ts +30 -0
  48. package/dist/e2e-server.d.ts.map +1 -0
  49. package/dist/e2e-server.js +111 -0
  50. package/dist/e2e-server.js.map +1 -0
  51. package/dist/example-workflows.d.ts +20 -0
  52. package/dist/example-workflows.d.ts.map +1 -0
  53. package/dist/example-workflows.js +123 -0
  54. package/dist/example-workflows.js.map +1 -0
  55. package/dist/extension-cdp-bridge.d.ts +85 -0
  56. package/dist/extension-cdp-bridge.d.ts.map +1 -0
  57. package/dist/extension-cdp-bridge.js +163 -0
  58. package/dist/extension-cdp-bridge.js.map +1 -0
  59. package/dist/external-mcp.d.ts +58 -0
  60. package/dist/external-mcp.d.ts.map +1 -0
  61. package/dist/external-mcp.js +288 -0
  62. package/dist/external-mcp.js.map +1 -0
  63. package/dist/fake-runtime.d.ts +114 -0
  64. package/dist/fake-runtime.d.ts.map +1 -0
  65. package/dist/fake-runtime.js +270 -0
  66. package/dist/fake-runtime.js.map +1 -0
  67. package/dist/file-api.d.ts +4 -7
  68. package/dist/file-api.d.ts.map +1 -1
  69. package/dist/file-api.js +122 -39
  70. package/dist/file-api.js.map +1 -1
  71. package/dist/frontend-renderer-registry.d.ts +75 -0
  72. package/dist/frontend-renderer-registry.d.ts.map +1 -0
  73. package/dist/frontend-renderer-registry.js +111 -0
  74. package/dist/frontend-renderer-registry.js.map +1 -0
  75. package/dist/generation-recovery.d.ts +13 -0
  76. package/dist/generation-recovery.d.ts.map +1 -0
  77. package/dist/generation-recovery.js +197 -0
  78. package/dist/generation-recovery.js.map +1 -0
  79. package/dist/headless-job-runner.d.ts +71 -0
  80. package/dist/headless-job-runner.d.ts.map +1 -0
  81. package/dist/headless-job-runner.js +198 -0
  82. package/dist/headless-job-runner.js.map +1 -0
  83. package/dist/http-api.d.ts +157 -24
  84. package/dist/http-api.d.ts.map +1 -1
  85. package/dist/http-api.js +613 -646
  86. package/dist/http-api.js.map +1 -1
  87. package/dist/http-body-schemas.d.ts +109 -0
  88. package/dist/http-body-schemas.d.ts.map +1 -0
  89. package/dist/http-body-schemas.js +146 -0
  90. package/dist/http-body-schemas.js.map +1 -0
  91. package/dist/index.d.ts +11 -3
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +11 -3
  94. package/dist/index.js.map +1 -1
  95. package/dist/job-control.d.ts +57 -0
  96. package/dist/job-control.d.ts.map +1 -0
  97. package/dist/job-control.js +134 -0
  98. package/dist/job-control.js.map +1 -0
  99. package/dist/job-queue.d.ts +47 -0
  100. package/dist/job-queue.d.ts.map +1 -0
  101. package/dist/job-queue.js +52 -0
  102. package/dist/job-queue.js.map +1 -0
  103. package/dist/job-run-registry.d.ts +176 -0
  104. package/dist/job-run-registry.d.ts.map +1 -0
  105. package/dist/job-run-registry.js +312 -0
  106. package/dist/job-run-registry.js.map +1 -0
  107. package/dist/lib/app-build-service.d.ts +16 -0
  108. package/dist/lib/app-build-service.d.ts.map +1 -0
  109. package/dist/lib/app-build-service.js +39 -0
  110. package/dist/lib/app-build-service.js.map +1 -0
  111. package/dist/lib/app-debug-service.d.ts +16 -0
  112. package/dist/lib/app-debug-service.d.ts.map +1 -0
  113. package/dist/lib/app-debug-service.js +48 -0
  114. package/dist/lib/app-debug-service.js.map +1 -0
  115. package/dist/lib/app-deployment-service.d.ts +82 -0
  116. package/dist/lib/app-deployment-service.d.ts.map +1 -0
  117. package/dist/lib/app-deployment-service.js +282 -0
  118. package/dist/lib/app-deployment-service.js.map +1 -0
  119. package/dist/lib/app-run-server.d.ts +7 -0
  120. package/dist/lib/app-run-server.d.ts.map +1 -0
  121. package/dist/lib/app-run-server.js +6 -0
  122. package/dist/lib/app-run-server.js.map +1 -0
  123. package/dist/lib/app-session-scope.d.ts +91 -0
  124. package/dist/lib/app-session-scope.d.ts.map +1 -0
  125. package/dist/lib/app-session-scope.js +91 -0
  126. package/dist/lib/app-session-scope.js.map +1 -0
  127. package/dist/lib/applications-service.d.ts +173 -0
  128. package/dist/lib/applications-service.d.ts.map +1 -0
  129. package/dist/lib/applications-service.js +491 -0
  130. package/dist/lib/applications-service.js.map +1 -0
  131. package/dist/lib/asset-duration.d.ts +7 -0
  132. package/dist/lib/asset-duration.d.ts.map +1 -0
  133. package/dist/lib/asset-duration.js +24 -0
  134. package/dist/lib/asset-duration.js.map +1 -0
  135. package/dist/lib/asset-export.d.ts +34 -0
  136. package/dist/lib/asset-export.d.ts.map +1 -0
  137. package/dist/lib/asset-export.js +113 -0
  138. package/dist/lib/asset-export.js.map +1 -0
  139. package/dist/lib/asset-model-interface.d.ts +41 -0
  140. package/dist/lib/asset-model-interface.d.ts.map +1 -0
  141. package/dist/lib/asset-model-interface.js +74 -0
  142. package/dist/lib/asset-model-interface.js.map +1 -0
  143. package/dist/lib/asset-paths.d.ts +52 -0
  144. package/dist/lib/asset-paths.d.ts.map +1 -1
  145. package/dist/lib/asset-paths.js +145 -1
  146. package/dist/lib/asset-paths.js.map +1 -1
  147. package/dist/lib/asset-response.d.ts +12 -0
  148. package/dist/lib/asset-response.d.ts.map +1 -0
  149. package/dist/lib/asset-response.js +88 -0
  150. package/dist/lib/asset-response.js.map +1 -0
  151. package/dist/lib/audio-peaks.d.ts +23 -0
  152. package/dist/lib/audio-peaks.d.ts.map +1 -0
  153. package/dist/lib/audio-peaks.js +331 -0
  154. package/dist/lib/audio-peaks.js.map +1 -0
  155. package/dist/lib/bridge.d.ts +9 -1
  156. package/dist/lib/bridge.d.ts.map +1 -1
  157. package/dist/lib/bridge.js +27 -4
  158. package/dist/lib/bridge.js.map +1 -1
  159. package/dist/lib/compression.d.ts +3 -0
  160. package/dist/lib/compression.d.ts.map +1 -0
  161. package/dist/lib/compression.js +3 -0
  162. package/dist/lib/compression.js.map +1 -0
  163. package/dist/lib/document-model-interfaces.d.ts +30 -0
  164. package/dist/lib/document-model-interfaces.d.ts.map +1 -0
  165. package/dist/lib/document-model-interfaces.js +295 -0
  166. package/dist/lib/document-model-interfaces.js.map +1 -0
  167. package/dist/lib/env-config.d.ts +9 -0
  168. package/dist/lib/env-config.d.ts.map +1 -0
  169. package/dist/lib/env-config.js +18 -0
  170. package/dist/lib/env-config.js.map +1 -0
  171. package/dist/lib/example-apps.d.ts +43 -0
  172. package/dist/lib/example-apps.d.ts.map +1 -0
  173. package/dist/lib/example-apps.js +145 -0
  174. package/dist/lib/example-apps.js.map +1 -0
  175. package/dist/lib/example-games.d.ts +5 -0
  176. package/dist/lib/example-games.d.ts.map +1 -0
  177. package/dist/lib/example-games.js +13 -0
  178. package/dist/lib/example-games.js.map +1 -0
  179. package/dist/lib/example-recipes.d.ts +33 -0
  180. package/dist/lib/example-recipes.d.ts.map +1 -0
  181. package/dist/lib/example-recipes.js +213 -0
  182. package/dist/lib/example-recipes.js.map +1 -0
  183. package/dist/lib/example-storyboards.d.ts +40 -0
  184. package/dist/lib/example-storyboards.d.ts.map +1 -0
  185. package/dist/lib/example-storyboards.js +131 -0
  186. package/dist/lib/example-storyboards.js.map +1 -0
  187. package/dist/lib/example-thumbnail.d.ts +21 -0
  188. package/dist/lib/example-thumbnail.d.ts.map +1 -0
  189. package/dist/lib/example-thumbnail.js +50 -0
  190. package/dist/lib/example-thumbnail.js.map +1 -0
  191. package/dist/lib/example-timelines.d.ts +9 -0
  192. package/dist/lib/example-timelines.d.ts.map +1 -0
  193. package/dist/lib/example-timelines.js +25 -0
  194. package/dist/lib/example-timelines.js.map +1 -0
  195. package/dist/lib/extension-dist.d.ts +24 -0
  196. package/dist/lib/extension-dist.d.ts.map +1 -0
  197. package/dist/lib/extension-dist.js +67 -0
  198. package/dist/lib/extension-dist.js.map +1 -0
  199. package/dist/lib/external-asset-lookup.d.ts +30 -0
  200. package/dist/lib/external-asset-lookup.d.ts.map +1 -0
  201. package/dist/lib/external-asset-lookup.js +99 -0
  202. package/dist/lib/external-asset-lookup.js.map +1 -0
  203. package/dist/lib/external-assets.d.ts +13 -0
  204. package/dist/lib/external-assets.d.ts.map +1 -0
  205. package/dist/lib/external-assets.js +46 -0
  206. package/dist/lib/external-assets.js.map +1 -0
  207. package/dist/lib/fly-replay.d.ts +34 -0
  208. package/dist/lib/fly-replay.d.ts.map +1 -0
  209. package/dist/lib/fly-replay.js +111 -0
  210. package/dist/lib/fly-replay.js.map +1 -0
  211. package/dist/lib/http-rate-limit.d.ts +32 -0
  212. package/dist/lib/http-rate-limit.d.ts.map +1 -0
  213. package/dist/lib/http-rate-limit.js +47 -0
  214. package/dist/lib/http-rate-limit.js.map +1 -0
  215. package/dist/lib/instance-id.d.ts +25 -0
  216. package/dist/lib/instance-id.d.ts.map +1 -0
  217. package/dist/lib/instance-id.js +44 -0
  218. package/dist/lib/instance-id.js.map +1 -0
  219. package/dist/lib/link-codes.d.ts +82 -0
  220. package/dist/lib/link-codes.d.ts.map +1 -0
  221. package/dist/lib/link-codes.js +100 -0
  222. package/dist/lib/link-codes.js.map +1 -0
  223. package/dist/lib/local-asset-upload.d.ts +5 -0
  224. package/dist/lib/local-asset-upload.d.ts.map +1 -0
  225. package/dist/lib/local-asset-upload.js +69 -0
  226. package/dist/lib/local-asset-upload.js.map +1 -0
  227. package/dist/lib/local-file-access.d.ts +31 -0
  228. package/dist/lib/local-file-access.d.ts.map +1 -0
  229. package/dist/lib/local-file-access.js +169 -0
  230. package/dist/lib/local-file-access.js.map +1 -0
  231. package/dist/lib/localhost-trust.d.ts +59 -0
  232. package/dist/lib/localhost-trust.d.ts.map +1 -0
  233. package/dist/lib/localhost-trust.js +139 -0
  234. package/dist/lib/localhost-trust.js.map +1 -0
  235. package/dist/lib/mcp-mount.d.ts +25 -0
  236. package/dist/lib/mcp-mount.d.ts.map +1 -0
  237. package/dist/lib/mcp-mount.js +32 -0
  238. package/dist/lib/mcp-mount.js.map +1 -0
  239. package/dist/lib/media-size.d.ts +21 -0
  240. package/dist/lib/media-size.d.ts.map +1 -0
  241. package/dist/lib/media-size.js +48 -0
  242. package/dist/lib/media-size.js.map +1 -0
  243. package/dist/lib/media.d.ts +38 -0
  244. package/dist/lib/media.d.ts.map +1 -0
  245. package/dist/lib/media.js +146 -0
  246. package/dist/lib/media.js.map +1 -0
  247. package/dist/lib/package-asset-export.d.ts +83 -0
  248. package/dist/lib/package-asset-export.d.ts.map +1 -0
  249. package/dist/lib/package-asset-export.js +212 -0
  250. package/dist/lib/package-asset-export.js.map +1 -0
  251. package/dist/lib/package-assets.d.ts +41 -0
  252. package/dist/lib/package-assets.d.ts.map +1 -0
  253. package/dist/lib/package-assets.js +155 -0
  254. package/dist/lib/package-assets.js.map +1 -0
  255. package/dist/lib/project-delete.d.ts +11 -0
  256. package/dist/lib/project-delete.d.ts.map +1 -0
  257. package/dist/lib/project-delete.js +67 -0
  258. package/dist/lib/project-delete.js.map +1 -0
  259. package/dist/lib/project-document-copy.d.ts +31 -0
  260. package/dist/lib/project-document-copy.d.ts.map +1 -0
  261. package/dist/lib/project-document-copy.js +473 -0
  262. package/dist/lib/project-document-copy.js.map +1 -0
  263. package/dist/lib/public-app-assets.d.ts +8 -0
  264. package/dist/lib/public-app-assets.d.ts.map +1 -0
  265. package/dist/lib/public-app-assets.js +77 -0
  266. package/dist/lib/public-app-assets.js.map +1 -0
  267. package/dist/lib/public-routes.d.ts +95 -0
  268. package/dist/lib/public-routes.d.ts.map +1 -0
  269. package/dist/lib/public-routes.js +154 -0
  270. package/dist/lib/public-routes.js.map +1 -0
  271. package/dist/lib/runtime-detection.d.ts +17 -0
  272. package/dist/lib/runtime-detection.d.ts.map +1 -0
  273. package/dist/lib/runtime-detection.js +90 -0
  274. package/dist/lib/runtime-detection.js.map +1 -0
  275. package/dist/lib/storage-access.d.ts +21 -0
  276. package/dist/lib/storage-access.d.ts.map +1 -0
  277. package/dist/lib/storage-access.js +74 -0
  278. package/dist/lib/storage-access.js.map +1 -0
  279. package/dist/lib/storage.d.ts +9 -0
  280. package/dist/lib/storage.d.ts.map +1 -0
  281. package/dist/lib/storage.js +28 -0
  282. package/dist/lib/storage.js.map +1 -0
  283. package/dist/lib/storyboard-export.d.ts +64 -0
  284. package/dist/lib/storyboard-export.d.ts.map +1 -0
  285. package/dist/lib/storyboard-export.js +183 -0
  286. package/dist/lib/storyboard-export.js.map +1 -0
  287. package/dist/lib/style-presets.d.ts +34 -0
  288. package/dist/lib/style-presets.d.ts.map +1 -0
  289. package/dist/lib/style-presets.js +62 -0
  290. package/dist/lib/style-presets.js.map +1 -0
  291. package/dist/lib/temp-url-resolver.d.ts +35 -0
  292. package/dist/lib/temp-url-resolver.d.ts.map +1 -0
  293. package/dist/lib/temp-url-resolver.js +88 -0
  294. package/dist/lib/temp-url-resolver.js.map +1 -0
  295. package/dist/lib/thumbnail.d.ts +74 -0
  296. package/dist/lib/thumbnail.d.ts.map +1 -0
  297. package/dist/lib/thumbnail.js +302 -0
  298. package/dist/lib/thumbnail.js.map +1 -0
  299. package/dist/lib/timeline-bundle.d.ts +1689 -0
  300. package/dist/lib/timeline-bundle.d.ts.map +1 -0
  301. package/dist/lib/timeline-bundle.js +330 -0
  302. package/dist/lib/timeline-bundle.js.map +1 -0
  303. package/dist/lib/video-proxy.d.ts +114 -0
  304. package/dist/lib/video-proxy.d.ts.map +1 -0
  305. package/dist/lib/video-proxy.js +545 -0
  306. package/dist/lib/video-proxy.js.map +1 -0
  307. package/dist/lib/wire-values.d.ts +31 -0
  308. package/dist/lib/wire-values.d.ts.map +1 -0
  309. package/dist/lib/wire-values.js +49 -0
  310. package/dist/lib/wire-values.js.map +1 -0
  311. package/dist/lib/workflow-bundle.d.ts +102 -0
  312. package/dist/lib/workflow-bundle.d.ts.map +1 -0
  313. package/dist/lib/workflow-bundle.js +306 -0
  314. package/dist/lib/workflow-bundle.js.map +1 -0
  315. package/dist/lib/workflow-service.d.ts +82 -0
  316. package/dist/lib/workflow-service.d.ts.map +1 -0
  317. package/dist/lib/workflow-service.js +194 -0
  318. package/dist/lib/workflow-service.js.map +1 -0
  319. package/dist/lib/workflow-workspace.d.ts +20 -0
  320. package/dist/lib/workflow-workspace.d.ts.map +1 -0
  321. package/dist/lib/workflow-workspace.js +65 -0
  322. package/dist/lib/workflow-workspace.js.map +1 -0
  323. package/dist/lib/ws-connection-health.d.ts +10 -0
  324. package/dist/lib/ws-connection-health.d.ts.map +1 -0
  325. package/dist/lib/ws-connection-health.js +38 -0
  326. package/dist/lib/ws-connection-health.js.map +1 -0
  327. package/dist/lib/ws-rate-limit.d.ts +22 -0
  328. package/dist/lib/ws-rate-limit.d.ts.map +1 -0
  329. package/dist/lib/ws-rate-limit.js +45 -0
  330. package/dist/lib/ws-rate-limit.js.map +1 -0
  331. package/dist/lib/ws-upgrade.d.ts +40 -0
  332. package/dist/lib/ws-upgrade.d.ts.map +1 -0
  333. package/dist/lib/ws-upgrade.js +66 -0
  334. package/dist/lib/ws-upgrade.js.map +1 -0
  335. package/dist/lib/zip-stream.d.ts +46 -0
  336. package/dist/lib/zip-stream.d.ts.map +1 -0
  337. package/dist/lib/zip-stream.js +140 -0
  338. package/dist/lib/zip-stream.js.map +1 -0
  339. package/dist/mcp-agent-tools.d.ts +56 -0
  340. package/dist/mcp-agent-tools.d.ts.map +1 -0
  341. package/dist/mcp-agent-tools.js +1216 -0
  342. package/dist/mcp-agent-tools.js.map +1 -0
  343. package/dist/mcp-server.d.ts +56 -7
  344. package/dist/mcp-server.d.ts.map +1 -1
  345. package/dist/mcp-server.js +255 -762
  346. package/dist/mcp-server.js.map +1 -1
  347. package/dist/mcp-tool-deps.d.ts +20 -0
  348. package/dist/mcp-tool-deps.d.ts.map +1 -0
  349. package/dist/mcp-tool-deps.js +61 -0
  350. package/dist/mcp-tool-deps.js.map +1 -0
  351. package/dist/messagepack.d.ts +7 -0
  352. package/dist/messagepack.d.ts.map +1 -0
  353. package/dist/messagepack.js +19 -0
  354. package/dist/messagepack.js.map +1 -0
  355. package/dist/model-download-runtime.d.ts +9 -0
  356. package/dist/model-download-runtime.d.ts.map +1 -0
  357. package/dist/model-download-runtime.js +77 -0
  358. package/dist/model-download-runtime.js.map +1 -0
  359. package/dist/model-execution-availability.d.ts +14 -0
  360. package/dist/model-execution-availability.d.ts.map +1 -0
  361. package/dist/model-execution-availability.js +161 -0
  362. package/dist/model-execution-availability.js.map +1 -0
  363. package/dist/models-api.d.ts +51 -5
  364. package/dist/models-api.d.ts.map +1 -1
  365. package/dist/models-api.js +160 -690
  366. package/dist/models-api.js.map +1 -1
  367. package/dist/node-registry-setup.d.ts +84 -0
  368. package/dist/node-registry-setup.d.ts.map +1 -0
  369. package/dist/node-registry-setup.js +256 -0
  370. package/dist/node-registry-setup.js.map +1 -0
  371. package/dist/oauth/cimd.d.ts +28 -0
  372. package/dist/oauth/cimd.d.ts.map +1 -0
  373. package/dist/oauth/cimd.js +177 -0
  374. package/dist/oauth/cimd.js.map +1 -0
  375. package/dist/oauth/gate.d.ts +38 -0
  376. package/dist/oauth/gate.d.ts.map +1 -0
  377. package/dist/oauth/gate.js +77 -0
  378. package/dist/oauth/gate.js.map +1 -0
  379. package/dist/oauth/metadata.d.ts +35 -0
  380. package/dist/oauth/metadata.d.ts.map +1 -0
  381. package/dist/oauth/metadata.js +56 -0
  382. package/dist/oauth/metadata.js.map +1 -0
  383. package/dist/oauth/pending-store.d.ts +79 -0
  384. package/dist/oauth/pending-store.d.ts.map +1 -0
  385. package/dist/oauth/pending-store.js +133 -0
  386. package/dist/oauth/pending-store.js.map +1 -0
  387. package/dist/oauth/pkce.d.ts +16 -0
  388. package/dist/oauth/pkce.d.ts.map +1 -0
  389. package/dist/oauth/pkce.js +29 -0
  390. package/dist/oauth/pkce.js.map +1 -0
  391. package/dist/oauth/validate.d.ts +25 -0
  392. package/dist/oauth/validate.d.ts.map +1 -0
  393. package/dist/oauth/validate.js +62 -0
  394. package/dist/oauth/validate.js.map +1 -0
  395. package/dist/oauth/www-authenticate.d.ts +9 -0
  396. package/dist/oauth/www-authenticate.d.ts.map +1 -0
  397. package/dist/oauth/www-authenticate.js +33 -0
  398. package/dist/oauth/www-authenticate.js.map +1 -0
  399. package/dist/oauth-api.d.ts +19 -0
  400. package/dist/oauth-api.d.ts.map +1 -1
  401. package/dist/oauth-api.js +531 -12
  402. package/dist/oauth-api.js.map +1 -1
  403. package/dist/openai-api.d.ts.map +1 -1
  404. package/dist/openai-api.js +4 -9
  405. package/dist/openai-api.js.map +1 -1
  406. package/dist/pack-snapshot.d.ts +24 -0
  407. package/dist/pack-snapshot.d.ts.map +1 -0
  408. package/dist/pack-snapshot.js +35 -0
  409. package/dist/pack-snapshot.js.map +1 -0
  410. package/dist/plugins/websocket.d.ts +21 -4
  411. package/dist/plugins/websocket.d.ts.map +1 -1
  412. package/dist/plugins/websocket.js +136 -290
  413. package/dist/plugins/websocket.js.map +1 -1
  414. package/dist/resolve-media-urls.d.ts +17 -3
  415. package/dist/resolve-media-urls.d.ts.map +1 -1
  416. package/dist/resolve-media-urls.js +117 -31
  417. package/dist/resolve-media-urls.js.map +1 -1
  418. package/dist/resource-events.d.ts +28 -0
  419. package/dist/resource-events.d.ts.map +1 -0
  420. package/dist/resource-events.js +20 -0
  421. package/dist/resource-events.js.map +1 -0
  422. package/dist/routes/account.d.ts +100 -0
  423. package/dist/routes/account.d.ts.map +1 -0
  424. package/dist/routes/account.js +197 -0
  425. package/dist/routes/account.js.map +1 -0
  426. package/dist/routes/applications.d.ts +36 -0
  427. package/dist/routes/applications.d.ts.map +1 -0
  428. package/dist/routes/applications.js +183 -0
  429. package/dist/routes/applications.js.map +1 -0
  430. package/dist/routes/assets.d.ts +3 -3
  431. package/dist/routes/assets.d.ts.map +1 -1
  432. package/dist/routes/assets.js +78 -44
  433. package/dist/routes/assets.js.map +1 -1
  434. package/dist/routes/atlascloud-webhook.d.ts +93 -0
  435. package/dist/routes/atlascloud-webhook.d.ts.map +1 -0
  436. package/dist/routes/atlascloud-webhook.js +369 -0
  437. package/dist/routes/atlascloud-webhook.js.map +1 -0
  438. package/dist/routes/collections.d.ts.map +1 -1
  439. package/dist/routes/config.d.ts +26 -0
  440. package/dist/routes/config.d.ts.map +1 -0
  441. package/dist/routes/config.js +64 -0
  442. package/dist/routes/config.js.map +1 -0
  443. package/dist/routes/documents.d.ts +27 -0
  444. package/dist/routes/documents.d.ts.map +1 -0
  445. package/dist/routes/documents.js +155 -0
  446. package/dist/routes/documents.js.map +1 -0
  447. package/dist/routes/fal-credits.d.ts +4 -0
  448. package/dist/routes/fal-credits.d.ts.map +1 -0
  449. package/dist/routes/fal-credits.js +101 -0
  450. package/dist/routes/fal-credits.js.map +1 -0
  451. package/dist/routes/fal-pricing-estimate.d.ts +4 -0
  452. package/dist/routes/fal-pricing-estimate.d.ts.map +1 -0
  453. package/dist/routes/fal-pricing-estimate.js +114 -0
  454. package/dist/routes/fal-pricing-estimate.js.map +1 -0
  455. package/dist/routes/fal-pricing.d.ts +4 -0
  456. package/dist/routes/fal-pricing.d.ts.map +1 -0
  457. package/dist/routes/fal-pricing.js +93 -0
  458. package/dist/routes/fal-pricing.js.map +1 -0
  459. package/dist/routes/fal-webhook.d.ts +102 -0
  460. package/dist/routes/fal-webhook.d.ts.map +1 -0
  461. package/dist/routes/fal-webhook.js +379 -0
  462. package/dist/routes/fal-webhook.js.map +1 -0
  463. package/dist/routes/files.d.ts.map +1 -1
  464. package/dist/routes/files.js +4 -3
  465. package/dist/routes/files.js.map +1 -1
  466. package/dist/routes/health.d.ts +1 -0
  467. package/dist/routes/health.d.ts.map +1 -1
  468. package/dist/routes/health.js +41 -7
  469. package/dist/routes/health.js.map +1 -1
  470. package/dist/routes/integrations.d.ts +39 -0
  471. package/dist/routes/integrations.d.ts.map +1 -0
  472. package/dist/routes/integrations.js +211 -0
  473. package/dist/routes/integrations.js.map +1 -0
  474. package/dist/routes/js-scripts.d.ts +26 -0
  475. package/dist/routes/js-scripts.d.ts.map +1 -0
  476. package/dist/routes/js-scripts.js +116 -0
  477. package/dist/routes/js-scripts.js.map +1 -0
  478. package/dist/routes/kie-credits.d.ts +4 -0
  479. package/dist/routes/kie-credits.d.ts.map +1 -0
  480. package/dist/routes/kie-credits.js +93 -0
  481. package/dist/routes/kie-credits.js.map +1 -0
  482. package/dist/routes/kie-pricing.d.ts +4 -0
  483. package/dist/routes/kie-pricing.d.ts.map +1 -0
  484. package/dist/routes/kie-pricing.js +45 -0
  485. package/dist/routes/kie-pricing.js.map +1 -0
  486. package/dist/routes/kie-webhook.d.ts +4 -0
  487. package/dist/routes/kie-webhook.d.ts.map +1 -0
  488. package/dist/routes/kie-webhook.js +70 -0
  489. package/dist/routes/kie-webhook.js.map +1 -0
  490. package/dist/routes/nodes.d.ts.map +1 -1
  491. package/dist/routes/nodes.js +6 -59
  492. package/dist/routes/nodes.js.map +1 -1
  493. package/dist/routes/oauth-as.d.ts +30 -0
  494. package/dist/routes/oauth-as.d.ts.map +1 -0
  495. package/dist/routes/oauth-as.js +412 -0
  496. package/dist/routes/oauth-as.js.map +1 -0
  497. package/dist/routes/oauth.d.ts.map +1 -1
  498. package/dist/routes/openai.d.ts.map +1 -1
  499. package/dist/routes/public-apps.d.ts +27 -0
  500. package/dist/routes/public-apps.d.ts.map +1 -0
  501. package/dist/routes/public-apps.js +99 -0
  502. package/dist/routes/public-apps.js.map +1 -0
  503. package/dist/routes/sandbox-modules.d.ts +17 -0
  504. package/dist/routes/sandbox-modules.d.ts.map +1 -0
  505. package/dist/routes/sandbox-modules.js +85 -0
  506. package/dist/routes/sandbox-modules.js.map +1 -0
  507. package/dist/routes/sdk-v1.d.ts +24 -0
  508. package/dist/routes/sdk-v1.d.ts.map +1 -0
  509. package/dist/routes/sdk-v1.js +320 -0
  510. package/dist/routes/sdk-v1.js.map +1 -0
  511. package/dist/routes/storage.d.ts.map +1 -1
  512. package/dist/routes/storyboards.d.ts +16 -0
  513. package/dist/routes/storyboards.d.ts.map +1 -0
  514. package/dist/routes/storyboards.js +97 -0
  515. package/dist/routes/storyboards.js.map +1 -0
  516. package/dist/routes/timeline-animations.d.ts +23 -0
  517. package/dist/routes/timeline-animations.d.ts.map +1 -0
  518. package/dist/routes/timeline-animations.js +99 -0
  519. package/dist/routes/timeline-animations.js.map +1 -0
  520. package/dist/routes/timeline-audio-bake.d.ts +51 -0
  521. package/dist/routes/timeline-audio-bake.d.ts.map +1 -0
  522. package/dist/routes/timeline-audio-bake.js +110 -0
  523. package/dist/routes/timeline-audio-bake.js.map +1 -0
  524. package/dist/routes/timeline-fonts.d.ts +29 -0
  525. package/dist/routes/timeline-fonts.d.ts.map +1 -0
  526. package/dist/routes/timeline-fonts.js +64 -0
  527. package/dist/routes/timeline-fonts.js.map +1 -0
  528. package/dist/routes/timeline-isolate-subject.d.ts +55 -0
  529. package/dist/routes/timeline-isolate-subject.d.ts.map +1 -0
  530. package/dist/routes/timeline-isolate-subject.js +128 -0
  531. package/dist/routes/timeline-isolate-subject.js.map +1 -0
  532. package/dist/routes/timeline-track-object.d.ts +39 -0
  533. package/dist/routes/timeline-track-object.d.ts.map +1 -0
  534. package/dist/routes/timeline-track-object.js +97 -0
  535. package/dist/routes/timeline-track-object.js.map +1 -0
  536. package/dist/routes/timelines.d.ts +18 -0
  537. package/dist/routes/timelines.d.ts.map +1 -0
  538. package/dist/routes/timelines.js +191 -0
  539. package/dist/routes/timelines.js.map +1 -0
  540. package/dist/routes/workflows.d.ts +6 -9
  541. package/dist/routes/workflows.d.ts.map +1 -1
  542. package/dist/routes/workflows.js +100 -11
  543. package/dist/routes/workflows.js.map +1 -1
  544. package/dist/routes/workspace.d.ts.map +1 -1
  545. package/dist/run-supervisor.d.ts +30 -0
  546. package/dist/run-supervisor.d.ts.map +1 -0
  547. package/dist/run-supervisor.js +86 -0
  548. package/dist/run-supervisor.js.map +1 -0
  549. package/dist/sandbox-catalog.d.ts +28 -0
  550. package/dist/sandbox-catalog.d.ts.map +1 -0
  551. package/dist/sandbox-catalog.js +74 -0
  552. package/dist/sandbox-catalog.js.map +1 -0
  553. package/dist/screenshot-projects.d.ts +27 -0
  554. package/dist/screenshot-projects.d.ts.map +1 -0
  555. package/dist/screenshot-projects.js +880 -0
  556. package/dist/screenshot-projects.js.map +1 -0
  557. package/dist/screenshot-server.js +947 -43
  558. package/dist/screenshot-server.js.map +1 -1
  559. package/dist/sdk/sdk-cached-model-inventory.d.ts +21 -0
  560. package/dist/sdk/sdk-cached-model-inventory.d.ts.map +1 -0
  561. package/dist/sdk/sdk-cached-model-inventory.js +51 -0
  562. package/dist/sdk/sdk-cached-model-inventory.js.map +1 -0
  563. package/dist/sdk/sdk-capabilities-service.d.ts +32 -0
  564. package/dist/sdk/sdk-capabilities-service.d.ts.map +1 -0
  565. package/dist/sdk/sdk-capabilities-service.js +47 -0
  566. package/dist/sdk/sdk-capabilities-service.js.map +1 -0
  567. package/dist/sdk/sdk-execution-readiness-probe.d.ts +19 -0
  568. package/dist/sdk/sdk-execution-readiness-probe.d.ts.map +1 -0
  569. package/dist/sdk/sdk-execution-readiness-probe.js +90 -0
  570. package/dist/sdk/sdk-execution-readiness-probe.js.map +1 -0
  571. package/dist/sdk/sdk-execution-target-readiness.d.ts +15 -0
  572. package/dist/sdk/sdk-execution-target-readiness.d.ts.map +1 -0
  573. package/dist/sdk/sdk-execution-target-readiness.js +50 -0
  574. package/dist/sdk/sdk-execution-target-readiness.js.map +1 -0
  575. package/dist/sdk/sdk-feature-flags.d.ts +18 -0
  576. package/dist/sdk/sdk-feature-flags.d.ts.map +1 -0
  577. package/dist/sdk/sdk-feature-flags.js +30 -0
  578. package/dist/sdk/sdk-feature-flags.js.map +1 -0
  579. package/dist/sdk/sdk-huggingface-download-state.d.ts +15 -0
  580. package/dist/sdk/sdk-huggingface-download-state.d.ts.map +1 -0
  581. package/dist/sdk/sdk-huggingface-download-state.js +25 -0
  582. package/dist/sdk/sdk-huggingface-download-state.js.map +1 -0
  583. package/dist/sdk/sdk-live-runner-registry.d.ts +21 -0
  584. package/dist/sdk/sdk-live-runner-registry.d.ts.map +1 -0
  585. package/dist/sdk/sdk-live-runner-registry.js +26 -0
  586. package/dist/sdk/sdk-live-runner-registry.js.map +1 -0
  587. package/dist/sdk/sdk-model-catalog-service.d.ts +20 -0
  588. package/dist/sdk/sdk-model-catalog-service.d.ts.map +1 -0
  589. package/dist/sdk/sdk-model-catalog-service.js +219 -0
  590. package/dist/sdk/sdk-model-catalog-service.js.map +1 -0
  591. package/dist/sdk/sdk-model-download-service.d.ts +39 -0
  592. package/dist/sdk/sdk-model-download-service.d.ts.map +1 -0
  593. package/dist/sdk/sdk-model-download-service.js +274 -0
  594. package/dist/sdk/sdk-model-download-service.js.map +1 -0
  595. package/dist/sdk/sdk-model-requirement-probe.d.ts +30 -0
  596. package/dist/sdk/sdk-model-requirement-probe.d.ts.map +1 -0
  597. package/dist/sdk/sdk-model-requirement-probe.js +120 -0
  598. package/dist/sdk/sdk-model-requirement-probe.js.map +1 -0
  599. package/dist/sdk/sdk-node-package-requirement-probe.d.ts +20 -0
  600. package/dist/sdk/sdk-node-package-requirement-probe.d.ts.map +1 -0
  601. package/dist/sdk/sdk-node-package-requirement-probe.js +44 -0
  602. package/dist/sdk/sdk-node-package-requirement-probe.js.map +1 -0
  603. package/dist/sdk/sdk-node-type-inventory-service.d.ts +11 -0
  604. package/dist/sdk/sdk-node-type-inventory-service.d.ts.map +1 -0
  605. package/dist/sdk/sdk-node-type-inventory-service.js +36 -0
  606. package/dist/sdk/sdk-node-type-inventory-service.js.map +1 -0
  607. package/dist/sdk/sdk-preflight-orchestrator.d.ts +54 -0
  608. package/dist/sdk/sdk-preflight-orchestrator.d.ts.map +1 -0
  609. package/dist/sdk/sdk-preflight-orchestrator.js +63 -0
  610. package/dist/sdk/sdk-preflight-orchestrator.js.map +1 -0
  611. package/dist/sdk/sdk-preflight-requirement-resolver.d.ts +35 -0
  612. package/dist/sdk/sdk-preflight-requirement-resolver.d.ts.map +1 -0
  613. package/dist/sdk/sdk-preflight-requirement-resolver.js +67 -0
  614. package/dist/sdk/sdk-preflight-requirement-resolver.js.map +1 -0
  615. package/dist/sdk/sdk-preflight-service.d.ts +61 -0
  616. package/dist/sdk/sdk-preflight-service.d.ts.map +1 -0
  617. package/dist/sdk/sdk-preflight-service.js +104 -0
  618. package/dist/sdk/sdk-preflight-service.js.map +1 -0
  619. package/dist/sdk/sdk-preflight-workflow-source.d.ts +18 -0
  620. package/dist/sdk/sdk-preflight-workflow-source.d.ts.map +1 -0
  621. package/dist/sdk/sdk-preflight-workflow-source.js +42 -0
  622. package/dist/sdk/sdk-preflight-workflow-source.js.map +1 -0
  623. package/dist/sdk/sdk-route-policy.d.ts +3 -0
  624. package/dist/sdk/sdk-route-policy.d.ts.map +1 -0
  625. package/dist/sdk/sdk-route-policy.js +18 -0
  626. package/dist/sdk/sdk-route-policy.js.map +1 -0
  627. package/dist/sdk/sdk-runtime-capabilities-service.d.ts +60 -0
  628. package/dist/sdk/sdk-runtime-capabilities-service.d.ts.map +1 -0
  629. package/dist/sdk/sdk-runtime-capabilities-service.js +30 -0
  630. package/dist/sdk/sdk-runtime-capabilities-service.js.map +1 -0
  631. package/dist/sdk/sdk-runtime-requirement-probe.d.ts +13 -0
  632. package/dist/sdk/sdk-runtime-requirement-probe.d.ts.map +1 -0
  633. package/dist/sdk/sdk-runtime-requirement-probe.js +55 -0
  634. package/dist/sdk/sdk-runtime-requirement-probe.js.map +1 -0
  635. package/dist/sdk/sdk-static-preflight-service.d.ts +66 -0
  636. package/dist/sdk/sdk-static-preflight-service.d.ts.map +1 -0
  637. package/dist/sdk/sdk-static-preflight-service.js +492 -0
  638. package/dist/sdk/sdk-static-preflight-service.js.map +1 -0
  639. package/dist/sdk/sdk-temporary-asset-service.d.ts +18 -0
  640. package/dist/sdk/sdk-temporary-asset-service.d.ts.map +1 -0
  641. package/dist/sdk/sdk-temporary-asset-service.js +33 -0
  642. package/dist/sdk/sdk-temporary-asset-service.js.map +1 -0
  643. package/dist/sdk/sdk-temporary-asset-upload-http-handler.d.ts +9 -0
  644. package/dist/sdk/sdk-temporary-asset-upload-http-handler.d.ts.map +1 -0
  645. package/dist/sdk/sdk-temporary-asset-upload-http-handler.js +65 -0
  646. package/dist/sdk/sdk-temporary-asset-upload-http-handler.js.map +1 -0
  647. package/dist/sdk/sdk-v1-handler-map.d.ts +44 -0
  648. package/dist/sdk/sdk-v1-handler-map.d.ts.map +1 -0
  649. package/dist/sdk/sdk-v1-handler-map.js +37 -0
  650. package/dist/sdk/sdk-v1-handler-map.js.map +1 -0
  651. package/dist/sdk/sdk-v1-service-error.d.ts +21 -0
  652. package/dist/sdk/sdk-v1-service-error.d.ts.map +1 -0
  653. package/dist/sdk/sdk-v1-service-error.js +62 -0
  654. package/dist/sdk/sdk-v1-service-error.js.map +1 -0
  655. package/dist/sdk/sdk-v1-service.d.ts +74 -0
  656. package/dist/sdk/sdk-v1-service.d.ts.map +1 -0
  657. package/dist/sdk/sdk-v1-service.js +219 -0
  658. package/dist/sdk/sdk-v1-service.js.map +1 -0
  659. package/dist/sdk/sdk-worker-readiness-adapter.d.ts +22 -0
  660. package/dist/sdk/sdk-worker-readiness-adapter.d.ts.map +1 -0
  661. package/dist/sdk/sdk-worker-readiness-adapter.js +29 -0
  662. package/dist/sdk/sdk-worker-readiness-adapter.js.map +1 -0
  663. package/dist/server.js +1019 -201
  664. package/dist/server.js.map +1 -1
  665. package/dist/session/asset-autosave.d.ts +80 -0
  666. package/dist/session/asset-autosave.d.ts.map +1 -0
  667. package/dist/session/asset-autosave.js +456 -0
  668. package/dist/session/asset-autosave.js.map +1 -0
  669. package/dist/session/chat-compaction.d.ts +107 -0
  670. package/dist/session/chat-compaction.d.ts.map +1 -0
  671. package/dist/session/chat-compaction.js +191 -0
  672. package/dist/session/chat-compaction.js.map +1 -0
  673. package/dist/session/chat-history.d.ts +75 -0
  674. package/dist/session/chat-history.d.ts.map +1 -0
  675. package/dist/session/chat-history.js +194 -0
  676. package/dist/session/chat-history.js.map +1 -0
  677. package/dist/session/chat-prompt.d.ts +45 -0
  678. package/dist/session/chat-prompt.d.ts.map +1 -0
  679. package/dist/session/chat-prompt.js +480 -0
  680. package/dist/session/chat-prompt.js.map +1 -0
  681. package/dist/session/chat-turn.d.ts +336 -0
  682. package/dist/session/chat-turn.d.ts.map +1 -0
  683. package/dist/session/chat-turn.js +3516 -0
  684. package/dist/session/chat-turn.js.map +1 -0
  685. package/dist/session/client-session.d.ts +59 -0
  686. package/dist/session/client-session.d.ts.map +1 -0
  687. package/dist/session/client-session.js +2 -0
  688. package/dist/session/client-session.js.map +1 -0
  689. package/dist/session/commands.d.ts +133 -0
  690. package/dist/session/commands.d.ts.map +1 -0
  691. package/dist/session/commands.js +778 -0
  692. package/dist/session/commands.js.map +1 -0
  693. package/dist/session/inference.d.ts +213 -0
  694. package/dist/session/inference.d.ts.map +1 -0
  695. package/dist/session/inference.js +1072 -0
  696. package/dist/session/inference.js.map +1 -0
  697. package/dist/session/job-execution.d.ts +477 -0
  698. package/dist/session/job-execution.d.ts.map +1 -0
  699. package/dist/session/job-execution.js +2089 -0
  700. package/dist/session/job-execution.js.map +1 -0
  701. package/dist/session/media-generation.d.ts +94 -0
  702. package/dist/session/media-generation.d.ts.map +1 -0
  703. package/dist/session/media-generation.js +234 -0
  704. package/dist/session/media-generation.js.map +1 -0
  705. package/dist/session/model-interfaces.d.ts +27 -0
  706. package/dist/session/model-interfaces.d.ts.map +1 -0
  707. package/dist/session/model-interfaces.js +132 -0
  708. package/dist/session/model-interfaces.js.map +1 -0
  709. package/dist/session/sanitize.d.ts +7 -0
  710. package/dist/session/sanitize.d.ts.map +1 -0
  711. package/dist/session/sanitize.js +64 -0
  712. package/dist/session/sanitize.js.map +1 -0
  713. package/dist/settings-registry.d.ts +6 -15
  714. package/dist/settings-registry.d.ts.map +1 -1
  715. package/dist/settings-registry.js +6 -103
  716. package/dist/settings-registry.js.map +1 -1
  717. package/dist/storage-api.d.ts +20 -0
  718. package/dist/storage-api.d.ts.map +1 -1
  719. package/dist/storage-api.js +260 -77
  720. package/dist/storage-api.js.map +1 -1
  721. package/dist/storage-retention.d.ts +9 -0
  722. package/dist/storage-retention.d.ts.map +1 -0
  723. package/dist/storage-retention.js +74 -0
  724. package/dist/storage-retention.js.map +1 -0
  725. package/dist/system-stats.d.ts +52 -0
  726. package/dist/system-stats.d.ts.map +1 -0
  727. package/dist/system-stats.js +134 -0
  728. package/dist/system-stats.js.map +1 -0
  729. package/dist/test-ui-server.d.ts +46 -0
  730. package/dist/test-ui-server.d.ts.map +1 -1
  731. package/dist/test-ui-server.js +188 -41
  732. package/dist/test-ui-server.js.map +1 -1
  733. package/dist/triggers/boot.d.ts +73 -0
  734. package/dist/triggers/boot.d.ts.map +1 -0
  735. package/dist/triggers/boot.js +149 -0
  736. package/dist/triggers/boot.js.map +1 -0
  737. package/dist/triggers/dispatcher.d.ts +234 -0
  738. package/dist/triggers/dispatcher.d.ts.map +1 -0
  739. package/dist/triggers/dispatcher.js +525 -0
  740. package/dist/triggers/dispatcher.js.map +1 -0
  741. package/dist/triggers/file-watch.d.ts +90 -0
  742. package/dist/triggers/file-watch.d.ts.map +1 -0
  743. package/dist/triggers/file-watch.js +405 -0
  744. package/dist/triggers/file-watch.js.map +1 -0
  745. package/dist/triggers/registration-sync.d.ts +34 -0
  746. package/dist/triggers/registration-sync.d.ts.map +1 -0
  747. package/dist/triggers/registration-sync.js +162 -0
  748. package/dist/triggers/registration-sync.js.map +1 -0
  749. package/dist/triggers/schedule-timing.d.ts +52 -0
  750. package/dist/triggers/schedule-timing.d.ts.map +1 -0
  751. package/dist/triggers/schedule-timing.js +90 -0
  752. package/dist/triggers/schedule-timing.js.map +1 -0
  753. package/dist/triggers/scheduler.d.ts +55 -0
  754. package/dist/triggers/scheduler.d.ts.map +1 -0
  755. package/dist/triggers/scheduler.js +122 -0
  756. package/dist/triggers/scheduler.js.map +1 -0
  757. package/dist/triggers/settle.d.ts +66 -0
  758. package/dist/triggers/settle.d.ts.map +1 -0
  759. package/dist/triggers/settle.js +99 -0
  760. package/dist/triggers/settle.js.map +1 -0
  761. package/dist/triggers/stores.d.ts +38 -0
  762. package/dist/triggers/stores.d.ts.map +1 -0
  763. package/dist/triggers/stores.js +168 -0
  764. package/dist/triggers/stores.js.map +1 -0
  765. package/dist/triggers/webhook-route.d.ts +49 -0
  766. package/dist/triggers/webhook-route.d.ts.map +1 -0
  767. package/dist/triggers/webhook-route.js +148 -0
  768. package/dist/triggers/webhook-route.js.map +1 -0
  769. package/dist/trpc/context.d.ts +52 -5
  770. package/dist/trpc/context.d.ts.map +1 -1
  771. package/dist/trpc/context.js +4 -1
  772. package/dist/trpc/context.js.map +1 -1
  773. package/dist/trpc/error-formatter.d.ts.map +1 -1
  774. package/dist/trpc/error-formatter.js +23 -2
  775. package/dist/trpc/error-formatter.js.map +1 -1
  776. package/dist/trpc/error-logging.d.ts +14 -0
  777. package/dist/trpc/error-logging.d.ts.map +1 -0
  778. package/dist/trpc/error-logging.js +27 -0
  779. package/dist/trpc/error-logging.js.map +1 -0
  780. package/dist/trpc/index.d.ts +2 -2
  781. package/dist/trpc/index.d.ts.map +1 -1
  782. package/dist/trpc/index.js +0 -2
  783. package/dist/trpc/index.js.map +1 -1
  784. package/dist/trpc/middleware.d.ts +6 -3
  785. package/dist/trpc/middleware.d.ts.map +1 -1
  786. package/dist/trpc/router.d.ts +29791 -1303
  787. package/dist/trpc/router.d.ts.map +1 -1
  788. package/dist/trpc/router.js +46 -3
  789. package/dist/trpc/router.js.map +1 -1
  790. package/dist/trpc/routers/agent-access.d.ts +149 -0
  791. package/dist/trpc/routers/agent-access.d.ts.map +1 -0
  792. package/dist/trpc/routers/agent-access.js +224 -0
  793. package/dist/trpc/routers/agent-access.js.map +1 -0
  794. package/dist/trpc/routers/applications.d.ts +1117 -0
  795. package/dist/trpc/routers/applications.d.ts.map +1 -0
  796. package/dist/trpc/routers/applications.js +218 -0
  797. package/dist/trpc/routers/applications.js.map +1 -0
  798. package/dist/trpc/routers/assets.d.ts +184 -42
  799. package/dist/trpc/routers/assets.d.ts.map +1 -1
  800. package/dist/trpc/routers/assets.js +499 -105
  801. package/dist/trpc/routers/assets.js.map +1 -1
  802. package/dist/trpc/routers/code-gen.d.ts +99 -0
  803. package/dist/trpc/routers/code-gen.d.ts.map +1 -0
  804. package/dist/trpc/routers/code-gen.js +164 -0
  805. package/dist/trpc/routers/code-gen.js.map +1 -0
  806. package/dist/trpc/routers/collections.d.ts +1 -26
  807. package/dist/trpc/routers/collections.d.ts.map +1 -1
  808. package/dist/trpc/routers/collections.js +145 -78
  809. package/dist/trpc/routers/collections.js.map +1 -1
  810. package/dist/trpc/routers/costs.d.ts +40 -96
  811. package/dist/trpc/routers/costs.d.ts.map +1 -1
  812. package/dist/trpc/routers/costs.js +14 -89
  813. package/dist/trpc/routers/costs.js.map +1 -1
  814. package/dist/trpc/routers/credits.d.ts +110 -0
  815. package/dist/trpc/routers/credits.d.ts.map +1 -0
  816. package/dist/trpc/routers/credits.js +69 -0
  817. package/dist/trpc/routers/credits.js.map +1 -0
  818. package/dist/trpc/routers/custom-providers.d.ts +88 -0
  819. package/dist/trpc/routers/custom-providers.d.ts.map +1 -0
  820. package/dist/trpc/routers/custom-providers.js +140 -0
  821. package/dist/trpc/routers/custom-providers.js.map +1 -0
  822. package/dist/trpc/routers/documents.d.ts +61 -0
  823. package/dist/trpc/routers/documents.d.ts.map +1 -0
  824. package/dist/trpc/routers/documents.js +23 -0
  825. package/dist/trpc/routers/documents.js.map +1 -0
  826. package/dist/trpc/routers/extension.d.ts +24 -0
  827. package/dist/trpc/routers/extension.d.ts.map +1 -0
  828. package/dist/trpc/routers/extension.js +28 -0
  829. package/dist/trpc/routers/extension.js.map +1 -0
  830. package/dist/trpc/routers/external-mcp.d.ts +108 -0
  831. package/dist/trpc/routers/external-mcp.d.ts.map +1 -0
  832. package/dist/trpc/routers/external-mcp.js +70 -0
  833. package/dist/trpc/routers/external-mcp.js.map +1 -0
  834. package/dist/trpc/routers/files.d.ts +9 -2
  835. package/dist/trpc/routers/files.d.ts.map +1 -1
  836. package/dist/trpc/routers/files.js +67 -37
  837. package/dist/trpc/routers/files.js.map +1 -1
  838. package/dist/trpc/routers/fonts.d.ts +30 -0
  839. package/dist/trpc/routers/fonts.d.ts.map +1 -0
  840. package/dist/trpc/routers/fonts.js +112 -0
  841. package/dist/trpc/routers/fonts.js.map +1 -0
  842. package/dist/trpc/routers/games.d.ts +7610 -0
  843. package/dist/trpc/routers/games.d.ts.map +1 -0
  844. package/dist/trpc/routers/games.js +456 -0
  845. package/dist/trpc/routers/games.js.map +1 -0
  846. package/dist/trpc/routers/integrations.d.ts +86 -0
  847. package/dist/trpc/routers/integrations.d.ts.map +1 -0
  848. package/dist/trpc/routers/integrations.js +129 -0
  849. package/dist/trpc/routers/integrations.js.map +1 -0
  850. package/dist/trpc/routers/jobs.d.ts +88 -27
  851. package/dist/trpc/routers/jobs.d.ts.map +1 -1
  852. package/dist/trpc/routers/jobs.js +113 -35
  853. package/dist/trpc/routers/jobs.js.map +1 -1
  854. package/dist/trpc/routers/js-scripts.d.ts +410 -0
  855. package/dist/trpc/routers/js-scripts.d.ts.map +1 -0
  856. package/dist/trpc/routers/js-scripts.js +303 -0
  857. package/dist/trpc/routers/js-scripts.js.map +1 -0
  858. package/dist/trpc/routers/mcp-config.d.ts +1 -1
  859. package/dist/trpc/routers/mcp-config.js +5 -3
  860. package/dist/trpc/routers/mcp-config.js.map +1 -1
  861. package/dist/trpc/routers/memories.d.ts +79 -0
  862. package/dist/trpc/routers/memories.d.ts.map +1 -0
  863. package/dist/trpc/routers/memories.js +66 -0
  864. package/dist/trpc/routers/memories.js.map +1 -0
  865. package/dist/trpc/routers/messages.d.ts +4 -77
  866. package/dist/trpc/routers/messages.d.ts.map +1 -1
  867. package/dist/trpc/routers/messages.js +9 -58
  868. package/dist/trpc/routers/messages.js.map +1 -1
  869. package/dist/trpc/routers/models.d.ts +1403 -238
  870. package/dist/trpc/routers/models.d.ts.map +1 -1
  871. package/dist/trpc/routers/models.js +532 -294
  872. package/dist/trpc/routers/models.js.map +1 -1
  873. package/dist/trpc/routers/nodes.d.ts +32 -16
  874. package/dist/trpc/routers/nodes.d.ts.map +1 -1
  875. package/dist/trpc/routers/nodes.js +70 -31
  876. package/dist/trpc/routers/nodes.js.map +1 -1
  877. package/dist/trpc/routers/packs.d.ts +184 -0
  878. package/dist/trpc/routers/packs.d.ts.map +1 -0
  879. package/dist/trpc/routers/packs.js +259 -0
  880. package/dist/trpc/routers/packs.js.map +1 -0
  881. package/dist/trpc/routers/projects.d.ts +422 -0
  882. package/dist/trpc/routers/projects.d.ts.map +1 -0
  883. package/dist/trpc/routers/projects.js +373 -0
  884. package/dist/trpc/routers/projects.js.map +1 -0
  885. package/dist/trpc/routers/resources.d.ts +120 -0
  886. package/dist/trpc/routers/resources.d.ts.map +1 -0
  887. package/dist/trpc/routers/resources.js +212 -0
  888. package/dist/trpc/routers/resources.js.map +1 -0
  889. package/dist/trpc/routers/scripts.d.ts +1116 -0
  890. package/dist/trpc/routers/scripts.d.ts.map +1 -0
  891. package/dist/trpc/routers/scripts.js +131 -0
  892. package/dist/trpc/routers/scripts.js.map +1 -0
  893. package/dist/trpc/routers/segmentation.d.ts +62 -0
  894. package/dist/trpc/routers/segmentation.d.ts.map +1 -0
  895. package/dist/trpc/routers/segmentation.js +146 -0
  896. package/dist/trpc/routers/segmentation.js.map +1 -0
  897. package/dist/trpc/routers/settings.d.ts +177 -10
  898. package/dist/trpc/routers/settings.d.ts.map +1 -1
  899. package/dist/trpc/routers/settings.js +123 -33
  900. package/dist/trpc/routers/settings.js.map +1 -1
  901. package/dist/trpc/routers/sketch.d.ts +854 -0
  902. package/dist/trpc/routers/sketch.d.ts.map +1 -0
  903. package/dist/trpc/routers/sketch.js +617 -0
  904. package/dist/trpc/routers/sketch.js.map +1 -0
  905. package/dist/trpc/routers/skills.d.ts +76 -22
  906. package/dist/trpc/routers/skills.d.ts.map +1 -1
  907. package/dist/trpc/routers/skills.js +195 -170
  908. package/dist/trpc/routers/skills.js.map +1 -1
  909. package/dist/trpc/routers/storage.d.ts +3 -30
  910. package/dist/trpc/routers/storage.d.ts.map +1 -1
  911. package/dist/trpc/routers/storage.js +60 -117
  912. package/dist/trpc/routers/storage.js.map +1 -1
  913. package/dist/trpc/routers/storyboards.d.ts +5907 -0
  914. package/dist/trpc/routers/storyboards.d.ts.map +1 -0
  915. package/dist/trpc/routers/storyboards.js +156 -0
  916. package/dist/trpc/routers/storyboards.js.map +1 -0
  917. package/dist/trpc/routers/threads.d.ts +33 -21
  918. package/dist/trpc/routers/threads.d.ts.map +1 -1
  919. package/dist/trpc/routers/threads.js +36 -30
  920. package/dist/trpc/routers/threads.js.map +1 -1
  921. package/dist/trpc/routers/timeline.d.ts +8516 -0
  922. package/dist/trpc/routers/timeline.d.ts.map +1 -0
  923. package/dist/trpc/routers/timeline.js +493 -0
  924. package/dist/trpc/routers/timeline.js.map +1 -0
  925. package/dist/trpc/routers/triggers.d.ts +46 -0
  926. package/dist/trpc/routers/triggers.d.ts.map +1 -0
  927. package/dist/trpc/routers/triggers.js +117 -0
  928. package/dist/trpc/routers/triggers.js.map +1 -0
  929. package/dist/trpc/routers/users.d.ts +1 -14
  930. package/dist/trpc/routers/users.d.ts.map +1 -1
  931. package/dist/trpc/routers/users.js +1 -12
  932. package/dist/trpc/routers/users.js.map +1 -1
  933. package/dist/trpc/routers/worker.d.ts +242 -0
  934. package/dist/trpc/routers/worker.d.ts.map +1 -0
  935. package/dist/trpc/routers/worker.js +220 -0
  936. package/dist/trpc/routers/worker.js.map +1 -0
  937. package/dist/trpc/routers/workflows.d.ts +385 -197
  938. package/dist/trpc/routers/workflows.d.ts.map +1 -1
  939. package/dist/trpc/routers/workflows.js +326 -345
  940. package/dist/trpc/routers/workflows.js.map +1 -1
  941. package/dist/trpc/routers/workspace.d.ts +59 -33
  942. package/dist/trpc/routers/workspace.d.ts.map +1 -1
  943. package/dist/trpc/routers/workspace.js +230 -93
  944. package/dist/trpc/routers/workspace.js.map +1 -1
  945. package/dist/trpc/sandbox-coverage.d.ts +64 -0
  946. package/dist/trpc/sandbox-coverage.d.ts.map +1 -0
  947. package/dist/trpc/sandbox-coverage.js +1049 -0
  948. package/dist/trpc/sandbox-coverage.js.map +1 -0
  949. package/dist/websocket-client-session.d.ts +320 -0
  950. package/dist/websocket-client-session.d.ts.map +1 -0
  951. package/dist/websocket-client-session.js +1253 -0
  952. package/dist/websocket-client-session.js.map +1 -0
  953. package/dist/workflow-interface-service.d.ts +87 -0
  954. package/dist/workflow-interface-service.d.ts.map +1 -0
  955. package/dist/workflow-interface-service.js +131 -0
  956. package/dist/workflow-interface-service.js.map +1 -0
  957. package/dist/workspace-api.d.ts.map +1 -1
  958. package/dist/workspace-api.js +33 -23
  959. package/dist/workspace-api.js.map +1 -1
  960. package/dist/ws-adapter.d.ts +46 -3
  961. package/dist/ws-adapter.d.ts.map +1 -1
  962. package/dist/ws-adapter.js +164 -26
  963. package/dist/ws-adapter.js.map +1 -1
  964. package/package.json +81 -35
  965. package/dist/agent/agent-runtime.d.ts +0 -47
  966. package/dist/agent/agent-runtime.d.ts.map +0 -1
  967. package/dist/agent/agent-runtime.js +0 -579
  968. package/dist/agent/agent-runtime.js.map +0 -1
  969. package/dist/agent/codex-agent.d.ts +0 -41
  970. package/dist/agent/codex-agent.d.ts.map +0 -1
  971. package/dist/agent/codex-agent.js +0 -334
  972. package/dist/agent/codex-agent.js.map +0 -1
  973. package/dist/agent/index.d.ts +0 -6
  974. package/dist/agent/index.d.ts.map +0 -1
  975. package/dist/agent/index.js +0 -4
  976. package/dist/agent/index.js.map +0 -1
  977. package/dist/agent/llm-agent.d.ts +0 -51
  978. package/dist/agent/llm-agent.d.ts.map +0 -1
  979. package/dist/agent/llm-agent.js +0 -526
  980. package/dist/agent/llm-agent.js.map +0 -1
  981. package/dist/agent/mcp-tool-server.d.ts +0 -33
  982. package/dist/agent/mcp-tool-server.d.ts.map +0 -1
  983. package/dist/agent/mcp-tool-server.js +0 -292
  984. package/dist/agent/mcp-tool-server.js.map +0 -1
  985. package/dist/agent/opencode-agent.d.ts +0 -32
  986. package/dist/agent/opencode-agent.d.ts.map +0 -1
  987. package/dist/agent/opencode-agent.js +0 -332
  988. package/dist/agent/opencode-agent.js.map +0 -1
  989. package/dist/agent/pi-agent.d.ts +0 -42
  990. package/dist/agent/pi-agent.d.ts.map +0 -1
  991. package/dist/agent/pi-agent.js +0 -429
  992. package/dist/agent/pi-agent.js.map +0 -1
  993. package/dist/agent/sdk-provider.d.ts +0 -46
  994. package/dist/agent/sdk-provider.d.ts.map +0 -1
  995. package/dist/agent/sdk-provider.js +0 -50
  996. package/dist/agent/sdk-provider.js.map +0 -1
  997. package/dist/agent/socket-route.d.ts +0 -11
  998. package/dist/agent/socket-route.d.ts.map +0 -1
  999. package/dist/agent/socket-route.js +0 -262
  1000. package/dist/agent/socket-route.js.map +0 -1
  1001. package/dist/agent/transport.d.ts +0 -39
  1002. package/dist/agent/transport.d.ts.map +0 -1
  1003. package/dist/agent/transport.js +0 -17
  1004. package/dist/agent/transport.js.map +0 -1
  1005. package/dist/agent/types.d.ts +0 -11
  1006. package/dist/agent/types.d.ts.map +0 -1
  1007. package/dist/agent/types.js +0 -10
  1008. package/dist/agent/types.js.map +0 -1
  1009. package/dist/routes/jobs.d.ts +0 -16
  1010. package/dist/routes/jobs.d.ts.map +0 -1
  1011. package/dist/routes/jobs.js +0 -25
  1012. package/dist/routes/jobs.js.map +0 -1
  1013. package/dist/trpc/routers/sandboxes.d.ts +0 -64
  1014. package/dist/trpc/routers/sandboxes.d.ts.map +0 -1
  1015. package/dist/trpc/routers/sandboxes.js +0 -327
  1016. package/dist/trpc/routers/sandboxes.js.map +0 -1
  1017. package/dist/unified-websocket-runner.d.ts +0 -280
  1018. package/dist/unified-websocket-runner.d.ts.map +0 -1
  1019. package/dist/unified-websocket-runner.js +0 -3840
  1020. package/dist/unified-websocket-runner.js.map +0 -1
@@ -0,0 +1,3516 @@
1
+ import { createLogger } from "@nodetool-ai/config";
2
+ import { randomUUID } from "node:crypto";
3
+ import { SUPERSEDED_TOOL_RESULT, repairOrphanedToolCalls } from "../chat-tool-call-repair.js";
4
+ import { attachChatPredictionForwarder } from "../chat-prediction-forwarder.js";
5
+ import { isGoogleWorkspaceEnabled } from "@nodetool-ai/config";
6
+ import { isFunctionValue, isNonEmptyString, isNumber, isObjectLike, isRecord, isString } from "../lib/wire-values.js";
7
+ import { storeAssetWithThumbnail } from "../lib/thumbnail.js";
8
+ import { createGenerationRun, generateSpeechBytes, generateMusicBytes } from "./media-generation.js";
9
+ import { attachRunCostLedger, linkGenerationAssets, ExecutionSession, isExecutionPreflightError, toRawGraphInput } from "@nodetool-ai/execution";
10
+ import { Asset, COMPACTION_EVENT_TYPE, compactionMessageContent, isCompactionMessage, Job, Message, Prediction, Project, Skill, Thread, Memory, Workflow } from "@nodetool-ai/models";
11
+ import { WORKFLOW_DOCUMENT_TOOL_NAMES } from "@nodetool-ai/node-sdk";
12
+ import { ACTIVE_MODEL_CONTEXT_KEY, DIRECT_TOOL_NAMES, RUN_BUDGET_CONTEXT_KEY, createRunBudget, detectImageMime, IMAGE_MIME_TO_EXT, expandEntitiesForGeneration, getProcessSandboxModuleCatalog, estimatePromptTokens, isToolCall, isProviderSessionUpdate, isProviderMessageEvent, isProviderStop, httpStatusFromError, groqRequestFailureMessage, mediaResolverFor, providerFailureDetail, resolveContextWindow } from "@nodetool-ai/runtime";
13
+ import { isModelSelection, PROVIDER_IDS, NO_MODEL_SELECTED_MESSAGE, noMediaModelSelectedMessage } from "@nodetool-ai/protocol";
14
+ import { Tool, compactAssetUris, compactResourceIds } from "@nodetool-ai/agents";
15
+ import { createChatCodeActSession, createSandboxClock, sandboxPackagesForChat, CODEACT_RESIDENT_TOOL_NAMES, EXECUTE_CODE_TOOL_NAME } from "@nodetool-ai/agents";
16
+ import { getBuiltinTools, getAllMcpTools, getGoogleWorkspaceTools, getApifyTools, getSerpApiTools, toolForCapabilityName, gateTools, capabilityFromTool, createCapabilityRun, contextSecretAvailability, BackgroundSubtaskRegistry, UNGATED, PERMISSION_GATE_CONTEXT_KEY, GAME_ASSET_NODE_RUNNER_CONTEXT_KEY, extractInjectableImages, registerTemporaryImageHandle } from "@nodetool-ai/agents";
17
+ import { mcpToolHostDeps } from "../mcp-tool-deps.js";
18
+ import { externalMcpToolsFor } from "../external-mcp.js";
19
+ import { formatSkillCatalogForPrompt, mergeSystemSkills, formatMemoriesForPrompt } from "@nodetool-ai/agents";
20
+ import { RunNodeTool } from "../agent/run-node-tool.js";
21
+ import { extractEmbeddedImage } from "./asset-autosave.js";
22
+ import { buildChatAgentSystemPrompt, focusedUiToolNames, normalizeToolCallName, RESIDENT_TOOL_NAMES, unroutableToolMessage } from "./chat-prompt.js";
23
+ import { createRuntimeContext } from "./model-interfaces.js";
24
+ import { getSetting } from "../settings-registry.js";
25
+ import { appendContextToLastUser, createWorkflowResponseContent, dbMessageToProviderMessage, extractTextContent, historySinceCompaction, invokedSkillsSection, toolResultDisplayText } from "./chat-history.js";
26
+ import { chooseCompactionCut, compactionThreshold, COMPACTION_SUMMARY_MAX_TOKENS, holdsTranscriptServerSide, readCompactionSettings, summarizeForCompaction } from "./chat-compaction.js";
27
+ import { createRelayActivityWaiter, DEFAULT_RUN_JOB_EXECUTION_OPTIONS } from "./job-execution.js";
28
+ const log = createLogger("nodetool.websocket.runner");
29
+ const GENERIC_CHAT_ERROR = "Something went wrong while processing your request. Please try again.";
30
+ /**
31
+ * Keep implementation details out of the client error envelope. Provider
32
+ * messages remain useful when they are short and user-facing, but database
33
+ * diagnostics and query arguments can contain schema details or secrets.
34
+ */
35
+ function safeClientErrorText(error) {
36
+ const text = error instanceof Error ? error.message : String(error);
37
+ if (text.length > 500 ||
38
+ /\b(?:select|insert|update|delete)\b[\s\S]*\b(?:from|where|into|set)\b/i.test(text) ||
39
+ /(?:sqlite|sqlstate|drizzle|query\s+(?:failed|parameter)|bind\s+parameter|database\s+error|stack\s+trace|\bparams?\s*[:=]|\bparameters?\s*[:=]|\b(?:api[_ -]?key|authorization|bearer)\s*[:=]|https?:\/\/\S*\?\S*=)/i.test(text)) {
40
+ return GENERIC_CHAT_ERROR;
41
+ }
42
+ return text || GENERIC_CHAT_ERROR;
43
+ }
44
+ /**
45
+ * Gemini wraps JSON failures in a plain Error (`Gemini API error 429: …`), so
46
+ * the shared status helper cannot see the status when the wrapper says
47
+ * `API error` instead of `HTTP`. Parse only that known wrapper and retain a
48
+ * bounded, user-actionable classification rather than forwarding the JSON.
49
+ */
50
+ function geminiFailureSummary(error, providerId) {
51
+ if (providerId.toLowerCase() !== "gemini") {
52
+ return null;
53
+ }
54
+ const text = error instanceof Error ? error.message : "";
55
+ const wrapper = text.match(/^Gemini API error(?:\s+(\d{3}))?:\s*([\s\S]+)$/i);
56
+ if (!wrapper) {
57
+ return null;
58
+ }
59
+ const wrapperStatus = Number(wrapper[1]);
60
+ let status = wrapperStatus >= 100 && wrapperStatus <= 599 ? wrapperStatus : null;
61
+ let message = wrapper[2].trim();
62
+ try {
63
+ const parsed = JSON.parse(wrapper[2]);
64
+ if (isObjectLike(parsed)) {
65
+ const root = parsed;
66
+ const nested = isObjectLike(root.error) ? root.error : root;
67
+ if (isObjectLike(nested)) {
68
+ const code = nested.code;
69
+ if (status === null &&
70
+ isNumber(code) &&
71
+ Number.isInteger(code) &&
72
+ code >= 100 &&
73
+ code <= 599) {
74
+ status = code;
75
+ }
76
+ if (isString(nested.message)) {
77
+ message = nested.message;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ // The provider sometimes returns plain text after the same wrapper.
84
+ }
85
+ const lower = message.toLowerCase();
86
+ let kind = null;
87
+ if (/quota|resource_exhausted|rate limit|billing|credit|limit exceeded|daily limit/.test(lower)) {
88
+ kind = "quota";
89
+ }
90
+ else if (/no longer available|model[^.]{0,80}(?:not found|unavailable|unsupported|does not exist)|(?:not found|unavailable|unsupported)[^.]{0,80}model/.test(lower)) {
91
+ kind = "model";
92
+ }
93
+ else if (/tool|function call|function calling|function_declaration/.test(lower) &&
94
+ /not support|unsupported|invalid|disable|not enabled/.test(lower)) {
95
+ kind = "tools";
96
+ }
97
+ const safeMessage = safeClientErrorText(message);
98
+ return {
99
+ status,
100
+ kind,
101
+ message: safeMessage === GENERIC_CHAT_ERROR ? null : safeMessage
102
+ };
103
+ }
104
+ function providerFailureMessage(error, providerId, model) {
105
+ const gemini = geminiFailureSummary(error, providerId);
106
+ const groq = groqRequestFailureMessage(error);
107
+ const status = httpStatusFromError(error) ?? gemini?.status ?? null;
108
+ const rawMessage = groq ?? gemini?.message ?? safeClientErrorText(error);
109
+ let bodyMessage = null;
110
+ if (isObjectLike(error)) {
111
+ const body = error.body ?? error.response;
112
+ if (isObjectLike(body)) {
113
+ const bodyError = isObjectLike(body.error) ? body.error : body;
114
+ if (isObjectLike(bodyError) && isString(bodyError.message)) {
115
+ const safe = safeClientErrorText(bodyError.message);
116
+ bodyMessage = safe === GENERIC_CHAT_ERROR ? null : safe;
117
+ }
118
+ }
119
+ }
120
+ if (providerId.toLowerCase() === "openai" &&
121
+ /\borganization must be verified\b/i.test(bodyMessage ?? rawMessage)) {
122
+ return {
123
+ errorType: status === null ? "error" : "http_status_error",
124
+ message: "This model requires a verified OpenAI organization. Verify your provider organization or choose a model your account can access.",
125
+ statusCode: status ?? undefined
126
+ };
127
+ }
128
+ const detail = providerFailureDetail(error);
129
+ if (detail?.code === "context_exceeded") {
130
+ const target = model ? `${providerId}/${model}` : providerId;
131
+ return {
132
+ errorType: "error",
133
+ message: `The ${target} request is too large for the model context window. Shorten the conversation or remove some attachments and try again.`,
134
+ statusCode: status ?? undefined
135
+ };
136
+ }
137
+ if (detail?.code === "provider_auth") {
138
+ return {
139
+ errorType: "error",
140
+ message: `Authentication failed: ${detail.provider} rejected the configured credentials. Check the API key in Settings → Models & Providers.`,
141
+ statusCode: status ?? undefined
142
+ };
143
+ }
144
+ if (status === null) {
145
+ return { errorType: "error", message: rawMessage, statusCode: undefined };
146
+ }
147
+ let message = rawMessage;
148
+ if (gemini?.kind === "quota") {
149
+ message = `Account quota exhausted for ${providerId}/${model}. Check your ${providerId} quota or billing and try again later.`;
150
+ }
151
+ else if (gemini?.kind === "model") {
152
+ message = `Model ${model || "requested"} is unavailable on ${providerId}. Choose another model or check that your account has access to it.`;
153
+ }
154
+ else if (gemini?.kind === "tools") {
155
+ message = `Model ${model || "requested"} does not support the requested tools on ${providerId}. Choose a model with tool support or disable tools.`;
156
+ }
157
+ else if (status === 400) {
158
+ message = `Bad request: ${groq ?? bodyMessage ?? rawMessage}`;
159
+ }
160
+ else if (status === 401) {
161
+ message = "Authentication failed: Invalid API key or token";
162
+ }
163
+ else if (status === 402) {
164
+ message = `Account billing or credit limit reached for ${providerId}. Check your ${providerId} plan or billing.`;
165
+ }
166
+ else if (status === 403) {
167
+ message =
168
+ "Access forbidden: You don't have permission for this resource. Check the provider key's model and account access.";
169
+ }
170
+ else if (status === 404) {
171
+ message = `Model ${model || "requested"} was not found or is unavailable on ${providerId}. Choose another model or check account access.`;
172
+ }
173
+ else if (status === 413) {
174
+ const detail = groq ?? bodyMessage ?? rawMessage;
175
+ message =
176
+ groq ??
177
+ (detail === GENERIC_CHAT_ERROR
178
+ ? "Request is too large for the provider. Shorten the conversation or remove some attachments and try again."
179
+ : `Request is too large for the provider (${detail}). Shorten the conversation or remove some attachments and try again.`);
180
+ }
181
+ else if (status === 429) {
182
+ message = groq
183
+ ? groq
184
+ : "Rate limited: Too many requests or insufficient provider quota. Check your provider plan and try again later.";
185
+ }
186
+ else if (status >= 500) {
187
+ message =
188
+ groq ??
189
+ bodyMessage ??
190
+ `Server error (${status}): The service is temporarily unavailable`;
191
+ }
192
+ else {
193
+ message = `HTTP error (${status}): ${rawMessage}`;
194
+ }
195
+ return { errorType: "http_status_error", message, statusCode: status };
196
+ }
197
+ /**
198
+ * How many of a user's newest memories the turn reads to build its block. The
199
+ * read doubles as the "how many live in other threads" count, so it is capped:
200
+ * the number is a nudge toward `memory_search`, not an audit.
201
+ */
202
+ const MEMORY_SCAN_LIMIT = 400;
203
+ /** How many of this thread's memories are pasted into the block. */
204
+ const MEMORY_BLOCK_LIMIT = 100;
205
+ /**
206
+ * How many recent messages to scan when probing for a resumable session before
207
+ * deciding whether the full thread needs loading. Large enough to clear the
208
+ * occasional errored turn between sessioned assistant replies, tiny next to a
209
+ * full thread load.
210
+ */
211
+ const SESSION_PROBE_WINDOW = 50;
212
+ /**
213
+ * Output-token worst case assumed when reserving a chat turn. The chat loop
214
+ * sends no `maxTokens`, so nothing bounds the answer from this side; the
215
+ * reservation charges the same ceiling the Agent node defaults to, which is
216
+ * what makes the dollar cap hold instead of being noticed after the money is
217
+ * spent.
218
+ */
219
+ const CHAT_TURN_MAX_OUTPUT_TOKENS = 16_384;
220
+ /** Documented defaults for the five `NODETOOL_AGENT_*` budget settings. */
221
+ const AGENT_BUDGET_DEFAULTS = {
222
+ costCapUsd: 5,
223
+ deadlineMs: 1_800_000,
224
+ maxConcurrency: 8,
225
+ maxTurns: 200,
226
+ unpricedTokenCeiling: 400_000
227
+ };
228
+ /**
229
+ * Read one budget setting. The store is best-effort — the same rule the rest
230
+ * of the runner follows for settings: an unreachable database falls back to
231
+ * the documented default rather than failing the turn.
232
+ */
233
+ async function readBudgetSetting(key) {
234
+ try {
235
+ return await getSetting(key);
236
+ }
237
+ catch {
238
+ // Settings store unavailable — the caller's default stands.
239
+ return null;
240
+ }
241
+ }
242
+ async function budgetSettingNumber(key, fallback) {
243
+ const raw = await readBudgetSetting(key);
244
+ if (raw === null)
245
+ return fallback;
246
+ const parsed = Number(raw);
247
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
248
+ }
249
+ /**
250
+ * The run's USD ceiling, or `null` for no ceiling at all.
251
+ *
252
+ * `Number("")` is 0 and a cap of 0 refuses every turn rather than lifting the
253
+ * ceiling, so a blank or non-positive value means *no cap* — which is what a
254
+ * local-only install wants. An absent setting takes the documented $5 default;
255
+ * `getSetting` cannot tell an empty stored value from an unset one (it falls
256
+ * through both), so "no cap" is expressed by storing a non-positive number.
257
+ */
258
+ async function budgetCostCapUsd() {
259
+ const raw = await readBudgetSetting("NODETOOL_AGENT_TURN_COST_CAP_USD");
260
+ // `getSetting` answers null for both an unset and an empty stored value, so
261
+ // clearing the field restores the default rather than removing the cap.
262
+ // Removing it is `0`, which is why a non-positive number means "no cap"
263
+ // instead of a cap nothing could ever pass.
264
+ if (raw === null)
265
+ return AGENT_BUDGET_DEFAULTS.costCapUsd;
266
+ const parsed = Number(raw.trim());
267
+ if (!Number.isFinite(parsed) || parsed <= 0)
268
+ return null;
269
+ return parsed;
270
+ }
271
+ /**
272
+ * The one budget a chat turn shares downward (invariant I-2): the capability
273
+ * runs, the sub-agent runtime, the provider loop, and — through the context
274
+ * key — an Agent node started by `run_node` and any JS script the turn runs.
275
+ * Every one of them reserves against the same cap instead of opening its own.
276
+ */
277
+ async function createChatTurnBudget() {
278
+ const [capUsd, deadlineMs, maxConcurrency, maxTurns, unpricedTokenCeiling] = await Promise.all([
279
+ budgetCostCapUsd(),
280
+ budgetSettingNumber("NODETOOL_AGENT_TURN_DEADLINE_MS", AGENT_BUDGET_DEFAULTS.deadlineMs),
281
+ budgetSettingNumber("NODETOOL_AGENT_MAX_CONCURRENCY", AGENT_BUDGET_DEFAULTS.maxConcurrency),
282
+ budgetSettingNumber("NODETOOL_AGENT_MAX_TURNS", AGENT_BUDGET_DEFAULTS.maxTurns),
283
+ budgetSettingNumber("NODETOOL_AGENT_UNPRICED_TOKEN_CEILING", AGENT_BUDGET_DEFAULTS.unpricedTokenCeiling)
284
+ ]);
285
+ return createRunBudget({
286
+ capUsd,
287
+ maxOutputTokens: CHAT_TURN_MAX_OUTPUT_TOKENS,
288
+ unpricedTokenCeiling,
289
+ deadlineMs,
290
+ maxConcurrency,
291
+ maxTurns
292
+ });
293
+ }
294
+ /**
295
+ * Find the continuation token to resume this thread with: the `provider_session`
296
+ * of the most recent assistant message, but only if it was produced by the same
297
+ * `provider` and `model` as the incoming request (a session is bound to both).
298
+ * Returns null when there is nothing to resume, so the provider starts fresh.
299
+ */
300
+ function lastMatchingProviderSession(dbMessages, providerId, model) {
301
+ for (let i = dbMessages.length - 1; i >= 0; i--) {
302
+ const m = dbMessages[i];
303
+ if (m.role !== "assistant")
304
+ continue;
305
+ const session = m.provider_session;
306
+ if (!session)
307
+ continue;
308
+ return session.providerId === providerId && session.model === model
309
+ ? session
310
+ : null;
311
+ }
312
+ return null;
313
+ }
314
+ /**
315
+ * One connection's chat turns: the turn counter and abort controller, the
316
+ * per-thread permission state, the capability run, and the three message
317
+ * shapes a `chat_message` frame can take — an agent turn, a workflow run, or
318
+ * a media generation.
319
+ *
320
+ * It reaches the connection only through {@link ClientSession}. Everything
321
+ * else another region owns arrives in {@link ChatTurnDeps}.
322
+ */
323
+ export class ChatTurnHandler {
324
+ session;
325
+ deps;
326
+ chatRequestSeq = 0;
327
+ /**
328
+ * Aborts the in-flight chat/inference turn. The seq counter above only filters
329
+ * stale output at yield boundaries — it cannot interrupt a provider that is
330
+ * blocked awaiting a response, nor tell one that owns a subprocess (the Claude
331
+ * Agent provider) to stop working. This signal does, and is threaded into
332
+ * every provider call the turn makes.
333
+ */
334
+ chatAbort = null;
335
+ /**
336
+ * Per-thread set of tool names the user approved for the rest of the chat
337
+ * via "Allow for this chat". Persists across messages within a thread.
338
+ */
339
+ chatSessionAllow = new Map();
340
+ /**
341
+ * Live permission mode for an in-flight turn. `set_permission_mode` writes
342
+ * here so switching to Auto mid-turn applies to the next gated call.
343
+ */
344
+ chatTurnPermissionMode = new Map();
345
+ /**
346
+ * The capability run for the chat turn this connection is executing — the
347
+ * gate, the context, and everything a capability needs that only exists per
348
+ * turn. Built beside the toolbelt; the sandbox still calls the belt, and PR
349
+ * 11 is what switches the guest onto `run.invoke`.
350
+ */
351
+ chatCapabilityRun = null;
352
+ /**
353
+ * Input window per `provider/model`, resolved once per connection and again
354
+ * whenever a turn selects a model this connection has not used.
355
+ */
356
+ contextWindows = new Map();
357
+ /**
358
+ * The provider-reported size of each thread's last call, keyed by thread,
359
+ * with the `provider/model` it was measured on. The next turn's prompt is at
360
+ * least this large, and this counts what the local estimate cannot see.
361
+ */
362
+ measuredThreadTokens = new Map();
363
+ constructor(session, deps) {
364
+ this.session = session;
365
+ this.deps = deps;
366
+ }
367
+ /** The run built for the last chat turn — what PR 11 hands to the sandbox. */
368
+ getCapabilityRun() {
369
+ return this.chatCapabilityRun;
370
+ }
371
+ /**
372
+ * Open a chat/inference turn: cancel whatever was running and hand back the
373
+ * seq + signal the new turn runs under. A superseding message cancels the
374
+ * previous turn exactly as an explicit Stop does.
375
+ */
376
+ beginTurn() {
377
+ this.cancel();
378
+ this.chatRequestSeq += 1;
379
+ this.chatAbort = new AbortController();
380
+ return {
381
+ seq: this.chatRequestSeq,
382
+ signal: this.chatAbort.signal,
383
+ controller: this.chatAbort
384
+ };
385
+ }
386
+ /** Bump the turn counter without opening a new turn — what `stop` does. */
387
+ bumpRequestSeq() {
388
+ this.chatRequestSeq += 1;
389
+ }
390
+ /** The turn a caller's `requestSeq` is compared against. */
391
+ get currentRequestSeq() {
392
+ return this.chatRequestSeq;
393
+ }
394
+ /** Abort the in-flight turn, if any. Idempotent. */
395
+ cancel() {
396
+ this.chatAbort?.abort();
397
+ this.chatAbort = null;
398
+ }
399
+ /**
400
+ * Retire a turn that finished on its own. Clears the controller only when it
401
+ * is still the current one — a superseding turn has already installed its
402
+ * own, and clearing that would make a later Stop a no-op.
403
+ */
404
+ endTurn(controller) {
405
+ if (controller && this.chatAbort === controller)
406
+ this.chatAbort = null;
407
+ }
408
+ /** Resolve a client tool call this turn is waiting on. */
409
+ resolveToolResult(toolCallId, payload) {
410
+ this.deps.toolBridge.resolveResult(toolCallId, payload);
411
+ }
412
+ /**
413
+ * Apply a mid-turn permission-mode switch. Auto also releases every
414
+ * approval already waiting on this thread — the user just said yes to all
415
+ * of them.
416
+ */
417
+ setPermissionMode(threadId, mode) {
418
+ const liveMode = this.chatTurnPermissionMode.get(threadId);
419
+ if (liveMode) {
420
+ liveMode.value = mode;
421
+ }
422
+ if (mode === "auto") {
423
+ this.deps.approvalBridge.resolveScope(threadId, { decision: "allow" });
424
+ }
425
+ }
426
+ /**
427
+ * Which workflow decides where this conversation's files go.
428
+ *
429
+ * The workspace must not move between turns of one conversation: a file
430
+ * written in the previous message has to still be there in the next. The
431
+ * message's own `workflow_id` is not stable enough to key it on — the client
432
+ * attaches the id it has when it sends, so a turn sent before the thread list
433
+ * loaded carries none, resolves the default workspace instead of the
434
+ * workflow's, and the files the agent wrote a moment ago read as wiped. The
435
+ * thread's binding is the durable one, so it fills in whenever the message
436
+ * omits it.
437
+ */
438
+ async threadWorkspaceWorkflowId(userId, threadId, messageWorkflowId) {
439
+ if (messageWorkflowId)
440
+ return messageWorkflowId;
441
+ if (!threadId)
442
+ return null;
443
+ try {
444
+ const thread = await Thread.find(userId, threadId);
445
+ return isNonEmptyString(thread?.workflow_id) ? thread.workflow_id : null;
446
+ }
447
+ catch (err) {
448
+ this.session.logError("thread workspace lookup failed", err);
449
+ return null;
450
+ }
451
+ }
452
+ async ensureThreadExists(threadId, workflowId, projectId) {
453
+ const userId = this.session.requireUserId();
454
+ if (!threadId) {
455
+ if (projectId && projectId !== "default") {
456
+ await Project.requireOwned(userId, projectId);
457
+ }
458
+ const thread = await Thread.create({
459
+ user_id: userId,
460
+ workflow_id: workflowId ?? null,
461
+ project_id: projectId ?? "default",
462
+ title: ""
463
+ });
464
+ return thread.id;
465
+ }
466
+ const existing = await Thread.find(userId, threadId);
467
+ if (existing)
468
+ return existing.id;
469
+ const thread = await Thread.create({
470
+ id: threadId,
471
+ user_id: userId,
472
+ workflow_id: workflowId ?? null,
473
+ project_id: projectId ?? "default",
474
+ title: ""
475
+ });
476
+ return thread.id;
477
+ }
478
+ /**
479
+ * Save a message dict to the database.
480
+ * Mirrors Python's _save_message_to_db_async: pops id, type, user_id before create.
481
+ */
482
+ async saveMessageToDb(messageData) {
483
+ const data = { ...messageData };
484
+ delete data.id;
485
+ delete data.type;
486
+ const threadId = isString(data.thread_id) ? data.thread_id : "";
487
+ delete data.thread_id;
488
+ const userId = this.session.requireUserId();
489
+ delete data.user_id;
490
+ return Message.create({
491
+ thread_id: threadId,
492
+ user_id: userId,
493
+ ...data
494
+ });
495
+ }
496
+ /**
497
+ * Persist raw image bytes carried on an assistant message (native providers
498
+ * that run a server-side image tool emit them inline) as real assets, and
499
+ * rewrite each such block to the wire shape `{ type: "image_url", image: {
500
+ * type: "image", asset_id, mimeType } }`. Blocks that already reference an
501
+ * asset (uri / asset_id, no raw data) and non-image blocks pass through
502
+ * untouched. Raw base64 is never persisted or sent.
503
+ */
504
+ async materializeAssistantImageContent(content, userId, workflowId, projectId = "default") {
505
+ const out = [];
506
+ for (const block of content) {
507
+ if (block.type !== "image_url") {
508
+ out.push({ ...block });
509
+ continue;
510
+ }
511
+ const image = block.image;
512
+ const rawData = image.data;
513
+ let bytes = null;
514
+ if (rawData instanceof Uint8Array) {
515
+ bytes = rawData;
516
+ }
517
+ else if (isString(rawData) && rawData) {
518
+ bytes = new Uint8Array(Buffer.from(rawData, "base64"));
519
+ }
520
+ if (!bytes) {
521
+ // Already an asset/uri reference (or empty) — leave as-is.
522
+ out.push({ ...block });
523
+ continue;
524
+ }
525
+ const mimeType = isString(image.mimeType) ? image.mimeType : "image/png";
526
+ const ext = IMAGE_MIME_TO_EXT[mimeType] ?? "png";
527
+ // Per-block isolation: a storage failure must not abort the whole turn —
528
+ // the image is already generated (and billed), and the assistant text
529
+ // plus any sibling images should still reach the user. Degrade the
530
+ // failed block to a text notice; never fall back to raw base64.
531
+ try {
532
+ const asset = new Asset({
533
+ user_id: userId,
534
+ workflow_id: workflowId ?? null,
535
+ project_id: projectId,
536
+ name: `image_${Date.now()}`,
537
+ content_type: mimeType,
538
+ // Home — see the chat media generation path.
539
+ parent_id: userId
540
+ });
541
+ const fileName = `${asset.id}.${ext}`;
542
+ await storeAssetWithThumbnail(asset.user_id, asset.id, fileName, bytes, mimeType);
543
+ asset.size = bytes.length;
544
+ await asset.save();
545
+ // The DB / wire shape mirrors handleMediaGenerationMessage: an asset_id
546
+ // reference (never raw bytes). resolveContentUrls / resolveContentForProvider
547
+ // dereference asset_id on the way out and on the next turn.
548
+ out.push({
549
+ type: "image_url",
550
+ image: { type: "image", asset_id: asset.id, mimeType }
551
+ });
552
+ }
553
+ catch (err) {
554
+ log.error("Failed to store generated image as asset", {
555
+ error: err instanceof Error ? err.message : String(err)
556
+ });
557
+ out.push({
558
+ type: "text",
559
+ text: "[a generated image could not be saved]"
560
+ });
561
+ }
562
+ }
563
+ return out;
564
+ }
565
+ /**
566
+ * Recursively process tool results, handling asset-like objects.
567
+ * Mirrors Python's RegularChatProcessor._process_tool_result().
568
+ *
569
+ * - Asset-like objects (have type + uri/data): materialized via storage
570
+ * - Date/datetime: converted to ISO string
571
+ * - Arrays/objects: recursed into
572
+ * - Primitives: returned as-is
573
+ */
574
+ // HOLDOUT (anti-slop/no-unknown-returns): a tool result is an arbitrary
575
+ // value — the same open domain `ProcessingContext.normalizeOutputValue`
576
+ // rewrites — and this walk answers in that domain.
577
+ async processToolResult(obj, ctx) {
578
+ if (obj === null || obj === undefined)
579
+ return obj;
580
+ // Asset-like objects: { type: "image"|"audio"|"video"|..., uri?: string, data?: ... }
581
+ if (isRecord(obj)) {
582
+ const record = obj;
583
+ // Check if it's an asset-like object (has type + uri or data)
584
+ if ("type" in record &&
585
+ ("uri" in record || "data" in record || "asset_id" in record)) {
586
+ // Use ProcessingContext's normalizeOutputValue to handle asset materialization
587
+ return ctx.normalizeOutputValue(record, "storage_url");
588
+ }
589
+ // Date objects
590
+ if (obj instanceof Date) {
591
+ return obj.toISOString();
592
+ }
593
+ // Regular objects — recurse into values
594
+ const result = {};
595
+ for (const [key, value] of Object.entries(record)) {
596
+ result[key] = await this.processToolResult(value, ctx);
597
+ }
598
+ return result;
599
+ }
600
+ // Arrays — recurse into items
601
+ if (Array.isArray(obj)) {
602
+ return Promise.all(obj.map((item) => this.processToolResult(item, ctx)));
603
+ }
604
+ // Uint8Array/Buffer — store as asset
605
+ if (obj instanceof Uint8Array) {
606
+ if (!ctx.storage)
607
+ return obj;
608
+ const key = `assets/${randomUUID()}.bin`;
609
+ const uri = await ctx.storage.store(key, obj);
610
+ return { type: "asset", uri };
611
+ }
612
+ // Primitives
613
+ return obj;
614
+ }
615
+ /**
616
+ * Persist image bytes to temp storage and return a handle `view_image` can
617
+ * resolve — a bare `<uuid>.<ext>` storage key. No DB asset row is created, so
618
+ * these captures never clutter the user's asset library; the bytes live only
619
+ * in the request's temp storage. Returns null if there is no storage adapter
620
+ * or the write fails.
621
+ */
622
+ async storeTempImageAsset(ctx, bytes, mimeType) {
623
+ if (!ctx.storage)
624
+ return null;
625
+ const ext = IMAGE_MIME_TO_EXT[mimeType] ?? "png";
626
+ const key = `${randomUUID()}.${ext}`;
627
+ try {
628
+ await ctx.storage.store(key, bytes, mimeType);
629
+ registerTemporaryImageHandle(ctx, key);
630
+ return key;
631
+ }
632
+ catch (err) {
633
+ log.error("Failed to store temp image asset", {
634
+ error: err instanceof Error ? err.message : String(err)
635
+ });
636
+ return null;
637
+ }
638
+ }
639
+ /**
640
+ * Replace embedded image pixels in a tool result — timeline `frames[]` or an
641
+ * `image_content` blob (e.g. `ui_3d_capture_view`) — with temp-asset handles.
642
+ * The model receives a handle and an instruction to call `view_image`, which
643
+ * is the single mechanism that pulls pixels into context. Keeps image bytes
644
+ * out of the standing chat history. Non-image results pass through untouched.
645
+ */
646
+ // HOLDOUT (anti-slop/no-unknown-returns): same open tool-result domain as
647
+ // `processToolResult`; non-image results pass through untouched.
648
+ async materializeToolResultImages(toolResult, ctx) {
649
+ if (!isRecord(toolResult)) {
650
+ return toolResult;
651
+ }
652
+ const record = toolResult;
653
+ const handleFor = async (payload) => {
654
+ if (!payload)
655
+ return null;
656
+ if ("uri" in payload)
657
+ return payload.uri;
658
+ return this.storeTempImageAsset(ctx, payload.bytes, payload.mimeType);
659
+ };
660
+ // Timeline frames → one image handle per frame.
661
+ if (Array.isArray(record.frames)) {
662
+ const handles = [];
663
+ let stored = 0;
664
+ for (const frame of record.frames) {
665
+ if (!isObjectLike(frame)) {
666
+ handles.push(frame);
667
+ continue;
668
+ }
669
+ const f = { ...frame };
670
+ const payload = extractEmbeddedImage({
671
+ uri: f.dataUrl,
672
+ mimeType: "image/jpeg"
673
+ });
674
+ delete f.dataUrl;
675
+ const id = await handleFor(payload);
676
+ if (id) {
677
+ f.image_id = id;
678
+ stored++;
679
+ }
680
+ handles.push(f);
681
+ }
682
+ const out = { ...record, frames: handles };
683
+ if (stored > 0) {
684
+ out.note = `Captured ${stored} timeline frame(s) as image assets. Call view_image({ image_id }) to inspect a frame.`;
685
+ }
686
+ return out;
687
+ }
688
+ // Single image_content blob → one image handle.
689
+ if (isObjectLike(record.image_content)) {
690
+ const payload = extractEmbeddedImage(record.image_content);
691
+ const id = await handleFor(payload);
692
+ const out = { ...record };
693
+ delete out.image_content;
694
+ if (id) {
695
+ out.image_id = id;
696
+ const base = isString(record.note) ? record.note : "Captured an image.";
697
+ out.note = `${base} Saved as image asset "${id}". Call view_image({ image_id: "${id}" }) to inspect it.`;
698
+ }
699
+ return out;
700
+ }
701
+ return toolResult;
702
+ }
703
+ /**
704
+ * Render the turn's memory block.
705
+ *
706
+ * Memory is user-scoped, but only **this thread's** memories are pasted in:
707
+ * the store grows for the life of the account, and a block that grew with it
708
+ * would eventually cost more than the turn. What the agent gets instead is
709
+ * this thread's notes in full plus a count of the ones saved elsewhere, so
710
+ * it knows to reach them with `memory_search` rather than assume the block
711
+ * is everything.
712
+ *
713
+ * Resource refs are used as stored (asset refs already carry the `asset://`
714
+ * uri captured at save time) — one indexed query, no per-asset lookups on
715
+ * the hot path. Best-effort: a DB hiccup returns an empty block rather than
716
+ * breaking the turn.
717
+ */
718
+ async buildMemoryBlock(userId, threadId) {
719
+ try {
720
+ // One read over the user's newest memories, split by thread here rather
721
+ // than issuing a second count query.
722
+ const recent = await Memory.list(userId, { limit: MEMORY_SCAN_LIMIT });
723
+ const mine = recent.filter((memory) => memory.thread_id === threadId);
724
+ const elsewhere = recent.length - mine.length;
725
+ if (mine.length === 0 && elsewhere === 0)
726
+ return "";
727
+ // Stored with full ids; shown with short ones, which every capability
728
+ // accepts back. Free text is only rewritten where the scheme types the
729
+ // id (`asset://…`), so a hash in a note stays intact.
730
+ const rendered = mine.slice(0, MEMORY_BLOCK_LIMIT).map((memory) => ({
731
+ kind: memory.kind,
732
+ title: memory.title,
733
+ content: compactAssetUris(memory.content),
734
+ resources: compactResourceIds(Array.isArray(memory.resources) ? memory.resources : [])
735
+ }));
736
+ return formatMemoriesForPrompt(rendered, elsewhere);
737
+ }
738
+ catch (err) {
739
+ log.warn("Failed to build memory block", {
740
+ threadId,
741
+ error: err instanceof Error ? err.message : String(err)
742
+ });
743
+ return "";
744
+ }
745
+ }
746
+ async buildGenerationBlock(userId, threadId) {
747
+ try {
748
+ const [rows, next] = await Prediction.listGenerations(userId, {
749
+ threadId,
750
+ limit: 50
751
+ });
752
+ if (rows.length === 0)
753
+ return "";
754
+ const generations = rows.map((row) => ({
755
+ generation_id: row.id,
756
+ status: row.status,
757
+ model: row.model,
758
+ capability: row.capability,
759
+ tool_call_id: row.tool_call_id,
760
+ asset_uris: (row.asset_ids ?? []).map((id) => `asset://${id}`)
761
+ }));
762
+ return [
763
+ "Saved generations for this conversation (database snapshot):",
764
+ JSON.stringify(generations),
765
+ "Reuse completed assets. A missing tool response does not mean the generation failed. " +
766
+ "Use get_generation or await_generation to check pending work before starting another paid render. " +
767
+ "An interrupted record does not prove that the provider cancelled its request.",
768
+ next
769
+ ? "Older records are available through list_generations with this thread_id."
770
+ : ""
771
+ ]
772
+ .filter(Boolean)
773
+ .join("\n");
774
+ }
775
+ catch (error) {
776
+ log.warn("Failed to load conversation generations", {
777
+ threadId,
778
+ error: error instanceof Error ? error.message : String(error)
779
+ });
780
+ return "Generation history is temporarily unavailable. Check list_generations before retrying earlier renders.";
781
+ }
782
+ }
783
+ /**
784
+ * The user's skills, read once per turn.
785
+ *
786
+ * Two halves with different lifetimes, which is why the caller splits them.
787
+ * The catalog (name + description) changes only when the skills table does,
788
+ * so it belongs in the system prompt where a provider's automatic prefix
789
+ * cache can keep it. The body of a skill the message invoked with `/<name>`
790
+ * changes per turn, so it rides at the tail with the other volatile context.
791
+ *
792
+ * Best-effort like the memory block: a DB hiccup costs the skills, not the
793
+ * turn.
794
+ */
795
+ async loadUserSkills(userId) {
796
+ let rows = [];
797
+ try {
798
+ rows = await Skill.listByUser(userId);
799
+ }
800
+ catch (err) {
801
+ log.warn("Failed to load user skills", {
802
+ error: err instanceof Error ? err.message : String(err)
803
+ });
804
+ }
805
+ // The shipped skills ride in the same catalog. They come off disk rather
806
+ // than the table, so a DB hiccup costs the user's own rows and not these.
807
+ return mergeSystemSkills(rows.map((row) => ({
808
+ name: row.name,
809
+ description: row.description,
810
+ content: row.content
811
+ })));
812
+ }
813
+ /**
814
+ * Round-trip a permission approval to the client and resolve with the
815
+ * user's decision. Emits a `tool_approval_request`, then waits for the
816
+ * matching `tool_approval_response` (resolved via {@link approvalBridge}).
817
+ * A cancelled wait (stop) is treated as a denial.
818
+ */
819
+ async requestToolApproval(threadId, request) {
820
+ const approvalId = `appr_${randomUUID()}`;
821
+ await this.session.send({
822
+ type: "tool_approval_request",
823
+ thread_id: threadId,
824
+ approval_id: approvalId,
825
+ tool_name: request.toolName,
826
+ category: request.category,
827
+ message: request.message,
828
+ description: request.description ?? "",
829
+ args: request.args
830
+ });
831
+ try {
832
+ // No timeout — the user may take a while; `stop` cancels this thread.
833
+ const response = await this.deps.approvalBridge.createWaiter(approvalId, 0, threadId);
834
+ const decision = response.decision;
835
+ if (decision === "allow" ||
836
+ decision === "allow_for_chat" ||
837
+ decision === "deny") {
838
+ return decision;
839
+ }
840
+ return "deny";
841
+ }
842
+ catch {
843
+ // Cancelled (generation stopped) — treat as a denial.
844
+ return "deny";
845
+ }
846
+ }
847
+ /**
848
+ * Open the bespoke secret dialog on the client and resolve with what the
849
+ * user did — never with what they typed.
850
+ *
851
+ * The value does not travel over this socket in either direction. The
852
+ * dialog writes it with the client's own `settings.secrets.upsert` call, so
853
+ * the credential never enters the chat transcript, the run's message log, or
854
+ * the model's context; this frame only asks, and the response only reports.
855
+ *
856
+ * A cancelled wait (the user pressed Stop) is a decline, which is the same
857
+ * fail-closed reading the approval prompts take.
858
+ */
859
+ async requestSecretEntry(threadId, request) {
860
+ const approvalId = `secret_${randomUUID()}`;
861
+ await this.session.send({
862
+ type: "secret_request",
863
+ thread_id: threadId,
864
+ approval_id: approvalId,
865
+ key: request.key,
866
+ description: request.description ?? null,
867
+ reason: request.reason ?? null,
868
+ help_url: request.helpUrl ?? null
869
+ });
870
+ try {
871
+ // No timeout — finding an API key takes as long as it takes; `stop`
872
+ // cancels this thread.
873
+ const response = await this.deps.approvalBridge.createWaiter(approvalId, 0, threadId);
874
+ return response.status === "saved" ? "saved" : "declined";
875
+ }
876
+ catch {
877
+ return "declined";
878
+ }
879
+ }
880
+ /**
881
+ * Execute a single node by type and return its output. Builds a one-node
882
+ * graph and runs it through a fresh `ExecutionSession` (@nodetool-ai/execution),
883
+ * then returns the
884
+ * node's completed result. Backs the `run_node` chat tool.
885
+ */
886
+ // HOLDOUT (anti-slop/no-unknown-returns): answers with the node's own output
887
+ // — an arbitrary workflow value — or an `{ error }` bag when the run failed.
888
+ async runSingleNode(nodeType, inputs, userId, projectId = null,
889
+ /** The calling turn's gate, when a chat turn is what started this node. */
890
+ gate = null) {
891
+ const jobId = randomUUID();
892
+ const nodeId = "node_0";
893
+ const rawGraph = {
894
+ nodes: [{ id: nodeId, type: nodeType, data: inputs ?? {} }],
895
+ edges: []
896
+ };
897
+ let graph;
898
+ try {
899
+ graph = await this.deps.hydrateGraph(rawGraph);
900
+ }
901
+ catch (err) {
902
+ return {
903
+ error: `Failed to prepare node '${nodeType}': ${err instanceof Error ? err.message : String(err)}`
904
+ };
905
+ }
906
+ if (this.deps.beforeRunJob) {
907
+ try {
908
+ await this.deps.beforeRunJob(graph);
909
+ }
910
+ catch (err) {
911
+ return {
912
+ error: `Node prerequisites failed: ${err instanceof Error ? err.message : String(err)}`
913
+ };
914
+ }
915
+ }
916
+ const context = createRuntimeContext({
917
+ jobId,
918
+ workflowId: null,
919
+ projectId,
920
+ userId,
921
+ workspace: this.session.workspaceResolver
922
+ ? await this.session.workspaceResolver(null, userId)
923
+ : null,
924
+ assetOutputMode: this.session.mode === "text" ? "data_uri" : "temp_url"
925
+ });
926
+ // This context is built here rather than copied from the turn, so the
927
+ // turn's gate has to be put on it by hand. The node's own loops read it
928
+ // with `gateFromContext`; a node run outside a chat turn carries none and
929
+ // gates headless.
930
+ if (gate)
931
+ context.set(PERMISSION_GATE_CONTEXT_KEY, gate);
932
+ context.setResolveExecutor((node) => this.session.resolveExecutor(node));
933
+ if (this.session.resolveNodeType) {
934
+ const resolverObj = isFunctionValue(this.session.resolveNodeType)
935
+ ? { resolveNodeType: this.session.resolveNodeType }
936
+ : this.session.resolveNodeType;
937
+ context.setResolveNodeType((type) => resolverObj.resolveNodeType(type));
938
+ }
939
+ // A node selecting a model this runtime cannot honour is refused before
940
+ // the kernel starts; the tool answers with the reason, like every other
941
+ // preparation failure here.
942
+ let session;
943
+ try {
944
+ session = await ExecutionSession.create({
945
+ graph: toRawGraphInput(graph),
946
+ resolveExecutor: (node) => this.session.resolveExecutor(node),
947
+ bridgeFactory: async () => null,
948
+ jobLifecycleBridge: this.session.pythonBridge ?? null,
949
+ jobId,
950
+ context,
951
+ params: {},
952
+ // A node run from a project's agent thread is that project's spend;
953
+ // the ledger this session attaches writes it. Null outside a project.
954
+ projectId: projectId ?? null,
955
+ validateNode: this.session.validateNode
956
+ });
957
+ }
958
+ catch (err) {
959
+ if (!isExecutionPreflightError(err))
960
+ throw err;
961
+ return { error: err.message };
962
+ }
963
+ const result = await session.result;
964
+ // Capture the node's completed result from the streamed updates.
965
+ let nodeResult;
966
+ while (context.hasMessages()) {
967
+ const msg = context.popMessage();
968
+ if (msg &&
969
+ msg.type === "node_update" &&
970
+ msg.node_id === nodeId &&
971
+ msg.status === "completed" &&
972
+ msg.result != null) {
973
+ nodeResult = msg.result;
974
+ }
975
+ }
976
+ if (result.status === "failed") {
977
+ return { error: result.error ?? `Node '${nodeType}' failed` };
978
+ }
979
+ if (nodeResult === undefined) {
980
+ // Fall back to the runner's collected outputs (e.g. Output nodes).
981
+ return result.outputs ?? { status: result.status };
982
+ }
983
+ return this.processToolResult(nodeResult, context);
984
+ }
985
+ /**
986
+ * Handle an incoming chat message.
987
+ *
988
+ * Mirrors Python's full 3-layer flow:
989
+ * handle_chat_message → handle_message_impl → process_messages
990
+ * → _run_processor + RegularChatProcessor.process()
991
+ *
992
+ * The processor sends messages to a queue. _run_processor reads them:
993
+ * - type === "message" → persist to DB AND forward to client
994
+ * - anything else → forward to client only
995
+ *
996
+ * RegularChatProcessor.process():
997
+ * 1. Prepend system prompt if first message isn't system role
998
+ * 2. while True: messages_to_send = chat_history + unprocessed_messages
999
+ * 3. Stream chunks (type: "chunk") — forwarded to client (not persisted)
1000
+ * 4. On tool call: build assistant Message + tool result Message (type: "message")
1001
+ * → persisted to DB AND forwarded to client
1002
+ * 5. If unprocessed_messages empty, break
1003
+ * 6. Send done chunk + final assistant Message
1004
+ */
1005
+ async handleChatMessage(data, requestSeq, signal) {
1006
+ const messageWorkflowId = isString(data.workflow_id)
1007
+ ? data.workflow_id
1008
+ : null;
1009
+ const threadId = await this.ensureThreadExists(isString(data.thread_id) ? data.thread_id : undefined, messageWorkflowId, isString(data.project_id) ? data.project_id : undefined);
1010
+ data.thread_id = threadId;
1011
+ // Route this turn takes: a workflow chatbot and a media generation carry
1012
+ // their own model selection (checked in their handlers), a plain chat turn
1013
+ // is served by the language model the composer picked.
1014
+ const workflowTargetHint = isString(data.workflow_target)
1015
+ ? data.workflow_target
1016
+ : null;
1017
+ const mediaModeHint = isObjectLike(data.media_generation)
1018
+ ? data.media_generation.mode
1019
+ : null;
1020
+ const isPlainChatTurn = workflowTargetHint !== "workflow" &&
1021
+ (!isString(mediaModeHint) || mediaModeHint === "chat");
1022
+ // A plain chat turn without a chosen model used to fall through to the
1023
+ // built-in default and die deep in provider resolution ("No provider
1024
+ // registered for \"empty\"") — after the user's message had been
1025
+ // persisted. Reject it up front and say what to do instead.
1026
+ if (isPlainChatTurn && !isModelSelection(data.provider, data.model)) {
1027
+ await this.session.send({
1028
+ type: "error",
1029
+ message: NO_MODEL_SELECTED_MESSAGE,
1030
+ thread_id: threadId
1031
+ });
1032
+ return;
1033
+ }
1034
+ // Apply defaults — matches Python's handle_chat_message
1035
+ if (!data.model)
1036
+ data.model = this.deps.defaults.model;
1037
+ if (!data.provider)
1038
+ data.provider = this.deps.defaults.provider;
1039
+ const providerId = data.provider;
1040
+ const model = data.model;
1041
+ const workflowId = messageWorkflowId;
1042
+ const userId = this.session.requireUserId();
1043
+ log.debug("Chat message", { threadId, model, provider: providerId });
1044
+ // Save user message to DB — matches Python's _save_message_to_db_async(data)
1045
+ const turnMessage = await this.saveMessageToDb(data);
1046
+ if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
1047
+ return;
1048
+ if (!this.session.resolveProvider) {
1049
+ await this.session.send({
1050
+ type: "error",
1051
+ message: "No provider resolver configured",
1052
+ thread_id: threadId
1053
+ });
1054
+ return;
1055
+ }
1056
+ // Route to the workflow processor ONLY when the client explicitly opts in
1057
+ // via `workflow_target: "workflow"`. A bare `workflow_id` is context, not a
1058
+ // routing signal: the editor binds the open workflow so `ui_*` tools target
1059
+ // it, and that ambient id must not hijack the turn into running the
1060
+ // workflow as a chatbot. Genuine workflow-chatbot runs set `workflow_target`
1061
+ // (and carry `workflow_id`/`graph` for the processor to load/execute).
1062
+ const workflowTarget = isString(data.workflow_target)
1063
+ ? data.workflow_target
1064
+ : null;
1065
+ if (workflowTarget === "workflow") {
1066
+ await this.handleWorkflowMessage(data, requestSeq, signal);
1067
+ return;
1068
+ }
1069
+ // Route to media generation when the client requests a text-to-image or
1070
+ // text-to-video turn. The composer attaches a `media_generation` field
1071
+ // with mode + params; when mode is a media mode we invoke the provider's
1072
+ // textToImage / textToVideo instead of a regular LLM round and return an
1073
+ // assistant message containing MessageImageContent / MessageVideoContent.
1074
+ const mediaGeneration = isObjectLike(data.media_generation)
1075
+ ? data.media_generation
1076
+ : null;
1077
+ if (mediaGeneration &&
1078
+ isString(mediaGeneration.mode) &&
1079
+ mediaGeneration.mode !== "chat") {
1080
+ await this.handleMediaGenerationMessage(data, mediaGeneration, requestSeq, signal);
1081
+ return;
1082
+ }
1083
+ const provider = await this.session.resolveProvider(providerId, userId);
1084
+ // Permission mode for this turn. Governs whether gated tool calls run,
1085
+ // ask for approval, or are blocked. Defaults to "default".
1086
+ const permissionMode = data.permission_mode === "plan" ||
1087
+ data.permission_mode === "auto" ||
1088
+ data.permission_mode === "default"
1089
+ ? data.permission_mode
1090
+ : "default";
1091
+ // A surface can send a context-specific system-prompt addendum (e.g. the
1092
+ // App Builder's build-an-app-UI guidance), layered after the base prompt.
1093
+ const extraSystemPrompt = isString(data.system_prompt)
1094
+ ? data.system_prompt
1095
+ : null;
1096
+ // Which documents the user has open, and which one has focus. The `ui_*`
1097
+ // tools all require an explicit document id, so this is what makes them
1098
+ // usable — see `formatUiContext`.
1099
+ const uiContext = isObjectLike(data.ui_context)
1100
+ ? data.ui_context
1101
+ : null;
1102
+ // History load. Session-based providers (e.g. claude_agent) keep the
1103
+ // conversation upstream, so when a resumable session exists for this
1104
+ // provider+model we do NOT reload the whole thread: we probe a bounded
1105
+ // recent window, send only the turns since the session, and hand the
1106
+ // provider a `loadFullHistory` thunk it calls only if it must prime context
1107
+ // (resume failed / system prompt changed). Otherwise we load the full
1108
+ // history and use the standard slice-based resume. The DB column is the
1109
+ // source of truth; the provider also keeps an in-process cache.
1110
+ // The action contract + tool catalog section, assigned once the codeact
1111
+ // session exists (before the system message is materialized). Read lazily
1112
+ // here because the resume `loadFullHistory` thunk and the prepend both run
1113
+ // after tool resolution.
1114
+ let codeactPromptSection = "";
1115
+ // The user's skills, read once. The catalog goes into the system prompt
1116
+ // below because it is stable — it changes only when the skills table does,
1117
+ // so a provider's prefix cache keeps it across turns. The body of an
1118
+ // invoked skill is volatile and rides at the tail instead.
1119
+ const userSkills = await this.loadUserSkills(userId);
1120
+ const skillCatalogSection = formatSkillCatalogForPrompt(userSkills);
1121
+ const buildSystemContent = () => {
1122
+ const base = buildChatAgentSystemPrompt(permissionMode, extraSystemPrompt, uiContext, workflowId);
1123
+ const sections = [base];
1124
+ if (codeactPromptSection)
1125
+ sections.push(codeactPromptSection);
1126
+ if (skillCatalogSection)
1127
+ sections.push(skillCatalogSection);
1128
+ return sections.join("\n\n");
1129
+ };
1130
+ const systemChatMessage = () => ({
1131
+ role: "system",
1132
+ content: buildSystemContent(),
1133
+ toolCallId: null,
1134
+ toolCalls: null,
1135
+ threadId: null
1136
+ });
1137
+ const convertDbMessages = (rows) => {
1138
+ const out = [];
1139
+ for (const m of rows) {
1140
+ const pm = dbMessageToProviderMessage(m, this.session.userId);
1141
+ if (pm)
1142
+ out.push(pm);
1143
+ }
1144
+ // A thread that took an interleave before this was fixed still holds a
1145
+ // tool call with no result. Anthropic 400s on one, so loading such a
1146
+ // thread under a different provider or model would fail every turn from
1147
+ // here on. Patch the history we send; the stored rows are left alone.
1148
+ return repairOrphanedToolCalls(out);
1149
+ };
1150
+ let chatHistory;
1151
+ let priorSession = null;
1152
+ // The provider calls this only on a priming fallback; null on the full path.
1153
+ let loadFullHistory = null;
1154
+ // Absolute checkpoint to persist on the assistant message when the fast path
1155
+ // sent only a delta (so the stored value matches the full-load path); null
1156
+ // when the provider's own checkpoint is already absolute.
1157
+ let sessionCheckpointOverride = null;
1158
+ {
1159
+ const [recent] = await Message.paginate(threadId, {
1160
+ reverse: true,
1161
+ limit: SESSION_PROBE_WINDOW
1162
+ });
1163
+ const probeHasWholeThread = recent.length < SESSION_PROBE_WINDOW;
1164
+ // `recent` is newest-first. Walk to the first boundary marker: an
1165
+ // assistant carrying a session token, or a compaction record. Both claim
1166
+ // to say where history starts, and whichever is newer wins — the walk
1167
+ // meets it first, so the loop decides by stopping.
1168
+ //
1169
+ // A compaction newer than the session must win: the upstream transcript
1170
+ // that session token resumes is the very history compaction just replaced,
1171
+ // so resuming it would send back everything the cut removed (and the cut
1172
+ // usually happened because that transcript no longer fits).
1173
+ let probeSession = null;
1174
+ let probeCompaction = null;
1175
+ const sinceMarkerNewestFirst = [];
1176
+ for (const m of recent) {
1177
+ if (isCompactionMessage(m)) {
1178
+ probeCompaction = m;
1179
+ break;
1180
+ }
1181
+ if (m.role === "assistant" && m.provider_session) {
1182
+ const s = m.provider_session;
1183
+ if (s.providerId === providerId && s.model === model)
1184
+ probeSession = s;
1185
+ break;
1186
+ }
1187
+ sinceMarkerNewestFirst.push(m);
1188
+ }
1189
+ if (probeCompaction) {
1190
+ // COMPACTED path: the summary row plus everything after it, already in
1191
+ // hand from the probe. No session resumes across a compaction, so
1192
+ // `priorSession` stays null and the provider rebuilds from these rows.
1193
+ chatHistory = convertDbMessages([
1194
+ probeCompaction,
1195
+ ...sinceMarkerNewestFirst.reverse()
1196
+ ]);
1197
+ }
1198
+ else if (probeSession) {
1199
+ // RESUME fast path: the SDK already holds the prior turns, so send only
1200
+ // the messages since the session — no full-thread load.
1201
+ const newTurns = convertDbMessages(sinceMarkerNewestFirst.reverse());
1202
+ chatHistory = newTurns;
1203
+ // The single system message prepended below sits at index 0, so the new
1204
+ // turns begin at index 1 (the provider's relative resume checkpoint).
1205
+ priorSession = {
1206
+ providerId,
1207
+ model,
1208
+ token: probeSession.token,
1209
+ systemHash: probeSession.systemHash,
1210
+ checkpoint: 1
1211
+ };
1212
+ // Absolute position to persist: prior prefix + the prior assistant + the
1213
+ // new turns — identical to what the full-load path would store.
1214
+ sessionCheckpointOverride =
1215
+ probeSession.checkpoint + 1 + newTurns.length;
1216
+ loadFullHistory = async () => {
1217
+ const [rows] = await Message.paginate(threadId, { limit: 1000 });
1218
+ const full = convertDbMessages(historySinceCompaction(rows));
1219
+ full.unshift(systemChatMessage());
1220
+ return full;
1221
+ };
1222
+ }
1223
+ else if (probeHasWholeThread) {
1224
+ // The whole thread fit in the probe window — reuse it, no second query.
1225
+ const rows = historySinceCompaction([...recent].reverse());
1226
+ chatHistory = convertDbMessages(rows);
1227
+ priorSession = lastMatchingProviderSession(rows, providerId, model);
1228
+ }
1229
+ else {
1230
+ // Long thread without a resumable session in the recent window: load it
1231
+ // all (a far-back session still resumes via the slice path). A session
1232
+ // older than the compaction cut is not one of them: searching only the
1233
+ // compacted slice is what keeps the newest marker winning here too.
1234
+ const [rows] = await Message.paginate(threadId, { limit: 1000 });
1235
+ const kept = historySinceCompaction(rows);
1236
+ chatHistory = convertDbMessages(kept);
1237
+ priorSession = lastMatchingProviderSession(kept, providerId, model);
1238
+ }
1239
+ }
1240
+ // Expose the read-only `run_search` fan-out primitive by default. A client
1241
+ // can opt out by sending `enable_read_only_search: false`.
1242
+ const enableReadOnlySearch = data.enable_read_only_search !== false;
1243
+ // Assemble the fixed, always-on toolbelt. There is no per-message tool
1244
+ // selection anymore — the agent reasons over the full toolbelt and the
1245
+ // permission gate (below) governs execution.
1246
+ // Google Workspace runs on the token from the user's Google sign-in, so it
1247
+ // only exists on deployments that have a login. Local mode never sees it.
1248
+ const googleWorkspace = isGoogleWorkspaceEnabled();
1249
+ const chatProviders = await this.deps.configuredProviders(userId);
1250
+ // The project this conversation belongs to, when it is a project's own
1251
+ // agent thread. Documents the turn creates land in it rather than in the
1252
+ // loose bucket, without the model having to pass `project_id` every time,
1253
+ // and everything the turn spends is billed to it.
1254
+ const chatProjectId = (await Project.findByThread(userId, threadId))?.id ?? undefined;
1255
+ // The permission gate the belt is wrapped in below. Built before the belt
1256
+ // because the Apify tools carry it into their own run: in discovery mode
1257
+ // the actor policy asks this gate to approve an actor the install has not
1258
+ // allowlisted, so the user sees that question in the same place as every
1259
+ // other permission prompt. The session allow-set is shared per thread so
1260
+ // "Allow for this chat" sticks.
1261
+ const sessionAllow = this.chatSessionAllow.get(threadId) ?? new Set();
1262
+ this.chatSessionAllow.set(threadId, sessionAllow);
1263
+ // A gated call inside a code action parks the guest program until the user
1264
+ // answers, and the gate stops the clock for exactly that long — the wait is
1265
+ // the user's, not the program's, and charged to the action's wall clock it
1266
+ // would kill the very program that asked.
1267
+ const codeactClock = createSandboxClock();
1268
+ const liveMode = { value: permissionMode };
1269
+ this.chatTurnPermissionMode.set(threadId, liveMode);
1270
+ const chatGate = {
1271
+ get mode() {
1272
+ return liveMode.value;
1273
+ },
1274
+ sessionAllow,
1275
+ requestApproval: async (request) => this.requestToolApproval(threadId, request),
1276
+ clock: codeactClock
1277
+ };
1278
+ // The single-node runner is a closure only this package can build, so
1279
+ // `run_node` reaches a capability run as a host-supplied capability rather
1280
+ // than out of the registry. It runs the node on a context of its own, so
1281
+ // the turn's gate is handed over explicitly — without it an agent loop
1282
+ // inside the node finds no gate and runs headless, and approving
1283
+ // `run_node` becomes approval for whatever the node then calls.
1284
+ const runNodeTool = new RunNodeTool((nodeType, inputs) => this.runSingleNode(nodeType, inputs, userId, chatProjectId, chatGate));
1285
+ // The bounds this turn runs under, created once and shared by everything
1286
+ // it starts (invariant I-2). A loop that made its own would hand every
1287
+ // nested agent a fresh allowance, which is no ceiling at all.
1288
+ const turnBudget = await createChatTurnBudget();
1289
+ const gatedRun = (context) => createCapabilityRun({
1290
+ context,
1291
+ gate: chatGate,
1292
+ projectId: chatProjectId,
1293
+ availableSecrets: contextSecretAvailability(context),
1294
+ budget: turnBudget
1295
+ });
1296
+ // The user's external MCP servers (Blender, Hugging Face, …). Their tools
1297
+ // are plain belt tools named `mcp_<server>_<tool>`, so every loop reaches
1298
+ // them the same way; a server that does not answer is skipped, not fatal.
1299
+ let externalMcpTools = [];
1300
+ try {
1301
+ externalMcpTools = await externalMcpToolsFor(userId);
1302
+ }
1303
+ catch (err) {
1304
+ log.warn("External MCP tools unavailable for this turn", {
1305
+ error: err instanceof Error ? err.message : String(err)
1306
+ });
1307
+ }
1308
+ const rawToolbelt = [
1309
+ ...getBuiltinTools(),
1310
+ ...(googleWorkspace ? getGoogleWorkspaceTools() : []),
1311
+ // Apify and SerpAPI have no `nodetool.*` namespace, so the belt is how
1312
+ // a chat discovers them (`nodetool.searchTools("apify")`) at all.
1313
+ ...getApifyTools(gatedRun),
1314
+ ...getSerpApiTools(gatedRun),
1315
+ ...externalMcpTools,
1316
+ ...getAllMcpTools({
1317
+ registry: this.session.nodeRegistry,
1318
+ providers: chatProviders,
1319
+ ...mcpToolHostDeps()
1320
+ }),
1321
+ toolForCapabilityName("list_collections"),
1322
+ toolForCapabilityName("query_collection"),
1323
+ runNodeTool
1324
+ ];
1325
+ // De-duplicate by name (builtins / mcp / extras may overlap); first wins.
1326
+ const dedupedToolbelt = [];
1327
+ const seenToolNames = new Set();
1328
+ for (const tool of rawToolbelt) {
1329
+ if (seenToolNames.has(tool.name))
1330
+ continue;
1331
+ seenToolNames.add(tool.name);
1332
+ dedupedToolbelt.push(tool);
1333
+ }
1334
+ // Wrap the toolbelt in the permission gate. The wrapper is transparent
1335
+ // except for `process()`, so the chat loop AND any `run_subtask` child
1336
+ // loop inherit gating by simply calling `tool.process()`.
1337
+ const baseTools = gateTools(dedupedToolbelt, chatGate);
1338
+ // Inject the recursive-decomposition primitive (ungated — it spawns a
1339
+ // child loop whose own tools are the gated `baseTools`). Child events
1340
+ // stream back tagged with `parent_tool_call_id` so the UI can nest cards.
1341
+ const serverTools = baseTools.slice();
1342
+ // The same runtime the delegation tools take, kept for the capability run
1343
+ // below: provider, model, the parent belt, and the event forwarder.
1344
+ let subAgentRuntime;
1345
+ {
1346
+ const subtaskThreadId = threadId;
1347
+ const subtaskWorkflowId = workflowId;
1348
+ const forwardSubtaskMessage = async (msg) => {
1349
+ const enriched = { ...msg };
1350
+ if (enriched.thread_id == null)
1351
+ enriched.thread_id = subtaskThreadId;
1352
+ if (enriched.workflow_id == null)
1353
+ enriched.workflow_id = subtaskWorkflowId;
1354
+ try {
1355
+ await this.session.send(enriched);
1356
+ // Tool calls inside a subtask only arrive here as transient
1357
+ // tool_call_update events; the chat UI needs a persistent assistant
1358
+ // message with tool_calls to render a ToolCallCard. Emit a synthetic
1359
+ // one so child tool calls show up as cards nested below the parent
1360
+ // run_subtask card.
1361
+ await this.emitSyntheticToolCallCard(enriched);
1362
+ }
1363
+ catch (err) {
1364
+ log.warn("Failed to forward subtask event", {
1365
+ error: err instanceof Error ? err.message : String(err)
1366
+ });
1367
+ }
1368
+ };
1369
+ subAgentRuntime = {
1370
+ provider,
1371
+ model,
1372
+ parentTools: () => baseTools,
1373
+ forwardMessage: forwardSubtaskMessage,
1374
+ background: new BackgroundSubtaskRegistry(),
1375
+ // Every delegated loop reserves against this turn's cap rather than
1376
+ // opening one of its own.
1377
+ budget: turnBudget
1378
+ };
1379
+ // All four delegation tools reach the belt as capabilities over this
1380
+ // runtime. The class is still what runs — the `agents` module builds one
1381
+ // per call — so the depth gate, the child's inherited belt (with a
1382
+ // `run_subtask` of its own stitched in by `buildChildToolset`, since this
1383
+ // snapshot deliberately predates the unshift), and the
1384
+ // `parent_tool_call_id` / `subtask_depth` tagging are unchanged.
1385
+ // `start_subtask` / `wait_subtasks` share the per-turn registry above:
1386
+ // spawn returns immediately, and the parent collects on its own terms.
1387
+ const delegationRun = (context) => createCapabilityRun({
1388
+ context,
1389
+ // Ungated on purpose, as before: spawning a child loop has no side
1390
+ // effect of its own, and the child's tools are the gated `baseTools`.
1391
+ gate: UNGATED,
1392
+ availableSecrets: contextSecretAvailability(context),
1393
+ subAgent: subAgentRuntime,
1394
+ budget: turnBudget
1395
+ });
1396
+ serverTools.unshift(toolForCapabilityName("run_subtask", delegationRun));
1397
+ serverTools.unshift(toolForCapabilityName("start_subtask", delegationRun));
1398
+ serverTools.unshift(toolForCapabilityName("wait_subtasks", delegationRun));
1399
+ // Plan mode's own capability: `create_plan` runs the TaskPlanner over the
1400
+ // objective and streams the plan into the thread, executing nothing. It
1401
+ // is offered only here, because plan mode is the one place where a plan
1402
+ // *is* the deliverable — everywhere else the agent does the work instead
1403
+ // of describing it. The belt is a per-turn snapshot, so this reads the
1404
+ // mode the turn started in — `liveMode` covers a mid-turn switch for the
1405
+ // gate, not for which tools were offered.
1406
+ if (permissionMode === "plan") {
1407
+ serverTools.unshift(toolForCapabilityName("create_plan", delegationRun));
1408
+ }
1409
+ // The other half: `execute_plan` runs a plan the user has already seen,
1410
+ // on the same ParallelTaskExecutor `Agent` uses. It is on the belt in
1411
+ // every mode, gated — in plan mode the gate answers
1412
+ // `blocked_in_plan_mode`, which tells the model to have the user switch
1413
+ // out, where dropping it from the belt entirely would only say the tool
1414
+ // does not exist. It is *offered* everywhere but plan mode, below.
1415
+ serverTools.unshift(...gateTools([toolForCapabilityName("execute_plan", delegationRun)], chatGate));
1416
+ // Read-only fan-out search (opt-in). Reuses the same runtime — the
1417
+ // capability filters the parent belt to its read-only allowlist
1418
+ // internally, so passing the full snapshot is correct.
1419
+ if (enableReadOnlySearch) {
1420
+ serverTools.unshift(toolForCapabilityName("run_search", delegationRun));
1421
+ }
1422
+ }
1423
+ const serverToolMap = new Map(serverTools.map((t) => [t.name, t]));
1424
+ const workflowDocumentToolNames = new Set(WORKFLOW_DOCUMENT_TOOL_NAMES);
1425
+ log.info("Resolved server tools", {
1426
+ permissionMode,
1427
+ resolved: serverTools.map((t) => t.name)
1428
+ });
1429
+ const serverSchemas = serverTools.map((t) => t.toProviderTool());
1430
+ // Every client tool the connected UI registered is exposed. They used to be
1431
+ // gated on an active workflow, which made the editor tools unreachable from
1432
+ // plain chat; they are deferred behind `nodetool.searchTools()` anyway, and each one now
1433
+ // takes an explicit document id, so the gate cost reach without buying
1434
+ // safety. Which ids are valid comes from `ui_context` in the system prompt.
1435
+ const clientToolNames = Object.keys(this.deps.clientTools());
1436
+ const clientSchemas = [];
1437
+ for (const [name, manifest] of Object.entries(this.deps.clientTools())) {
1438
+ if (serverToolMap.has(name))
1439
+ continue;
1440
+ // The frontend manifest carries the JSON schema under `parameters`
1441
+ // (FrontendToolRegistry.getManifest); accept `inputSchema` too for any
1442
+ // client that uses the provider-tool field name.
1443
+ const schema = typeof manifest.parameters === "object"
1444
+ ? manifest.parameters
1445
+ : typeof manifest.inputSchema === "object"
1446
+ ? manifest.inputSchema
1447
+ : undefined;
1448
+ clientSchemas.push({
1449
+ name,
1450
+ description: isString(manifest.description)
1451
+ ? manifest.description
1452
+ : undefined,
1453
+ inputSchema: schema
1454
+ });
1455
+ }
1456
+ const allSchemas = [...serverSchemas, ...clientSchemas];
1457
+ // A chat turn with tools always runs in CodeAct: the model acts by writing
1458
+ // sandboxed JavaScript over the toolbelt (docs/codeact-design.md), and the
1459
+ // session's in-sandbox `nodetool.searchTools()` is the discovery path. The session
1460
+ // is created below, once the tool router and processing context exist.
1461
+ const useCodeAct = allSchemas.length > 0;
1462
+ // The tool list handed to the provider: `execute_code`, the direct tools
1463
+ // (DIRECT_TOOL_NAMES) and `view_image`, pushed once the session exists.
1464
+ const providerToolSchemas = [];
1465
+ log.info("Provider tool schemas", {
1466
+ permissionMode,
1467
+ serverToolCount: serverTools.length,
1468
+ clientToolCount: clientToolNames.length,
1469
+ codeact: useCodeAct
1470
+ });
1471
+ // Create a processing context for tool execution
1472
+ // A chat turn without a workflow still resolves a workspace — the user's
1473
+ // default one. It used to fall back to the whole OS temp dir, which is
1474
+ // neither bounded nor anywhere the user would look for what an agent wrote.
1475
+ const chatWorkspace = this.session.workspaceResolver
1476
+ ? await this.session.workspaceResolver(await this.threadWorkspaceWorkflowId(userId, threadId, workflowId), userId)
1477
+ : null;
1478
+ const ctx = createRuntimeContext({
1479
+ jobId: randomUUID(),
1480
+ workflowId,
1481
+ threadId: threadId || null,
1482
+ projectId: chatProjectId,
1483
+ userId,
1484
+ workspace: chatWorkspace,
1485
+ authToken: this.deps.authToken()
1486
+ });
1487
+ ctx.set("chat_message_id", turnMessage.id);
1488
+ const detachPredictions = attachChatPredictionForwarder((listener) => ctx.addMessageListener(listener), (msg) => this.session.sendDetached(msg), { threadId: threadId || null, workflowId });
1489
+ // A chat turn that generates an image or a video spends real money without
1490
+ // ever constructing an ExecutionSession, so the ledger is attached here
1491
+ // too — otherwise the turn is invisible to `nodetool costs`.
1492
+ const detachCostLedger = attachRunCostLedger(ctx, {
1493
+ userId,
1494
+ workflowId: workflowId ?? null,
1495
+ // A project's own thread attributes its spend to that project, so
1496
+ // `nodetool costs` can answer what a project cost.
1497
+ projectId: chatProjectId ?? null,
1498
+ resolveSecret: (key) => ctx.getSecret(key)
1499
+ });
1500
+ // Stamp the turn's own selection so a tool that launches another harness
1501
+ // inherits this chat's provider/model when the call doesn't name one.
1502
+ ctx.set(ACTIVE_MODEL_CONTEXT_KEY, {
1503
+ provider: providerId,
1504
+ model
1505
+ });
1506
+ // The channel every nested loop already carries: an Agent node started
1507
+ // through `run_node`, a JS script, a sub-agent three levels down all read
1508
+ // the budget off the context instead of creating one (`budgetFromContext`).
1509
+ ctx.set(RUN_BUDGET_CONTEXT_KEY, turnBudget);
1510
+ // The same channel carries the turn's permission gate, so a loop this
1511
+ // package never constructs gates through the user's mode instead of
1512
+ // building an ungated run of its own (`gateFromContext`, invariant I-1).
1513
+ // The object is shared by reference: `chatGate.mode` reads `liveMode`, so
1514
+ // a mid-turn `set_permission_mode` reaches a node that started before it.
1515
+ ctx.set(PERMISSION_GATE_CONTEXT_KEY, chatGate);
1516
+ // The capability run for this turn: the same gate the belt is wrapped in,
1517
+ // this context, and the singletons the tool constructors take today. Every
1518
+ // capability a host must supply itself goes in `capabilities` — `run_node`
1519
+ // carries a closure only this package can build. The codeact session below
1520
+ // mounts it, so an action can import
1521
+ // `@nodetool-ai/sandbox-nodetool/<namespace>` and land on `run.invoke`.
1522
+ this.chatCapabilityRun = createCapabilityRun({
1523
+ context: ctx,
1524
+ gate: chatGate,
1525
+ projectId: chatProjectId,
1526
+ availableSecrets: contextSecretAvailability(ctx),
1527
+ nodeRegistry: this.session.nodeRegistry,
1528
+ providers: chatProviders,
1529
+ subAgent: subAgentRuntime,
1530
+ budget: turnBudget,
1531
+ secretPrompt: (request) => this.requestSecretEntry(threadId, request),
1532
+ ...mcpToolHostDeps(),
1533
+ capabilities: [capabilityFromTool(runNodeTool)]
1534
+ });
1535
+ const gameAssetRun = this.chatCapabilityRun;
1536
+ const gameAssetNodeRunner = (args) => gameAssetRun.invoke("run_node", args);
1537
+ ctx.set(GAME_ASSET_NODE_RUNNER_CONTEXT_KEY, gameAssetNodeRunner);
1538
+ // CodeAct session for this turn. Created here so its prompt section is in
1539
+ // place before the system message is materialized below. The tool router
1540
+ // (`executeTool`, defined further down) is late-bound through a ref; it is
1541
+ // assigned before the provider loop can run any action.
1542
+ let codeactExecuteToolRef = null;
1543
+ let codeactSession = null;
1544
+ if (useCodeAct) {
1545
+ // The core set (file, search, fetch, todo, delegation) and discovery
1546
+ // (which providers, models and node types this install has) are also
1547
+ // plain tool calls: one question with one answer, which routing through
1548
+ // a sandbox action only delays. They stay on the belt so code can still
1549
+ // compose them; the prompt documents the direct call.
1550
+ // `create_plan` joins them in plan mode: the plan is that turn's whole
1551
+ // deliverable, and a deliverable reached only by writing a sandbox action
1552
+ // to import it is one the model routinely does not reach for.
1553
+ // `execute_plan` is direct for the same reason, in every other mode:
1554
+ // running the plan the user just approved is that turn's whole point.
1555
+ const directNames = new Set(DIRECT_TOOL_NAMES);
1556
+ if (permissionMode === "plan")
1557
+ directNames.add("create_plan");
1558
+ else
1559
+ directNames.add("execute_plan");
1560
+ const directSchemas = allSchemas.filter((s) => s.name !== "view_image" && directNames.has(s.name));
1561
+ codeactSession = createChatCodeActSession({
1562
+ includeProductKnowledge: false,
1563
+ tools: allSchemas
1564
+ .filter((s) => s.name !== "view_image")
1565
+ .map((s) => ({
1566
+ name: s.name,
1567
+ description: s.description,
1568
+ inputSchema: s.inputSchema
1569
+ })),
1570
+ sandboxPackages: sandboxPackagesForChat({
1571
+ source: uiContext?.source,
1572
+ focusedType: uiContext?.focused?.type,
1573
+ catalog: getProcessSandboxModuleCatalog()
1574
+ }),
1575
+ directToolNames: directSchemas.map((s) => s.name),
1576
+ executeTool: async (call) => {
1577
+ if (!codeactExecuteToolRef) {
1578
+ throw new Error("Tool router not ready");
1579
+ }
1580
+ return codeactExecuteToolRef({
1581
+ id: call.id,
1582
+ name: call.name,
1583
+ args: call.args
1584
+ });
1585
+ },
1586
+ residentToolNames: [
1587
+ ...CODEACT_RESIDENT_TOOL_NAMES,
1588
+ ...RESIDENT_TOOL_NAMES,
1589
+ ...focusedUiToolNames(uiContext, allSchemas.map((schema) => schema.name))
1590
+ ],
1591
+ context: ctx,
1592
+ signal,
1593
+ clock: codeactClock,
1594
+ capabilityRun: this.chatCapabilityRun ?? undefined
1595
+ });
1596
+ providerToolSchemas.push(codeactSession.providerTool);
1597
+ providerToolSchemas.push(...directSchemas);
1598
+ // `view_image` stays a direct provider tool: it is the one channel that
1599
+ // puts pixels into the model's context, and pixels cannot ride the
1600
+ // sandbox's JSON observation envelope.
1601
+ const viewImage = allSchemas.find((s) => s.name === "view_image");
1602
+ if (viewImage)
1603
+ providerToolSchemas.push(viewImage);
1604
+ codeactPromptSection = codeactSession.systemPromptSection;
1605
+ log.info("Chat turn running in codeact mode", {
1606
+ threadId,
1607
+ toolCount: allSchemas.length,
1608
+ directToolCount: directSchemas.length
1609
+ });
1610
+ }
1611
+ // Prepend system prompt if first message isn't system role — matches Python
1612
+ if (chatHistory.length === 0 || chatHistory[0].role !== "system") {
1613
+ chatHistory.unshift({
1614
+ role: "system",
1615
+ content: buildSystemContent(),
1616
+ toolCallId: null,
1617
+ toolCalls: null,
1618
+ threadId: null
1619
+ });
1620
+ }
1621
+ // The agent now discovers and queries collections itself via the
1622
+ // list_collections / query_collection tools, so there is no client-driven
1623
+ // RAG pre-query here.
1624
+ const userContent = extractTextContent(data.content);
1625
+ // Final assistant text. Updated as the provider emits assistant
1626
+ // messages; the last one wins.
1627
+ let content = "";
1628
+ // The session token to persist onto the assistant message. Seeds from the
1629
+ // prior turn's token (so a session-based provider resumes) and is refreshed
1630
+ // whenever the provider emits a new ProviderSessionUpdate this turn.
1631
+ let capturedSession = priorSession;
1632
+ // What to persist onto the assistant message: the provider's token, but with
1633
+ // the absolute checkpoint when the fast path sent only a delta (the
1634
+ // provider's emitted checkpoint is relative to the trimmed view).
1635
+ const sessionForPersist = () => sessionCheckpointOverride != null && capturedSession
1636
+ ? { ...capturedSession, checkpoint: sessionCheckpointOverride }
1637
+ : capturedSession;
1638
+ // Cap on tool-calling rounds before the loop stops. Generous enough to
1639
+ // build a multi-component app UI or run a long edit session in one turn —
1640
+ // 10 was too low and cut off the app builder mid-build.
1641
+ const MAX_TOOL_ROUNDS = 50;
1642
+ // Items still read from a superseded turn, purely to catch tool results
1643
+ // already in flight. A provider that honors the abort ends well inside it.
1644
+ const MAX_SUPERSEDED_DRAIN_ITEMS = 200;
1645
+ const useTools = providerToolSchemas.length > 0;
1646
+ // The wire messages. The provider's generateLoop owns the tool-calling
1647
+ // rounds and message assembly from here.
1648
+ let messagesToSend = [...chatHistory];
1649
+ // Everything turn-scoped, gathered in one place and appended once, after
1650
+ // media resolution, to the last user message. None of it is persisted, so
1651
+ // whatever is injected here is absent from the history a later turn sends
1652
+ // — which is exactly why it must sit behind every byte a later turn will
1653
+ // reuse. See `appendContextToLastUser`.
1654
+ const volatileContext = [];
1655
+ // Durable memories (memory_* tools), so the agent starts each turn aware
1656
+ // of what it recorded — project facts, decisions, and the assets it
1657
+ // generated for reuse. This thread's in full, the rest as a count it can
1658
+ // search. Deterministic and always-on.
1659
+ if (threadId) {
1660
+ const memoryBlock = await this.buildMemoryBlock(userId, threadId);
1661
+ if (memoryBlock)
1662
+ volatileContext.push(memoryBlock);
1663
+ const generationBlock = await this.buildGenerationBlock(userId, threadId);
1664
+ if (generationBlock)
1665
+ volatileContext.push(generationBlock);
1666
+ }
1667
+ // The bodies of any skills the message invoked with `/<name>`. The catalog
1668
+ // half is stable and already sits in the system prompt.
1669
+ const invokedSkills = invokedSkillsSection(userSkills, userContent);
1670
+ if (invokedSkills)
1671
+ volatileContext.push(invokedSkills);
1672
+ // Expand any `asset://<id>.<ext>` references the composer or a prior turn
1673
+ // attached and dereference the URIs to data the provider can consume.
1674
+ // Image / audio mentions typed inline in a text part get split into proper
1675
+ // blocks first (mirroring what the workflow agent node does in
1676
+ // `buildUserMessage`), then every block with an `asset://` / storage URI is
1677
+ // resolved to a data URI. Text-document mentions are inlined as their
1678
+ // decoded contents. Without this step the provider would see literal
1679
+ // `asset://…` text and never look at the referenced media.
1680
+ //
1681
+ // The volatile block is appended after resolution, so a memory's
1682
+ // `asset://` reference stays a reference instead of being inlined as a
1683
+ // data URI. Both steps are one closure because compaction rebuilds the
1684
+ // wire messages from a shortened history and must reproduce them exactly.
1685
+ const withTurnContext = async (base) => {
1686
+ const resolved = await ctx.resolveMessageMediaUris(base);
1687
+ return volatileContext.length > 0
1688
+ ? appendContextToLastUser(resolved, volatileContext.join("\n\n"))
1689
+ : resolved;
1690
+ };
1691
+ messagesToSend = await withTurnContext(messagesToSend);
1692
+ // Run one tool call and return the result to feed back to the model. Owns
1693
+ // server/client tool routing, side effects (client round-trips via the
1694
+ // ToolBridge), and asset materialization; the provider's loop orchestrates.
1695
+ // Image results (e.g. ui_3d_capture_view) return MessageContent blocks so
1696
+ // vision providers can see them; everything else returns result text.
1697
+ const executeTool = async (toolCall) => {
1698
+ let toolResult;
1699
+ const serverTool = serverToolMap.get(toolCall.name);
1700
+ const preferClientDocumentTool = workflowDocumentToolNames.has(toolCall.name) &&
1701
+ this.deps.clientTools()[toolCall.name] !== undefined;
1702
+ if (preferClientDocumentTool) {
1703
+ // The renderer owns live, potentially unsaved state. Use it when it is
1704
+ // present; the server implementation remains the headless fallback.
1705
+ await this.session.send({
1706
+ type: "tool_call",
1707
+ thread_id: threadId,
1708
+ tool_call_id: toolCall.id,
1709
+ name: toolCall.name,
1710
+ args: toolCall.args
1711
+ });
1712
+ const clientResult = await this.deps.toolBridge.createWaiter(toolCall.id, 300_000, threadId);
1713
+ toolResult =
1714
+ clientResult.result ?? clientResult.content ?? clientResult;
1715
+ }
1716
+ else if (serverTool) {
1717
+ try {
1718
+ toolResult = await Tool.executeTool(serverTool, ctx, toolCall.args, {
1719
+ toolCallId: toolCall.id
1720
+ });
1721
+ }
1722
+ catch (err) {
1723
+ const errMsg = err instanceof Error ? err.message : String(err);
1724
+ log.error("Tool execution failed", {
1725
+ tool: toolCall.name,
1726
+ error: errMsg
1727
+ });
1728
+ toolResult = { error: errMsg };
1729
+ }
1730
+ }
1731
+ else if (this.deps.clientTools()[toolCall.name]) {
1732
+ // Client-side tool — round-trip through the UI via the ToolBridge.
1733
+ await this.session.send({
1734
+ type: "tool_call",
1735
+ thread_id: threadId,
1736
+ tool_call_id: toolCall.id,
1737
+ name: toolCall.name,
1738
+ args: toolCall.args
1739
+ });
1740
+ const clientResult = await this.deps.toolBridge.createWaiter(toolCall.id, 300_000, threadId);
1741
+ toolResult =
1742
+ clientResult.result ?? clientResult.content ?? clientResult;
1743
+ }
1744
+ else {
1745
+ toolResult = { error: `Tool "${toolCall.name}" not available` };
1746
+ }
1747
+ // view_image is the ONE mechanism that puts pixels into the model's
1748
+ // context: its image_content rides the tool message so the model sees the
1749
+ // image this turn. (The chat loop builds tool messages inside
1750
+ // provider.generateLoop, so the tool return value is the only hook.)
1751
+ if (toolCall.name === "view_image") {
1752
+ const injected = extractInjectableImages(toolResult);
1753
+ if (injected) {
1754
+ return [{ type: "text", text: injected.text }, ...injected.images];
1755
+ }
1756
+ }
1757
+ // Every other tool that produced pixels (timeline frames, 3D capture, …)
1758
+ // gets them persisted as temp image assets; the model receives only a
1759
+ // handle and calls view_image when it wants to look.
1760
+ toolResult = await this.materializeToolResultImages(toolResult, ctx);
1761
+ const processed = compactResourceIds(await this.processToolResult(toolResult, ctx));
1762
+ return isString(processed) ? processed : JSON.stringify(processed);
1763
+ };
1764
+ // Late-bind the codeact bridge to the router above, and route
1765
+ // `execute_code` calls into the sandbox session; everything else (only
1766
+ // `view_image` is offered in codeact mode) keeps the normal path.
1767
+ codeactExecuteToolRef = executeTool;
1768
+ const session = codeactSession;
1769
+ const beltToolNames = new Set(allSchemas.map((s) => s.name));
1770
+ const effectiveExecuteTool = async (rawCall) => {
1771
+ // A model that read the CodeAct prompt sometimes calls
1772
+ // `tools.<name>` at the top level. Recover the plain name first, so the
1773
+ // call reaches the tool instead of dying as "no such tool".
1774
+ const name = normalizeToolCallName(rawCall.name);
1775
+ const toolCall = name === rawCall.name ? rawCall : { ...rawCall, name };
1776
+ if (!session)
1777
+ return executeTool(toolCall);
1778
+ if (name === EXECUTE_CODE_TOOL_NAME) {
1779
+ return session.executeAction(toolCall.args);
1780
+ }
1781
+ // A belt tool named directly still runs: the router is the same gate the
1782
+ // sandbox bridge goes through, so answering the call is strictly better
1783
+ // than refusing it and spending a round on the correction.
1784
+ if (beltToolNames.has(name))
1785
+ return executeTool(toolCall);
1786
+ // Answer, do not throw: the model can only correct course if the
1787
+ // recovery instructions arrive as this call's tool result.
1788
+ return JSON.stringify({ error: unroutableToolMessage(name) });
1789
+ };
1790
+ // Tool name by call id, so persisted tool messages keep their name (the
1791
+ // provider Message carries only the id).
1792
+ const toolNames = new Map();
1793
+ // Calls announced by an assistant message that have not been answered by a
1794
+ // tool message yet. Whatever is still here when the turn tears down never
1795
+ // got its result row, and the `finally` below writes a stand-in so the
1796
+ // transcript stays well-formed.
1797
+ const openToolCalls = new Set();
1798
+ // Set when a newer turn supersedes this one. The loop then stops feeding
1799
+ // the client and only rescues the results still coming.
1800
+ let superseded = false;
1801
+ let drainedItems = 0;
1802
+ let terminalDoneSent = false;
1803
+ const sendTerminalDone = async () => {
1804
+ if (terminalDoneSent)
1805
+ return;
1806
+ terminalDoneSent = true;
1807
+ await this.session.send({
1808
+ type: "chunk",
1809
+ content: "",
1810
+ done: true,
1811
+ thread_id: threadId
1812
+ });
1813
+ };
1814
+ /**
1815
+ * Write one message this turn produced. `echo` is false for a superseded
1816
+ * turn: the row still belongs in the thread, but the client has moved on
1817
+ * and replaying it there would interleave a dead turn into a live one.
1818
+ */
1819
+ const persistTurnMessage = async (m, echo) => {
1820
+ if (m.role === "assistant") {
1821
+ // Content may be a plain string or a MessageContent[] carrying
1822
+ // native-image blocks. Raw image bytes are turned into real assets
1823
+ // here so base64 never lands in the DB or on the wire.
1824
+ let persistedContent = m.content ?? null;
1825
+ if (isString(m.content)) {
1826
+ if (echo)
1827
+ content = m.content;
1828
+ }
1829
+ else if (Array.isArray(m.content)) {
1830
+ const materialized = await this.materializeAssistantImageContent(m.content, userId, workflowId, chatProjectId ?? "default");
1831
+ persistedContent = materialized;
1832
+ if (echo) {
1833
+ content = materialized
1834
+ .filter((c) => c.type === "text" && isString(c.text))
1835
+ .map((c) => c.text)
1836
+ .join("");
1837
+ }
1838
+ }
1839
+ const toolCalls = Array.isArray(m.toolCalls)
1840
+ ? m.toolCalls.map((tc) => ({
1841
+ id: tc.id,
1842
+ name: tc.name,
1843
+ args: tc.args,
1844
+ result: null,
1845
+ // Gemini 3 rejects a history that replays a function call
1846
+ // without the signature it issued, so it rides into the DB.
1847
+ thought_signature: tc.thought_signature ?? null
1848
+ }))
1849
+ : null;
1850
+ for (const tc of toolCalls ?? []) {
1851
+ if (!isString(tc.id))
1852
+ continue;
1853
+ openToolCalls.add(tc.id);
1854
+ toolNames.set(tc.id, tc.name);
1855
+ }
1856
+ const assistantMsgData = {
1857
+ type: "message",
1858
+ role: "assistant",
1859
+ content: persistedContent
1860
+ };
1861
+ if (toolCalls) {
1862
+ assistantMsgData.tool_calls = toolCalls;
1863
+ }
1864
+ assistantMsgData.thread_id = threadId;
1865
+ assistantMsgData.workflow_id = workflowId;
1866
+ assistantMsgData.provider = providerId;
1867
+ assistantMsgData.model = model;
1868
+ assistantMsgData.provider_session = sessionForPersist();
1869
+ await this.saveMessageToDb(assistantMsgData);
1870
+ if (echo)
1871
+ await this.session.send(assistantMsgData);
1872
+ return;
1873
+ }
1874
+ if (m.role !== "tool")
1875
+ return;
1876
+ // Image tool results carry MessageContent blocks; persist/echo only
1877
+ // their note text so chat history stays light (the base64 rode the
1878
+ // in-flight provider message, never the DB).
1879
+ const toolContent = Array.isArray(m.content)
1880
+ ? toolResultDisplayText(m.content)
1881
+ : isString(m.content)
1882
+ ? m.content
1883
+ : "";
1884
+ if (isString(m.toolCallId))
1885
+ openToolCalls.delete(m.toolCallId);
1886
+ const toolMsgData = {
1887
+ type: "message",
1888
+ role: "tool",
1889
+ tool_call_id: m.toolCallId ?? null,
1890
+ name: m.toolCallId ? (toolNames.get(m.toolCallId) ?? null) : null,
1891
+ content: toolContent,
1892
+ thread_id: threadId,
1893
+ workflow_id: workflowId,
1894
+ provider: providerId,
1895
+ model
1896
+ };
1897
+ await this.saveMessageToDb(toolMsgData);
1898
+ if (echo)
1899
+ await this.session.send(toolMsgData);
1900
+ };
1901
+ // The Claude Agent provider runs the SDK's own loop, which resolves skills
1902
+ // through its native `Skill` tool (progressive disclosure) rather than the
1903
+ // always-on catalog every other provider reads from the system prompt. Hand
1904
+ // it the user's DB skills so that loop can list and load them; it
1905
+ // materializes them into an isolated local plugin (no `settingSources`
1906
+ // leakage). Other providers get skills via the system-prompt catalog above
1907
+ // and ignore this field.
1908
+ const skillsForProvider = provider.provider === PROVIDER_IDS.CLAUDE_AGENT_SDK
1909
+ ? userSkills.map((skill) => ({
1910
+ name: skill.name,
1911
+ description: skill.description,
1912
+ content: skill.content
1913
+ }))
1914
+ : undefined;
1915
+ // ── Compaction (D5) ──────────────────────────────────────────────────
1916
+ // A long thread eventually stops fitting, and from then on every turn dies
1917
+ // on a provider error the user cannot act on. Compaction replaces the
1918
+ // turns before the last few with one summary row and sends that instead.
1919
+ // The rows stay in the database for the UI and `nodetool.threads.*`; only
1920
+ // the provider's view is cut.
1921
+ const compaction = await readCompactionSettings();
1922
+ const modelKey = `${providerId}/${model}`;
1923
+ let contextWindow = this.contextWindows.get(modelKey);
1924
+ if (!contextWindow && compaction.thresholdTokens === null) {
1925
+ contextWindow = resolveContextWindow(provider, model);
1926
+ this.contextWindows.set(modelKey, contextWindow);
1927
+ }
1928
+ const compactionThresholdTokens = compactionThreshold(compaction, contextWindow ? (await contextWindow).tokens : 0);
1929
+ /** Spent by the reactive trigger below; bounds the turn to one retry. */
1930
+ let compactionRetryUsed = false;
1931
+ /** Tell the user what was compacted, or why it was not. */
1932
+ const compactionLog = async (content, severity) => {
1933
+ await this.session.send({
1934
+ type: "log_update",
1935
+ node_id: "",
1936
+ node_name: "compaction",
1937
+ content,
1938
+ severity,
1939
+ thread_id: threadId
1940
+ });
1941
+ };
1942
+ /**
1943
+ * Which model writes the summary: `NODETOOL_COMPACTION_MODEL` when set,
1944
+ * otherwise the one the turn is already running on. The setting takes the
1945
+ * `provider/model` form the supervisor's does, and a bare id names a model
1946
+ * on this turn's provider.
1947
+ */
1948
+ const summarizerModel = async () => {
1949
+ const configured = compaction.model;
1950
+ if (configured === null)
1951
+ return { provider, model };
1952
+ const slash = configured.indexOf("/");
1953
+ if (slash <= 0)
1954
+ return { provider, model: configured };
1955
+ const otherId = configured.slice(0, slash);
1956
+ const otherModel = configured.slice(slash + 1);
1957
+ if (otherId === providerId)
1958
+ return { provider, model: otherModel };
1959
+ const resolved = await this.session.resolveProvider?.(otherId, userId);
1960
+ if (!resolved) {
1961
+ throw new Error(`No provider registered for "${otherId}"`);
1962
+ }
1963
+ return { provider: resolved, model: otherModel };
1964
+ };
1965
+ /**
1966
+ * Summarize this thread up to the cut, persist the record, and rebuild the
1967
+ * wire messages from what survives. Answers whether it compacted.
1968
+ *
1969
+ * Everything that can go wrong here leaves the turn running against the
1970
+ * history it already had: a thread too short to cut, a summarizer that
1971
+ * failed, a summarizer with nothing to say. This is the one place A4 does
1972
+ * not fail closed (I-4), because the alternative to a missing summary is a
1973
+ * turn that cannot run at all.
1974
+ */
1975
+ const compactThread = async (reason) => {
1976
+ const [rows] = await Message.paginate(threadId, { limit: 1000 });
1977
+ const cut = chooseCompactionCut(historySinceCompaction(rows), compaction.keepUserTurns, {
1978
+ // Leave room for the summary, turn context and subsequent replies.
1979
+ maxTokens: compactionThresholdTokens * 0.75 -
1980
+ COMPACTION_SUMMARY_MAX_TOKENS -
1981
+ estimatePromptTokens([systemChatMessage()]) -
1982
+ estimatePromptTokens([
1983
+ { role: "user", content: volatileContext.join("\n\n") }
1984
+ ]),
1985
+ countTokens: (row) => estimatePromptTokens(convertDbMessages([row]))
1986
+ });
1987
+ if (!cut)
1988
+ return false;
1989
+ let summary;
1990
+ try {
1991
+ const summarizer = await summarizerModel();
1992
+ summary = await summarizeForCompaction({
1993
+ provider: summarizer.provider,
1994
+ model: summarizer.model,
1995
+ // The stored rows, not the wire messages: an `asset://` uri is still
1996
+ // a uri here rather than the data it resolves to, which is what lets
1997
+ // the summary carry the reference forward.
1998
+ messages: convertDbMessages(cut.summarize),
1999
+ signal
2000
+ });
2001
+ }
2002
+ catch (err) {
2003
+ const detail = err instanceof Error ? err.message : String(err);
2004
+ log.warn("Compaction summary failed", { threadId, error: detail });
2005
+ await compactionLog(`Could not summarize this conversation (${detail}). Sending it in full.`, "warning");
2006
+ return false;
2007
+ }
2008
+ if (summary === null) {
2009
+ await compactionLog("The summary of this conversation came back empty. Sending it in full.", "warning");
2010
+ return false;
2011
+ }
2012
+ // The record belongs at the cut, not at the end of the thread: history
2013
+ // assembly starts from the newest one, so a record written after the kept
2014
+ // turns would drop them from the next turn's view. Rows are ordered by
2015
+ // `created_at`, so it takes the millisecond before the first survivor.
2016
+ const boundary = Date.parse(cut.keep[0].created_at);
2017
+ const record = await Message.create({
2018
+ thread_id: threadId,
2019
+ user_id: userId,
2020
+ role: "user",
2021
+ execution_event_type: COMPACTION_EVENT_TYPE,
2022
+ content: compactionMessageContent(summary),
2023
+ created_at: Number.isFinite(boundary)
2024
+ ? new Date(boundary - 1).toISOString()
2025
+ : cut.keep[0].created_at
2026
+ });
2027
+ messagesToSend = await withTurnContext([
2028
+ systemChatMessage(),
2029
+ ...convertDbMessages([record, ...cut.keep])
2030
+ ]);
2031
+ // The upstream transcript a session token resumes is the history the cut
2032
+ // just replaced, so nothing resumes across a compaction.
2033
+ capturedSession = null;
2034
+ this.measuredThreadTokens.delete(threadId);
2035
+ loadFullHistory = null;
2036
+ sessionCheckpointOverride = null;
2037
+ await this.session.send({
2038
+ type: "message",
2039
+ role: "user",
2040
+ execution_event_type: COMPACTION_EVENT_TYPE,
2041
+ content: record.content,
2042
+ thread_id: threadId,
2043
+ workflow_id: workflowId
2044
+ });
2045
+ log.info("Compacted chat history", {
2046
+ threadId,
2047
+ summarized: cut.summarize.length,
2048
+ kept: cut.keep.length
2049
+ });
2050
+ await compactionLog(`Summarized the earlier part of this conversation because ${reason}.`, "info");
2051
+ return true;
2052
+ };
2053
+ // Trigger 1, proactive: the prompt reached the threshold, 90% of the
2054
+ // model's input window unless `NODETOOL_CHAT_COMPACTION_TOKENS` sets it.
2055
+ // The size is the larger of two numbers. The provider-reported size of
2056
+ // this thread's last call counts the system prompt and tool definitions.
2057
+ // `estimatePromptTokens` counts the messages added since, scoring each
2058
+ // attached image, audio, video or document at a fixed per-modality figure
2059
+ // so a resolved `data:` uri never reaches the tokenizer.
2060
+ //
2061
+ // A provider holding the conversation upstream is sent only the turns since
2062
+ // its session token, so this number would describe a fraction of what it
2063
+ // actually has. Those get trigger 2 alone: the provider itself says when
2064
+ // the transcript stopped fitting.
2065
+ if (!holdsTranscriptServerSide(providerId, capturedSession !== null)) {
2066
+ const measured = this.measuredThreadTokens.get(threadId);
2067
+ const promptTokens = Math.max(estimatePromptTokens(messagesToSend), measured?.modelKey === modelKey ? measured.tokens : 0);
2068
+ if (promptTokens >= compactionThresholdTokens) {
2069
+ await compactThread(`it reached about ${promptTokens} tokens, at or over the ` +
2070
+ `${compactionThresholdTokens}-token limit`);
2071
+ }
2072
+ }
2073
+ /**
2074
+ * One pass of the provider's tool-calling loop over the current
2075
+ * `messagesToSend`. A function rather than a statement so the reactive
2076
+ * compaction below can run it a second time against a shortened history.
2077
+ */
2078
+ const streamTurn = async () => {
2079
+ for await (const item of provider.generateLoop({
2080
+ messages: messagesToSend,
2081
+ model,
2082
+ tools: useTools ? providerToolSchemas : undefined,
2083
+ threadId,
2084
+ providerSession: capturedSession,
2085
+ loadFullHistory: loadFullHistory ?? undefined,
2086
+ executeTool: useTools ? effectiveExecuteTool : undefined,
2087
+ maxIterations: MAX_TOOL_ROUNDS,
2088
+ turnBudget,
2089
+ sequentialTools: session ? true : undefined,
2090
+ workspaceDir: chatWorkspace?.localDir ?? undefined,
2091
+ skills: skillsForProvider,
2092
+ resolveMedia: mediaResolverFor(ctx),
2093
+ signal
2094
+ })) {
2095
+ // A newer turn has taken over. Stop driving the client, but do NOT
2096
+ // drop what this turn already produced: the provider checks its abort
2097
+ // signal before dispatching a tool, never during one, so a call in
2098
+ // flight runs to completion and its result is arriving right now.
2099
+ // Discarding it left the model blind to a side effect it had already
2100
+ // caused, and it silently redid the work.
2101
+ if (!superseded &&
2102
+ requestSeq !== undefined &&
2103
+ requestSeq !== this.chatRequestSeq) {
2104
+ superseded = true;
2105
+ }
2106
+ if (superseded) {
2107
+ if (isProviderMessageEvent(item)) {
2108
+ await persistTurnMessage(item.message, false);
2109
+ }
2110
+ // Nothing left outstanding: the rest of this turn is work the user
2111
+ // has already moved on from.
2112
+ if (openToolCalls.size === 0)
2113
+ break;
2114
+ // The turn's signal is already aborted, so a provider that honors it
2115
+ // ends within a few items. One that does not must never hold this
2116
+ // handler open — stop reading and let the `finally` stand in for
2117
+ // whatever is still missing.
2118
+ if (++drainedItems > MAX_SUPERSEDED_DRAIN_ITEMS) {
2119
+ log.warn("Superseded turn kept producing; stopped draining", {
2120
+ threadId,
2121
+ openToolCalls: openToolCalls.size
2122
+ });
2123
+ break;
2124
+ }
2125
+ continue;
2126
+ }
2127
+ if (isProviderStop(item)) {
2128
+ // A limit ended the turn, not the model finishing its answer. Said
2129
+ // nowhere, the two are indistinguishable to the user (invariant
2130
+ // I-3), so the reason is persisted as an ordinary assistant message:
2131
+ // it belongs in the transcript the next turn reads, and it is
2132
+ // per-turn text that must never reach the system prompt (I-7).
2133
+ //
2134
+ // `aborted` is the user pressing Stop or a newer message superseding
2135
+ // this one. They already know why it stopped, and a notice there
2136
+ // would scold them for using the control. The other three are limits
2137
+ // nobody asked for in the moment: the dollar cap, the wall clock,
2138
+ // and the tool-round ceiling all leave the turn unfinished mid-work.
2139
+ if (item.reason !== "aborted") {
2140
+ // The budget names the ceiling it hit ("turn budget of $5
2141
+ // reached"); the round cap is not a budget stop, and reading a
2142
+ // budget a sub-agent exhausted earlier would name the wrong limit.
2143
+ const detail = item.reason === "iterations"
2144
+ ? item.detail
2145
+ : (turnBudget.exhausted?.detail ?? item.detail);
2146
+ await persistTurnMessage({ role: "assistant", content: `Stopped: ${detail}` }, true);
2147
+ }
2148
+ else if (signal?.reason === "shutdown") {
2149
+ // …except a shutdown, which nobody asked for either. The reply
2150
+ // stops mid-sentence and, said nowhere, reads as the model
2151
+ // finishing; the note says the server went away, and is what the
2152
+ // next turn reads when the client retries on the machine that
2153
+ // stayed.
2154
+ await persistTurnMessage({ role: "assistant", content: "Stopped: server restarting" }, true);
2155
+ }
2156
+ continue;
2157
+ }
2158
+ if (isProviderSessionUpdate(item)) {
2159
+ // Internal continuity token — capture for persistence, never wired.
2160
+ capturedSession = item.session;
2161
+ continue;
2162
+ }
2163
+ if (isProviderMessageEvent(item)) {
2164
+ await persistTurnMessage(item.message, true);
2165
+ continue;
2166
+ }
2167
+ if (isToolCall(item)) {
2168
+ // Informational: the loop executes it through executeTool.
2169
+ toolNames.set(item.id, item.name);
2170
+ log.info("Tool call", { tool: item.name, args: item.args });
2171
+ continue;
2172
+ }
2173
+ // `isChunk` from the runtime additionally requires string content, so
2174
+ // it drops the native-`Float32Array` audio chunks the client decodes.
2175
+ // `type === "chunk"` is Chunk's alone in this union, so test that.
2176
+ if (item.type === "chunk") {
2177
+ // The provider loop emits a terminal chunk for its completion, and
2178
+ // this handler emits the turn terminal below. Keep one envelope
2179
+ // terminal even when a provider double emits its own `done` item.
2180
+ if (!item.thread_id)
2181
+ item.thread_id = threadId;
2182
+ await this.session.send(item.done === true ? { ...item, done: false } : { ...item });
2183
+ }
2184
+ }
2185
+ };
2186
+ try {
2187
+ // At most two passes. The second happens only when the first ended with
2188
+ // the provider's own context-exceeded signal and compaction answered it
2189
+ // by shortening the history. A second such failure is surfaced: the
2190
+ // shortened transcript did not fit either, and compacting again would
2191
+ // buy another summarizer call to learn the same thing.
2192
+ //
2193
+ // A provider rejects an oversized request before it generates anything,
2194
+ // so the retry re-reads a turn that produced no messages of its own.
2195
+ for (;;) {
2196
+ try {
2197
+ await streamTurn();
2198
+ const lastCallTokens = provider.lastCallTokens;
2199
+ if (typeof lastCallTokens === "number") {
2200
+ this.measuredThreadTokens.set(threadId, {
2201
+ modelKey,
2202
+ tokens: lastCallTokens
2203
+ });
2204
+ }
2205
+ break;
2206
+ }
2207
+ catch (err) {
2208
+ if (compactionRetryUsed ||
2209
+ providerFailureDetail(err)?.code !== "context_exceeded") {
2210
+ throw err;
2211
+ }
2212
+ compactionRetryUsed = true;
2213
+ const compacted = await compactThread("the provider refused the request: this conversation no longer fits its context window");
2214
+ if (!compacted)
2215
+ throw err;
2216
+ log.info("Retrying the turn against the compacted history", {
2217
+ threadId
2218
+ });
2219
+ }
2220
+ }
2221
+ // A superseded turn is done once its outstanding results are saved. The
2222
+ // completion chunk and the memory pass belong to the turn the user is
2223
+ // actually watching, which has its own.
2224
+ if (superseded)
2225
+ return;
2226
+ // Log provider call for cost tracking — matches Python's _log_provider_call()
2227
+ await this._logProviderCall(userId, provider, providerId, model, workflowId, chatProjectId ?? null);
2228
+ // What the run spent, where the user can see it. A number only the
2229
+ // server log carries is a number nobody reads; `unpricedTurns` rides
2230
+ // along when there were any, so the total reads as the lower bound it
2231
+ // is rather than as the whole bill (invariant I-4).
2232
+ const spentUsd = turnBudget.turns.spentUsd;
2233
+ const unpricedTurns = turnBudget.turns.unpricedTurns;
2234
+ await this.session.send({
2235
+ type: "log_update",
2236
+ node_id: "",
2237
+ node_name: "budget",
2238
+ content: unpricedTurns > 0
2239
+ ? `Run spent at least $${spentUsd.toFixed(4)} (${unpricedTurns} turn(s) on a model with no catalog price)`
2240
+ : `Run spent $${spentUsd.toFixed(4)}`,
2241
+ severity: "info",
2242
+ thread_id: threadId
2243
+ });
2244
+ // Signal completion — matches Python's done chunk.
2245
+ await sendTerminalDone();
2246
+ log.debug("Chat complete", {
2247
+ threadId,
2248
+ chars: content.length,
2249
+ // What the whole run — this loop plus every sub-agent and node it
2250
+ // started — reserved and committed against the shared budget.
2251
+ spentUsd: turnBudget.turns.spentUsd,
2252
+ stoppedBy: turnBudget.exhausted?.kind ?? null
2253
+ });
2254
+ }
2255
+ catch (err) {
2256
+ // Stop and supersede are expected control flow. Their provider errors
2257
+ // must not become durable assistant failures or a second terminal frame.
2258
+ if (signal?.aborted === true ||
2259
+ (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)) {
2260
+ return;
2261
+ }
2262
+ const errMsg = err instanceof Error ? err.message : String(err);
2263
+ log.error("Chat processing error", { threadId, error: errMsg });
2264
+ let errorType = "error";
2265
+ let statusCode;
2266
+ let formattedMsg = safeClientErrorText(err);
2267
+ // Connection errors (ECONNREFUSED, ENOTFOUND, etc.)
2268
+ if (errMsg.includes("ECONNREFUSED") ||
2269
+ errMsg.includes("ENOTFOUND") ||
2270
+ errMsg.includes("fetch failed") ||
2271
+ errMsg.includes("nodename nor servname")) {
2272
+ errorType = "connection_error";
2273
+ if (errMsg.includes("ENOTFOUND") ||
2274
+ errMsg.includes("nodename nor servname")) {
2275
+ formattedMsg =
2276
+ "Connection error: Unable to resolve hostname. Please check your network connection and API endpoint configuration.";
2277
+ }
2278
+ else {
2279
+ formattedMsg = `Connection error: ${safeClientErrorText(err)}`;
2280
+ }
2281
+ }
2282
+ else {
2283
+ const providerFailure = providerFailureMessage(err, providerId, model);
2284
+ errorType = providerFailure.errorType;
2285
+ statusCode = providerFailure.statusCode;
2286
+ formattedMsg = providerFailure.message;
2287
+ }
2288
+ const errorMessage = {
2289
+ type: "error",
2290
+ message: formattedMsg,
2291
+ error_type: errorType
2292
+ };
2293
+ if (statusCode !== undefined) {
2294
+ errorMessage.status_code = statusCode;
2295
+ }
2296
+ errorMessage.thread_id = threadId;
2297
+ errorMessage.workflow_id = workflowId;
2298
+ await this.session.send(errorMessage);
2299
+ // Signal completion even on error — matches Python
2300
+ await sendTerminalDone();
2301
+ const errorMsgData = {
2302
+ type: "message",
2303
+ role: "assistant",
2304
+ content: errorType === "connection_error"
2305
+ ? `I encountered a connection error: ${formattedMsg}. Please check your network connection and try again.`
2306
+ : errorType === "http_status_error"
2307
+ ? `I encountered an API error (HTTP ${statusCode}): ${formattedMsg}`
2308
+ : `I encountered an error: ${formattedMsg}`,
2309
+ thread_id: threadId,
2310
+ workflow_id: workflowId,
2311
+ provider: providerId,
2312
+ model
2313
+ };
2314
+ await this.saveMessageToDb(errorMsgData);
2315
+ await this.session.send(errorMsgData);
2316
+ }
2317
+ finally {
2318
+ detachPredictions();
2319
+ detachCostLedger();
2320
+ try {
2321
+ await chatWorkspace?.cleanupScratch?.();
2322
+ }
2323
+ catch (err) {
2324
+ log.warn("Chat workspace scratch cleanup failed", {
2325
+ threadId,
2326
+ error: err instanceof Error ? err.message : String(err)
2327
+ });
2328
+ }
2329
+ // Whatever is still outstanding never got a result row. Leaving the gap
2330
+ // makes the thread malformed — Anthropic rejects a `tool_use` with no
2331
+ // `tool_result` — and leaves the model unaware the call was abandoned,
2332
+ // which is what made it silently redo the work.
2333
+ for (const toolCallId of openToolCalls) {
2334
+ try {
2335
+ await this.saveMessageToDb({
2336
+ type: "message",
2337
+ role: "tool",
2338
+ tool_call_id: toolCallId,
2339
+ name: toolNames.get(toolCallId) ?? null,
2340
+ content: SUPERSEDED_TOOL_RESULT,
2341
+ thread_id: threadId,
2342
+ workflow_id: workflowId,
2343
+ provider: providerId,
2344
+ model
2345
+ });
2346
+ }
2347
+ catch (err) {
2348
+ // Best effort: a turn that already failed must not fail again here.
2349
+ this.session.logError("superseded tool result save failed", err);
2350
+ }
2351
+ }
2352
+ openToolCalls.clear();
2353
+ }
2354
+ }
2355
+ /**
2356
+ * Log a provider call for cost tracking — mirrors Python's _log_provider_call().
2357
+ * Best-effort: never throws, logs warnings on failure.
2358
+ */
2359
+ async _logProviderCall(userId, provider, providerId, model, workflowId, projectId) {
2360
+ if (!providerId || !model) {
2361
+ log.warn("Cannot log provider call: missing provider or model");
2362
+ return;
2363
+ }
2364
+ try {
2365
+ // A provider that could not price a call reports why. Its running total
2366
+ // is then missing that spend, so a zero is written as null — the row
2367
+ // reads unpriced in `nodetool costs` instead of summing as free, the
2368
+ // same posture the cost ledger takes.
2369
+ const unpricedReason = provider.unpricedReason;
2370
+ const cost = provider.cost;
2371
+ const unpriced = cost === 0 && unpricedReason != null;
2372
+ await Prediction.create({
2373
+ user_id: userId,
2374
+ provider: providerId,
2375
+ model,
2376
+ cost: unpriced ? null : cost,
2377
+ metadata: unpricedReason ? { unpriced_reason: unpricedReason } : null,
2378
+ workflow_id: workflowId,
2379
+ // Token spend from a project's agent thread is the project's; a turn
2380
+ // outside a project records a null rather than a bucket name.
2381
+ project_id: projectId,
2382
+ status: "completed",
2383
+ node_id: ""
2384
+ });
2385
+ log.debug("Logged provider call", { provider: providerId, model, cost });
2386
+ }
2387
+ catch (err) {
2388
+ if (err instanceof TypeError || err instanceof ReferenceError) {
2389
+ log.warn("Failed to log provider call due to invalid data", {
2390
+ error: err instanceof Error ? err.message : String(err)
2391
+ });
2392
+ }
2393
+ else {
2394
+ log.error("Unexpected error logging provider call", {
2395
+ error: err instanceof Error ? err.message : String(err)
2396
+ });
2397
+ }
2398
+ }
2399
+ }
2400
+ /**
2401
+ * Detect message input node names from a workflow graph.
2402
+ * Mirrors Python's WorkflowMessageProcessor._detect_message_input_names().
2403
+ *
2404
+ * Scans graph nodes for types ending in .MessageInput / .MessageListInput
2405
+ * and returns their data.name values.
2406
+ */
2407
+ detectMessageInputNames(graph) {
2408
+ let messageName = null;
2409
+ let messagesName = null;
2410
+ for (const node of graph.nodes) {
2411
+ const nodeType = isString(node.type) ? node.type : "";
2412
+ const data = isObjectLike(node.data)
2413
+ ? node.data
2414
+ : {};
2415
+ const nodeName = isString(data.name) ? data.name.trim() : "";
2416
+ if (!nodeName)
2417
+ continue;
2418
+ if (messageName === null &&
2419
+ (nodeType === "nodetool.input.MessageInput" ||
2420
+ nodeType.endsWith(".MessageInput"))) {
2421
+ messageName = nodeName;
2422
+ }
2423
+ if (messagesName === null &&
2424
+ (nodeType === "nodetool.input.MessageListInput" ||
2425
+ nodeType.endsWith(".MessageListInput"))) {
2426
+ messagesName = nodeName;
2427
+ }
2428
+ }
2429
+ return { messageName, messagesName };
2430
+ }
2431
+ /**
2432
+ * Handle a chat_message with a `media_generation` payload by invoking the
2433
+ * selected provider's textToImage / textToVideo API, storing the resulting
2434
+ * asset(s), and returning them to the client as an assistant `Message`
2435
+ * whose `content` is an array of `MessageImageContent` / `MessageVideoContent`
2436
+ * blocks.
2437
+ *
2438
+ * The generated bytes are persisted via `ctx.storage.store()` so each
2439
+ * output receives a stable URI the client can resolve as a server asset.
2440
+ * The `media_generation` echo on the assistant message lets the UI render
2441
+ * the generation header (model, variation count, resolution, etc.) in the
2442
+ * conversation stream.
2443
+ */
2444
+ async handleMediaGenerationMessage(data, mediaGeneration, requestSeq, signal) {
2445
+ const threadId = isString(data.thread_id) ? data.thread_id : "";
2446
+ const workflowId = isString(data.workflow_id) ? data.workflow_id : null;
2447
+ const userId = this.session.requireUserId();
2448
+ const mode = String(mediaGeneration.mode ?? "");
2449
+ // The media composer's own selection first; a client without a separate
2450
+ // media picker (mobile) sends only the message-level one. The built-in
2451
+ // chat default is not in the chain — a text model can never serve a
2452
+ // generation, so falling back to it only buys an obscure provider error.
2453
+ const providerId = String(mediaGeneration.provider ?? data.provider ?? "");
2454
+ const modelId = String(mediaGeneration.model ?? data.model ?? "");
2455
+ const prompt = extractTextContent(data.content);
2456
+ /**
2457
+ * Whether this turn has been cancelled. The media provider APIs take no
2458
+ * AbortSignal, so an in-flight generation runs to completion regardless —
2459
+ * but its result must not be stored as an asset or delivered to a user who
2460
+ * pressed Stop. Checked after every provider call, before any write.
2461
+ */
2462
+ const cancelled = () => signal?.aborted === true ||
2463
+ (requestSeq !== undefined && requestSeq !== this.chatRequestSeq);
2464
+ let terminalDoneSent = false;
2465
+ const sendTerminalDone = async () => {
2466
+ if (terminalDoneSent)
2467
+ return;
2468
+ terminalDoneSent = true;
2469
+ await this.session.send({
2470
+ type: "chunk",
2471
+ thread_id: threadId,
2472
+ content: "",
2473
+ done: true
2474
+ });
2475
+ };
2476
+ log.info("Media generation", {
2477
+ threadId,
2478
+ mode,
2479
+ provider: providerId,
2480
+ model: modelId,
2481
+ promptLen: prompt.length
2482
+ });
2483
+ if (!isModelSelection(providerId, modelId)) {
2484
+ await this.session.send({
2485
+ type: "error",
2486
+ message: noMediaModelSelectedMessage(mode),
2487
+ thread_id: threadId
2488
+ });
2489
+ return;
2490
+ }
2491
+ if (!prompt) {
2492
+ await this.session.send({
2493
+ type: "error",
2494
+ message: "Please enter a prompt",
2495
+ thread_id: threadId
2496
+ });
2497
+ return;
2498
+ }
2499
+ if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
2500
+ return;
2501
+ let projectId = null;
2502
+ try {
2503
+ projectId = (await Project.findByThread(userId, threadId))?.id ?? null;
2504
+ if (!this.session.resolveProvider) {
2505
+ throw new Error("No provider resolver configured");
2506
+ }
2507
+ // Entity expansion and provider resolution are part of the guarded
2508
+ // setup. A missing entity or provider must follow the same error path as
2509
+ // a provider call and leave a durable assistant failure row.
2510
+ const { prompt: expandedPrompt, referenceImages } = await expandEntitiesForGeneration(prompt, this.deps.entityRefResolver(userId));
2511
+ const entityImageBytes = await this.deps.resolveEntityReferenceImages(userId, referenceImages);
2512
+ const provider = await this.session.resolveProvider(providerId, userId);
2513
+ provider.setMessageEmitter((msg) => {
2514
+ this.session.sendDetached(msg);
2515
+ });
2516
+ const { generate, seamAssetId, storeAsset: storeMediaAsset } = createGenerationRun({
2517
+ userId,
2518
+ providerId,
2519
+ modelId,
2520
+ provider,
2521
+ origin: {
2522
+ surface: "chat",
2523
+ thread_id: threadId || null,
2524
+ ...(isString(data.request_id)
2525
+ ? { request_id: data.request_id }
2526
+ : requestSeq !== undefined
2527
+ ? { request_id: `chat:${threadId}:${requestSeq}` }
2528
+ : {})
2529
+ },
2530
+ threadId,
2531
+ workflowId: workflowId ?? null,
2532
+ projectId,
2533
+ assetNamePrefix: mode,
2534
+ signal
2535
+ });
2536
+ if (mode === "image") {
2537
+ const variations = Math.max(1, Math.min(Number(mediaGeneration.variations ?? 1), 8));
2538
+ const width = isNumber(mediaGeneration.width)
2539
+ ? mediaGeneration.width
2540
+ : undefined;
2541
+ const height = isNumber(mediaGeneration.height)
2542
+ ? mediaGeneration.height
2543
+ : undefined;
2544
+ const imageModel = {
2545
+ id: modelId,
2546
+ name: modelId,
2547
+ provider: providerId
2548
+ };
2549
+ const params = {
2550
+ model: imageModel,
2551
+ prompt: expandedPrompt,
2552
+ width,
2553
+ height,
2554
+ signal
2555
+ };
2556
+ // Surface a progress chunk so the UI can show the request flight
2557
+ await this.session.send({
2558
+ type: "chunk",
2559
+ thread_id: threadId,
2560
+ content: "",
2561
+ content_type: "text",
2562
+ content_metadata: { media_generation: mediaGeneration },
2563
+ done: false
2564
+ });
2565
+ if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
2566
+ return;
2567
+ // A mentioned entity carries a reference image: the generation becomes
2568
+ // an edit against those images, mirroring the generate_media RPC.
2569
+ const generated = entityImageBytes.length > 0
2570
+ ? await generate("image_to_image", {
2571
+ prompt: expandedPrompt,
2572
+ target_width: width ?? null,
2573
+ target_height: height ?? null,
2574
+ num_images: variations,
2575
+ images: entityImageBytes
2576
+ }, {}, (abort) => provider.imageToImages(entityImageBytes, {
2577
+ model: imageModel,
2578
+ prompt: expandedPrompt,
2579
+ targetWidth: width ?? null,
2580
+ targetHeight: height ?? null,
2581
+ signal: abort
2582
+ }, variations))
2583
+ : await generate("text_to_image", {
2584
+ prompt: expandedPrompt,
2585
+ width,
2586
+ height,
2587
+ num_images: variations
2588
+ }, {}, (abort) => provider.textToImages({ ...params, signal: abort }, variations));
2589
+ if (cancelled())
2590
+ return;
2591
+ const imageContents = [];
2592
+ for (const [index, bytes] of generated.output.entries()) {
2593
+ // Per-variation: a cancel partway through must not keep persisting.
2594
+ if (cancelled())
2595
+ return;
2596
+ const mimeType = detectImageMime(bytes);
2597
+ const assetId = seamAssetId(generated, index) ??
2598
+ (await storeMediaAsset(bytes, mimeType, IMAGE_MIME_TO_EXT[mimeType] ?? "png"));
2599
+ imageContents.push({
2600
+ type: "image_url",
2601
+ image: { type: "image", asset_id: assetId, mimeType }
2602
+ });
2603
+ }
2604
+ const assistantMsgData = {
2605
+ type: "message",
2606
+ role: "assistant",
2607
+ content: imageContents,
2608
+ thread_id: threadId,
2609
+ workflow_id: workflowId,
2610
+ provider: providerId,
2611
+ model: modelId,
2612
+ media_generation: mediaGeneration
2613
+ };
2614
+ // Re-check: cancellation may have landed while the asset was persisting.
2615
+ if (cancelled())
2616
+ return;
2617
+ await this.saveMessageToDb(assistantMsgData);
2618
+ await this.session.send(assistantMsgData);
2619
+ await sendTerminalDone();
2620
+ return;
2621
+ }
2622
+ if (mode === "video") {
2623
+ const aspectRatio = isString(mediaGeneration.aspect_ratio)
2624
+ ? mediaGeneration.aspect_ratio
2625
+ : null;
2626
+ const resolution = isString(mediaGeneration.resolution)
2627
+ ? mediaGeneration.resolution
2628
+ : null;
2629
+ const duration = isNumber(mediaGeneration.duration)
2630
+ ? mediaGeneration.duration
2631
+ : null;
2632
+ const videoModel = {
2633
+ id: modelId,
2634
+ name: modelId,
2635
+ provider: providerId
2636
+ };
2637
+ await this.session.send({
2638
+ type: "chunk",
2639
+ thread_id: threadId,
2640
+ content: "",
2641
+ content_type: "text",
2642
+ content_metadata: { media_generation: mediaGeneration },
2643
+ done: false
2644
+ });
2645
+ // If the user referenced/attached an image, they want it animated:
2646
+ // route to image-to-video so the image actually reaches the provider.
2647
+ // Many "video" models (e.g. fal-ai/stable-video) in fact require an
2648
+ // image and reject a text-only request with an opaque 422.
2649
+ const sourceBytes = await this.deps.resolveSourceImageBytes(data, mediaGeneration, userId);
2650
+ const videoParams = {
2651
+ prompt: expandedPrompt,
2652
+ aspect_ratio: aspectRatio,
2653
+ resolution,
2654
+ duration_seconds: duration
2655
+ };
2656
+ const generated = sourceBytes && sourceBytes.length > 0
2657
+ ? await generate("image_to_video", { ...videoParams, images: [sourceBytes] }, { mime: "video/mp4" }, (abort) => provider.imageToVideo(sourceBytes, {
2658
+ model: videoModel,
2659
+ prompt: expandedPrompt,
2660
+ aspectRatio,
2661
+ resolution,
2662
+ durationSeconds: duration,
2663
+ numInferenceSteps: null,
2664
+ signal: abort
2665
+ }))
2666
+ : await generate("text_to_video", videoParams, { mime: "video/mp4" }, (abort) => provider.textToVideo({
2667
+ model: videoModel,
2668
+ prompt: expandedPrompt,
2669
+ aspectRatio,
2670
+ resolution,
2671
+ durationSeconds: duration,
2672
+ signal: abort
2673
+ }));
2674
+ if (cancelled())
2675
+ return;
2676
+ const assetId = seamAssetId(generated) ??
2677
+ (await storeMediaAsset(generated.output, "video/mp4", "mp4"));
2678
+ const assistantMsgData = {
2679
+ type: "message",
2680
+ role: "assistant",
2681
+ content: [
2682
+ {
2683
+ type: "video",
2684
+ video: {
2685
+ type: "video",
2686
+ asset_id: assetId,
2687
+ format: "mp4",
2688
+ duration: duration
2689
+ }
2690
+ }
2691
+ ],
2692
+ thread_id: threadId,
2693
+ workflow_id: workflowId,
2694
+ provider: providerId,
2695
+ model: modelId,
2696
+ media_generation: mediaGeneration
2697
+ };
2698
+ // Re-check: cancellation may have landed while the asset was persisting.
2699
+ if (cancelled())
2700
+ return;
2701
+ await this.saveMessageToDb(assistantMsgData);
2702
+ await this.session.send(assistantMsgData);
2703
+ await sendTerminalDone();
2704
+ return;
2705
+ }
2706
+ if (mode === "reference_to_video") {
2707
+ // The whole set of attachments conditions one clip, so every image
2708
+ // and video on the message reaches the provider — plus the reference
2709
+ // images of any `@`-mentioned entity, which is the same kind of input
2710
+ // arriving by a different route.
2711
+ const references = await this.deps.resolveReferenceMediaBytes(data, mediaGeneration, userId);
2712
+ const referenceImages = [...references.images, ...entityImageBytes];
2713
+ const referenceVideos = references.videos;
2714
+ if (referenceImages.length === 0 && referenceVideos.length === 0) {
2715
+ await this.session.send({
2716
+ type: "error",
2717
+ message: "Reference to video needs at least one reference — attach an image or a video first",
2718
+ thread_id: threadId
2719
+ });
2720
+ return;
2721
+ }
2722
+ const aspectRatio = isString(mediaGeneration.aspect_ratio)
2723
+ ? mediaGeneration.aspect_ratio
2724
+ : null;
2725
+ const resolution = isString(mediaGeneration.resolution)
2726
+ ? mediaGeneration.resolution
2727
+ : null;
2728
+ const duration = isNumber(mediaGeneration.duration)
2729
+ ? mediaGeneration.duration
2730
+ : null;
2731
+ // Only some models take it, and a model that does not rejects the
2732
+ // request rather than silently dropping the audio, so the flag is
2733
+ // forwarded only when the composer set it.
2734
+ const useReferenceVideoAudio = typeof mediaGeneration.use_reference_video_audio === "boolean"
2735
+ ? mediaGeneration.use_reference_video_audio
2736
+ : undefined;
2737
+ const r2vModel = {
2738
+ id: modelId,
2739
+ name: modelId,
2740
+ provider: providerId
2741
+ };
2742
+ await this.session.send({
2743
+ type: "chunk",
2744
+ thread_id: threadId,
2745
+ content: "",
2746
+ content_type: "text",
2747
+ content_metadata: { media_generation: mediaGeneration },
2748
+ done: false
2749
+ });
2750
+ const generated = await generate("reference_to_video", {
2751
+ prompt: expandedPrompt,
2752
+ aspect_ratio: aspectRatio,
2753
+ resolution,
2754
+ duration_seconds: duration,
2755
+ reference_images: referenceImages,
2756
+ reference_videos: referenceVideos,
2757
+ use_reference_video_audio: useReferenceVideoAudio
2758
+ }, { mime: "video/mp4" }, (abort) => provider.referenceToVideo({ images: referenceImages, videos: referenceVideos }, {
2759
+ model: r2vModel,
2760
+ prompt: expandedPrompt,
2761
+ aspectRatio,
2762
+ resolution,
2763
+ durationSeconds: duration,
2764
+ useReferenceVideoAudio,
2765
+ signal: abort
2766
+ }));
2767
+ if (cancelled())
2768
+ return;
2769
+ const assetId = seamAssetId(generated) ??
2770
+ (await storeMediaAsset(generated.output, "video/mp4", "mp4"));
2771
+ const assistantMsgData = {
2772
+ type: "message",
2773
+ role: "assistant",
2774
+ content: [
2775
+ {
2776
+ type: "video",
2777
+ video: {
2778
+ type: "video",
2779
+ asset_id: assetId,
2780
+ format: "mp4",
2781
+ duration
2782
+ }
2783
+ }
2784
+ ],
2785
+ thread_id: threadId,
2786
+ workflow_id: workflowId,
2787
+ provider: providerId,
2788
+ model: modelId,
2789
+ media_generation: mediaGeneration
2790
+ };
2791
+ // Re-check: cancellation may have landed while the asset was persisting.
2792
+ if (cancelled())
2793
+ return;
2794
+ await this.saveMessageToDb(assistantMsgData);
2795
+ await this.session.send(assistantMsgData);
2796
+ await sendTerminalDone();
2797
+ return;
2798
+ }
2799
+ if (mode === "audio" || mode === "music") {
2800
+ const voice = isString(mediaGeneration.voice)
2801
+ ? mediaGeneration.voice
2802
+ : undefined;
2803
+ const speed = isNumber(mediaGeneration.speed)
2804
+ ? mediaGeneration.speed
2805
+ : 1.0;
2806
+ const requestedFormatRaw = isString(mediaGeneration.audio_format)
2807
+ ? mediaGeneration.audio_format.toLowerCase()
2808
+ : null;
2809
+ const supportedFormats = new Set([
2810
+ "mp3",
2811
+ "wav",
2812
+ "pcm",
2813
+ "opus",
2814
+ "flac",
2815
+ "aac"
2816
+ ]);
2817
+ const requestedFormat = requestedFormatRaw && supportedFormats.has(requestedFormatRaw)
2818
+ ? requestedFormatRaw
2819
+ : null;
2820
+ await this.session.send({
2821
+ type: "chunk",
2822
+ thread_id: threadId,
2823
+ content: "",
2824
+ content_type: "text",
2825
+ content_metadata: { media_generation: mediaGeneration },
2826
+ done: false
2827
+ });
2828
+ const durationSeconds = isNumber(mediaGeneration.duration)
2829
+ ? mediaGeneration.duration
2830
+ : undefined;
2831
+ const audioGenerationId = randomUUID();
2832
+ let assetId;
2833
+ let audioMimeType;
2834
+ await generate(mode === "music" ? "text_to_music" : "text_to_speech", mode === "music"
2835
+ ? { prompt: expandedPrompt, duration_seconds: durationSeconds }
2836
+ : {
2837
+ text: expandedPrompt,
2838
+ voice,
2839
+ speed,
2840
+ audio_format: requestedFormat
2841
+ }, null, async () => {
2842
+ const speech = mode === "music"
2843
+ ? await generateMusicBytes(provider, {
2844
+ model: {
2845
+ id: modelId,
2846
+ name: modelId,
2847
+ provider: providerId
2848
+ },
2849
+ prompt: expandedPrompt,
2850
+ durationSeconds
2851
+ }, cancelled)
2852
+ : await generateSpeechBytes(provider, {
2853
+ text: expandedPrompt,
2854
+ model: modelId,
2855
+ voice,
2856
+ speed,
2857
+ audioFormat: requestedFormat
2858
+ }, cancelled);
2859
+ if (!speech)
2860
+ return null;
2861
+ assetId = await storeMediaAsset(speech.bytes, speech.mimeType, speech.ext);
2862
+ audioMimeType = speech.mimeType;
2863
+ return assetId;
2864
+ }, audioGenerationId);
2865
+ if (cancelled())
2866
+ return;
2867
+ if (!assetId || !audioMimeType) {
2868
+ throw new Error("Provider returned no audio data");
2869
+ }
2870
+ await linkGenerationAssets([audioGenerationId], [assetId]);
2871
+ const assistantMsgData = {
2872
+ type: "message",
2873
+ role: "assistant",
2874
+ content: [
2875
+ {
2876
+ type: "audio",
2877
+ audio: {
2878
+ type: "audio",
2879
+ asset_id: assetId,
2880
+ mimeType: audioMimeType
2881
+ }
2882
+ }
2883
+ ],
2884
+ thread_id: threadId,
2885
+ workflow_id: workflowId,
2886
+ provider: providerId,
2887
+ model: modelId,
2888
+ media_generation: mediaGeneration
2889
+ };
2890
+ // Re-check: cancellation may have landed while the asset was persisting.
2891
+ if (cancelled())
2892
+ return;
2893
+ await this.saveMessageToDb(assistantMsgData);
2894
+ await this.session.send(assistantMsgData);
2895
+ await sendTerminalDone();
2896
+ return;
2897
+ }
2898
+ if (mode === "image_edit" || mode === "image_to_video") {
2899
+ // Resolve the source image from either the message content (most
2900
+ // common path: user dropped an image into the composer) or from the
2901
+ // explicit `source_asset_id` echo on the media_generation payload.
2902
+ const sourceBytes = await this.deps.resolveSourceImageBytes(data, mediaGeneration, userId);
2903
+ // A zero-length buffer (e.g. a storage read that resolved but came
2904
+ // back empty) is truthy — without the length check it sails past this
2905
+ // guard, then silently drops out of `attachAssets`'s image field
2906
+ // downstream, so fal gets a request with no image at all and rejects
2907
+ // it with an opaque 422 instead of the friendly error below.
2908
+ if (!sourceBytes || sourceBytes.length === 0) {
2909
+ await this.session.send({
2910
+ type: "error",
2911
+ message: "A source image is required — drop or attach an image first",
2912
+ thread_id: threadId
2913
+ });
2914
+ return;
2915
+ }
2916
+ await this.session.send({
2917
+ type: "chunk",
2918
+ thread_id: threadId,
2919
+ content: "",
2920
+ content_type: "text",
2921
+ content_metadata: { media_generation: mediaGeneration },
2922
+ done: false
2923
+ });
2924
+ if (mode === "image_edit") {
2925
+ const variations = Math.max(1, Math.min(Number(mediaGeneration.variations ?? 1), 8));
2926
+ const targetWidth = isNumber(mediaGeneration.width)
2927
+ ? mediaGeneration.width
2928
+ : undefined;
2929
+ const targetHeight = isNumber(mediaGeneration.height)
2930
+ ? mediaGeneration.height
2931
+ : undefined;
2932
+ const strength = isNumber(mediaGeneration.strength)
2933
+ ? mediaGeneration.strength
2934
+ : undefined;
2935
+ const numInferenceSteps = isNumber(mediaGeneration.num_inference_steps)
2936
+ ? mediaGeneration.num_inference_steps
2937
+ : undefined;
2938
+ const editModel = {
2939
+ id: modelId,
2940
+ name: modelId,
2941
+ provider: providerId
2942
+ };
2943
+ const params = {
2944
+ model: editModel,
2945
+ prompt: expandedPrompt,
2946
+ targetWidth: targetWidth ?? null,
2947
+ targetHeight: targetHeight ?? null,
2948
+ strength: strength ?? null,
2949
+ numInferenceSteps: numInferenceSteps ?? null,
2950
+ signal
2951
+ };
2952
+ if (requestSeq !== undefined && requestSeq !== this.chatRequestSeq)
2953
+ return;
2954
+ const generated = await generate("image_to_image", {
2955
+ prompt: expandedPrompt,
2956
+ target_width: targetWidth ?? null,
2957
+ target_height: targetHeight ?? null,
2958
+ strength: strength ?? null,
2959
+ num_inference_steps: numInferenceSteps ?? null,
2960
+ num_images: variations,
2961
+ images: [sourceBytes, ...entityImageBytes]
2962
+ }, {}, (abort) => provider.imageToImages([sourceBytes, ...entityImageBytes], { ...params, signal: abort }, variations));
2963
+ if (cancelled())
2964
+ return;
2965
+ const imageContents = [];
2966
+ for (const [index, bytes] of generated.output.entries()) {
2967
+ // Per-variation: a cancel partway through must not keep persisting.
2968
+ if (cancelled())
2969
+ return;
2970
+ const mimeType = detectImageMime(bytes);
2971
+ const assetId = seamAssetId(generated, index) ??
2972
+ (await storeMediaAsset(bytes, mimeType, IMAGE_MIME_TO_EXT[mimeType] ?? "png"));
2973
+ imageContents.push({
2974
+ type: "image_url",
2975
+ image: {
2976
+ type: "image",
2977
+ asset_id: assetId,
2978
+ mimeType
2979
+ }
2980
+ });
2981
+ }
2982
+ const assistantMsgData = {
2983
+ type: "message",
2984
+ role: "assistant",
2985
+ content: imageContents,
2986
+ thread_id: threadId,
2987
+ workflow_id: workflowId,
2988
+ provider: providerId,
2989
+ model: modelId,
2990
+ media_generation: mediaGeneration
2991
+ };
2992
+ // Re-check: cancellation may have landed while the asset was persisting.
2993
+ if (cancelled())
2994
+ return;
2995
+ await this.saveMessageToDb(assistantMsgData);
2996
+ await this.session.send(assistantMsgData);
2997
+ await sendTerminalDone();
2998
+ return;
2999
+ }
3000
+ // image_to_video
3001
+ const aspectRatio = isString(mediaGeneration.aspect_ratio)
3002
+ ? mediaGeneration.aspect_ratio
3003
+ : null;
3004
+ const resolution = isString(mediaGeneration.resolution)
3005
+ ? mediaGeneration.resolution
3006
+ : null;
3007
+ const duration = isNumber(mediaGeneration.duration)
3008
+ ? mediaGeneration.duration
3009
+ : null;
3010
+ const numInferenceSteps = isNumber(mediaGeneration.num_inference_steps)
3011
+ ? mediaGeneration.num_inference_steps
3012
+ : null;
3013
+ const i2vModel = {
3014
+ id: modelId,
3015
+ name: modelId,
3016
+ provider: providerId
3017
+ };
3018
+ const params = {
3019
+ model: i2vModel,
3020
+ prompt: expandedPrompt,
3021
+ aspectRatio,
3022
+ resolution,
3023
+ durationSeconds: duration,
3024
+ numInferenceSteps,
3025
+ signal
3026
+ };
3027
+ const generated = await generate("image_to_video", {
3028
+ prompt: expandedPrompt,
3029
+ aspect_ratio: aspectRatio,
3030
+ resolution,
3031
+ duration_seconds: duration,
3032
+ num_inference_steps: numInferenceSteps,
3033
+ images: [sourceBytes]
3034
+ }, { mime: "video/mp4" }, (abort) => provider.imageToVideo(sourceBytes, { ...params, signal: abort }));
3035
+ if (cancelled())
3036
+ return;
3037
+ const assetId = seamAssetId(generated) ??
3038
+ (await storeMediaAsset(generated.output, "video/mp4", "mp4"));
3039
+ const assistantMsgData = {
3040
+ type: "message",
3041
+ role: "assistant",
3042
+ content: [
3043
+ {
3044
+ type: "video",
3045
+ video: {
3046
+ type: "video",
3047
+ asset_id: assetId,
3048
+ format: "mp4",
3049
+ duration
3050
+ }
3051
+ }
3052
+ ],
3053
+ thread_id: threadId,
3054
+ workflow_id: workflowId,
3055
+ provider: providerId,
3056
+ model: modelId,
3057
+ media_generation: mediaGeneration
3058
+ };
3059
+ // Re-check: cancellation may have landed while the asset was persisting.
3060
+ if (cancelled())
3061
+ return;
3062
+ await this.saveMessageToDb(assistantMsgData);
3063
+ await this.session.send(assistantMsgData);
3064
+ await sendTerminalDone();
3065
+ return;
3066
+ }
3067
+ // Modes not yet implemented on the backend — fall back to an informative
3068
+ // error so the client can render the unsupported state cleanly.
3069
+ await this.session.send({
3070
+ type: "error",
3071
+ message: `Media generation mode "${mode}" is not yet supported`,
3072
+ thread_id: threadId
3073
+ });
3074
+ }
3075
+ catch (err) {
3076
+ if (cancelled())
3077
+ return;
3078
+ const errMsg = err instanceof Error ? err.message : String(err);
3079
+ log.error("Media generation error", { threadId, mode, error: errMsg });
3080
+ const failure = providerFailureMessage(err, providerId, modelId);
3081
+ const failureMessage = `Generation failed: ${failure.message}`;
3082
+ const assistantMsgData = {
3083
+ type: "message",
3084
+ role: "assistant",
3085
+ content: failureMessage,
3086
+ thread_id: threadId,
3087
+ workflow_id: workflowId,
3088
+ provider: providerId,
3089
+ model: modelId,
3090
+ media_generation: mediaGeneration
3091
+ };
3092
+ try {
3093
+ await this.saveMessageToDb(assistantMsgData);
3094
+ }
3095
+ catch (persistError) {
3096
+ this.session.logError("media failure message save failed", persistError);
3097
+ }
3098
+ await this.session.send({
3099
+ type: "error",
3100
+ message: failureMessage,
3101
+ thread_id: threadId
3102
+ });
3103
+ await this.session.send(assistantMsgData);
3104
+ await sendTerminalDone();
3105
+ }
3106
+ }
3107
+ /**
3108
+ * Handle a chat message that targets a workflow.
3109
+ *
3110
+ * Mirrors Python's process_messages_for_workflow → WorkflowMessageProcessor/
3111
+ * ChatWorkflowMessageProcessor flow:
3112
+ * 1. Load workflow from DB
3113
+ * 2. Detect message input node names from graph
3114
+ * 3. Prepare params (serialized message + history)
3115
+ * 4. Run workflow via ExecutionSession (@nodetool-ai/execution)
3116
+ * 5. Stream events (job_update, node_update, output_update)
3117
+ * 6. Collect output_update results
3118
+ * 7. Send response message with typed content, then the terminal chunk
3119
+ */
3120
+ async handleWorkflowMessage(data, requestSeq, signal) {
3121
+ const threadId = isString(data.thread_id) ? data.thread_id : "";
3122
+ const workflowId = isString(data.workflow_id) ? data.workflow_id : null;
3123
+ const providerId = isString(data.provider)
3124
+ ? data.provider
3125
+ : this.deps.defaults.provider;
3126
+ const model = isString(data.model) ? data.model : this.deps.defaults.model;
3127
+ const userId = this.session.requireUserId();
3128
+ const chatProjectId = (await Project.findByThread(userId, threadId))?.id ?? undefined;
3129
+ const jobId = randomUUID();
3130
+ log.info("Workflow message", { threadId, workflowId, jobId });
3131
+ // Assigned once the run's abort listener is registered; released in the
3132
+ // finally so a completed workflow's listener can't cancel() a runner that
3133
+ // already finished when a later Stop/disconnect fires the same signal.
3134
+ let releaseAbortListener = null;
3135
+ try {
3136
+ if (!workflowId) {
3137
+ throw new Error("workflow_id is required for workflow processing");
3138
+ }
3139
+ // Load workflow from DB
3140
+ const workflow = await Workflow.find(userId, workflowId);
3141
+ if (!workflow) {
3142
+ throw new Error(`Workflow ${workflowId} not found`);
3143
+ }
3144
+ const rawGraph = workflow.graph;
3145
+ // Detect message input names from raw graph (reads node.data) — matches Python
3146
+ const { messageName, messagesName } = this.detectMessageInputNames(rawGraph);
3147
+ const graph = await this.deps.hydrateGraph(rawGraph);
3148
+ if (this.deps.beforeRunJob) {
3149
+ await this.deps.beforeRunJob(graph);
3150
+ }
3151
+ const messageInputName = (isString(data.workflow_message_input_name)
3152
+ ? data.workflow_message_input_name
3153
+ : null) ??
3154
+ messageName ??
3155
+ "message";
3156
+ const messagesInputName = (isString(data.workflow_messages_input_name)
3157
+ ? data.workflow_messages_input_name
3158
+ : null) ??
3159
+ messagesName ??
3160
+ "messages";
3161
+ // Build chat history for params — matches Python
3162
+ const [dbMessages] = await Message.paginate(threadId, { limit: 1000 });
3163
+ const chatHistorySerialized = dbMessages.map((m) => ({
3164
+ role: m.role,
3165
+ content: m.content,
3166
+ created_at: m.created_at,
3167
+ thread_id: m.thread_id
3168
+ }));
3169
+ // Serialize current message
3170
+ const currentMessage = {
3171
+ role: isString(data.role) ? data.role : "user",
3172
+ content: data.content,
3173
+ thread_id: threadId,
3174
+ workflow_id: workflowId,
3175
+ provider: providerId,
3176
+ model
3177
+ };
3178
+ // Prepare params — matches Python's WorkflowMessageProcessor
3179
+ const params = {
3180
+ [messageInputName]: currentMessage,
3181
+ [messagesInputName]: [...chatHistorySerialized, currentMessage]
3182
+ };
3183
+ if (isObjectLike(data.params)) {
3184
+ Object.assign(params, data.params);
3185
+ }
3186
+ // If chat workflow, add legacy params — matches Python's ChatWorkflowMessageProcessor
3187
+ if (workflow.run_mode === "chat") {
3188
+ const legacyChatInput = chatHistorySerialized.map((m) => ({
3189
+ role: m.role,
3190
+ content: extractTextContent(m.content),
3191
+ created_at: m.created_at
3192
+ }));
3193
+ params["chat_input"] = legacyChatInput;
3194
+ if (messagesInputName !== "messages") {
3195
+ params["messages"] = legacyChatInput;
3196
+ }
3197
+ }
3198
+ // Create processing context
3199
+ const workspace = this.session.workspaceResolver
3200
+ ? await this.session.workspaceResolver(workflowId, userId)
3201
+ : null;
3202
+ const context = createRuntimeContext({
3203
+ jobId,
3204
+ workflowId,
3205
+ projectId: chatProjectId,
3206
+ userId,
3207
+ workspace,
3208
+ assetOutputMode: this.session.mode === "text" ? "data_uri" : "temp_url"
3209
+ });
3210
+ // Expose executor/node-type resolution for sub-workflow nodes
3211
+ context.setResolveExecutor((node) => this.session.resolveExecutor(node));
3212
+ if (this.session.resolveNodeType) {
3213
+ const resolverObj = isFunctionValue(this.session.resolveNodeType)
3214
+ ? { resolveNodeType: this.session.resolveNodeType }
3215
+ : this.session.resolveNodeType;
3216
+ context.setResolveNodeType((nodeType) => resolverObj.resolveNodeType(nodeType));
3217
+ }
3218
+ // Create and run workflow (A5: via the ExecutionSession facade — see
3219
+ // the identical note in `startJobInner`).
3220
+ const session = await ExecutionSession.create({
3221
+ graph: toRawGraphInput(graph),
3222
+ resolveExecutor: (node) => this.session.resolveExecutor(node),
3223
+ bridgeFactory: async () => null,
3224
+ jobLifecycleBridge: this.session.pythonBridge ?? null,
3225
+ jobId,
3226
+ workflowId,
3227
+ context,
3228
+ params,
3229
+ validateNode: this.session.validateNode
3230
+ });
3231
+ const active = {
3232
+ jobId,
3233
+ workflowId,
3234
+ context,
3235
+ session,
3236
+ graph,
3237
+ finished: false,
3238
+ status: "running",
3239
+ requireTerminalResult: false,
3240
+ executionOptions: { ...DEFAULT_RUN_JOB_EXECUTION_OPTIONS },
3241
+ timings: {
3242
+ acceptedAt: performance.now(),
3243
+ queueMs: 0,
3244
+ graphLoadedMs: 0,
3245
+ graphHydratedMs: 0,
3246
+ preRunMs: 0,
3247
+ persistenceMs: 0,
3248
+ kernelStartedAt: performance.now()
3249
+ }
3250
+ };
3251
+ this.deps.jobs.registerJob(jobId, active);
3252
+ // Persist job to DB (best-effort)
3253
+ try {
3254
+ await Job.create({
3255
+ id: jobId,
3256
+ workflow_id: workflowId,
3257
+ user_id: userId,
3258
+ status: "running",
3259
+ params,
3260
+ graph
3261
+ });
3262
+ }
3263
+ catch (error) {
3264
+ this.session.logError("workflow job persistence failed", error);
3265
+ }
3266
+ // The run already started inside `ExecutionSession.create()` above.
3267
+ const executePromise = session.result;
3268
+ // Stream events, collect output_update results
3269
+ const result = {};
3270
+ await this.session.send({
3271
+ type: "job_update",
3272
+ status: "running",
3273
+ job_id: jobId,
3274
+ workflow_id: workflowId
3275
+ });
3276
+ let finalOutputs = {};
3277
+ const executionSettled = executePromise
3278
+ .then((r) => {
3279
+ active.status = r.status;
3280
+ active.error = r.error;
3281
+ finalOutputs = r.outputs ?? {};
3282
+ })
3283
+ .catch((err) => {
3284
+ active.status = "failed";
3285
+ active.error = err instanceof Error ? err.message : String(err);
3286
+ })
3287
+ .finally(() => {
3288
+ active.finished = true;
3289
+ });
3290
+ const waitForActivity = createRelayActivityWaiter(active.context, executionSettled, signal);
3291
+ const nodeTypes = new Map();
3292
+ const graphNodes = graph.nodes ?? [];
3293
+ for (const n of graphNodes) {
3294
+ if (n.id) {
3295
+ nodeTypes.set(String(n.id), isString(n.type) ? n.type : "");
3296
+ }
3297
+ }
3298
+ // A chat Stop / superseding message bumps chatRequestSeq. Unlike the
3299
+ // other chat handlers this one owns a workflow runner, so cancel it and
3300
+ // stop streaming when our turn is no longer current — otherwise the run
3301
+ // completes and delivers an assistant message after the user stopped.
3302
+ const superseded = () => requestSeq !== undefined && requestSeq !== this.chatRequestSeq;
3303
+ // Cancel the moment Stop fires rather than waiting for the streaming loop
3304
+ // below to come back around — the run may be parked inside a long node.
3305
+ const onAbort = () => {
3306
+ try {
3307
+ active.session.cancel();
3308
+ }
3309
+ catch {
3310
+ // best-effort cancel
3311
+ }
3312
+ };
3313
+ if (signal?.aborted) {
3314
+ onAbort();
3315
+ }
3316
+ else if (signal) {
3317
+ signal.addEventListener("abort", onAbort, { once: true });
3318
+ releaseAbortListener = () => signal.removeEventListener("abort", onAbort);
3319
+ }
3320
+ while (!active.finished || active.context.hasMessages()) {
3321
+ if (superseded()) {
3322
+ try {
3323
+ active.session.cancel();
3324
+ }
3325
+ catch {
3326
+ // best-effort cancel
3327
+ }
3328
+ active.status = "cancelled";
3329
+ try {
3330
+ const job = await Job.get(jobId);
3331
+ if (job && job.status !== "cancelled") {
3332
+ job.markCancelled();
3333
+ await job.save();
3334
+ }
3335
+ }
3336
+ catch (err) {
3337
+ this.session.logError("workflow chat cancellation persistence failed", err);
3338
+ }
3339
+ this.deps.jobs.dropJob(jobId);
3340
+ return;
3341
+ }
3342
+ while (active.context.hasMessages()) {
3343
+ const msg = active.context.popMessage();
3344
+ if (!msg)
3345
+ break;
3346
+ const outbound = { ...msg };
3347
+ outbound.job_id ??= jobId;
3348
+ outbound.workflow_id ??= workflowId;
3349
+ // Every message, not just node updates: a `prediction` is where
3350
+ // ledger-priced generation spend (Replicate, Gemini, OpenAI, …)
3351
+ // reports itself.
3352
+ this.deps.jobs.handleNodeProviderCost(active, outbound);
3353
+ if (outbound.type === "node_update" ||
3354
+ outbound.type === "output_update") {
3355
+ const nodeId = String(outbound.node_id ?? "");
3356
+ const nodeType = nodeTypes.get(nodeId) ?? "";
3357
+ // Capture output_update values for the response message
3358
+ if (outbound.type === "output_update") {
3359
+ if (nodeType.includes("Output")) {
3360
+ const nodeName = isString(outbound.node_name)
3361
+ ? outbound.node_name
3362
+ : nodeType;
3363
+ result[nodeName] = outbound.value;
3364
+ }
3365
+ else {
3366
+ continue; // Skip non-output node output_updates
3367
+ }
3368
+ }
3369
+ }
3370
+ await this.session.send(outbound);
3371
+ }
3372
+ if (!active.finished) {
3373
+ await waitForActivity();
3374
+ }
3375
+ }
3376
+ // Collect any outputs from the runner result — only Output-type nodes
3377
+ // The kernel considers all leaf nodes as "output nodes", but for the
3378
+ // response message we only want nodes whose type includes "Output"
3379
+ // (matching Python's WorkflowMessageProcessor behavior).
3380
+ for (const [nodeType, values] of Object.entries(finalOutputs)) {
3381
+ if (!nodeType.includes("Output"))
3382
+ continue;
3383
+ if (!result[nodeType] && Array.isArray(values) && values.length > 0) {
3384
+ result[nodeType] = values.length === 1 ? values[0] : values;
3385
+ }
3386
+ }
3387
+ // Send terminal job_update if not already sent
3388
+ await this.session.send({
3389
+ type: "job_update",
3390
+ status: active.status,
3391
+ job_id: jobId,
3392
+ workflow_id: workflowId,
3393
+ error: active.error,
3394
+ result: { outputs: finalOutputs }
3395
+ });
3396
+ // Persist final job status
3397
+ try {
3398
+ const job = (await Job.get(jobId));
3399
+ // Don't overwrite a cancelled row (DB-only tRPC cancel) when the
3400
+ // in-flight run finishes — keep the cancellation authoritative.
3401
+ if (job) {
3402
+ if (job.status !== "cancelled") {
3403
+ if (active.status === "completed")
3404
+ job.markCompleted();
3405
+ else if (active.status === "failed")
3406
+ job.markFailed(active.error ?? "Unknown error");
3407
+ else if (active.status === "cancelled")
3408
+ job.markCancelled();
3409
+ }
3410
+ job.cost = this.deps.jobs.runMeasuredCost(active);
3411
+ await job.save();
3412
+ }
3413
+ }
3414
+ catch (error) {
3415
+ this.session.logError("workflow job persistence (final) failed", error);
3416
+ }
3417
+ // Create response message from workflow outputs — matches Python's _create_response_message
3418
+ const responseContent = createWorkflowResponseContent(result);
3419
+ const responseMsg = {
3420
+ type: "message",
3421
+ role: "assistant",
3422
+ content: responseContent,
3423
+ thread_id: threadId,
3424
+ workflow_id: workflowId,
3425
+ provider: providerId,
3426
+ model,
3427
+ job_id: jobId
3428
+ };
3429
+ await this.saveMessageToDb(responseMsg);
3430
+ await this.session.send(responseMsg);
3431
+ // Signal completion — done chunk with job_id + workflow_id
3432
+ await this.session.send({
3433
+ type: "chunk",
3434
+ content: "",
3435
+ done: true,
3436
+ job_id: jobId,
3437
+ workflow_id: workflowId,
3438
+ thread_id: threadId
3439
+ });
3440
+ log.debug("Workflow message complete", { threadId, workflowId, jobId });
3441
+ }
3442
+ catch (err) {
3443
+ const errMsg = err instanceof Error ? err.message : String(err);
3444
+ log.error("Workflow message error", {
3445
+ threadId,
3446
+ workflowId,
3447
+ error: errMsg
3448
+ });
3449
+ await this.session.send({
3450
+ type: "error",
3451
+ message: `Error processing workflow: ${errMsg}`,
3452
+ job_id: jobId,
3453
+ workflow_id: workflowId,
3454
+ thread_id: threadId
3455
+ });
3456
+ // Send done chunk even on error — matches Python
3457
+ await this.session.send({
3458
+ type: "chunk",
3459
+ content: "",
3460
+ done: true,
3461
+ job_id: jobId,
3462
+ workflow_id: workflowId,
3463
+ thread_id: threadId
3464
+ });
3465
+ }
3466
+ finally {
3467
+ releaseAbortListener?.();
3468
+ // Always release the concurrency slot and drain the queue, even if
3469
+ // streaming/persist/sendMessage threw above. Otherwise a mid-stream
3470
+ // socket-write failure would orphan the ActiveJob and permanently shrink
3471
+ // the MAX_CONCURRENT_JOBS cap (run_job then queues forever).
3472
+ this.deps.jobs.releaseJob(jobId);
3473
+ }
3474
+ }
3475
+ /**
3476
+ * If `event` is a tool_call_update, also emit a synthetic assistant message
3477
+ * whose `tool_calls` array contains this call. The chat UI renders a
3478
+ * persistent ToolCallCard from messages with tool_calls; tool_call_update
3479
+ * by itself only drives transient "now running" state. We skip events that
3480
+ * already carry `agent_execution_id` because those are routed to
3481
+ * ExecutionTree via the agent_execution path.
3482
+ */
3483
+ async emitSyntheticToolCallCard(event) {
3484
+ if (event["type"] !== "tool_call_update")
3485
+ return;
3486
+ const toolCallId = event["tool_call_id"];
3487
+ const name = event["name"];
3488
+ if (!isString(toolCallId) || !isString(name))
3489
+ return;
3490
+ if (!toolCallId || !name)
3491
+ return;
3492
+ const args = isObjectLike(event["args"])
3493
+ ? event["args"]
3494
+ : {};
3495
+ const message = isString(event["message"]) ? event["message"] : null;
3496
+ await this.session.send({
3497
+ type: "message",
3498
+ role: "assistant",
3499
+ content: null,
3500
+ tool_calls: [
3501
+ {
3502
+ id: toolCallId,
3503
+ name,
3504
+ args,
3505
+ message,
3506
+ result: null
3507
+ }
3508
+ ],
3509
+ parent_tool_call_id: event["parent_tool_call_id"] ?? null,
3510
+ subtask_depth: event["subtask_depth"] ?? null,
3511
+ thread_id: event["thread_id"] ?? null,
3512
+ workflow_id: event["workflow_id"] ?? null
3513
+ });
3514
+ }
3515
+ }
3516
+ //# sourceMappingURL=chat-turn.js.map