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

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