@nodetool-ai/runtime 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 (713) hide show
  1. package/README.md +29 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/agent-memory.d.ts +118 -0
  4. package/dist/agent-memory.d.ts.map +1 -0
  5. package/dist/agent-memory.js +186 -0
  6. package/dist/agent-memory.js.map +1 -0
  7. package/dist/async-local-storage.d.ts +20 -0
  8. package/dist/async-local-storage.d.ts.map +1 -0
  9. package/dist/async-local-storage.js +28 -0
  10. package/dist/async-local-storage.js.map +1 -0
  11. package/dist/blender-executor.d.ts +93 -0
  12. package/dist/blender-executor.d.ts.map +1 -0
  13. package/dist/blender-executor.js +294 -0
  14. package/dist/blender-executor.js.map +1 -0
  15. package/dist/comfy-executor.d.ts +23 -1
  16. package/dist/comfy-executor.d.ts.map +1 -1
  17. package/dist/comfy-executor.js +234 -55
  18. package/dist/comfy-executor.js.map +1 -1
  19. package/dist/context-packer.d.ts +1 -1
  20. package/dist/context-packer.d.ts.map +1 -1
  21. package/dist/context-packer.js +23 -25
  22. package/dist/context-packer.js.map +1 -1
  23. package/dist/context.d.ts +982 -127
  24. package/dist/context.d.ts.map +1 -1
  25. package/dist/context.js +2649 -530
  26. package/dist/context.js.map +1 -1
  27. package/dist/cost-reconciler.d.ts +32 -0
  28. package/dist/cost-reconciler.d.ts.map +1 -0
  29. package/dist/cost-reconciler.js +21 -0
  30. package/dist/cost-reconciler.js.map +1 -0
  31. package/dist/entities.d.ts +33 -0
  32. package/dist/entities.d.ts.map +1 -0
  33. package/dist/entities.js +43 -0
  34. package/dist/entities.js.map +1 -0
  35. package/dist/external-media-fetch.d.ts +29 -0
  36. package/dist/external-media-fetch.d.ts.map +1 -0
  37. package/dist/external-media-fetch.js +38 -0
  38. package/dist/external-media-fetch.js.map +1 -0
  39. package/dist/generation-receipt.d.ts +86 -0
  40. package/dist/generation-receipt.d.ts.map +1 -0
  41. package/dist/generation-receipt.js +137 -0
  42. package/dist/generation-receipt.js.map +1 -0
  43. package/dist/generation-registry.d.ts +75 -0
  44. package/dist/generation-registry.d.ts.map +1 -0
  45. package/dist/generation-registry.js +144 -0
  46. package/dist/generation-registry.js.map +1 -0
  47. package/dist/google/client.d.ts +196 -0
  48. package/dist/google/client.d.ts.map +1 -0
  49. package/dist/google/client.js +396 -0
  50. package/dist/google/client.js.map +1 -0
  51. package/dist/google/index.d.ts +3 -0
  52. package/dist/google/index.d.ts.map +1 -0
  53. package/dist/google/index.js +3 -0
  54. package/dist/google/index.js.map +1 -0
  55. package/dist/google/token.d.ts +15 -0
  56. package/dist/google/token.d.ts.map +1 -0
  57. package/dist/google/token.js +24 -0
  58. package/dist/google/token.js.map +1 -0
  59. package/dist/host-binaries.d.ts +106 -0
  60. package/dist/host-binaries.d.ts.map +1 -0
  61. package/dist/host-binaries.js +352 -0
  62. package/dist/host-binaries.js.map +1 -0
  63. package/dist/image-codec.d.ts +76 -0
  64. package/dist/image-codec.d.ts.map +1 -0
  65. package/dist/image-codec.js +293 -0
  66. package/dist/image-codec.js.map +1 -0
  67. package/dist/image-model-tasks.d.ts +15 -0
  68. package/dist/image-model-tasks.d.ts.map +1 -0
  69. package/dist/image-model-tasks.js +24 -0
  70. package/dist/image-model-tasks.js.map +1 -0
  71. package/dist/index.d.ts +67 -6
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +51 -4
  74. package/dist/index.js.map +1 -1
  75. package/dist/invocation-account.d.ts +43 -0
  76. package/dist/invocation-account.d.ts.map +1 -0
  77. package/dist/invocation-account.js +60 -0
  78. package/dist/invocation-account.js.map +1 -0
  79. package/dist/media-ref-bytes.d.ts +35 -0
  80. package/dist/media-ref-bytes.d.ts.map +1 -0
  81. package/dist/media-ref-bytes.js +191 -0
  82. package/dist/media-ref-bytes.js.map +1 -0
  83. package/dist/node-executor.d.ts +119 -2
  84. package/dist/node-executor.d.ts.map +1 -1
  85. package/dist/permission-gate.d.ts +152 -0
  86. package/dist/permission-gate.d.ts.map +1 -0
  87. package/dist/permission-gate.js +87 -0
  88. package/dist/permission-gate.js.map +1 -0
  89. package/dist/prefixed-storage-adapter.d.ts +33 -0
  90. package/dist/prefixed-storage-adapter.d.ts.map +1 -0
  91. package/dist/prefixed-storage-adapter.js +70 -0
  92. package/dist/prefixed-storage-adapter.js.map +1 -0
  93. package/dist/prompt-asset-refs.d.ts +222 -0
  94. package/dist/prompt-asset-refs.d.ts.map +1 -0
  95. package/dist/prompt-asset-refs.js +751 -0
  96. package/dist/prompt-asset-refs.js.map +1 -0
  97. package/dist/provider-cache.d.ts +3 -0
  98. package/dist/provider-cache.d.ts.map +1 -0
  99. package/dist/provider-cache.js +8 -0
  100. package/dist/provider-cache.js.map +1 -0
  101. package/dist/providers/aki-provider.d.ts +35 -10
  102. package/dist/providers/aki-provider.d.ts.map +1 -1
  103. package/dist/providers/aki-provider.js +90 -77
  104. package/dist/providers/aki-provider.js.map +1 -1
  105. package/dist/providers/alibaba-provider.d.ts +39 -0
  106. package/dist/providers/alibaba-provider.d.ts.map +1 -0
  107. package/dist/providers/alibaba-provider.js +86 -0
  108. package/dist/providers/alibaba-provider.js.map +1 -0
  109. package/dist/providers/anthropic-provider.d.ts +52 -3
  110. package/dist/providers/anthropic-provider.d.ts.map +1 -1
  111. package/dist/providers/anthropic-provider.js +1020 -338
  112. package/dist/providers/anthropic-provider.js.map +1 -1
  113. package/dist/providers/atlascloud-provider.d.ts +103 -0
  114. package/dist/providers/atlascloud-provider.d.ts.map +1 -0
  115. package/dist/providers/atlascloud-provider.js +1031 -0
  116. package/dist/providers/atlascloud-provider.js.map +1 -0
  117. package/dist/providers/atlascloud-transport.d.ts +100 -0
  118. package/dist/providers/atlascloud-transport.d.ts.map +1 -0
  119. package/dist/providers/atlascloud-transport.js +364 -0
  120. package/dist/providers/atlascloud-transport.js.map +1 -0
  121. package/dist/providers/atlascloud-webhook-registry.d.ts +35 -0
  122. package/dist/providers/atlascloud-webhook-registry.d.ts.map +1 -0
  123. package/dist/providers/atlascloud-webhook-registry.js +104 -0
  124. package/dist/providers/atlascloud-webhook-registry.js.map +1 -0
  125. package/dist/providers/audio-mime.d.ts +11 -0
  126. package/dist/providers/audio-mime.d.ts.map +1 -0
  127. package/dist/providers/audio-mime.js +46 -0
  128. package/dist/providers/audio-mime.js.map +1 -0
  129. package/dist/providers/base-provider.d.ts +501 -32
  130. package/dist/providers/base-provider.d.ts.map +1 -1
  131. package/dist/providers/base-provider.js +1280 -87
  132. package/dist/providers/base-provider.js.map +1 -1
  133. package/dist/providers/cassette-provider.d.ts +217 -0
  134. package/dist/providers/cassette-provider.d.ts.map +1 -0
  135. package/dist/providers/cassette-provider.js +502 -0
  136. package/dist/providers/cassette-provider.js.map +1 -0
  137. package/dist/providers/cerebras-provider.d.ts +6 -12
  138. package/dist/providers/cerebras-provider.d.ts.map +1 -1
  139. package/dist/providers/cerebras-provider.js +8 -33
  140. package/dist/providers/cerebras-provider.js.map +1 -1
  141. package/dist/providers/claude-agent-provider.d.ts +107 -74
  142. package/dist/providers/claude-agent-provider.d.ts.map +1 -1
  143. package/dist/providers/claude-agent-provider.js +1279 -517
  144. package/dist/providers/claude-agent-provider.js.map +1 -1
  145. package/dist/providers/codex-provider.d.ts +84 -0
  146. package/dist/providers/codex-provider.d.ts.map +1 -0
  147. package/dist/providers/codex-provider.js +505 -0
  148. package/dist/providers/codex-provider.js.map +1 -0
  149. package/dist/providers/cohere-provider.d.ts +7 -3
  150. package/dist/providers/cohere-provider.d.ts.map +1 -1
  151. package/dist/providers/cohere-provider.js +14 -2
  152. package/dist/providers/cohere-provider.js.map +1 -1
  153. package/dist/providers/comfy-cloud-provider.d.ts +30 -0
  154. package/dist/providers/comfy-cloud-provider.d.ts.map +1 -0
  155. package/dist/providers/comfy-cloud-provider.js +42 -0
  156. package/dist/providers/comfy-cloud-provider.js.map +1 -0
  157. package/dist/providers/content-filter.d.ts +46 -0
  158. package/dist/providers/content-filter.d.ts.map +1 -0
  159. package/dist/providers/content-filter.js +92 -0
  160. package/dist/providers/content-filter.js.map +1 -0
  161. package/dist/providers/context-exceeded.d.ts +37 -0
  162. package/dist/providers/context-exceeded.d.ts.map +1 -0
  163. package/dist/providers/context-exceeded.js +114 -0
  164. package/dist/providers/context-exceeded.js.map +1 -0
  165. package/dist/providers/context-window.d.ts +44 -0
  166. package/dist/providers/context-window.d.ts.map +1 -0
  167. package/dist/providers/context-window.js +145 -0
  168. package/dist/providers/context-window.js.map +1 -0
  169. package/dist/providers/contract/index.d.ts +4 -0
  170. package/dist/providers/contract/index.d.ts.map +1 -0
  171. package/dist/providers/contract/index.js +4 -0
  172. package/dist/providers/contract/index.js.map +1 -0
  173. package/dist/providers/contract/probe-manifest.d.ts +75 -0
  174. package/dist/providers/contract/probe-manifest.d.ts.map +1 -0
  175. package/dist/providers/contract/probe-manifest.js +379 -0
  176. package/dist/providers/contract/probe-manifest.js.map +1 -0
  177. package/dist/providers/contract/probe-runner.d.ts +57 -0
  178. package/dist/providers/contract/probe-runner.d.ts.map +1 -0
  179. package/dist/providers/contract/probe-runner.js +180 -0
  180. package/dist/providers/contract/probe-runner.js.map +1 -0
  181. package/dist/providers/contract/redact.d.ts +29 -0
  182. package/dist/providers/contract/redact.d.ts.map +1 -0
  183. package/dist/providers/contract/redact.js +113 -0
  184. package/dist/providers/contract/redact.js.map +1 -0
  185. package/dist/providers/core-tools.d.ts +75 -0
  186. package/dist/providers/core-tools.d.ts.map +1 -0
  187. package/dist/providers/core-tools.js +128 -0
  188. package/dist/providers/core-tools.js.map +1 -0
  189. package/dist/providers/cost-calculator.d.ts +69 -50
  190. package/dist/providers/cost-calculator.d.ts.map +1 -1
  191. package/dist/providers/cost-calculator.js +207 -397
  192. package/dist/providers/cost-calculator.js.map +1 -1
  193. package/dist/providers/credential-check.d.ts +45 -0
  194. package/dist/providers/credential-check.d.ts.map +1 -0
  195. package/dist/providers/credential-check.js +164 -0
  196. package/dist/providers/credential-check.js.map +1 -0
  197. package/dist/providers/custom-model-kinds.d.ts +33 -0
  198. package/dist/providers/custom-model-kinds.d.ts.map +1 -0
  199. package/dist/providers/custom-model-kinds.js +96 -0
  200. package/dist/providers/custom-model-kinds.js.map +1 -0
  201. package/dist/providers/custom-openai-provider.d.ts +83 -0
  202. package/dist/providers/custom-openai-provider.d.ts.map +1 -0
  203. package/dist/providers/custom-openai-provider.js +190 -0
  204. package/dist/providers/custom-openai-provider.js.map +1 -0
  205. package/dist/providers/custom-provider-registry.d.ts +19 -0
  206. package/dist/providers/custom-provider-registry.d.ts.map +1 -0
  207. package/dist/providers/custom-provider-registry.js +52 -0
  208. package/dist/providers/custom-provider-registry.js.map +1 -0
  209. package/dist/providers/deepseek-provider.d.ts +8 -14
  210. package/dist/providers/deepseek-provider.d.ts.map +1 -1
  211. package/dist/providers/deepseek-provider.js +6 -35
  212. package/dist/providers/deepseek-provider.js.map +1 -1
  213. package/dist/providers/defaults.d.ts +12 -0
  214. package/dist/providers/defaults.d.ts.map +1 -0
  215. package/dist/providers/defaults.js +12 -0
  216. package/dist/providers/defaults.js.map +1 -0
  217. package/dist/providers/dreamina-models.d.ts +5 -0
  218. package/dist/providers/dreamina-models.d.ts.map +1 -0
  219. package/dist/providers/dreamina-models.js +57 -0
  220. package/dist/providers/dreamina-models.js.map +1 -0
  221. package/dist/providers/dreamina-music.d.ts +52 -0
  222. package/dist/providers/dreamina-music.d.ts.map +1 -0
  223. package/dist/providers/dreamina-music.js +115 -0
  224. package/dist/providers/dreamina-music.js.map +1 -0
  225. package/dist/providers/dreamina-provider.d.ts +62 -0
  226. package/dist/providers/dreamina-provider.d.ts.map +1 -0
  227. package/dist/providers/dreamina-provider.js +790 -0
  228. package/dist/providers/dreamina-provider.js.map +1 -0
  229. package/dist/providers/elevenlabs-provider.d.ts +50 -0
  230. package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
  231. package/dist/providers/elevenlabs-provider.js +147 -0
  232. package/dist/providers/elevenlabs-provider.js.map +1 -0
  233. package/dist/providers/entity-references.d.ts +31 -0
  234. package/dist/providers/entity-references.d.ts.map +1 -0
  235. package/dist/providers/entity-references.js +113 -0
  236. package/dist/providers/entity-references.js.map +1 -0
  237. package/dist/providers/evolink-provider.d.ts +40 -0
  238. package/dist/providers/evolink-provider.d.ts.map +1 -0
  239. package/dist/providers/evolink-provider.js +312 -0
  240. package/dist/providers/evolink-provider.js.map +1 -0
  241. package/dist/providers/fake-provider.d.ts +74 -2
  242. package/dist/providers/fake-provider.d.ts.map +1 -1
  243. package/dist/providers/fake-provider.js +213 -1
  244. package/dist/providers/fake-provider.js.map +1 -1
  245. package/dist/providers/fal-generations.d.ts +106 -0
  246. package/dist/providers/fal-generations.d.ts.map +1 -0
  247. package/dist/providers/fal-generations.js +286 -0
  248. package/dist/providers/fal-generations.js.map +1 -0
  249. package/dist/providers/fal-provider.d.ts +173 -8
  250. package/dist/providers/fal-provider.d.ts.map +1 -1
  251. package/dist/providers/fal-provider.js +1396 -64
  252. package/dist/providers/fal-provider.js.map +1 -1
  253. package/dist/providers/fal-queue.d.ts +47 -0
  254. package/dist/providers/fal-queue.d.ts.map +1 -0
  255. package/dist/providers/fal-queue.js +231 -0
  256. package/dist/providers/fal-queue.js.map +1 -0
  257. package/dist/providers/gemini-provider.d.ts +150 -9
  258. package/dist/providers/gemini-provider.d.ts.map +1 -1
  259. package/dist/providers/gemini-provider.js +1515 -316
  260. package/dist/providers/gemini-provider.js.map +1 -1
  261. package/dist/providers/gmi-provider.d.ts +21 -0
  262. package/dist/providers/gmi-provider.d.ts.map +1 -0
  263. package/dist/providers/gmi-provider.js +34 -0
  264. package/dist/providers/gmi-provider.js.map +1 -0
  265. package/dist/providers/gpt-image-size.d.ts +3 -0
  266. package/dist/providers/gpt-image-size.d.ts.map +1 -0
  267. package/dist/providers/gpt-image-size.js +37 -0
  268. package/dist/providers/gpt-image-size.js.map +1 -0
  269. package/dist/providers/groq-provider.d.ts +9 -12
  270. package/dist/providers/groq-provider.d.ts.map +1 -1
  271. package/dist/providers/groq-provider.js +19 -33
  272. package/dist/providers/groq-provider.js.map +1 -1
  273. package/dist/providers/groq-request.d.ts +38 -0
  274. package/dist/providers/groq-request.d.ts.map +1 -0
  275. package/dist/providers/groq-request.js +138 -0
  276. package/dist/providers/groq-request.js.map +1 -0
  277. package/dist/providers/higgsfield-provider.d.ts +31 -0
  278. package/dist/providers/higgsfield-provider.d.ts.map +1 -0
  279. package/dist/providers/higgsfield-provider.js +143 -0
  280. package/dist/providers/higgsfield-provider.js.map +1 -0
  281. package/dist/providers/higgsfield-transport.d.ts +106 -0
  282. package/dist/providers/higgsfield-transport.d.ts.map +1 -0
  283. package/dist/providers/higgsfield-transport.js +182 -0
  284. package/dist/providers/higgsfield-transport.js.map +1 -0
  285. package/dist/providers/http-transport.d.ts +74 -0
  286. package/dist/providers/http-transport.d.ts.map +1 -0
  287. package/dist/providers/http-transport.js +152 -0
  288. package/dist/providers/http-transport.js.map +1 -0
  289. package/dist/providers/huggingface-provider.d.ts +73 -4
  290. package/dist/providers/huggingface-provider.d.ts.map +1 -1
  291. package/dist/providers/huggingface-provider.js +240 -34
  292. package/dist/providers/huggingface-provider.js.map +1 -1
  293. package/dist/providers/image-mime.d.ts +39 -0
  294. package/dist/providers/image-mime.d.ts.map +1 -0
  295. package/dist/providers/image-mime.js +111 -0
  296. package/dist/providers/image-mime.js.map +1 -0
  297. package/dist/providers/image-ref.d.ts +21 -0
  298. package/dist/providers/image-ref.d.ts.map +1 -0
  299. package/dist/providers/image-ref.js +44 -0
  300. package/dist/providers/image-ref.js.map +1 -0
  301. package/dist/providers/index.d.ts +87 -8
  302. package/dist/providers/index.d.ts.map +1 -1
  303. package/dist/providers/index.js +250 -56
  304. package/dist/providers/index.js.map +1 -1
  305. package/dist/providers/jina-provider.d.ts +12 -3
  306. package/dist/providers/jina-provider.d.ts.map +1 -1
  307. package/dist/providers/jina-provider.js +21 -7
  308. package/dist/providers/jina-provider.js.map +1 -1
  309. package/dist/providers/kie-provider.d.ts +172 -10
  310. package/dist/providers/kie-provider.d.ts.map +1 -1
  311. package/dist/providers/kie-provider.js +1190 -228
  312. package/dist/providers/kie-provider.js.map +1 -1
  313. package/dist/providers/kie-webhook-registry.d.ts +28 -0
  314. package/dist/providers/kie-webhook-registry.d.ts.map +1 -0
  315. package/dist/providers/kie-webhook-registry.js +91 -0
  316. package/dist/providers/kie-webhook-registry.js.map +1 -0
  317. package/dist/providers/llama-provider.d.ts +23 -36
  318. package/dist/providers/llama-provider.d.ts.map +1 -1
  319. package/dist/providers/llama-provider.js +50 -355
  320. package/dist/providers/llama-provider.js.map +1 -1
  321. package/dist/providers/lmstudio-provider.d.ts +4 -7
  322. package/dist/providers/lmstudio-provider.d.ts.map +1 -1
  323. package/dist/providers/lmstudio-provider.js +24 -15
  324. package/dist/providers/lmstudio-provider.js.map +1 -1
  325. package/dist/providers/loop-tracing.d.ts +17 -0
  326. package/dist/providers/loop-tracing.d.ts.map +1 -0
  327. package/dist/providers/loop-tracing.js +132 -0
  328. package/dist/providers/loop-tracing.js.map +1 -0
  329. package/dist/providers/manifest-models.d.ts +341 -1
  330. package/dist/providers/manifest-models.d.ts.map +1 -1
  331. package/dist/providers/manifest-models.js +1106 -22
  332. package/dist/providers/manifest-models.js.map +1 -1
  333. package/dist/providers/media-mime.d.ts +14 -0
  334. package/dist/providers/media-mime.d.ts.map +1 -0
  335. package/dist/providers/media-mime.js +35 -0
  336. package/dist/providers/media-mime.js.map +1 -0
  337. package/dist/providers/meshy-provider.d.ts +16 -2
  338. package/dist/providers/meshy-provider.d.ts.map +1 -1
  339. package/dist/providers/meshy-provider.js +82 -46
  340. package/dist/providers/meshy-provider.js.map +1 -1
  341. package/dist/providers/meta-provider.d.ts +22 -0
  342. package/dist/providers/meta-provider.d.ts.map +1 -0
  343. package/dist/providers/meta-provider.js +54 -0
  344. package/dist/providers/meta-provider.js.map +1 -0
  345. package/dist/providers/minimax-provider.d.ts +27 -18
  346. package/dist/providers/minimax-provider.d.ts.map +1 -1
  347. package/dist/providers/minimax-provider.js +222 -72
  348. package/dist/providers/minimax-provider.js.map +1 -1
  349. package/dist/providers/mistral-provider.d.ts +8 -12
  350. package/dist/providers/mistral-provider.d.ts.map +1 -1
  351. package/dist/providers/mistral-provider.js +15 -34
  352. package/dist/providers/mistral-provider.js.map +1 -1
  353. package/dist/providers/moonshot-provider.d.ts +11 -15
  354. package/dist/providers/moonshot-provider.d.ts.map +1 -1
  355. package/dist/providers/moonshot-provider.js +33 -23
  356. package/dist/providers/moonshot-provider.js.map +1 -1
  357. package/dist/providers/node-llama-cpp-provider.d.ts +99 -0
  358. package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
  359. package/dist/providers/node-llama-cpp-provider.js +484 -0
  360. package/dist/providers/node-llama-cpp-provider.js.map +1 -0
  361. package/dist/providers/nodetool-provider.d.ts +49 -0
  362. package/dist/providers/nodetool-provider.d.ts.map +1 -0
  363. package/dist/providers/nodetool-provider.js +222 -0
  364. package/dist/providers/nodetool-provider.js.map +1 -0
  365. package/dist/providers/oauth/browser-launcher.d.ts +23 -0
  366. package/dist/providers/oauth/browser-launcher.d.ts.map +1 -0
  367. package/dist/providers/oauth/browser-launcher.js +45 -0
  368. package/dist/providers/oauth/browser-launcher.js.map +1 -0
  369. package/dist/providers/oauth/claude-code-credentials.d.ts +74 -0
  370. package/dist/providers/oauth/claude-code-credentials.d.ts.map +1 -0
  371. package/dist/providers/oauth/claude-code-credentials.js +133 -0
  372. package/dist/providers/oauth/claude-code-credentials.js.map +1 -0
  373. package/dist/providers/oauth/claude-code-login.d.ts +124 -0
  374. package/dist/providers/oauth/claude-code-login.d.ts.map +1 -0
  375. package/dist/providers/oauth/claude-code-login.js +230 -0
  376. package/dist/providers/oauth/claude-code-login.js.map +1 -0
  377. package/dist/providers/oauth/claude-code-oauth-client.d.ts +111 -0
  378. package/dist/providers/oauth/claude-code-oauth-client.d.ts.map +1 -0
  379. package/dist/providers/oauth/claude-code-oauth-client.js +207 -0
  380. package/dist/providers/oauth/claude-code-oauth-client.js.map +1 -0
  381. package/dist/providers/oauth/errors.d.ts +54 -0
  382. package/dist/providers/oauth/errors.d.ts.map +1 -0
  383. package/dist/providers/oauth/errors.js +79 -0
  384. package/dist/providers/oauth/errors.js.map +1 -0
  385. package/dist/providers/oauth/index.d.ts +105 -0
  386. package/dist/providers/oauth/index.d.ts.map +1 -0
  387. package/dist/providers/oauth/index.js +167 -0
  388. package/dist/providers/oauth/index.js.map +1 -0
  389. package/dist/providers/oauth/jwt-claims.d.ts +23 -0
  390. package/dist/providers/oauth/jwt-claims.d.ts.map +1 -0
  391. package/dist/providers/oauth/jwt-claims.js +67 -0
  392. package/dist/providers/oauth/jwt-claims.js.map +1 -0
  393. package/dist/providers/oauth/local-callback-server.d.ts +60 -0
  394. package/dist/providers/oauth/local-callback-server.d.ts.map +1 -0
  395. package/dist/providers/oauth/local-callback-server.js +148 -0
  396. package/dist/providers/oauth/local-callback-server.js.map +1 -0
  397. package/dist/providers/oauth/oauth-client.d.ts +55 -0
  398. package/dist/providers/oauth/oauth-client.d.ts.map +1 -0
  399. package/dist/providers/oauth/oauth-client.js +161 -0
  400. package/dist/providers/oauth/oauth-client.js.map +1 -0
  401. package/dist/providers/oauth/openai-oauth-provider.d.ts +129 -0
  402. package/dist/providers/oauth/openai-oauth-provider.d.ts.map +1 -0
  403. package/dist/providers/oauth/openai-oauth-provider.js +265 -0
  404. package/dist/providers/oauth/openai-oauth-provider.js.map +1 -0
  405. package/dist/providers/oauth/pkce-helper.d.ts +43 -0
  406. package/dist/providers/oauth/pkce-helper.d.ts.map +1 -0
  407. package/dist/providers/oauth/pkce-helper.js +57 -0
  408. package/dist/providers/oauth/pkce-helper.js.map +1 -0
  409. package/dist/providers/oauth/redaction.d.ts +22 -0
  410. package/dist/providers/oauth/redaction.d.ts.map +1 -0
  411. package/dist/providers/oauth/redaction.js +59 -0
  412. package/dist/providers/oauth/redaction.js.map +1 -0
  413. package/dist/providers/oauth/secure-credential-store.d.ts +67 -0
  414. package/dist/providers/oauth/secure-credential-store.d.ts.map +1 -0
  415. package/dist/providers/oauth/secure-credential-store.js +73 -0
  416. package/dist/providers/oauth/secure-credential-store.js.map +1 -0
  417. package/dist/providers/oauth/token-store.d.ts +53 -0
  418. package/dist/providers/oauth/token-store.d.ts.map +1 -0
  419. package/dist/providers/oauth/token-store.js +78 -0
  420. package/dist/providers/oauth/token-store.js.map +1 -0
  421. package/dist/providers/oauth/types.d.ts +64 -0
  422. package/dist/providers/oauth/types.d.ts.map +1 -0
  423. package/dist/providers/oauth/types.js +12 -0
  424. package/dist/providers/oauth/types.js.map +1 -0
  425. package/dist/providers/object-tracking.d.ts +47 -0
  426. package/dist/providers/object-tracking.d.ts.map +1 -0
  427. package/dist/providers/object-tracking.js +46 -0
  428. package/dist/providers/object-tracking.js.map +1 -0
  429. package/dist/providers/offline-model-index.d.ts +20 -0
  430. package/dist/providers/offline-model-index.d.ts.map +1 -0
  431. package/dist/providers/offline-model-index.js +112 -0
  432. package/dist/providers/offline-model-index.js.map +1 -0
  433. package/dist/providers/ollama-provider.d.ts +30 -2
  434. package/dist/providers/ollama-provider.d.ts.map +1 -1
  435. package/dist/providers/ollama-provider.js +198 -43
  436. package/dist/providers/ollama-provider.js.map +1 -1
  437. package/dist/providers/openai-compat/client.d.ts +57 -0
  438. package/dist/providers/openai-compat/client.d.ts.map +1 -0
  439. package/dist/providers/openai-compat/client.js +157 -0
  440. package/dist/providers/openai-compat/client.js.map +1 -0
  441. package/dist/providers/openai-compat/decode.d.ts +36 -0
  442. package/dist/providers/openai-compat/decode.d.ts.map +1 -0
  443. package/dist/providers/openai-compat/decode.js +55 -0
  444. package/dist/providers/openai-compat/decode.js.map +1 -0
  445. package/dist/providers/openai-compat/errors.d.ts +54 -0
  446. package/dist/providers/openai-compat/errors.d.ts.map +1 -0
  447. package/dist/providers/openai-compat/errors.js +144 -0
  448. package/dist/providers/openai-compat/errors.js.map +1 -0
  449. package/dist/providers/openai-compat/index.d.ts +8 -0
  450. package/dist/providers/openai-compat/index.d.ts.map +1 -0
  451. package/dist/providers/openai-compat/index.js +5 -0
  452. package/dist/providers/openai-compat/index.js.map +1 -0
  453. package/dist/providers/openai-compat/sse.d.ts +14 -0
  454. package/dist/providers/openai-compat/sse.d.ts.map +1 -0
  455. package/dist/providers/openai-compat/sse.js +69 -0
  456. package/dist/providers/openai-compat/sse.js.map +1 -0
  457. package/dist/providers/openai-compat/types.d.ts +75 -0
  458. package/dist/providers/openai-compat/types.d.ts.map +1 -0
  459. package/dist/providers/openai-compat/types.js +9 -0
  460. package/dist/providers/openai-compat/types.js.map +1 -0
  461. package/dist/providers/openai-compat-provider.d.ts +90 -0
  462. package/dist/providers/openai-compat-provider.d.ts.map +1 -0
  463. package/dist/providers/openai-compat-provider.js +282 -0
  464. package/dist/providers/openai-compat-provider.js.map +1 -0
  465. package/dist/providers/openai-provider.d.ts +98 -27
  466. package/dist/providers/openai-provider.d.ts.map +1 -1
  467. package/dist/providers/openai-provider.js +1037 -105
  468. package/dist/providers/openai-provider.js.map +1 -1
  469. package/dist/providers/openrouter-provider.d.ts +58 -13
  470. package/dist/providers/openrouter-provider.d.ts.map +1 -1
  471. package/dist/providers/openrouter-provider.js +383 -51
  472. package/dist/providers/openrouter-provider.js.map +1 -1
  473. package/dist/providers/provider-call-failure.d.ts +39 -0
  474. package/dist/providers/provider-call-failure.d.ts.map +1 -0
  475. package/dist/providers/provider-call-failure.js +132 -0
  476. package/dist/providers/provider-call-failure.js.map +1 -0
  477. package/dist/providers/provider-error.d.ts +59 -0
  478. package/dist/providers/provider-error.d.ts.map +1 -0
  479. package/dist/providers/provider-error.js +171 -0
  480. package/dist/providers/provider-error.js.map +1 -0
  481. package/dist/providers/provider-generations.d.ts +99 -0
  482. package/dist/providers/provider-generations.d.ts.map +1 -0
  483. package/dist/providers/provider-generations.js +71 -0
  484. package/dist/providers/provider-generations.js.map +1 -0
  485. package/dist/providers/provider-queue.d.ts +62 -0
  486. package/dist/providers/provider-queue.d.ts.map +1 -0
  487. package/dist/providers/provider-queue.js +28 -0
  488. package/dist/providers/provider-queue.js.map +1 -0
  489. package/dist/providers/provider-registry.d.ts +33 -14
  490. package/dist/providers/provider-registry.d.ts.map +1 -1
  491. package/dist/providers/provider-registry.js +85 -68
  492. package/dist/providers/provider-registry.js.map +1 -1
  493. package/dist/providers/provider-request-log.d.ts +53 -0
  494. package/dist/providers/provider-request-log.d.ts.map +1 -0
  495. package/dist/providers/provider-request-log.js +168 -0
  496. package/dist/providers/provider-request-log.js.map +1 -0
  497. package/dist/providers/provider-session.d.ts +3 -0
  498. package/dist/providers/provider-session.d.ts.map +1 -0
  499. package/dist/providers/provider-session.js +10 -0
  500. package/dist/providers/provider-session.js.map +1 -0
  501. package/dist/providers/provider-transport.d.ts +11 -0
  502. package/dist/providers/provider-transport.d.ts.map +1 -0
  503. package/dist/providers/provider-transport.js +11 -0
  504. package/dist/providers/provider-transport.js.map +1 -0
  505. package/dist/providers/python-provider.d.ts +26 -13
  506. package/dist/providers/python-provider.d.ts.map +1 -1
  507. package/dist/providers/python-provider.js +204 -25
  508. package/dist/providers/python-provider.js.map +1 -1
  509. package/dist/providers/replicate-provider.d.ts +91 -8
  510. package/dist/providers/replicate-provider.d.ts.map +1 -1
  511. package/dist/providers/replicate-provider.js +823 -65
  512. package/dist/providers/replicate-provider.js.map +1 -1
  513. package/dist/providers/replicate-retry.d.ts +30 -0
  514. package/dist/providers/replicate-retry.d.ts.map +1 -0
  515. package/dist/providers/replicate-retry.js +97 -0
  516. package/dist/providers/replicate-retry.js.map +1 -0
  517. package/dist/providers/requesty-provider.d.ts +30 -0
  518. package/dist/providers/requesty-provider.d.ts.map +1 -0
  519. package/dist/providers/requesty-provider.js +87 -0
  520. package/dist/providers/requesty-provider.js.map +1 -0
  521. package/dist/providers/responses-api.d.ts +27 -0
  522. package/dist/providers/responses-api.d.ts.map +1 -0
  523. package/dist/providers/responses-api.js +297 -0
  524. package/dist/providers/responses-api.js.map +1 -0
  525. package/dist/providers/reve-provider.d.ts +46 -0
  526. package/dist/providers/reve-provider.d.ts.map +1 -0
  527. package/dist/providers/reve-provider.js +165 -0
  528. package/dist/providers/reve-provider.js.map +1 -0
  529. package/dist/providers/rodin-provider.d.ts +25 -2
  530. package/dist/providers/rodin-provider.d.ts.map +1 -1
  531. package/dist/providers/rodin-provider.js +95 -51
  532. package/dist/providers/rodin-provider.js.map +1 -1
  533. package/dist/providers/safe-url.d.ts +70 -0
  534. package/dist/providers/safe-url.d.ts.map +1 -0
  535. package/dist/providers/safe-url.js +277 -0
  536. package/dist/providers/safe-url.js.map +1 -0
  537. package/dist/providers/scripted-provider.d.ts +9 -0
  538. package/dist/providers/scripted-provider.d.ts.map +1 -1
  539. package/dist/providers/scripted-provider.js +86 -10
  540. package/dist/providers/scripted-provider.js.map +1 -1
  541. package/dist/providers/structured-output.d.ts +42 -0
  542. package/dist/providers/structured-output.d.ts.map +1 -0
  543. package/dist/providers/structured-output.js +83 -0
  544. package/dist/providers/structured-output.js.map +1 -0
  545. package/dist/providers/together-operations.d.ts +27 -0
  546. package/dist/providers/together-operations.d.ts.map +1 -0
  547. package/dist/providers/together-operations.js +95 -0
  548. package/dist/providers/together-operations.js.map +1 -0
  549. package/dist/providers/together-provider.d.ts +18 -18
  550. package/dist/providers/together-provider.d.ts.map +1 -1
  551. package/dist/providers/together-provider.js +140 -367
  552. package/dist/providers/together-provider.js.map +1 -1
  553. package/dist/providers/topaz-provider.d.ts +45 -0
  554. package/dist/providers/topaz-provider.d.ts.map +1 -0
  555. package/dist/providers/topaz-provider.js +244 -0
  556. package/dist/providers/topaz-provider.js.map +1 -0
  557. package/dist/providers/types.d.ts +641 -4
  558. package/dist/providers/types.d.ts.map +1 -1
  559. package/dist/providers/types.js +81 -1
  560. package/dist/providers/types.js.map +1 -1
  561. package/dist/providers/useapi-provider.d.ts +28 -0
  562. package/dist/providers/useapi-provider.d.ts.map +1 -0
  563. package/dist/providers/useapi-provider.js +274 -0
  564. package/dist/providers/useapi-provider.js.map +1 -0
  565. package/dist/providers/video-duration.d.ts +6 -0
  566. package/dist/providers/video-duration.d.ts.map +1 -0
  567. package/dist/providers/video-duration.js +63 -0
  568. package/dist/providers/video-duration.js.map +1 -0
  569. package/dist/providers/video-frame-fallback.d.ts +39 -0
  570. package/dist/providers/video-frame-fallback.d.ts.map +1 -0
  571. package/dist/providers/video-frame-fallback.js +155 -0
  572. package/dist/providers/video-frame-fallback.js.map +1 -0
  573. package/dist/providers/video-frames.d.ts +61 -0
  574. package/dist/providers/video-frames.d.ts.map +1 -0
  575. package/dist/providers/video-frames.js +208 -0
  576. package/dist/providers/video-frames.js.map +1 -0
  577. package/dist/providers/video-mime.d.ts +14 -0
  578. package/dist/providers/video-mime.d.ts.map +1 -0
  579. package/dist/providers/video-mime.js +44 -0
  580. package/dist/providers/video-mime.js.map +1 -0
  581. package/dist/providers/video-to-audio.d.ts +34 -0
  582. package/dist/providers/video-to-audio.d.ts.map +1 -0
  583. package/dist/providers/video-to-audio.js +62 -0
  584. package/dist/providers/video-to-audio.js.map +1 -0
  585. package/dist/providers/vllm-provider.d.ts +20 -8
  586. package/dist/providers/vllm-provider.d.ts.map +1 -1
  587. package/dist/providers/vllm-provider.js +44 -23
  588. package/dist/providers/vllm-provider.js.map +1 -1
  589. package/dist/providers/voyage-provider.d.ts +12 -3
  590. package/dist/providers/voyage-provider.d.ts.map +1 -1
  591. package/dist/providers/voyage-provider.js +21 -7
  592. package/dist/providers/voyage-provider.js.map +1 -1
  593. package/dist/providers/xai-provider.d.ts +56 -13
  594. package/dist/providers/xai-provider.d.ts.map +1 -1
  595. package/dist/providers/xai-provider.js +307 -22
  596. package/dist/providers/xai-provider.js.map +1 -1
  597. package/dist/python-bridge-base.d.ts +358 -0
  598. package/dist/python-bridge-base.d.ts.map +1 -0
  599. package/dist/python-bridge-base.js +1557 -0
  600. package/dist/python-bridge-base.js.map +1 -0
  601. package/dist/python-bridge-codec.d.ts +28 -0
  602. package/dist/python-bridge-codec.d.ts.map +1 -0
  603. package/dist/python-bridge-codec.js +39 -0
  604. package/dist/python-bridge-codec.js.map +1 -0
  605. package/dist/python-bridge-factory.d.ts +52 -0
  606. package/dist/python-bridge-factory.d.ts.map +1 -0
  607. package/dist/python-bridge-factory.js +69 -0
  608. package/dist/python-bridge-factory.js.map +1 -0
  609. package/dist/python-bridge-framing.d.ts +68 -0
  610. package/dist/python-bridge-framing.d.ts.map +1 -0
  611. package/dist/python-bridge-framing.js +91 -0
  612. package/dist/python-bridge-framing.js.map +1 -0
  613. package/dist/python-bridge-types.d.ts +545 -2
  614. package/dist/python-bridge-types.d.ts.map +1 -1
  615. package/dist/python-bridge-types.js +49 -1
  616. package/dist/python-bridge-types.js.map +1 -1
  617. package/dist/python-graph-resolver.d.ts +57 -0
  618. package/dist/python-graph-resolver.d.ts.map +1 -0
  619. package/dist/python-graph-resolver.js +79 -0
  620. package/dist/python-graph-resolver.js.map +1 -0
  621. package/dist/python-node-executor.d.ts +36 -4
  622. package/dist/python-node-executor.d.ts.map +1 -1
  623. package/dist/python-node-executor.js +177 -86
  624. package/dist/python-node-executor.js.map +1 -1
  625. package/dist/python-stdio-bridge.d.ts +18 -37
  626. package/dist/python-stdio-bridge.d.ts.map +1 -1
  627. package/dist/python-stdio-bridge.js +148 -475
  628. package/dist/python-stdio-bridge.js.map +1 -1
  629. package/dist/python-websocket-bridge.d.ts +140 -0
  630. package/dist/python-websocket-bridge.d.ts.map +1 -0
  631. package/dist/python-websocket-bridge.js +561 -0
  632. package/dist/python-websocket-bridge.js.map +1 -0
  633. package/dist/python-worker-stderr.d.ts +4 -0
  634. package/dist/python-worker-stderr.d.ts.map +1 -0
  635. package/dist/python-worker-stderr.js +24 -0
  636. package/dist/python-worker-stderr.js.map +1 -0
  637. package/dist/recommended-models.d.ts +2 -2
  638. package/dist/recommended-models.d.ts.map +1 -1
  639. package/dist/recommended-models.js +44 -4
  640. package/dist/recommended-models.js.map +1 -1
  641. package/dist/recoverable-node-error.d.ts +31 -0
  642. package/dist/recoverable-node-error.d.ts.map +1 -0
  643. package/dist/recoverable-node-error.js +37 -0
  644. package/dist/recoverable-node-error.js.map +1 -0
  645. package/dist/redact-params.d.ts +13 -0
  646. package/dist/redact-params.d.ts.map +1 -0
  647. package/dist/redact-params.js +57 -0
  648. package/dist/redact-params.js.map +1 -0
  649. package/dist/run-context.d.ts +20 -0
  650. package/dist/run-context.d.ts.map +1 -0
  651. package/dist/run-context.js +18 -0
  652. package/dist/run-context.js.map +1 -0
  653. package/dist/run-trace-context.d.ts +30 -0
  654. package/dist/run-trace-context.d.ts.map +1 -0
  655. package/dist/run-trace-context.js +80 -0
  656. package/dist/run-trace-context.js.map +1 -0
  657. package/dist/run-trace-processor.d.ts +65 -0
  658. package/dist/run-trace-processor.d.ts.map +1 -0
  659. package/dist/run-trace-processor.js +417 -0
  660. package/dist/run-trace-processor.js.map +1 -0
  661. package/dist/run-trace-serialization.d.ts +5 -0
  662. package/dist/run-trace-serialization.d.ts.map +1 -0
  663. package/dist/run-trace-serialization.js +113 -0
  664. package/dist/run-trace-serialization.js.map +1 -0
  665. package/dist/sandbox-module-catalog.d.ts +43 -0
  666. package/dist/sandbox-module-catalog.d.ts.map +1 -0
  667. package/dist/sandbox-module-catalog.js +25 -0
  668. package/dist/sandbox-module-catalog.js.map +1 -0
  669. package/dist/storage-workspace.d.ts +62 -0
  670. package/dist/storage-workspace.d.ts.map +1 -0
  671. package/dist/storage-workspace.js +441 -0
  672. package/dist/storage-workspace.js.map +1 -0
  673. package/dist/swappable-python-bridge.d.ts +83 -0
  674. package/dist/swappable-python-bridge.d.ts.map +1 -0
  675. package/dist/swappable-python-bridge.js +246 -0
  676. package/dist/swappable-python-bridge.js.map +1 -0
  677. package/dist/telemetry.d.ts +16 -3
  678. package/dist/telemetry.d.ts.map +1 -1
  679. package/dist/telemetry.js +76 -20
  680. package/dist/telemetry.js.map +1 -1
  681. package/dist/testing.d.ts +81 -0
  682. package/dist/testing.d.ts.map +1 -0
  683. package/dist/testing.js +130 -0
  684. package/dist/testing.js.map +1 -0
  685. package/dist/token-counter.d.ts +30 -0
  686. package/dist/token-counter.d.ts.map +1 -0
  687. package/dist/token-counter.js +156 -0
  688. package/dist/token-counter.js.map +1 -0
  689. package/dist/trace-exporters.d.ts +11 -28
  690. package/dist/trace-exporters.d.ts.map +1 -1
  691. package/dist/trace-exporters.js +69 -22
  692. package/dist/trace-exporters.js.map +1 -1
  693. package/dist/tracing-helpers.d.ts +60 -26
  694. package/dist/tracing-helpers.d.ts.map +1 -1
  695. package/dist/tracing-helpers.js +100 -25
  696. package/dist/tracing-helpers.js.map +1 -1
  697. package/dist/trim-video-window.d.ts +8 -0
  698. package/dist/trim-video-window.d.ts.map +1 -0
  699. package/dist/trim-video-window.js +50 -0
  700. package/dist/trim-video-window.js.map +1 -0
  701. package/dist/turn-budget.d.ts +278 -0
  702. package/dist/turn-budget.d.ts.map +1 -0
  703. package/dist/turn-budget.js +365 -0
  704. package/dist/turn-budget.js.map +1 -0
  705. package/dist/variable-channel.d.ts +48 -0
  706. package/dist/variable-channel.d.ts.map +1 -0
  707. package/dist/variable-channel.js +110 -0
  708. package/dist/variable-channel.js.map +1 -0
  709. package/dist/workspace.d.ts +119 -0
  710. package/dist/workspace.d.ts.map +1 -0
  711. package/dist/workspace.js +112 -0
  712. package/dist/workspace.js.map +1 -0
  713. package/package.json +121 -19
package/dist/context.js CHANGED
@@ -1,18 +1,86 @@
1
+ import { getRunTraceScope } from "./run-trace-context.js";
2
+ import { withSpan } from "./tracing-helpers.js";
3
+ import { assetFileNameCandidates, buildAssetGenerationMetadata, isShortResourceId, packageAssetHttpPath, parsePackageAssetUri } from "@nodetool-ai/protocol";
4
+ import { AgentMemory } from "./agent-memory.js";
5
+ import { recordInvocationAsset, recordInvocationCost } from "./invocation-account.js";
6
+ import { VariableChannel } from "./variable-channel.js";
7
+ import { loadMediaRefBytes } from "./media-ref-bytes.js";
8
+ import { objectTrackingRequestSchema, parseObjectTrackingResult } from "./providers/object-tracking.js";
9
+ import { requestVideoToAudio, VIDEO_TO_AUDIO_TASK, VideoToAudioRequest } from "./providers/video-to-audio.js";
10
+ import { encodeRawImageRef, validateImageEvidence } from "./image-codec.js";
11
+ import { extForImageMime, sniffImageMime } from "./providers/image-mime.js";
12
+ import { GenerationScopeError, isAuthoritativeProviderTerminalError, runWithGenerationReceipt } from "./generation-receipt.js";
13
+ import { generationRegistry } from "./generation-registry.js";
14
+ import { redactGenerationParams } from "./redact-params.js";
15
+ import { isCallable, isBoolean, isNonEmptyString, isObjectLike, isNumber, isRecord, isString } from "@nodetool-ai/protocol";
16
+ import { expandAssetReferences, expandEntityRefs, inlineTextAssetRefs } from "./prompt-asset-refs.js";
17
+ import { getNodeBuiltinSync } from "@nodetool-ai/config";
18
+ // `node:fs/promises`, `node:path`, `node:url`, `node:crypto` are loaded
19
+ // lazily so this module loads in browser / Edge runtimes. `resolveWorkspacePath`
20
+ // and the `randomUUID` fallback both degrade gracefully when these are
21
+ // unavailable.
1
22
  /**
2
- * ProcessingContext – runtime context for node execution.
3
- *
4
- * Port of src/nodetool/workflows/processing_context.py.
5
- *
6
- * Provides:
7
- * - Message queue for emitting ProcessingMessages.
8
- * - Cache get/set interface.
9
- * - Output normalization (sanitize memory URIs, etc.).
10
- * - Asset handling with pluggable storage adapters.
23
+ * ProcessingContext variable key carrying the provider/model the currently
24
+ * running agent loop is itself talking to. Every loop that executes tools
25
+ * (chat turns, `StepExecutor` sub-agents) stamps it, so a tool that launches
26
+ * another harness inherits the caller's model by default instead of demanding
27
+ * one or falling back to a server env default.
28
+ */
29
+ export const ACTIVE_MODEL_CONTEXT_KEY = "active_agent_model";
30
+ const nodeCrypto = getNodeBuiltinSync("node:crypto");
31
+ const nodeFsP = getNodeBuiltinSync("node:fs/promises");
32
+ const nodePath = getNodeBuiltinSync("node:path");
33
+ const nodeUrl = getNodeBuiltinSync("node:url");
34
+ const randomUUID = nodeCrypto?.randomUUID
35
+ ? nodeCrypto.randomUUID
36
+ : () => {
37
+ const g = globalThis;
38
+ if (g.crypto?.randomUUID)
39
+ return g.crypto.randomUUID();
40
+ return `id_${Date.now().toString(36)}_${Math.random()
41
+ .toString(36)
42
+ .slice(2, 10)}`;
43
+ };
44
+ /**
45
+ * `process` is undefined in browser/edge runtimes. Reading `process.env`
46
+ * directly there throws `ReferenceError`, so this context (constructed by the
47
+ * in-browser workflow runner too) reads env through this guard instead.
11
48
  */
12
- import { randomUUID } from "node:crypto";
13
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
14
- import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
15
- import { fileURLToPath, pathToFileURL } from "node:url";
49
+ const safeProcessEnv = () => typeof process !== "undefined" && process.env ? process.env : {};
50
+ function waitForRetry(delayMs, signal) {
51
+ return new Promise((resolveWait, rejectWait) => {
52
+ if (signal?.aborted) {
53
+ rejectWait(signal.reason ?? new Error("HTTP request aborted"));
54
+ return;
55
+ }
56
+ const onAbort = () => {
57
+ clearTimeout(timer);
58
+ rejectWait(signal?.reason ?? new Error("HTTP request aborted"));
59
+ };
60
+ const timer = setTimeout(() => {
61
+ signal?.removeEventListener("abort", onAbort);
62
+ resolveWait();
63
+ }, delayMs);
64
+ signal?.addEventListener("abort", onAbort, { once: true });
65
+ });
66
+ }
67
+ // Eager local bindings; unavailable off-Node, throw at call time.
68
+ function notOnNode(api) {
69
+ throw new Error(`${api} is unavailable in this runtime — configure a StorageAdapter instead`);
70
+ }
71
+ const mkdir = (...a) => nodeFsP ? nodeFsP.mkdir(...a) : notOnNode("node:fs/promises.mkdir");
72
+ const readFile = (...a) => nodeFsP ? nodeFsP.readFile(...a) : notOnNode("node:fs/promises.readFile");
73
+ const writeFile = (...a) => nodeFsP ? nodeFsP.writeFile(...a) : notOnNode("node:fs/promises.writeFile");
74
+ const basename = (p, ext) => nodePath ? nodePath.basename(p, ext) : notOnNode("node:path.basename");
75
+ const dirname = (p) => nodePath ? nodePath.dirname(p) : notOnNode("node:path.dirname");
76
+ const extname = (p) => nodePath ? nodePath.extname(p) : notOnNode("node:path.extname");
77
+ const isAbsolute = (p) => nodePath ? nodePath.isAbsolute(p) : notOnNode("node:path.isAbsolute");
78
+ const join = (...parts) => nodePath ? nodePath.join(...parts) : notOnNode("node:path.join");
79
+ const relative = (from, to) => nodePath ? nodePath.relative(from, to) : notOnNode("node:path.relative");
80
+ const resolve = (...parts) => nodePath ? nodePath.resolve(...parts) : notOnNode("node:path.resolve");
81
+ const pathToFileURL = (p) => nodeUrl ? nodeUrl.pathToFileURL(p) : notOnNode("node:url.pathToFileURL");
82
+ import { CONTENT_FILTER_MAX_RETRIES, contentFilterRetryDelayMs, isContentFilterRefusal } from "./providers/content-filter.js";
83
+ import { getProcessSandboxModuleCatalog } from "./sandbox-module-catalog.js";
16
84
  /**
17
85
  * In-memory cache adapter (default for tests and single-process execution).
18
86
  */
@@ -26,6 +94,8 @@ export class MemoryCache {
26
94
  this._store.delete(key);
27
95
  return undefined;
28
96
  }
97
+ // SAFETY: `TValue` is what the caller stored under this key; a cache
98
+ // hands back exactly the value `set` was given for it.
29
99
  return entry.value;
30
100
  }
31
101
  async set(key, value, ttlSeconds) {
@@ -39,195 +109,173 @@ export class MemoryCache {
39
109
  this._store.delete(key);
40
110
  }
41
111
  }
112
+ /** Raised by a durable host when an idempotent request is already owned. */
113
+ export class GenerationAlreadyAcceptedError extends Error {
114
+ generationId;
115
+ constructor(generationId) {
116
+ super(`Generation ${generationId} was already accepted`);
117
+ this.generationId = generationId;
118
+ this.name = "GenerationAlreadyAcceptedError";
119
+ }
120
+ }
121
+ let defaultModelInterfaces = null;
122
+ /**
123
+ * Install the model interfaces every context in this process falls back to.
124
+ *
125
+ * A host installs its persistence once at startup instead of each entrance
126
+ * remembering to wire it onto the context it builds. That forgetting is a
127
+ * defect generator: `nodetool debug` wired none while `workflows run` wired
128
+ * four, so a workflow that saves an image ran under one command and threw
129
+ * under the other. A context that sets its own interfaces still wins — this
130
+ * is the floor, not an override.
131
+ *
132
+ * Pass `null` to uninstall (a test restoring global state).
133
+ */
134
+ export function setDefaultModelInterfaces(interfaces) {
135
+ defaultModelInterfaces = interfaces;
136
+ }
137
+ /** The process-wide default, or null when no host installed one. */
138
+ export function getDefaultModelInterfaces() {
139
+ return defaultModelInterfaces;
140
+ }
42
141
  function isWithinRoot(root, target) {
43
142
  const rel = relative(root, target);
44
143
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
45
144
  }
46
- function normalizeStorageKey(key) {
47
- const cleaned = normalize(key.replaceAll("\\", "/")).replace(/^\/+/, "");
48
- if (!cleaned ||
49
- cleaned === "." ||
50
- cleaned.startsWith("..") ||
51
- cleaned.includes(`..${sep}`)) {
52
- throw new Error(`Invalid storage key: ${key}`);
53
- }
54
- return cleaned;
55
- }
56
- function joinStorageKey(prefix, key) {
57
- const normalizedKey = normalizeStorageKey(key);
58
- if (!prefix)
59
- return normalizedKey;
60
- const normalizedPrefix = normalizeStorageKey(prefix);
61
- return `${normalizedPrefix}/${normalizedKey}`;
62
- }
63
145
  /**
64
- * In-memory storage adapter useful for tests and single-process ephemeral runs.
146
+ * Deterministic JSON serialization with object keys sorted at every depth.
147
+ * Unlike `JSON.stringify(value, sortedKeys)` (a recursive replacer allow-list
148
+ * that drops nested values), this preserves the full nested structure, so two
149
+ * values are equal-keyed iff they are deeply equal.
65
150
  */
66
- export class InMemoryStorageAdapter {
67
- _store = new Map();
68
- async store(key, data, _contentType) {
69
- const normalized = normalizeStorageKey(key);
70
- this._store.set(normalized, new Uint8Array(data));
71
- return `memory://${normalized}`;
72
- }
73
- async retrieve(uri) {
74
- if (!uri.startsWith("memory://"))
75
- return null;
76
- const key = uri.slice("memory://".length);
77
- const value = this._store.get(key);
78
- return value ? new Uint8Array(value) : null;
151
+ function stableStringifyDeep(value) {
152
+ if (!isObjectLike(value)) {
153
+ return JSON.stringify(value ?? null);
79
154
  }
80
- async exists(uri) {
81
- if (!uri.startsWith("memory://"))
82
- return false;
83
- const key = uri.slice("memory://".length);
84
- return this._store.has(key);
155
+ if (Array.isArray(value)) {
156
+ return `[${value.map((v) => stableStringifyDeep(v)).join(",")}]`;
85
157
  }
158
+ const entries = Object.keys(value)
159
+ .sort()
160
+ .map((k) => `${JSON.stringify(k)}:${stableStringifyDeep(value[k])}`);
161
+ return `{${entries.join(",")}}`;
86
162
  }
87
163
  /**
88
- * File-system storage adapter rooted to a single base directory.
164
+ * Normalize the image input for image-to-image / image-to-video predictions
165
+ * into a list of byte buffers. Accepts the modern `images` array as well as
166
+ * the legacy singular `image` field so older callers keep working.
89
167
  */
90
- export class FileStorageAdapter {
91
- rootDir;
92
- constructor(rootDir) {
93
- this.rootDir = resolve(rootDir);
94
- }
95
- resolvePathFromKey(key) {
96
- const normalized = normalizeStorageKey(key);
97
- const absolute = resolve(join(this.rootDir, normalized));
98
- if (!isWithinRoot(this.rootDir, absolute)) {
99
- throw new Error(`Storage key escapes root: ${key}`);
100
- }
101
- return absolute;
102
- }
103
- resolvePathFromUri(uri) {
104
- let absolute = null;
105
- if (uri.startsWith("file://")) {
106
- try {
107
- absolute = resolve(fileURLToPath(uri));
108
- }
109
- catch {
110
- // Invalid file:// URI — treat as unresolvable.
111
- return null;
112
- }
113
- }
114
- else if (uri.startsWith("/api/storage/") ||
115
- uri.startsWith("api/storage/")) {
116
- const key = uri.replace(/^\/?api\/storage\//, "");
117
- absolute = this.resolvePathFromKey(key);
118
- }
119
- else if (/^https?:\/\//.test(uri)) {
120
- try {
121
- const parsed = new URL(uri);
122
- if (parsed.pathname.startsWith("/api/storage/")) {
123
- const key = parsed.pathname.replace(/^\/api\/storage\//, "");
124
- absolute = this.resolvePathFromKey(key);
125
- }
126
- }
127
- catch {
128
- // Malformed URL — treat as unresolvable.
129
- return null;
130
- }
131
- }
132
- else {
133
- return null;
134
- }
135
- if (absolute === null || !isWithinRoot(this.rootDir, absolute)) {
136
- return null;
137
- }
138
- return absolute;
139
- }
140
- async store(key, data, _contentType) {
141
- const absolutePath = this.resolvePathFromKey(key);
142
- await mkdir(dirname(absolutePath), { recursive: true });
143
- await writeFile(absolutePath, data);
144
- return pathToFileURL(absolutePath).toString();
145
- }
146
- async retrieve(uri) {
147
- const absolutePath = this.resolvePathFromUri(uri);
148
- if (!absolutePath)
149
- return null;
150
- try {
151
- return await readFile(absolutePath);
152
- }
153
- catch {
154
- // File not found or unreadable — caller handles null.
155
- return null;
156
- }
157
- }
158
- async exists(uri) {
159
- const absolutePath = this.resolvePathFromUri(uri);
160
- if (!absolutePath)
161
- return false;
162
- try {
163
- await access(absolutePath);
164
- return true;
165
- }
166
- catch {
167
- // File does not exist or is inaccessible.
168
- return false;
169
- }
168
+ function coerceImageList(params) {
169
+ const list = params.images;
170
+ if (Array.isArray(list)) {
171
+ return list.filter((b) => b instanceof Uint8Array);
170
172
  }
173
+ const single = params.image;
174
+ return single instanceof Uint8Array ? [single] : [];
175
+ }
176
+ /**
177
+ * Read an outpaint padding bag off the loose params. Only the four side keys
178
+ * are read, and only when numeric: a caller that passes nothing gets
179
+ * `undefined`, which lets the provider fall back to its endpoint's default
180
+ * expansion rather than sending zeros that would outpaint nothing.
181
+ */
182
+ function coerceOutpaintPadding(value) {
183
+ if (!isObjectLike(value))
184
+ return undefined;
185
+ const side = (key) => isNumber(value[key]) ? value[key] : undefined;
186
+ const padding = {
187
+ left: side("left"),
188
+ right: side("right"),
189
+ top: side("top"),
190
+ bottom: side("bottom")
191
+ };
192
+ return Object.values(padding).some((v) => v !== undefined)
193
+ ? padding
194
+ : undefined;
195
+ }
196
+ function coerceByteList(value) {
197
+ if (!Array.isArray(value))
198
+ return [];
199
+ return value.filter((item) => item instanceof Uint8Array && item.byteLength > 0);
200
+ }
201
+ function coerceStringList(value) {
202
+ if (!Array.isArray(value))
203
+ return [];
204
+ return value.filter(isNonEmptyString);
205
+ }
206
+ function firstNonEmptyByteList(value) {
207
+ return value.find((bytes) => bytes.byteLength > 0) ?? new Uint8Array();
208
+ }
209
+ function isAbortSignal(value) {
210
+ return (isObjectLike(value) &&
211
+ isBoolean(value.aborted) &&
212
+ isCallable(value.addEventListener));
171
213
  }
172
214
  /**
173
- * S3-backed storage adapter with injected client operations.
215
+ * Normalize `params.entities` into provider-level {@link EntityReference}s.
174
216
  *
175
- * This avoids hard-coupling runtime to any specific SDK while providing
176
- * predictable URI behavior (`s3://bucket/key`).
217
+ * Consumers send entities in whatever shape they hold: a full protocol Entity
218
+ * (`reference_images` list), a lean `{name, descriptor, image}` where `image`
219
+ * is bytes, an ImageRef-like object, or a bare URI string. Images are resolved
220
+ * to bytes here — the last point with a ProcessingContext — so providers only
221
+ * ever see `Uint8Array`s. Entities without a name are dropped; an unresolvable
222
+ * image degrades to a text-only entity (the descriptor still applies).
177
223
  */
178
- export class S3StorageAdapter {
179
- bucket;
180
- prefix;
181
- client;
182
- constructor(opts) {
183
- if (!opts.bucket) {
184
- throw new Error("S3 bucket is required");
185
- }
186
- this.bucket = opts.bucket;
187
- this.client = opts.client;
188
- this.prefix = opts.prefix ? normalizeStorageKey(opts.prefix) : null;
189
- }
190
- keyForStore(key) {
191
- return joinStorageKey(this.prefix ?? undefined, key);
192
- }
193
- parseUri(uri) {
194
- if (!uri.startsWith("s3://"))
195
- return null;
196
- const withoutScheme = uri.slice("s3://".length);
197
- const slashIndex = withoutScheme.indexOf("/");
198
- if (slashIndex <= 0 || slashIndex === withoutScheme.length - 1) {
199
- return null;
224
+ async function coerceEntityList(params, context) {
225
+ const raw = params.entities;
226
+ if (!Array.isArray(raw) || raw.length === 0)
227
+ return undefined;
228
+ const out = [];
229
+ for (const item of raw) {
230
+ if (!isObjectLike(item))
231
+ continue;
232
+ const entity = item;
233
+ const name = isString(entity.name) ? entity.name.trim() : "";
234
+ if (!name)
235
+ continue;
236
+ const descriptor = isString(entity.descriptor)
237
+ ? entity.descriptor
238
+ : undefined;
239
+ const imageSource = entity.image ??
240
+ (Array.isArray(entity.reference_images)
241
+ ? entity.reference_images[0]
242
+ : undefined);
243
+ let image;
244
+ if (imageSource instanceof Uint8Array) {
245
+ image = imageSource;
246
+ }
247
+ else if (isNonEmptyString(imageSource)) {
248
+ image =
249
+ (await loadMediaRefBytes({ type: "image", uri: imageSource }, context)) ?? undefined;
250
+ }
251
+ else if (isObjectLike(imageSource)) {
252
+ image =
253
+ (await loadMediaRefBytes(imageSource, context)) ??
254
+ undefined;
255
+ }
256
+ out.push({ name, descriptor, image });
257
+ }
258
+ return out.length > 0 ? out : undefined;
259
+ }
260
+ export function normalizeStorageKey(key) {
261
+ const segments = [];
262
+ for (const segment of key.replaceAll("\\", "/").split("/")) {
263
+ if (!segment || segment === ".")
264
+ continue;
265
+ if (segment === "..") {
266
+ if (segments.length === 0) {
267
+ throw new Error(`Invalid storage key: ${key}`);
268
+ }
269
+ segments.pop();
270
+ continue;
200
271
  }
201
- const bucket = withoutScheme.slice(0, slashIndex);
202
- const key = withoutScheme.slice(slashIndex + 1);
203
- return { bucket, key };
204
- }
205
- async store(key, data, contentType) {
206
- const objectKey = this.keyForStore(key);
207
- await this.client.putObject({
208
- bucket: this.bucket,
209
- key: objectKey,
210
- body: data,
211
- contentType
212
- });
213
- return `s3://${this.bucket}/${objectKey}`;
214
- }
215
- async retrieve(uri) {
216
- const parsed = this.parseUri(uri);
217
- if (!parsed)
218
- return null;
219
- if (parsed.bucket !== this.bucket)
220
- return null;
221
- return this.client.getObject(parsed);
272
+ segments.push(segment);
222
273
  }
223
- async exists(uri) {
224
- const parsed = this.parseUri(uri);
225
- if (!parsed)
226
- return false;
227
- if (parsed.bucket !== this.bucket)
228
- return false;
229
- return this.client.headObject(parsed);
274
+ const cleaned = segments.join("/");
275
+ if (!cleaned) {
276
+ throw new Error(`Invalid storage key: ${key}`);
230
277
  }
278
+ return cleaned;
231
279
  }
232
280
  /**
233
281
  * Resolve paths relative to a configured workspace root.
@@ -272,32 +320,158 @@ export function resolveWorkspacePath(workspaceDir, path) {
272
320
  }
273
321
  return absPath;
274
322
  }
275
- // ---------------------------------------------------------------------------
276
- // ProcessingContext
277
- // ---------------------------------------------------------------------------
323
+ let workspaceFactory = (opts) => opts.workspace ?? null;
324
+ /** Installed once by `storage-workspace.ts`; see {@link ProcessingContext.workspace}. */
325
+ export function setWorkspaceFactory(factory) {
326
+ workspaceFactory = factory;
327
+ }
328
+ function resolveWorkspaceOption(opts) {
329
+ return workspaceFactory(opts);
330
+ }
331
+ /**
332
+ * The listed entry holding the asset `bareId`, or undefined.
333
+ *
334
+ * A short resource id is a prefix of the stored id, so the key continues with
335
+ * more hex before its extension; a full id is followed by the extension
336
+ * itself. A hierarchical id (`user-1/image`) lives in the full key, a flat id
337
+ * in the last segment — both forms are matched. Generated thumbnails
338
+ * (`<id>_thumb.<ext>`) are skipped unless they are the id being resolved, so a
339
+ * legit `banner_thumb` still matches instead of being dropped by a substring
340
+ * test. Preview proxies (`<id>_proxy.<ext>`) are skipped the same way.
341
+ */
342
+ function findListedAsset(entries, bareId) {
343
+ const bareEndsWithThumb = bareId.endsWith("_thumb");
344
+ const needle = isShortResourceId(bareId) ? bareId : `${bareId}.`;
345
+ return entries.find((entry) => {
346
+ const key = entry.key;
347
+ const lastSegment = key.split("/").pop() ?? "";
348
+ if (!key.startsWith(needle) && !lastSegment.startsWith(needle)) {
349
+ return false;
350
+ }
351
+ // A preview proxy (`<id>_proxy.mp4`) and its manifest sit next to the
352
+ // asset. A short id must never resolve to them: renders read the original.
353
+ if (/_proxy\.[^./]+$/.test(lastSegment) && !bareId.endsWith("_proxy")) {
354
+ return false;
355
+ }
356
+ const isThumb = /_thumb\.[^./]+$/.test(lastSegment);
357
+ return !isThumb || bareEndsWithThumb;
358
+ });
359
+ }
278
360
  export class ProcessingContext {
279
361
  jobId;
280
362
  workflowId;
363
+ /** Chat thread id, when this context is serving a chat-driven agent run. */
364
+ threadId;
281
365
  userId;
282
366
  workspaceDir;
283
367
  assetOutputMode;
368
+ /**
369
+ * Whether Output nodes should turn transient media into durable Asset rows.
370
+ * Hosts can disable this for explicitly temporary SDK executions.
371
+ */
372
+ persistOutputAssets;
284
373
  environment;
374
+ /** Bearer token for authenticated calls back to the owning NodeTool API. */
375
+ authToken;
376
+ /** Read-only sandbox module catalog supplied by the execution host. */
377
+ sandboxModuleCatalog;
378
+ /**
379
+ * Run-level cancellation. Set by the kernel to the signal
380
+ * `WorkflowRunner.cancel()` aborts, so long-running node work (agent loops,
381
+ * provider calls) can be interrupted rather than merely having its result
382
+ * discarded. Nodes reached through the streaming `run()` path can also read
383
+ * the same signal via `inputs.signal`.
384
+ *
385
+ * Defaults to a never-aborted signal so contexts built outside a kernel run
386
+ * (tools, tests, CLI) need not supply one.
387
+ */
388
+ signal = new AbortController().signal;
389
+ /**
390
+ * Wake-up payload for a trigger-driven run. Set by the kernel
391
+ * (`WorkflowRunner._runImpl`) from `RunJobRequest.trigger_event` at the
392
+ * start of each run, alongside {@link signal} — so it must be a mutable
393
+ * property, not a constructor-only value. The trigger node's actor reads
394
+ * `triggerEvent?.node_id === this.id` to enter its event-emitting entry
395
+ * point instead of the normal streaming path. `null` for interactive/
396
+ * live-test runs.
397
+ */
398
+ triggerEvent;
285
399
  /** Message queue: all emitted processing messages. */
286
400
  _messages = [];
401
+ /** Whether emit() feeds the pull queue (see constructor option). */
402
+ _retainMessageQueue = true;
287
403
  /** Latest node status by node id. */
288
404
  _nodeStatuses = new Map();
289
405
  /** Latest edge status by edge id. */
290
406
  _edgeStatuses = new Map();
291
- /** Optional message listener (for real-time streaming). */
292
- _onMessage = null;
407
+ /** Message listeners (for real-time streaming). */
408
+ _messageListeners = new Set();
409
+ /**
410
+ * One media object can appear in both a live output_update and the
411
+ * authoritative terminal result. Reuse its materialization within this
412
+ * execution instead of encoding/storing the same large payload twice.
413
+ */
414
+ _normalizedOutputAssets = new WeakMap();
293
415
  /** Cache adapter. */
294
416
  cache;
295
417
  /** Storage adapter (optional, for asset handling). */
296
418
  storage;
419
+ /**
420
+ * Asset storage adapter — where user assets (`asset://<id>.<ext>`) live.
421
+ * Separate from {@link storage}, which servers point at the *temp* store:
422
+ * without this, an `asset://` reference could only be resolved over HTTP
423
+ * through `/api/storage`, which authorizes by `x-user-id` and 404s for
424
+ * anyone but user `1`. Null falls back to {@link storage}.
425
+ */
426
+ assetStorage;
427
+ /**
428
+ * Workspace storage adapter — the agent's working directory abstracted
429
+ * behind {@link StorageAdapter}. Tools that read/write/list files use
430
+ * this adapter so the same code paths work locally (FS-backed) and in
431
+ * cloud deployments (S3/Supabase-backed).
432
+ *
433
+ * If null, file-tool operations should return a clear error rather than
434
+ * fall back to direct FS access — there is no implicit workspace.
435
+ */
436
+ /**
437
+ * @deprecated Use {@link workspace}, which works on a cloud deployment too.
438
+ * Only set when a host passed a bare adapter; a host that passes a
439
+ * `workspace` leaves this null.
440
+ */
441
+ workspaceStorage;
442
+ /**
443
+ * The run's workspace as an interface — the way to read and write run files.
444
+ *
445
+ * Null when the host wired none, in which case a file operation must report
446
+ * that plainly rather than reaching for the process working directory.
447
+ * Prefer this over {@link workspaceDir}, which is only non-null when the
448
+ * workspace happens to be a real folder: a cloud run's workspace lives in
449
+ * object storage and has no path.
450
+ */
451
+ workspace;
452
+ /** Project captured when this run started. */
453
+ projectId;
454
+ /**
455
+ * Unified, structured agent memory. The single source of truth for results
456
+ * shared between agents, tasks, steps and tools. Keys use the namespaces
457
+ * `step:`, `task:`, `input:`, `shared:` (see {@link memoryKeys}).
458
+ */
459
+ memory = new AgentMemory();
297
460
  /** Variables shared across node execution. */
298
461
  _variables;
462
+ /** Named async channels backing Set/Get Variable (see {@link getChannel}). */
463
+ _channels = new Map();
464
+ /** Remaining Set Variable writers per channel; channel closes at zero. */
465
+ _channelWriters = new Map();
299
466
  /** Optional async secret resolver. */
300
467
  _secretResolver = null;
468
+ /** Every secret value the resolver handed out this run (see {@link ProcessingContext.getResolvedSecretValues}). */
469
+ _resolvedSecrets = new Set();
470
+ /** Identity shared by all work beneath an app operation. */
471
+ appRunContext;
472
+ runTraceContext;
473
+ appRunCostAccount;
474
+ _appRunDocuments = new Map();
301
475
  /** Fetch function used by HTTP helpers. */
302
476
  _fetch;
303
477
  /** Optional temporary URL resolver for stored assets. */
@@ -308,10 +482,21 @@ export class ProcessingContext {
308
482
  _modelInterfaces = null;
309
483
  /** Provider cache keyed by provider id. */
310
484
  _providers = new Map();
485
+ _providerPromises = new Map();
311
486
  /** In-context memory URI cache (memory:// key-value objects). */
312
487
  _memory = new Map();
313
488
  /** Optional control event dispatcher (set by workflow runner). */
314
489
  _sendControlEvent = null;
490
+ /**
491
+ * Executable tools supplied by the caller that owns this run (set by the
492
+ * agent workflow runner). Agent nodes merge these into their own tool list
493
+ * by name, so a node that selects `read_file` gets the caller's live,
494
+ * fully-wired tool instead of a builtin stub. Tools the caller does not
495
+ * provide fall back to builtin hydration.
496
+ */
497
+ _injectedTools = [];
498
+ /** Provider charge (USD) reported by the current node execution (e.g. FAL/KIE generation). */
499
+ _providerCost = null;
315
500
  /** Optional executor resolver for sub-workflow execution. */
316
501
  _resolveExecutor = null;
317
502
  /** Optional node type resolver for sub-workflow graph hydration. */
@@ -320,48 +505,119 @@ export class ProcessingContext {
320
505
  _totalCost = 0;
321
506
  /** Per-operation cost entries. */
322
507
  _operationCosts = [];
508
+ _generationLifecycle;
323
509
  constructor(opts) {
324
510
  this.jobId = opts.jobId;
325
511
  this.workflowId = opts.workflowId ?? null;
512
+ this.projectId = opts.projectId ?? null;
513
+ this.threadId = opts.threadId ?? null;
326
514
  this.userId = opts.userId ?? "default";
327
- this.workspaceDir = opts.workspaceDir ?? null;
328
- this.assetOutputMode = opts.assetOutputMode ?? "python";
515
+ this.workspace = resolveWorkspaceOption(opts);
516
+ this.workspaceDir = this.workspace?.localDir ?? opts.workspaceDir ?? null;
517
+ this.assetOutputMode = opts.assetOutputMode ?? "native";
518
+ this.persistOutputAssets = opts.persistOutputAssets ?? true;
519
+ this._generationLifecycle = opts.generationLifecycle ?? null;
520
+ this.appRunContext = opts.appRunContext ? Object.freeze({ ...opts.appRunContext }) : null;
521
+ this.runTraceContext = opts.runTraceContext ?? getRunTraceScope() ?? null;
522
+ if (this.runTraceContext?.secretValues instanceof Set) {
523
+ this._resolvedSecrets = this.runTraceContext.secretValues;
524
+ }
525
+ this.appRunCostAccount = opts.appRunCostAccount ?? (this.appRunContext ? { llmCostUsd: 0, unpriced: false } : null);
329
526
  this.cache = opts.cache ?? new MemoryCache();
330
527
  this.storage = opts.storage ?? null;
331
- this._onMessage = opts.onMessage ?? null;
528
+ this.assetStorage = opts.assetStorage ?? null;
529
+ this.workspaceStorage = opts.workspaceStorage ?? null;
530
+ if (opts.onMessage) {
531
+ this._messageListeners.add(opts.onMessage);
532
+ }
332
533
  this._variables = { ...(opts.variables ?? {}) };
333
534
  const env = {};
334
- for (const [k, v] of Object.entries(process.env)) {
335
- if (typeof v === "string")
535
+ for (const [k, v] of Object.entries(safeProcessEnv())) {
536
+ if (v != null)
336
537
  env[k] = v;
337
538
  }
338
539
  this.environment = { ...env, ...(opts.environment ?? {}) };
540
+ this.authToken = opts.authToken ?? null;
541
+ // An explicit option — including null — wins over the host default.
542
+ this.sandboxModuleCatalog =
543
+ opts.sandboxModuleCatalog !== undefined
544
+ ? opts.sandboxModuleCatalog
545
+ : getProcessSandboxModuleCatalog();
339
546
  this._secretResolver = opts.secretResolver ?? null;
340
547
  this._fetch =
341
548
  opts.fetchFn ??
342
549
  ((input, init) => fetch(input, init));
343
550
  this._tempUrlResolver = opts.tempUrlResolver ?? null;
344
551
  this._modelInterfaces = opts.modelInterfaces ?? null;
552
+ this._retainMessageQueue = opts.retainMessageQueue ?? true;
553
+ this.triggerEvent = opts.triggerEvent ?? null;
345
554
  }
346
- copy() {
555
+ /**
556
+ * Derive a child context.
557
+ *
558
+ * Defaults to full isolation: fresh agent memory, fresh message queue, the
559
+ * parent's message listeners inherited. Two opt-ins exist because a child
560
+ * that must *not* be isolated in one respect should not have to reach around
561
+ * the copy:
562
+ *
563
+ * - `shareMemory` shares the parent's {@link AgentMemory} instance, so a
564
+ * scoped run (own injected tools, own emit fan-out) still reads and
565
+ * writes the same step/task results.
566
+ * - `inheritMessageListeners: false` drops the parent's listeners, for a
567
+ * child that forwards messages itself and would otherwise double-notify.
568
+ *
569
+ * The run-level cancellation {@link signal} always carries over: a child that
570
+ * cannot observe the parent's Stop keeps burning provider calls after the
571
+ * user cancelled.
572
+ */
573
+ copy(opts) {
347
574
  const next = new ProcessingContext({
348
- jobId: this.jobId,
349
- workflowId: this.workflowId,
575
+ jobId: opts?.jobId ?? this.jobId,
576
+ projectId: this.projectId,
577
+ workflowId: opts?.workflowId !== undefined ? opts.workflowId : this.workflowId,
578
+ threadId: this.threadId,
350
579
  userId: this.userId,
351
- workspaceDir: this.workspaceDir,
580
+ workspace: opts?.workspace !== undefined ? opts.workspace : this.workspace,
352
581
  assetOutputMode: this.assetOutputMode,
582
+ persistOutputAssets: this.persistOutputAssets,
353
583
  cache: this.cache,
354
584
  storage: this.storage,
355
- onMessage: this._onMessage ?? undefined,
585
+ assetStorage: this.assetStorage,
586
+ workspaceStorage: this.workspaceStorage,
356
587
  variables: { ...this._variables },
357
588
  environment: { ...this.environment },
589
+ authToken: this.authToken,
590
+ sandboxModuleCatalog: this.sandboxModuleCatalog,
358
591
  fetchFn: this._fetch,
359
592
  secretResolver: this._secretResolver ?? undefined,
360
- tempUrlResolver: this._tempUrlResolver ?? undefined
593
+ tempUrlResolver: this._tempUrlResolver ?? undefined,
594
+ modelInterfaces: this._modelInterfaces ?? undefined,
595
+ retainMessageQueue: this._retainMessageQueue,
596
+ triggerEvent: this.triggerEvent,
597
+ generationLifecycle: opts?.generationLifecycle ?? this._generationLifecycle ?? undefined,
598
+ appRunContext: opts?.appRunContext ?? this.appRunContext ?? undefined,
599
+ runTraceContext: opts?.runTraceContext ?? this.runTraceContext ?? undefined,
600
+ appRunCostAccount: !opts?.appRunContext || opts.appRunContext.appRunId === this.appRunContext?.appRunId ? this.appRunCostAccount ?? undefined : undefined
361
601
  });
602
+ if (opts?.inheritMessageListeners !== false) {
603
+ for (const listener of this._messageListeners) {
604
+ next.addMessageListener(listener);
605
+ }
606
+ }
607
+ if (opts?.shareMemory) {
608
+ next.memory = this.memory;
609
+ }
610
+ if (!opts?.runTraceContext) {
611
+ next._resolvedSecrets = this._resolvedSecrets;
612
+ }
613
+ if (next.appRunContext?.appRunId === this.appRunContext?.appRunId) {
614
+ next._appRunDocuments = this._appRunDocuments;
615
+ }
616
+ next.signal = this.signal;
362
617
  next._providerResolver = this._providerResolver;
363
618
  next._modelInterfaces = this._modelInterfaces;
364
619
  next._sendControlEvent = this._sendControlEvent;
620
+ next._injectedTools = this._injectedTools;
365
621
  next._providers = new Map(this._providers);
366
622
  next._totalCost = this._totalCost;
367
623
  next._operationCosts = this._operationCosts.map((c) => ({ ...c }));
@@ -373,9 +629,66 @@ export class ProcessingContext {
373
629
  setProviderResolver(resolver) {
374
630
  this._providerResolver = resolver;
375
631
  }
632
+ /**
633
+ * Whether this context serves providers from an injected resolver rather
634
+ * than the process-wide registry.
635
+ *
636
+ * When it does, {@link getProvider} never reaches the registry and never
637
+ * reads a credential — so a credential the run will not use is not a reason
638
+ * to refuse the run. The execution preflight reads this to decide whether
639
+ * its credential check applies at all.
640
+ */
641
+ get hasProviderResolver() {
642
+ return this._providerResolver !== null;
643
+ }
376
644
  setModelInterfaces(modelInterfaces) {
377
645
  this._modelInterfaces = modelInterfaces;
378
646
  }
647
+ /**
648
+ * The interfaces this context answers with: its own when a caller wired
649
+ * them, else the host's process-wide default.
650
+ *
651
+ * The default is what keeps a run from depending on which entrance built
652
+ * its context. Six hosts assembled these by hand and three of them forgot
653
+ * `createAsset`, so the same workflow saved an image under `workflows run`
654
+ * and died under `debug` with "model interface is not configured".
655
+ *
656
+ * The two compose key by key, the context's own winning, so wiring one
657
+ * interface does not uninstall the rest of the floor.
658
+ */
659
+ modelInterfaces() {
660
+ const own = this._modelInterfaces;
661
+ const floor = getDefaultModelInterfaces();
662
+ if (!own)
663
+ return floor;
664
+ if (!floor)
665
+ return own;
666
+ // Per key, not per object: a host that wires two interfaces of its own
667
+ // keeps the other twelve. Replacing wholesale is how the HTTP run route
668
+ // lost `getTimelineSequence` while setting only the asset pair.
669
+ return { ...floor, ...own };
670
+ }
671
+ /**
672
+ * Whether a given model interface is wired for this context — its own or
673
+ * the host default. Lets callers decide between the DB-backed path
674
+ * (interface present — let errors propagate) and an in-memory fallback
675
+ * (interface absent — hermetic CLI runs, tests) without invoking a method
676
+ * that would throw when unwired.
677
+ */
678
+ hasModelInterface(name) {
679
+ return isCallable(this.modelInterfaces()?.[name]);
680
+ }
681
+ /** Read a user setting, falling back to this run's environment. */
682
+ async getSetting(key) {
683
+ const fn = this.modelInterfaces()?.getSetting;
684
+ if (fn) {
685
+ const value = await fn({ userId: this.userId, key });
686
+ if (value !== null && value.length > 0)
687
+ return value;
688
+ }
689
+ const value = this.environment[key];
690
+ return value !== undefined && value.length > 0 ? value : null;
691
+ }
379
692
  // -----------------------------------------------------------------------
380
693
  // Control event dispatch
381
694
  // -----------------------------------------------------------------------
@@ -396,6 +709,19 @@ export class ProcessingContext {
396
709
  return null;
397
710
  return this._sendControlEvent(targetNodeId, properties);
398
711
  }
712
+ /**
713
+ * Supply the live tool set for this run. Agent nodes match their selected
714
+ * tool names against these and prefer them over builtin hydration, so tools
715
+ * that only the caller can wire (MCP, workspace, skills, security-gated
716
+ * wrappers) reach nodes the caller did not author.
717
+ */
718
+ setInjectedTools(tools) {
719
+ this._injectedTools = tools;
720
+ }
721
+ /** Injected tool matching `name`, or null when the caller supplied none. */
722
+ getInjectedTool(name) {
723
+ return this._injectedTools.find((tool) => tool.name === name) ?? null;
724
+ }
399
725
  // -----------------------------------------------------------------------
400
726
  // Sub-workflow executor resolution
401
727
  // -----------------------------------------------------------------------
@@ -411,9 +737,6 @@ export class ProcessingContext {
411
737
  get resolveNodeType() {
412
738
  return this._resolveNodeType;
413
739
  }
414
- /**
415
- * Check if control event dispatch is available.
416
- */
417
740
  get hasControlEventSupport() {
418
741
  return this._sendControlEvent !== null;
419
742
  }
@@ -427,40 +750,32 @@ export class ProcessingContext {
427
750
  const cached = this._providers.get(providerId);
428
751
  if (cached)
429
752
  return cached;
430
- if (!this._providerResolver) {
431
- const { getRegisteredProvider } = await import("./providers/index.js");
432
- const reg = getRegisteredProvider(providerId);
433
- if (!reg) {
434
- throw new Error(`No provider registered for "${providerId}"`);
435
- }
436
- // Resolve any missing secret kwargs from the context's own secret resolver
437
- const kwargs = { ...reg.kwargs };
438
- for (const [key, value] of Object.entries(kwargs)) {
439
- if (!value) {
440
- const envVal = process.env[key];
441
- if (envVal) {
442
- kwargs[key] = envVal;
443
- }
444
- else {
445
- const secret = await this.getSecret(key);
446
- if (secret) {
447
- kwargs[key] = secret;
448
- }
449
- }
450
- }
451
- }
452
- const resolved = new reg.cls(kwargs);
753
+ const pending = this._providerPromises.get(providerId);
754
+ if (pending)
755
+ return pending;
756
+ const resolution = (async () => {
757
+ const resolved = this._providerResolver
758
+ ? await this._providerResolver(providerId)
759
+ : await import("./providers/index.js").then(({ getProvider }) => getProvider(providerId, (key) => this.getSecret(key)));
453
760
  this._providers.set(providerId, resolved);
454
761
  resolved.setMessageEmitter((msg) => this.postMessage(msg));
455
762
  return resolved;
763
+ })();
764
+ this._providerPromises.set(providerId, resolution);
765
+ try {
766
+ return await resolution;
767
+ }
768
+ finally {
769
+ this._providerPromises.delete(providerId);
456
770
  }
457
- const resolved = await this._providerResolver(providerId);
458
- this._providers.set(providerId, resolved);
459
- resolved.setMessageEmitter((msg) => this.postMessage(msg));
460
- return resolved;
461
771
  }
462
- async get_provider(providerId) {
463
- return this.getProvider(providerId);
772
+ /**
773
+ * Check whether a registered provider has all required credentials
774
+ * resolvable through this context (DB/keychain/env).
775
+ */
776
+ async isProviderConfigured(providerId) {
777
+ const { isProviderConfigured: check } = await import("./providers/index.js");
778
+ return check(providerId, (key) => this.getSecret(key));
464
779
  }
465
780
  // -----------------------------------------------------------------------
466
781
  // Secrets / Variables API
@@ -471,12 +786,55 @@ export class ProcessingContext {
471
786
  setTempUrlResolver(resolver) {
472
787
  this._tempUrlResolver = resolver;
473
788
  }
789
+ /**
790
+ * Resolve a raw storage URI (e.g. `file:///.../storage/<key>`) to a URL the
791
+ * UI can fetch (e.g. `/api/storage/<key>`). Falls back to returning the URI
792
+ * unchanged when no resolver is wired (DSL / tests).
793
+ *
794
+ * Use this from tools that write binary output via `context.storage.store()`
795
+ * and want to surface a UI-renderable URL in their result.
796
+ */
797
+ async resolveTempUrl(uri) {
798
+ if (!this._tempUrlResolver)
799
+ return uri;
800
+ const resolvedUri = await this._tempUrlResolver(uri);
801
+ if (ProcessingContext.isClientFetchableResolvedAssetUri(uri, resolvedUri)) {
802
+ return resolvedUri;
803
+ }
804
+ throw new Error(`Temp URL resolver returned an unsafe URL for '${uri}': '${resolvedUri}'`);
805
+ }
806
+ /**
807
+ * Whether this run can reach a secret store at all.
808
+ *
809
+ * {@link getSecret} answers `null` both for "no store is wired" and for "the
810
+ * store does not hold this key", which a caller asking *which* of a list of
811
+ * credentials exists cannot tell apart — and reporting every declared
812
+ * credential as missing because no store was reachable is exactly the
813
+ * false alarm the graph validator's `missing_secret` check refuses to raise.
814
+ * A host installs the availability callback only when this is true.
815
+ */
816
+ get hasSecretResolver() {
817
+ return this._secretResolver !== null;
818
+ }
474
819
  async getSecret(key) {
475
820
  if (!this._secretResolver)
476
821
  return null;
477
822
  const value = await this._secretResolver(key, this.userId);
823
+ if (value != null && value.length > 0) {
824
+ this._resolvedSecrets.add(value);
825
+ }
478
826
  return value ?? null;
479
827
  }
828
+ /**
829
+ * Every secret value handed out during this run. The supervisor's escalation
830
+ * constructor masks against this set — without it, "mask every secret" has no
831
+ * subject, because the resolver forgets each key as soon as it answers.
832
+ * Values shorter than `MIN_MASKABLE_SECRET_LENGTH` are not recorded: masking
833
+ * a two-character value would redact unrelated text everywhere it occurs.
834
+ */
835
+ getResolvedSecretValues() {
836
+ return this._resolvedSecrets;
837
+ }
480
838
  async getSecretRequired(key) {
481
839
  const value = await this.getSecret(key);
482
840
  if (value === null || value === "") {
@@ -484,12 +842,6 @@ export class ProcessingContext {
484
842
  }
485
843
  return value;
486
844
  }
487
- async get_secret(key) {
488
- return this.getSecret(key);
489
- }
490
- async get_secret_required(key) {
491
- return this.getSecretRequired(key);
492
- }
493
845
  get(key, defaultValue) {
494
846
  if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
495
847
  return this._variables[key];
@@ -498,72 +850,93 @@ export class ProcessingContext {
498
850
  }
499
851
  set(key, value) {
500
852
  this._variables[key] = value;
501
- void this.persistVariableIfNeeded(key, value);
502
853
  if (key.startsWith("memory://")) {
503
854
  this._memory.set(key, value);
504
855
  }
505
856
  }
506
- async storeStepResult(key, value) {
507
- const path = this.workspacePathFor(`${key}.json`);
508
- await mkdir(dirname(path), { recursive: true });
509
- await writeFile(path, JSON.stringify(value, null, 2), "utf8");
510
- this._variables[key] = { __workspace_result__: `${key}.json` };
511
- return path;
857
+ /** Whether a shared variable with this name has been set. */
858
+ hasVariable(key) {
859
+ return Object.prototype.hasOwnProperty.call(this._variables, key);
512
860
  }
513
- async loadStepResult(key, defaultValue) {
514
- const marker = this._variables[key];
515
- if (marker &&
516
- typeof marker === "object" &&
517
- "__workspace_result__" in marker) {
518
- const rel = marker
519
- .__workspace_result__;
520
- const path = this.workspacePathFor(String(rel));
521
- try {
522
- const raw = await readFile(path, "utf8");
523
- return JSON.parse(raw);
524
- }
525
- catch {
526
- // File missing or corrupt — fall back to default.
527
- return defaultValue;
861
+ /**
862
+ * Snapshot of all shared variables (name → value). Used by nodes that
863
+ * resolve variable references against the workflow's shared context — e.g.
864
+ * the Prompt node merges these into its `{{ name }}` template variables so a
865
+ * Set Variable node upstream can supply values without an explicit wire.
866
+ */
867
+ getVariables() {
868
+ return { ...this._variables };
869
+ }
870
+ // -----------------------------------------------------------------------
871
+ // Variable channels (Set/Get Variable)
872
+ // -----------------------------------------------------------------------
873
+ /**
874
+ * Declare how many Set Variable nodes write to `name`. The runner calls this
875
+ * before any actor runs so a channel with no writers can close immediately
876
+ * (readers get end-of-stream instead of hanging).
877
+ */
878
+ registerChannelWriters(name, count) {
879
+ this._channelWriters.set(name, (this._channelWriters.get(name) ?? 0) + count);
880
+ }
881
+ /**
882
+ * Get (or lazily create) the named variable channel. A channel created for a
883
+ * name with no registered writers is returned already closed, so a reader
884
+ * referencing an undefined variable resolves immediately rather than waiting
885
+ * forever.
886
+ */
887
+ getChannel(name) {
888
+ let channel = this._channels.get(name);
889
+ if (!channel) {
890
+ channel = new VariableChannel();
891
+ this._channels.set(name, channel);
892
+ if ((this._channelWriters.get(name) ?? 0) === 0) {
893
+ channel.close();
528
894
  }
529
895
  }
530
- if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
531
- return this._variables[key];
532
- }
533
- return defaultValue;
896
+ return channel;
534
897
  }
535
- workspacePathFor(fileName) {
536
- if (!this.workspaceDir) {
537
- throw new Error("workspace_dir is required to persist workflow variables");
898
+ /**
899
+ * Record that one Set Variable writer for `name` has finished. When the last
900
+ * writer finishes, the channel closes — readers drain the buffered values
901
+ * then end. The runner calls this from each Set Variable actor's completion.
902
+ */
903
+ markChannelWriterDone(name) {
904
+ const remaining = (this._channelWriters.get(name) ?? 0) - 1;
905
+ this._channelWriters.set(name, Math.max(0, remaining));
906
+ if (remaining <= 0) {
907
+ this.getChannel(name).close();
538
908
  }
539
- return resolveWorkspacePath(this.workspaceDir, fileName);
540
909
  }
541
- async persistVariableIfNeeded(key, value) {
542
- if (!this.workspaceDir)
543
- return;
544
- if (value === undefined || value === null)
545
- return;
546
- if (typeof value === "function" ||
547
- typeof value === "symbol" ||
548
- typeof value === "bigint")
549
- return;
550
- const filePath = this.workspacePathFor(`var_${key}.json`);
551
- try {
552
- await mkdir(dirname(filePath), { recursive: true });
553
- await writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
910
+ /** Close every channel — a teardown backstop so no reader is left waiting. */
911
+ closeAllChannels() {
912
+ for (const channel of this._channels.values()) {
913
+ channel.close();
554
914
  }
555
- catch {
556
- // Best-effort persistence.
915
+ }
916
+ /** Whether any Set Variable node was registered as writing to `name`. */
917
+ hasChannelWriters(name) {
918
+ return (this._channelWriters.get(name) ?? 0) > 0;
919
+ }
920
+ async storeStepResult(key, value) {
921
+ this._variables[key] = value;
922
+ return "";
923
+ }
924
+ async loadStepResult(key, defaultValue) {
925
+ if (Object.prototype.hasOwnProperty.call(this._variables, key)) {
926
+ return this._variables[key];
557
927
  }
928
+ return defaultValue;
558
929
  }
559
930
  // -----------------------------------------------------------------------
560
931
  // Node result cache helpers
561
932
  // -----------------------------------------------------------------------
562
933
  generateNodeCacheKey(nodeType, nodeProps) {
563
- const normalizedProps = nodeProps && typeof nodeProps === "object"
564
- ? JSON.stringify(nodeProps, Object.keys(nodeProps).sort())
565
- : JSON.stringify(nodeProps ?? null);
566
- return `${this.userId}:${nodeType}:${normalizedProps}`;
934
+ // Deep, deterministic serialization: keys are sorted at EVERY nesting
935
+ // level. The previous `JSON.stringify(props, sortedTopLevelKeys)` form used
936
+ // the key array as a recursive replacer allow-list, which silently dropped
937
+ // every nested value — so nodes differing only in a nested prop collided on
938
+ // the same key and returned each other's cached (wrong) results.
939
+ return `${this.userId}:${nodeType}:${stableStringifyDeep(nodeProps ?? null)}`;
567
940
  }
568
941
  async getCachedResult(nodeType, nodeProps) {
569
942
  const key = this.generateNodeCacheKey(nodeType, nodeProps);
@@ -576,12 +949,20 @@ export class ProcessingContext {
576
949
  // -----------------------------------------------------------------------
577
950
  // Message queue API
578
951
  // -----------------------------------------------------------------------
952
+ addMessageListener(listener) {
953
+ this._messageListeners.add(listener);
954
+ return () => {
955
+ this._messageListeners.delete(listener);
956
+ };
957
+ }
579
958
  /**
580
959
  * Emit a processing message.
581
- * Appended to the internal queue and forwarded to listener if set.
960
+ * Appended to the internal queue and forwarded to listeners if set.
582
961
  */
583
962
  emit(msg) {
584
- this._messages.push(msg);
963
+ if (this._retainMessageQueue) {
964
+ this._messages.push(msg);
965
+ }
585
966
  this._notifyMessage();
586
967
  if (msg.type === "node_update" && msg.node_id) {
587
968
  this._nodeStatuses.set(msg.node_id, msg);
@@ -589,17 +970,29 @@ export class ProcessingContext {
589
970
  if (msg.type === "edge_update" && msg.edge_id) {
590
971
  this._edgeStatuses.set(msg.edge_id, msg);
591
972
  }
592
- if (this._onMessage) {
593
- this._onMessage(msg);
973
+ for (const listener of this._messageListeners) {
974
+ try {
975
+ listener(msg);
976
+ }
977
+ catch {
978
+ // Listeners are observers and must not interrupt workflow execution.
979
+ }
594
980
  }
595
981
  }
596
982
  postMessage(msg) {
597
983
  this.emit(msg);
598
984
  }
599
- post_message(msg) {
600
- this.emit(msg);
985
+ /** Record actual provider charge for the current node run (attached to completed NodeUpdate). */
986
+ setProviderCost(provider, amount, unit, details) {
987
+ this._providerCost = { provider, amount, unit, ...details };
988
+ recordInvocationCost(amount);
989
+ }
990
+ getProviderCost() {
991
+ return this._providerCost;
992
+ }
993
+ clearProviderCost() {
994
+ this._providerCost = null;
601
995
  }
602
- /** Get all emitted messages. */
603
996
  getMessages() {
604
997
  return this._messages;
605
998
  }
@@ -609,39 +1002,31 @@ export class ProcessingContext {
609
1002
  popMessage() {
610
1003
  return this._messages.shift();
611
1004
  }
612
- _messageResolve = null;
1005
+ _messageWaiters = [];
613
1006
  /** Notify that a new message has been pushed. */
614
1007
  _notifyMessage() {
615
- if (this._messageResolve) {
616
- const resolve = this._messageResolve;
617
- this._messageResolve = null;
1008
+ const waiters = this._messageWaiters;
1009
+ this._messageWaiters = [];
1010
+ for (const resolve of waiters)
618
1011
  resolve();
619
- }
620
1012
  }
621
1013
  async popMessageAsync() {
622
1014
  while (this._messages.length === 0) {
623
1015
  await new Promise((r) => {
624
- this._messageResolve = r;
1016
+ this._messageWaiters.push(r);
625
1017
  });
626
1018
  }
627
1019
  return this._messages.shift();
628
1020
  }
629
- async pop_message_async() {
630
- return this.popMessageAsync();
631
- }
632
1021
  getNodeStatuses() {
633
1022
  return Object.fromEntries(this._nodeStatuses);
634
1023
  }
635
1024
  getEdgeStatuses() {
636
1025
  return Object.fromEntries(this._edgeStatuses);
637
1026
  }
638
- /** Clear the message queue. */
639
1027
  clearMessages() {
640
1028
  this._messages = [];
641
1029
  }
642
- clear_messages() {
643
- this.clearMessages();
644
- }
645
1030
  trackOperationCost(operation, cost, metadata) {
646
1031
  const safeCost = Number.isFinite(cost) ? cost : 0;
647
1032
  this._totalCost += safeCost;
@@ -663,6 +1048,26 @@ export class ProcessingContext {
663
1048
  getTotalCost() {
664
1049
  return this._totalCost;
665
1050
  }
1051
+ /** Null means at least one LLM call could not be priced. */
1052
+ getAppRunLlmCost() {
1053
+ return this.appRunCostAccount && !this.appRunCostAccount.unpriced
1054
+ ? this.appRunCostAccount.llmCostUsd
1055
+ : null;
1056
+ }
1057
+ getAppRunDocuments() {
1058
+ return [...this._appRunDocuments.values()].map((reference) => ({
1059
+ ...reference
1060
+ }));
1061
+ }
1062
+ recordAppRunDocument(kind, document) {
1063
+ if (this.appRunContext && document) {
1064
+ this._appRunDocuments.set(`${kind}:${document.id}`, {
1065
+ kind,
1066
+ id: document.id
1067
+ });
1068
+ }
1069
+ return document;
1070
+ }
666
1071
  getOperationCosts() {
667
1072
  return this._operationCosts;
668
1073
  }
@@ -694,6 +1099,10 @@ export class ProcessingContext {
694
1099
  // -----------------------------------------------------------------------
695
1100
  // HTTP helpers
696
1101
  // -----------------------------------------------------------------------
1102
+ /**
1103
+ * Issue an HTTP request with the retry/backoff policy every node shares.
1104
+ * `httpGet` is the GET shorthand; other verbs go through this directly.
1105
+ */
697
1106
  async httpRequestWithRetries(method, url, opts = {}) {
698
1107
  const retry = opts.retry ?? {};
699
1108
  const maxRetries = retry.maxRetries ?? 3;
@@ -719,22 +1128,38 @@ export class ProcessingContext {
719
1128
  const retryAfter = response.headers.get("Retry-After");
720
1129
  const retryDelay = retryAfter ? Number(retryAfter) * 1000 : NaN;
721
1130
  const delayMs = Number.isFinite(retryDelay)
722
- ? retryDelay
1131
+ ? Math.min(Math.max(0, retryDelay), 30_000)
723
1132
  : backoffMs * 2 ** attempt;
724
- await new Promise((r) => setTimeout(r, Math.max(0, delayMs)));
1133
+ // Drain/cancel the unconsumed body before retrying, or under undici
1134
+ // the socket stays checked out of the connection pool (leaked until
1135
+ // GC) and emits a "body not consumed" warning.
1136
+ await response.body?.cancel().catch(() => { });
1137
+ await waitForRetry(Math.max(0, delayMs), opts.signal);
725
1138
  continue;
726
1139
  }
727
1140
  if (!response.ok) {
728
- throw new Error(`HTTP ${response.status} ${response.statusText} for ${method} ${url}`);
1141
+ // Non-retryable HTTP error (status not in retryStatuses): fail fast.
1142
+ // Throwing into the generic catch below would treat it like a network
1143
+ // error and retry it, hammering 4xx responses maxRetries times.
1144
+ const nonRetryable = new Error(`HTTP ${response.status} ${response.statusText} for ${method} ${url}`);
1145
+ nonRetryable.nonRetryable = true;
1146
+ await response.body?.cancel().catch(() => { });
1147
+ throw nonRetryable;
729
1148
  }
730
1149
  return response;
731
1150
  }
732
1151
  catch (error) {
733
1152
  lastError = error;
1153
+ if (opts.signal?.aborted)
1154
+ break;
1155
+ // A non-retryable HTTP error must not loop; only thrown fetch/network
1156
+ // errors and retryable statuses are retried.
1157
+ if (error?.nonRetryable)
1158
+ break;
734
1159
  if (attempt >= maxRetries - 1)
735
1160
  break;
736
1161
  const delayMs = backoffMs * 2 ** attempt;
737
- await new Promise((r) => setTimeout(r, delayMs));
1162
+ await waitForRetry(delayMs, opts.signal);
738
1163
  }
739
1164
  }
740
1165
  throw lastError instanceof Error
@@ -744,50 +1169,13 @@ export class ProcessingContext {
744
1169
  async httpGet(url, opts = {}) {
745
1170
  return this.httpRequestWithRetries("GET", url, opts);
746
1171
  }
747
- async httpPost(url, opts = {}) {
748
- return this.httpRequestWithRetries("POST", url, opts);
749
- }
750
- async httpPatch(url, opts = {}) {
751
- return this.httpRequestWithRetries("PATCH", url, opts);
752
- }
753
- async httpPut(url, opts = {}) {
754
- return this.httpRequestWithRetries("PUT", url, opts);
755
- }
756
- async httpDelete(url, opts = {}) {
757
- return this.httpRequestWithRetries("DELETE", url, opts);
758
- }
759
- async httpHead(url, opts = {}) {
760
- return this.httpRequestWithRetries("HEAD", url, opts);
761
- }
762
1172
  async downloadFile(url, opts = {}) {
763
1173
  const response = await this.httpGet(url, opts);
764
1174
  const arr = await response.arrayBuffer();
765
1175
  return new Uint8Array(arr);
766
1176
  }
767
- async downloadText(url, opts = {}) {
768
- const response = await this.httpGet(url, opts);
769
- return response.text();
770
- }
771
- async http_get(url, opts = {}) {
772
- return this.httpGet(url, opts);
773
- }
774
- async http_post(url, opts = {}) {
775
- return this.httpPost(url, opts);
776
- }
777
- async http_patch(url, opts = {}) {
778
- return this.httpPatch(url, opts);
779
- }
780
- async http_put(url, opts = {}) {
781
- return this.httpPut(url, opts);
782
- }
783
- async http_delete(url, opts = {}) {
784
- return this.httpDelete(url, opts);
785
- }
786
- async http_head(url, opts = {}) {
787
- return this.httpHead(url, opts);
788
- }
789
1177
  requireModelInterface(name) {
790
- const fn = this._modelInterfaces?.[name];
1178
+ const fn = this.modelInterfaces()?.[name];
791
1179
  if (!fn) {
792
1180
  throw new Error(`ProcessingContext model interface '${String(name)}' is not configured`);
793
1181
  }
@@ -797,34 +1185,155 @@ export class ProcessingContext {
797
1185
  const fn = this.requireModelInterface("getJob");
798
1186
  return fn({ userId: this.userId, jobId });
799
1187
  }
800
- async get_job(jobId) {
801
- return this.getJob(jobId);
802
- }
1188
+ /**
1189
+ * HOLDOUT (anti-slop/no-unknown-returns): see the `createAsset` port above —
1190
+ * narrowing the result breaks the `as Record<string, unknown>` assertions in
1191
+ * `image-nodes` and `audio-nodes`.
1192
+ */
803
1193
  async createAsset(args) {
804
1194
  const fn = this.requireModelInterface("createAsset");
805
1195
  const content = args.content ?? args.instructions;
806
1196
  if (!content) {
807
1197
  throw new Error("Asset content is required");
808
1198
  }
1199
+ recordInvocationAsset();
809
1200
  return fn({
810
1201
  userId: this.userId,
811
1202
  workflowId: this.workflowId,
812
1203
  jobId: this.jobId,
1204
+ projectId: this.projectId,
813
1205
  name: args.name,
814
1206
  contentType: args.contentType,
815
1207
  content,
816
1208
  parentId: args.parentId ?? null,
817
- nodeId: args.nodeId ?? null
1209
+ nodeId: args.nodeId ?? null,
1210
+ metadata: args.metadata ?? null
818
1211
  });
819
1212
  }
820
- async create_asset(args) {
821
- return this.createAsset(args);
1213
+ /**
1214
+ * Replace an owned asset's bytes, keeping its id. Null when the asset is
1215
+ * missing or belongs to someone else.
1216
+ */
1217
+ async updateAssetBytes(args) {
1218
+ const fn = this.requireModelInterface("updateAssetBytes");
1219
+ return fn({ userId: this.userId, ...args });
1220
+ }
1221
+ /** Load a persisted sketch (image document) owned by the current user. */
1222
+ async getImageDocument(id) {
1223
+ const fn = this.requireModelInterface("getImageDocument");
1224
+ return fn({ userId: this.userId, id });
1225
+ }
1226
+ /** Create a persisted sketch (image document) owned by the current user. */
1227
+ async createImageDocument(args) {
1228
+ const fn = this.requireModelInterface("createImageDocument");
1229
+ const document = await fn({ userId: this.userId, ...args });
1230
+ return this.recordAppRunDocument("sketch", document);
1231
+ }
1232
+ /** Load a persisted timeline sequence owned by the current user. */
1233
+ async getTimelineSequence(id) {
1234
+ const fn = this.requireModelInterface("getTimelineSequence");
1235
+ return fn({ userId: this.userId, id });
1236
+ }
1237
+ /** Create a persisted timeline sequence owned by the current user. */
1238
+ async createTimelineSequence(sequence) {
1239
+ const fn = this.requireModelInterface("createTimelineSequence");
1240
+ const document = await fn({ userId: this.userId, sequence });
1241
+ return this.recordAppRunDocument("timeline", document);
1242
+ }
1243
+ /** Replace a persisted timeline sequence's document. */
1244
+ async updateTimelineSequence(id, sequence) {
1245
+ const fn = this.requireModelInterface("updateTimelineSequence");
1246
+ const document = await fn({ userId: this.userId, id, sequence });
1247
+ return this.recordAppRunDocument("timeline", document);
1248
+ }
1249
+ /** Load a persisted script owned by the current user. */
1250
+ async getScript(id) {
1251
+ const fn = this.requireModelInterface("getScript");
1252
+ return fn({ userId: this.userId, id });
1253
+ }
1254
+ /** Create a persisted script owned by the current user. */
1255
+ async createScript(args) {
1256
+ const fn = this.requireModelInterface("createScript");
1257
+ const document = await fn({ userId: this.userId, ...args });
1258
+ return this.recordAppRunDocument("script", document);
1259
+ }
1260
+ /** Replace a persisted script's document (and optional timeline link). */
1261
+ async updateScript(id, args) {
1262
+ const fn = this.requireModelInterface("updateScript");
1263
+ const document = await fn({ userId: this.userId, id, ...args });
1264
+ return this.recordAppRunDocument("script", document);
1265
+ }
1266
+ /** Load a persisted storyboard owned by the current user. */
1267
+ async getStoryboard(id) {
1268
+ const fn = this.requireModelInterface("getStoryboard");
1269
+ return fn({ userId: this.userId, id });
1270
+ }
1271
+ /** The current user's storyboards, narrowed by the given filters. */
1272
+ async listStoryboards(args = {}) {
1273
+ const fn = this.requireModelInterface("listStoryboards");
1274
+ return fn({ userId: this.userId, ...args });
1275
+ }
1276
+ /** The copy this user derived from a template for one recast mapping. */
1277
+ async findRecastStoryboard(args) {
1278
+ const fn = this.requireModelInterface("findRecastStoryboard");
1279
+ return fn({ userId: this.userId, ...args });
1280
+ }
1281
+ /** Create a persisted storyboard owned by the current user. */
1282
+ async createStoryboard(args) {
1283
+ const fn = this.requireModelInterface("createStoryboard");
1284
+ const document = await fn({ userId: this.userId, ...args });
1285
+ return this.recordAppRunDocument("storyboard", document);
1286
+ }
1287
+ /** Replace a persisted storyboard's document (and optional timeline link). */
1288
+ async updateStoryboard(id, args) {
1289
+ const fn = this.requireModelInterface("updateStoryboard");
1290
+ const document = await fn({ userId: this.userId, id, ...args });
1291
+ return this.recordAppRunDocument("storyboard", document);
1292
+ }
1293
+ /** The current user's entity library, narrowed by the given filters. */
1294
+ async listEntities(args = {}) {
1295
+ const fn = this.requireModelInterface("listEntities");
1296
+ return fn({ userId: this.userId, ...args });
1297
+ }
1298
+ /** Read one entity owned by the current user. */
1299
+ async getEntity(id) {
1300
+ const fn = this.requireModelInterface("getEntity");
1301
+ return fn({ userId: this.userId, id });
1302
+ }
1303
+ /** Create or update one entity in the current user's library. */
1304
+ async upsertEntity(args) {
1305
+ const fn = this.requireModelInterface("upsertEntity");
1306
+ return fn({ userId: this.userId, ...args });
822
1307
  }
823
- resolveSandboxFilePath(path) {
824
- if (this.workspaceDir == null || this.workspaceDir === "") {
825
- throw new Error("workspaceDir is required for sandbox file operations");
826
- }
827
- const workspaceRoot = resolve(this.workspaceDir);
1308
+ /**
1309
+ * Recursively list the non-folder assets in `folderId`, or `null` when the id
1310
+ * is not a folder. Backed by the optional `listFolderAssets` model interface;
1311
+ * returns `null` when that interface is not configured.
1312
+ */
1313
+ async listFolderAssets(folderId) {
1314
+ const fn = this._modelInterfaces?.listFolderAssets;
1315
+ if (!fn) {
1316
+ return null;
1317
+ }
1318
+ return fn({ userId: this.userId, folderId });
1319
+ }
1320
+ /**
1321
+ * Look up one owned asset's identity + metadata, or `null` when missing /
1322
+ * not owned. Backed by the optional `getAssetInfo` model interface; returns
1323
+ * `null` when that interface is not configured.
1324
+ */
1325
+ async getAssetInfo(assetId) {
1326
+ const fn = this.modelInterfaces()?.getAssetInfo;
1327
+ if (!fn) {
1328
+ return null;
1329
+ }
1330
+ return fn({ userId: this.userId, assetId });
1331
+ }
1332
+ resolveSandboxFilePath(path) {
1333
+ if (this.workspaceDir == null || this.workspaceDir === "") {
1334
+ throw new Error("workspaceDir is required for sandbox file operations");
1335
+ }
1336
+ const workspaceRoot = resolve(this.workspaceDir);
828
1337
  const normalizedPath = path.replaceAll("\\", "/");
829
1338
  if ((isAbsolute(normalizedPath) || /^[A-Za-z]:\//.test(normalizedPath)) &&
830
1339
  isWithinRoot(workspaceRoot, resolve(normalizedPath))) {
@@ -876,110 +1385,393 @@ export class ProcessingContext {
876
1385
  if (!trimmed) {
877
1386
  return [];
878
1387
  }
879
- const raw = trimmed.startsWith("asset://") ? trimmed.slice("asset://".length) : trimmed;
880
- const primary = raw.split(/[/?#]/)[0];
1388
+ // Strip a leading reference prefix down to the bare storage key. `asset://`
1389
+ // is the ref scheme; `/api/storage/` (and its slash-less variant) is the
1390
+ // browser-facing storage route that older messages / callers still carry —
1391
+ // treating that whole path as the id probes a bogus key and builds a
1392
+ // double-prefixed, percent-encoded HTTP url that 404s.
1393
+ let raw = trimmed;
1394
+ if (raw.startsWith("asset://")) {
1395
+ raw = raw.slice("asset://".length);
1396
+ }
1397
+ else if (raw.startsWith("/api/storage/")) {
1398
+ raw = raw.slice("/api/storage/".length);
1399
+ }
1400
+ else if (raw.startsWith("api/storage/")) {
1401
+ raw = raw.slice("api/storage/".length);
1402
+ }
1403
+ // Preserve sub-paths: `asset://user-1/image.png` -> primary `user-1/image.png`,
1404
+ // so storage keys with hierarchical layouts still resolve. Drop any
1405
+ // `?query`/`#hash` before deriving the key.
1406
+ const primary = raw.split(/[?#]/)[0];
881
1407
  if (!primary) {
882
1408
  return [];
883
1409
  }
884
- const withoutExt = primary.replace(/\.[^.]+$/, "");
1410
+ // Strip the extension from the LAST path segment only. A `.` in an earlier
1411
+ // segment (`folder.v2/img`) is part of the id, not an extension — slicing
1412
+ // across `/` would yield a wrong-bytes candidate (`folder`).
1413
+ const slash = primary.lastIndexOf("/");
1414
+ const dir = slash >= 0 ? primary.slice(0, slash + 1) : "";
1415
+ const lastSegment = slash >= 0 ? primary.slice(slash + 1) : primary;
1416
+ const withoutExt = dir + lastSegment.replace(/\.[^.]+$/, "");
885
1417
  return Array.from(new Set([primary, withoutExt].filter(Boolean)));
886
1418
  }
887
- async assetToSandbox(assetId, path) {
888
- const outputPath = this.resolveSandboxFilePath(path);
889
- const uriCandidates = new Set();
890
- const idCandidates = this.parseAssetIdCandidates(assetId);
891
- const trimmed = assetId.trim();
892
- const resolutionAttempts = [];
893
- if (trimmed.includes("://") && !trimmed.startsWith("asset://")) {
894
- uriCandidates.add(trimmed);
1419
+ /**
1420
+ * The adapters an asset reference is looked up in. Assets live in the asset
1421
+ * store; `this.storage` is the *temp* store on server paths. The asset
1422
+ * adapter goes first and the temp adapter stays as the fallback so
1423
+ * runtime-materialized refs still resolve.
1424
+ */
1425
+ assetAdapters() {
1426
+ return [this.assetStorage, this.storage].filter((adapter, index, all) => adapter !== null && all.indexOf(adapter) === index);
1427
+ }
1428
+ /**
1429
+ * Keys an asset id candidate may be stored under: `<userId>/<id>.<ext>`
1430
+ * (uploads — the current owner-prefixed layout), `<id>.<ext>` (the flat
1431
+ * legacy layout) and `assets/<id>` (runtime-materialized refs). The
1432
+ * owner-prefixed key goes first: without it every reference misses all the
1433
+ * exact lookups and falls through to the prefix listing, which degrades to
1434
+ * `list("")` — a full recursive walk / whole-bucket `listObjectsV2` across
1435
+ * every tenant, on every asset reference.
1436
+ */
1437
+ assetKeysFor(candidate) {
1438
+ const keys = [candidate, `assets/${candidate}`];
1439
+ if (this.userId && !candidate.startsWith(`${this.userId}/`)) {
1440
+ keys.unshift(`${this.userId}/${candidate}`);
895
1441
  }
896
- for (const candidate of idCandidates) {
897
- for (const prefix of ["memory://", "file://", "s3://"]) {
898
- uriCandidates.add(`${prefix}${candidate}`);
899
- uriCandidates.add(`${prefix}assets/${candidate}`);
1442
+ return keys;
1443
+ }
1444
+ /**
1445
+ * Prefixes to list, narrowest first, when no exact key matched.
1446
+ *
1447
+ * S3 treats `list(<owner>/<bareId>)` as a raw-string prefix match — bounded
1448
+ * to a handful of entries. `list("")` is the last resort: it is a
1449
+ * multi-thousand-object scan on production S3 backends, and on Supabase it
1450
+ * lists only the bucket's immediate children — which under the
1451
+ * owner-prefixed layout are folders, so it never sees an asset at all.
1452
+ *
1453
+ * The owner prefix is what makes this work for uploads. They are stored at
1454
+ * `<userId>/<id>.<ext>`, and a ref that carries no extension (`asset://<id>`,
1455
+ * or a bare `asset_id`) misses every exact-key lookup. Listing the owner's
1456
+ * own folder finds it on every adapter: hierarchical ones
1457
+ * (file/supabase/memory) list its children, and prefix-matching ones (S3)
1458
+ * stay scoped to that one user.
1459
+ */
1460
+ assetListingPrefixes(bareId) {
1461
+ const owner = this.userId;
1462
+ return owner && !bareId.startsWith(`${owner}/`)
1463
+ ? [`${owner}/${bareId}`, bareId, owner, ""]
1464
+ : [bareId, ""];
1465
+ }
1466
+ /**
1467
+ * Resolve an asset reference to a local file holding its bytes — the path
1468
+ * counterpart of {@link resolveAssetBytes}, for callers such as ffmpeg that
1469
+ * read a file in place instead of loading it into memory.
1470
+ *
1471
+ * Looks the reference up in the same adapters and under the same keys,
1472
+ * including the extension-tolerant listing, and asks each adapter for
1473
+ * `localPath`. That finds a managed file under the local asset root and an
1474
+ * external asset's in-place file. Returns null when no adapter keeps a local
1475
+ * file for it (object stores, the in-memory store, HTTP-only and
1476
+ * `package://` references); the caller then falls back to
1477
+ * {@link resolveAssetBytes}. The path is for reading only, and it is an
1478
+ * asset-store path, not a workspace file.
1479
+ */
1480
+ async localPath(uri) {
1481
+ const trimmed = uri.trim();
1482
+ if (!trimmed || parsePackageAssetUri(trimmed)) {
1483
+ return null;
1484
+ }
1485
+ const adapters = this.assetAdapters();
1486
+ const tryLocalPath = async (adapter, storageUri) => {
1487
+ try {
1488
+ return await adapter.localPath(storageUri);
1489
+ }
1490
+ catch {
1491
+ // An adapter that cannot answer is treated as having no local file.
1492
+ return null;
1493
+ }
1494
+ };
1495
+ if (trimmed.includes("://") && !trimmed.startsWith("asset://")) {
1496
+ for (const adapter of adapters) {
1497
+ const direct = await tryLocalPath(adapter, trimmed);
1498
+ if (direct) {
1499
+ return direct;
1500
+ }
900
1501
  }
901
1502
  }
902
- let bytes = null;
903
- if (this.storage) {
904
- for (const uri of uriCandidates) {
1503
+ const idCandidates = this.parseAssetIdCandidates(trimmed);
1504
+ for (const adapter of adapters) {
1505
+ for (const candidate of idCandidates) {
1506
+ for (const key of this.assetKeysFor(candidate)) {
1507
+ const found = await tryLocalPath(adapter, adapter.uriForKey(key));
1508
+ if (found) {
1509
+ return found;
1510
+ }
1511
+ }
1512
+ }
1513
+ const bareId = idCandidates[idCandidates.length - 1];
1514
+ if (!bareId) {
1515
+ continue;
1516
+ }
1517
+ for (const prefix of this.assetListingPrefixes(bareId)) {
905
1518
  try {
906
- const retrieved = await this.storage.retrieve(uri);
907
- if (retrieved) {
908
- bytes = retrieved;
909
- break;
1519
+ const listing = await adapter.list(prefix);
1520
+ const match = findListedAsset(listing.entries, bareId);
1521
+ const found = match ? await tryLocalPath(adapter, match.uri) : null;
1522
+ if (found) {
1523
+ return found;
1524
+ }
1525
+ }
1526
+ catch {
1527
+ // A listing that fails leaves the next, wider prefix to try.
1528
+ }
1529
+ }
1530
+ }
1531
+ return null;
1532
+ }
1533
+ /**
1534
+ * Resolve an `asset://<id>[.ext]` reference (or bare id / storage URI) to its
1535
+ * raw bytes.
1536
+ *
1537
+ * Server-uploaded assets are stored under the key `<id>.<ext>` and served at
1538
+ * `/api/storage/<id>.<ext>`; runtime-materialized refs live under `assets/`.
1539
+ * Resolution order: the in-process storage adapter (file/s3/supabase/memory),
1540
+ * a prefix listing that tolerates extension mismatches, then an HTTP download
1541
+ * from the server's `/api/storage` route. Returns the bytes plus an `attempts`
1542
+ * trail for callers that build detailed errors; `null` bytes means unresolved.
1543
+ */
1544
+ async resolveAssetBytes(assetId, options) {
1545
+ let idCandidates = this.parseAssetIdCandidates(assetId);
1546
+ const trimmed = assetId.trim();
1547
+ const attempts = [];
1548
+ const adapters = this.assetAdapters();
1549
+ const tryStorageUri = async (adapter, uri) => {
1550
+ try {
1551
+ const retrieved = await adapter.retrieve(uri);
1552
+ if (retrieved) {
1553
+ return retrieved;
1554
+ }
1555
+ attempts.push(`storage miss: ${uri}`);
1556
+ }
1557
+ catch (error) {
1558
+ attempts.push(`storage error: ${uri} (${error instanceof Error ? error.message : String(error)})`);
1559
+ }
1560
+ return null;
1561
+ };
1562
+ // Constant package assets (`package://<pkg>/<path>`) ship with the install
1563
+ // — they have stable URIs and don't live in per-user storage. Resolve from
1564
+ // the bundled package-assets dir when configured, else over HTTP from the
1565
+ // server's package-asset route.
1566
+ const packageRef = parsePackageAssetUri(trimmed);
1567
+ if (packageRef) {
1568
+ const root = this.environment.NODETOOL_PACKAGE_ASSETS_DIR ??
1569
+ safeProcessEnv().NODETOOL_PACKAGE_ASSETS_DIR;
1570
+ if (root) {
1571
+ const segments = `${packageRef.packageName}/${packageRef.path}`
1572
+ .split("/")
1573
+ .filter((s) => s && s !== ".");
1574
+ if (!segments.some((s) => s === "..")) {
1575
+ const filePath = resolve(join(root, ...segments));
1576
+ const within = !relative(resolve(root), filePath).startsWith("..");
1577
+ if (within) {
1578
+ try {
1579
+ const bytes = (await readFile(filePath));
1580
+ attempts.push(`package asset: ${filePath}`);
1581
+ return { bytes, attempts };
1582
+ }
1583
+ catch (error) {
1584
+ attempts.push(`package asset miss: ${filePath} (${error instanceof Error ? error.message : String(error)})`);
1585
+ }
910
1586
  }
911
- resolutionAttempts.push(`storage miss: ${uri}`);
1587
+ }
1588
+ }
1589
+ const httpPath = packageAssetHttpPath(trimmed);
1590
+ if (httpPath) {
1591
+ let pkgBaseUrl = this.environment.NODETOOL_API_URL ??
1592
+ safeProcessEnv().NODETOOL_API_URL ??
1593
+ "http://localhost:7777";
1594
+ while (pkgBaseUrl.endsWith("/")) {
1595
+ pkgBaseUrl = pkgBaseUrl.slice(0, -1);
1596
+ }
1597
+ const url = `${pkgBaseUrl}${httpPath}`;
1598
+ try {
1599
+ const bytes = await this.downloadFile(url, {
1600
+ retry: { maxRetries: 1, backoffMs: 200 }
1601
+ });
1602
+ attempts.push(`downloaded: ${url}`);
1603
+ return { bytes, attempts };
912
1604
  }
913
1605
  catch (error) {
914
- resolutionAttempts.push(`storage error: ${uri} (${error instanceof Error ? error.message : String(error)})`);
1606
+ attempts.push(`package asset http miss: ${url} (${error instanceof Error ? error.message : String(error)})`);
915
1607
  }
916
1608
  }
1609
+ return { bytes: null, attempts };
917
1610
  }
918
- if (!bytes) {
919
- let baseUrl = (this.environment.NODETOOL_API_URL ??
920
- process.env.NODETOOL_API_URL ??
921
- "http://localhost:7777");
922
- while (baseUrl.endsWith("/")) {
923
- baseUrl = baseUrl.slice(0, -1);
1611
+ // A concrete storage URI / http URL handed in directly.
1612
+ if (trimmed.includes("://") && !trimmed.startsWith("asset://")) {
1613
+ for (const adapter of adapters) {
1614
+ const direct = await tryStorageUri(adapter, trimmed);
1615
+ if (direct) {
1616
+ return { bytes: direct, attempts };
1617
+ }
924
1618
  }
925
- for (const candidate of idCandidates) {
1619
+ if (/^https?:\/\//.test(trimmed)) {
926
1620
  try {
927
- const metaResponse = await this.httpGet(`${baseUrl}/api/assets/${encodeURIComponent(candidate)}`);
928
- const metadata = (await metaResponse.json());
929
- const getUrl = metadata.get_url;
930
- const uri = metadata.uri;
931
- if (typeof getUrl === "string" && getUrl) {
932
- const downloadUrl = getUrl.startsWith("/")
933
- ? `${baseUrl}${getUrl}`
934
- : getUrl;
935
- bytes = await this.downloadFile(downloadUrl, {
936
- retry: { maxRetries: 1, backoffMs: 200 }
937
- });
938
- resolutionAttempts.push(`downloaded: ${downloadUrl}`);
939
- break;
1621
+ const bytes = await this.downloadFile(trimmed, {
1622
+ retry: { maxRetries: 1, backoffMs: 200 }
1623
+ });
1624
+ attempts.push(`downloaded: ${trimmed}`);
1625
+ return { bytes, attempts };
1626
+ }
1627
+ catch (error) {
1628
+ attempts.push(`http error: ${trimmed} (${error instanceof Error ? error.message : String(error)})`);
1629
+ }
1630
+ }
1631
+ }
1632
+ // An asset row names the stored file exactly (`<owner>/<id>.<ext>`), so a
1633
+ // reference that resolves to one is read by key and never by listing.
1634
+ let assetRowFound = false;
1635
+ if (this.hasModelInterface("getAssetInfo")) {
1636
+ let asset = null;
1637
+ const ownerPrefix = `${this.userId}/`;
1638
+ const rowIdCandidates = new Set(idCandidates);
1639
+ for (const candidate of idCandidates) {
1640
+ if (candidate.startsWith(ownerPrefix)) {
1641
+ rowIdCandidates.add(candidate.slice(ownerPrefix.length));
1642
+ }
1643
+ }
1644
+ for (const candidate of rowIdCandidates) {
1645
+ asset = await this.getAssetInfo(candidate);
1646
+ if (asset) {
1647
+ break;
1648
+ }
1649
+ }
1650
+ if (asset) {
1651
+ assetRowFound = true;
1652
+ idCandidates = assetFileNameCandidates(asset.id, asset.content_type);
1653
+ }
1654
+ else if (options?.requireOwnedAsset) {
1655
+ attempts.push(`asset unavailable or not owned: ${trimmed}`);
1656
+ return { bytes: null, attempts };
1657
+ }
1658
+ }
1659
+ for (const adapter of adapters) {
1660
+ for (const candidate of idCandidates) {
1661
+ for (const key of this.assetKeysFor(candidate)) {
1662
+ const bytes = await tryStorageUri(adapter, adapter.uriForKey(key));
1663
+ if (bytes) {
1664
+ return { bytes, attempts };
940
1665
  }
941
- if (typeof uri === "string" && this.storage) {
942
- const retrieved = await this.storage.retrieve(uri);
943
- if (retrieved) {
944
- bytes = retrieved;
945
- resolutionAttempts.push(`storage hit via metadata: ${uri}`);
946
- break;
1666
+ }
1667
+ }
1668
+ // Extension-tolerant fallback: locate the stored file whose name starts
1669
+ // with the id, since the URN extension may differ from the one on disk
1670
+ // (e.g. jpeg vs jpg). Only reached when the exact-key lookups all miss
1671
+ // and no asset row named the file.
1672
+ const bareId = idCandidates[idCandidates.length - 1];
1673
+ if (bareId && !assetRowFound) {
1674
+ for (const prefix of this.assetListingPrefixes(bareId)) {
1675
+ try {
1676
+ const listing = await adapter.list(prefix);
1677
+ const match = findListedAsset(listing.entries, bareId);
1678
+ if (match) {
1679
+ const bytes = await tryStorageUri(adapter, match.uri);
1680
+ if (bytes) {
1681
+ return { bytes, attempts };
1682
+ }
947
1683
  }
948
- resolutionAttempts.push(`storage miss via metadata: ${uri}`);
1684
+ }
1685
+ catch (error) {
1686
+ attempts.push(`storage list error (${error instanceof Error ? error.message : String(error)})`);
949
1687
  }
950
1688
  }
951
- catch (error) {
952
- resolutionAttempts.push(`api lookup error for ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
1689
+ }
1690
+ }
1691
+ // HTTP fallback: the server streams bytes at `/api/storage/<key>`.
1692
+ let baseUrl = this.environment.NODETOOL_API_URL ??
1693
+ safeProcessEnv().NODETOOL_API_URL ??
1694
+ "http://localhost:7777";
1695
+ while (baseUrl.endsWith("/")) {
1696
+ baseUrl = baseUrl.slice(0, -1);
1697
+ }
1698
+ for (const candidate of idCandidates) {
1699
+ if (!candidate.includes(".")) {
1700
+ continue; // the storage route is keyed by filename (`<id>.<ext>`)
1701
+ }
1702
+ const url = `${baseUrl}/api/storage/${encodeURIComponent(candidate)}`;
1703
+ try {
1704
+ const bytes = await this.downloadFile(url, {
1705
+ retry: { maxRetries: 1, backoffMs: 200 }
1706
+ });
1707
+ attempts.push(`downloaded: ${url}`);
1708
+ return { bytes, attempts };
1709
+ }
1710
+ catch (error) {
1711
+ attempts.push(`http miss: ${url} (${error instanceof Error ? error.message : String(error)})`);
1712
+ }
1713
+ }
1714
+ // Last resort: ask the asset API for metadata and follow its `get_url`.
1715
+ // Handles bare ids (no extension, so the storage route can't be addressed
1716
+ // directly) and backends that expose a single-asset metadata endpoint.
1717
+ for (const candidate of idCandidates) {
1718
+ try {
1719
+ const metaResponse = await this.httpGet(`${baseUrl}/api/assets/${encodeURIComponent(candidate)}`);
1720
+ const metadata = (await metaResponse.json());
1721
+ const getUrl = metadata.get_url;
1722
+ if (isNonEmptyString(getUrl)) {
1723
+ const downloadUrl = getUrl.startsWith("/")
1724
+ ? `${baseUrl}${getUrl}`
1725
+ : getUrl;
1726
+ const bytes = await this.downloadFile(downloadUrl, {
1727
+ retry: { maxRetries: 1, backoffMs: 200 }
1728
+ });
1729
+ attempts.push(`downloaded: ${downloadUrl}`);
1730
+ return { bytes, attempts };
953
1731
  }
1732
+ const uri = metadata.uri;
1733
+ if (isString(uri)) {
1734
+ for (const adapter of adapters) {
1735
+ const bytes = await tryStorageUri(adapter, uri);
1736
+ if (bytes) {
1737
+ return { bytes, attempts };
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ catch (error) {
1743
+ attempts.push(`api lookup error for ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
954
1744
  }
955
1745
  }
1746
+ return { bytes: null, attempts };
1747
+ }
1748
+ async assetToSandbox(assetId, path) {
1749
+ const outputPath = this.resolveSandboxFilePath(path);
1750
+ const { bytes, attempts } = await this.resolveAssetBytes(assetId);
956
1751
  if (!bytes) {
957
- const details = resolutionAttempts.length > 0
958
- ? ` Attempts: ${resolutionAttempts.join("; ")}`
959
- : "";
1752
+ const details = attempts.length > 0 ? ` Attempts: ${attempts.join("; ")}` : "";
960
1753
  throw new Error(`Unable to resolve asset '${assetId}' to sandbox bytes.${details}`);
961
1754
  }
962
1755
  await mkdir(dirname(outputPath), { recursive: true });
963
1756
  await writeFile(outputPath, bytes);
964
1757
  return outputPath;
965
1758
  }
966
- async asset_to_sandbox(asset_id, path) {
967
- return this.assetToSandbox(asset_id, path);
968
- }
969
1759
  async sandboxToAsset(path) {
970
1760
  const filePath = this.resolveSandboxFilePath(path);
971
- const bytes = await readFile(filePath);
1761
+ const bytes = (await readFile(filePath));
972
1762
  const contentType = ProcessingContext.guessMimeFromPath(filePath);
1763
+ // SAFETY: the host's asset persistence answers with its own asset record;
1764
+ // both reads below re-check the field before using it.
973
1765
  const created = (await this.createAsset({
974
1766
  name: basename(filePath),
975
1767
  contentType,
976
1768
  content: bytes
977
1769
  }));
978
1770
  const type = ProcessingContext.assetTypeForMime(contentType);
979
- const assetId = typeof created.id === "string" ? created.id : null;
1771
+ const assetId = isString(created.id) ? created.id : null;
980
1772
  const uri = assetId !== null
981
1773
  ? `asset://${assetId}`
982
- : typeof created.uri === "string"
1774
+ : isString(created.uri)
983
1775
  ? created.uri
984
1776
  : pathToFileURL(filePath).toString();
985
1777
  return {
@@ -988,9 +1780,6 @@ export class ProcessingContext {
988
1780
  asset_id: assetId
989
1781
  };
990
1782
  }
991
- async sandbox_to_asset(path) {
992
- return this.sandboxToAsset(path);
993
- }
994
1783
  async createMessage(req) {
995
1784
  if (!req.thread_id) {
996
1785
  throw new Error("Thread ID is required");
@@ -998,9 +1787,6 @@ export class ProcessingContext {
998
1787
  const fn = this.requireModelInterface("createMessage");
999
1788
  return fn({ userId: this.userId, req });
1000
1789
  }
1001
- async create_message(req) {
1002
- return this.createMessage(req);
1003
- }
1004
1790
  async getThreadMessages(threadId, limit = 10, startKey = null, reverse = false) {
1005
1791
  const fn = this.requireModelInterface("getMessages");
1006
1792
  return fn({
@@ -1011,9 +1797,10 @@ export class ProcessingContext {
1011
1797
  reverse
1012
1798
  });
1013
1799
  }
1014
- async get_messages(thread_id, limit = 10, start_key = null, reverse = false) {
1015
- return this.getThreadMessages(thread_id, limit, start_key, reverse);
1016
- }
1800
+ // The four wrappers below and `normalizeOutputValue` are HOLDOUTs
1801
+ // (anti-slop/no-unknown-returns): they rewrite an arbitrary workflow value —
1802
+ // any node output, at any nesting depth — and NodeTool has no named union
1803
+ // for that value domain.
1017
1804
  async assetsToDataUri(value) {
1018
1805
  return this.normalizeOutputValue(value, "data_uri");
1019
1806
  }
@@ -1029,8 +1816,8 @@ export class ProcessingContext {
1029
1816
  // -----------------------------------------------------------------------
1030
1817
  // Provider prediction pipeline
1031
1818
  // -----------------------------------------------------------------------
1032
- emitPrediction(status, req, id, data, error, startedAt) {
1033
- this.emit({
1819
+ emitPrediction(status, req, id, data, error, startedAt, extra) {
1820
+ const message = {
1034
1821
  type: "prediction",
1035
1822
  id,
1036
1823
  user_id: this.userId,
@@ -1038,20 +1825,233 @@ export class ProcessingContext {
1038
1825
  workflow_id: req.workflowId ?? this.workflowId ?? null,
1039
1826
  provider: req.provider,
1040
1827
  model: req.model,
1828
+ capability: req.capability,
1041
1829
  status,
1042
- params: req.params ?? {},
1830
+ // Redacted: bytes and data URLs become their length. The row stores
1831
+ // this bag and every listener receives it.
1832
+ params: redactGenerationParams(req.params),
1043
1833
  data: data ?? null,
1044
1834
  error: error ?? null,
1045
- duration: startedAt ? Date.now() - startedAt : null
1046
- });
1835
+ duration: startedAt ? Date.now() - startedAt : null,
1836
+ origin: extra?.origin ?? this.generationOrigin(req),
1837
+ receipt: extra?.receipt ?? null
1838
+ };
1839
+ // Only a terminal message names assets; `running` has none to name.
1840
+ if (extra?.asset_ids)
1841
+ message.asset_ids = extra.asset_ids;
1842
+ this.emit(message);
1047
1843
  }
1048
1844
  /**
1049
- * Resolve /api/storage/ URIs in message content to data URIs so providers
1050
- * can fetch them without needing a base URL.
1845
+ * Who asked, defaulted from the context: a chat turn names its thread, a
1846
+ * run names its job and node. A capability passes `surface` and its
1847
+ * `tool_call_id` explicitly, the one id the context does not hold.
1848
+ */
1849
+ generationOrigin(req, override) {
1850
+ const base = this.threadId
1851
+ ? {
1852
+ surface: "chat",
1853
+ thread_id: this.threadId,
1854
+ job_id: this.jobId || null,
1855
+ node_id: req.nodeId ?? null
1856
+ }
1857
+ : {
1858
+ surface: "workflow",
1859
+ job_id: this.jobId || null,
1860
+ node_id: req.nodeId ?? null
1861
+ };
1862
+ if (!override)
1863
+ return base;
1864
+ const merged = { ...base };
1865
+ if (override.surface !== undefined)
1866
+ merged.surface = override.surface;
1867
+ if (override.thread_id !== undefined)
1868
+ merged.thread_id = override.thread_id;
1869
+ if (override.tool_call_id !== undefined)
1870
+ merged.tool_call_id = override.tool_call_id;
1871
+ if (override.request_id !== undefined)
1872
+ merged.request_id = override.request_id;
1873
+ if (override.job_id !== undefined)
1874
+ merged.job_id = override.job_id;
1875
+ if (override.node_id !== undefined)
1876
+ merged.node_id = override.node_id;
1877
+ return merged;
1878
+ }
1879
+ /**
1880
+ * Retrieve raw bytes for a media URI referenced in message content. Handles
1881
+ * the `asset://<id>` reference scheme (via {@link resolveAssetBytes}) as well
1882
+ * as opaque storage URIs (memory/file/s3) via the storage adapter.
1883
+ */
1884
+ async retrieveMediaBytes(uri) {
1885
+ // `/api/storage/<key>` (and its slash-less `api/storage/<key>` form) is the
1886
+ // browser-facing route, not a storage-adapter uri — the InMemory/S3 adapters
1887
+ // only accept `memory://`/`s3://`, so route it through resolveAssetBytes
1888
+ // (which parses the key) like `asset://`.
1889
+ if (uri.startsWith("asset://") ||
1890
+ uri.startsWith("/api/storage/") ||
1891
+ uri.startsWith("api/storage/")) {
1892
+ const { bytes } = await this.resolveAssetBytes(uri);
1893
+ return bytes;
1894
+ }
1895
+ for (const adapter of [this.assetStorage, this.storage]) {
1896
+ if (!adapter)
1897
+ continue;
1898
+ const bytes = await adapter.retrieve(uri);
1899
+ if (bytes)
1900
+ return bytes;
1901
+ }
1902
+ return null;
1903
+ }
1904
+ /**
1905
+ * Resolve non-data, non-http media URIs in message content to data URIs so
1906
+ * providers can consume them directly. Covers `/api/storage/` and other
1907
+ * storage URIs as well as the `asset://<id>` reference scheme (assets
1908
+ * mentioned inline in a prompt). Text parts that contain `asset://<id>.<ext>`
1909
+ * image / audio tokens are first split into proper blocks via
1910
+ * {@link expandAssetReferences} so the same provider path handles chat
1911
+ * surfaces (where the user typed / dropped a reference) and workflow node
1912
+ * surfaces (where the agent node builds the message). Text document mentions
1913
+ * (`asset://doc.md`, `.txt`, `.csv`, …) are inlined as their decoded
1914
+ * contents in place of the token.
1051
1915
  */
1052
1916
  async resolveMessageMediaUris(messages) {
1053
- if (!this.storage)
1054
- return messages;
1917
+ const IMAGE_MIME = {
1918
+ png: "image/png",
1919
+ jpg: "image/jpeg",
1920
+ jpeg: "image/jpeg",
1921
+ gif: "image/gif",
1922
+ webp: "image/webp"
1923
+ };
1924
+ const AUDIO_MIME = {
1925
+ mp3: "audio/mpeg",
1926
+ wav: "audio/wav",
1927
+ ogg: "audio/ogg",
1928
+ m4a: "audio/mp4",
1929
+ flac: "audio/flac"
1930
+ };
1931
+ const VIDEO_MIME = {
1932
+ mp4: "video/mp4",
1933
+ m4v: "video/mp4",
1934
+ webm: "video/webm",
1935
+ mov: "video/quicktime",
1936
+ avi: "video/x-msvideo"
1937
+ };
1938
+ // Strict, anchored sanitizer for the URIs that may flow into the asset /
1939
+ // storage retrieval path. Accept asset references, the browser-facing
1940
+ // storage route, and URIs from configured storage adapters. External URLs
1941
+ // and unrecognized schemes stay out of the storage adapter. `:` is allowed
1942
+ // for named owner ids such as `projects/personal:<user_id>/assets/…`.
1943
+ const RESOLVABLE_URI_RE = /^(?:asset:\/\/|\/?api\/storage\/|(?:memory|file|s3|supabase):\/\/)[A-Za-z0-9._~/%:-]+$/;
1944
+ const isResolvableUri = (uri) => RESOLVABLE_URI_RE.test(uri);
1945
+ // An `asset://` reference that cannot be dereferenced must not reach the
1946
+ // provider: providers `fetch` a non-data URI, and the SSRF guard then
1947
+ // rejects `asset://…` with "Refusing to fetch unsafe URL", which fails the
1948
+ // whole turn on a message the user cannot fix. Replace the block with a
1949
+ // note the model can act on instead — a stale attachment in the history
1950
+ // must not break every later turn in the thread.
1951
+ const unresolvedNote = (kind, uri) => ({
1952
+ type: "text",
1953
+ text: `[attached ${kind} could not be ${kind === "image" ? "shown" : "loaded"}: ${uri}]`
1954
+ });
1955
+ // A resolved block reaches the provider as a `data:` URI, which the model
1956
+ // cannot pass to a tool. Emit the stored handle beside it so "make it like
1957
+ // this" can become `edit_image({input_file: "asset://…"})` — only for the
1958
+ // URIs `isResolvableUri` admits, never for a `data:` URI the caller already
1959
+ // inlined.
1960
+ const attachedNote = (kind, uri) => ({
1961
+ type: "text",
1962
+ text: `[attached ${kind} ${uri}]`
1963
+ });
1964
+ const resolvePart = async (part,
1965
+ // Parts split out of a text block already carry the literal token next
1966
+ // to them (see `expandAssetReferences`), so a note there would repeat it.
1967
+ fromText = false) => {
1968
+ if (part.type === "image_url" &&
1969
+ part.image.uri &&
1970
+ isResolvableUri(part.image.uri)) {
1971
+ const bytes = await this.retrieveMediaBytes(part.image.uri);
1972
+ if (bytes) {
1973
+ const ext = part.image.uri.split(".").pop()?.toLowerCase() ?? "png";
1974
+ const mimeType = IMAGE_MIME[ext] ?? part.image.mimeType ?? "image/png";
1975
+ const b64 = Buffer.from(bytes).toString("base64");
1976
+ const block = {
1977
+ type: "image_url",
1978
+ image: { uri: `data:${mimeType};base64,${b64}`, mimeType }
1979
+ };
1980
+ return fromText
1981
+ ? [block]
1982
+ : [block, attachedNote("image", part.image.uri)];
1983
+ }
1984
+ return [unresolvedNote("image", part.image.uri)];
1985
+ }
1986
+ if (part.type === "audio" &&
1987
+ part.audio.uri &&
1988
+ isResolvableUri(part.audio.uri)) {
1989
+ const bytes = await this.retrieveMediaBytes(part.audio.uri);
1990
+ if (bytes) {
1991
+ const ext = part.audio.uri.split(".").pop()?.toLowerCase() ?? "mp3";
1992
+ const mimeType = AUDIO_MIME[ext] ?? part.audio.mimeType ?? "audio/mpeg";
1993
+ const b64 = Buffer.from(bytes).toString("base64");
1994
+ const block = {
1995
+ type: "audio",
1996
+ audio: { uri: `data:${mimeType};base64,${b64}`, mimeType }
1997
+ };
1998
+ return fromText
1999
+ ? [block]
2000
+ : [block, attachedNote("audio", part.audio.uri)];
2001
+ }
2002
+ return [unresolvedNote("audio", part.audio.uri)];
2003
+ }
2004
+ if (part.type === "video" &&
2005
+ part.video.uri &&
2006
+ isResolvableUri(part.video.uri)) {
2007
+ const bytes = await this.retrieveMediaBytes(part.video.uri);
2008
+ if (bytes) {
2009
+ const ext = part.video.uri.split(".").pop()?.toLowerCase() ?? "mp4";
2010
+ const mimeType = VIDEO_MIME[ext] ?? part.video.mimeType ?? "video/mp4";
2011
+ const b64 = Buffer.from(bytes).toString("base64");
2012
+ const block = {
2013
+ type: "video",
2014
+ video: { uri: `data:${mimeType};base64,${b64}`, mimeType }
2015
+ };
2016
+ return fromText
2017
+ ? [block]
2018
+ : [block, attachedNote("video", part.video.uri)];
2019
+ }
2020
+ return [unresolvedNote("video", part.video.uri)];
2021
+ }
2022
+ if (part.type === "text") {
2023
+ // Entity mentions resolve first: each `entity://<id>` becomes the
2024
+ // entity's name inline, a "Consistency references" line carrying its
2025
+ // descriptor, and an `asset://` token for its reference image — which
2026
+ // the asset pass below then turns into a real image block. Resolving
2027
+ // here, per turn, is what makes a later rename or re-description
2028
+ // reach every message that already mentions the entity.
2029
+ const text = await expandEntityRefs(part.text, this, true);
2030
+ if (!text.includes("asset://")) {
2031
+ return text === part.text ? [part] : [{ type: "text", text }];
2032
+ }
2033
+ // First split out image / audio mentions into their own blocks; what
2034
+ // stays as text is then run through the document inliner so any
2035
+ // .md/.txt/.csv mentions become their decoded contents. The image and
2036
+ // audio blocks fall through to the URI-resolution branches above on a
2037
+ // second pass so the provider receives data URIs.
2038
+ const expanded = expandAssetReferences(text);
2039
+ const out = [];
2040
+ for (const sub of expanded) {
2041
+ if (sub.type === "text") {
2042
+ const inlined = await inlineTextAssetRefs(sub.text, this);
2043
+ if (inlined)
2044
+ out.push({ type: "text", text: inlined });
2045
+ }
2046
+ else {
2047
+ const resolved = await resolvePart(sub, true);
2048
+ out.push(...resolved);
2049
+ }
2050
+ }
2051
+ return out;
2052
+ }
2053
+ return [part];
2054
+ };
1055
2055
  const resolved = [];
1056
2056
  for (const msg of messages) {
1057
2057
  if (!Array.isArray(msg.content)) {
@@ -1060,64 +2060,88 @@ export class ProcessingContext {
1060
2060
  }
1061
2061
  const parts = [];
1062
2062
  for (const part of msg.content) {
1063
- if (part.type === "image_url" &&
1064
- part.image.uri &&
1065
- !part.image.uri.startsWith("data:") &&
1066
- !part.image.uri.startsWith("http")) {
1067
- const bytes = await this.storage.retrieve(part.image.uri);
1068
- if (bytes) {
1069
- const ext = part.image.uri.split(".").pop()?.toLowerCase() ?? "png";
1070
- const mime = {
1071
- png: "image/png",
1072
- jpg: "image/jpeg",
1073
- jpeg: "image/jpeg",
1074
- gif: "image/gif",
1075
- webp: "image/webp"
1076
- };
1077
- const mimeType = mime[ext] ?? part.image.mimeType ?? "image/png";
1078
- const b64 = Buffer.from(bytes).toString("base64");
1079
- parts.push({
1080
- type: "image_url",
1081
- image: { uri: `data:${mimeType};base64,${b64}`, mimeType }
1082
- });
1083
- continue;
1084
- }
2063
+ parts.push(...(await resolvePart(part)));
2064
+ }
2065
+ resolved.push({ ...msg, content: parts });
2066
+ }
2067
+ return resolved;
2068
+ }
2069
+ async resolveRequiredImageEvidence(messages, required = []) {
2070
+ const evidence = new Map();
2071
+ const resolve = async (ref) => {
2072
+ const identity = ref.uri?.startsWith("data:")
2073
+ ? "inline image"
2074
+ : ref.uri ?? ref.asset_id ?? "inline image";
2075
+ const cached = ref.uri ? evidence.get(ref.uri) : undefined;
2076
+ if (cached) {
2077
+ return cached;
2078
+ }
2079
+ try {
2080
+ const bytes = await loadMediaRefBytes(ref, this);
2081
+ if (!bytes?.length) {
2082
+ throw new Error("no image bytes");
1085
2083
  }
1086
- if (part.type === "audio" &&
1087
- part.audio.uri &&
1088
- !part.audio.uri.startsWith("data:") &&
1089
- !part.audio.uri.startsWith("http")) {
1090
- const bytes = await this.storage.retrieve(part.audio.uri);
1091
- if (bytes) {
1092
- const ext = part.audio.uri.split(".").pop()?.toLowerCase() ?? "mp3";
1093
- const mime = {
1094
- mp3: "audio/mpeg",
1095
- wav: "audio/wav",
1096
- ogg: "audio/ogg",
1097
- m4a: "audio/mp4",
1098
- flac: "audio/flac"
1099
- };
1100
- const mimeType = mime[ext] ?? part.audio.mimeType ?? "audio/mpeg";
1101
- const b64 = Buffer.from(bytes).toString("base64");
1102
- parts.push({
1103
- type: "audio",
1104
- audio: { uri: `data:${mimeType};base64,${b64}`, mimeType }
1105
- });
1106
- continue;
2084
+ const mimeType = sniffImageMime(bytes);
2085
+ if (!mimeType ||
2086
+ !["image/png", "image/jpeg", "image/webp", "image/gif"].includes(mimeType)) {
2087
+ throw new Error("unsupported image format");
2088
+ }
2089
+ await validateImageEvidence(bytes);
2090
+ const block = {
2091
+ type: "image_url",
2092
+ image: {
2093
+ uri: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
2094
+ mimeType
1107
2095
  }
2096
+ };
2097
+ if (ref.uri) {
2098
+ evidence.set(ref.uri, block);
1108
2099
  }
1109
- parts.push(part);
2100
+ return block;
1110
2101
  }
1111
- resolved.push({ ...msg, content: parts });
2102
+ catch (error) {
2103
+ throw new Error(`Required image evidence unavailable: ${identity} (${error instanceof Error ? error.message : String(error)})`);
2104
+ }
2105
+ };
2106
+ for (const uri of required) {
2107
+ await resolve({ type: "image", uri });
1112
2108
  }
1113
- return resolved;
2109
+ const result = [];
2110
+ for (const message of messages) {
2111
+ if (!Array.isArray(message.content)) {
2112
+ result.push(message);
2113
+ continue;
2114
+ }
2115
+ const content = [];
2116
+ let imageIndex = 0;
2117
+ for (const part of message.content) {
2118
+ if (part.type === "image_url") {
2119
+ imageIndex++;
2120
+ content.push(await resolve(part.image), {
2121
+ type: "text",
2122
+ text: `[required image ${part.image.uri && !part.image.uri.startsWith("data:")
2123
+ ? part.image.uri
2124
+ : `inline image ${imageIndex}`}]`
2125
+ });
2126
+ }
2127
+ else {
2128
+ content.push(part);
2129
+ }
2130
+ }
2131
+ result.push({ ...message, content });
2132
+ }
2133
+ return result;
1114
2134
  }
1115
2135
  async dispatchCapability(provider, req) {
1116
2136
  const params = req.params ?? {};
1117
2137
  switch (req.capability) {
1118
2138
  case "generate_message":
1119
2139
  return provider.generateMessageTraced({
1120
- messages: await this.resolveMessageMediaUris(params.messages ?? []),
2140
+ messages: params.strict_image_evidence === true
2141
+ ? await this.resolveRequiredImageEvidence(params.messages ?? [], Array.isArray(params.required_images)
2142
+ ? params.required_images.filter(isString)
2143
+ : [])
2144
+ : await this.resolveMessageMediaUris(params.messages ?? []),
1121
2145
  model: req.model,
1122
2146
  tools: params.tools,
1123
2147
  toolChoice: params.tool_choice,
@@ -1129,7 +2153,9 @@ export class ProcessingContext {
1129
2153
  });
1130
2154
  case "text_to_image":
1131
2155
  return provider.textToImage({
2156
+ signal: params.signal,
1132
2157
  prompt: String(params.prompt ?? ""),
2158
+ entities: await coerceEntityList(params, this),
1133
2159
  model: { id: req.model, name: req.model, provider: req.provider },
1134
2160
  width: params.width,
1135
2161
  height: params.height,
@@ -1139,9 +2165,25 @@ export class ProcessingContext {
1139
2165
  quality: params.quality
1140
2166
  });
1141
2167
  case "image_to_image":
1142
- return provider.imageToImage(params.image, {
2168
+ return provider.imageToImage(coerceImageList(params), {
2169
+ signal: params.signal,
2170
+ prompt: String(params.prompt ?? ""),
2171
+ entities: await coerceEntityList(params, this),
2172
+ model: { id: req.model, name: req.model, provider: req.provider },
2173
+ negativePrompt: params.negative_prompt,
2174
+ targetWidth: params.target_width,
2175
+ targetHeight: params.target_height,
2176
+ aspectRatio: params.aspect_ratio,
2177
+ resolution: params.resolution,
2178
+ strength: params.strength,
2179
+ quality: params.quality
2180
+ });
2181
+ case "inpainting":
2182
+ return provider.inpaint(coerceImageList(params), {
1143
2183
  prompt: String(params.prompt ?? ""),
2184
+ entities: await coerceEntityList(params, this),
1144
2185
  model: { id: req.model, name: req.model, provider: req.provider },
2186
+ mask: params.mask,
1145
2187
  negativePrompt: params.negative_prompt,
1146
2188
  targetWidth: params.target_width,
1147
2189
  targetHeight: params.target_height,
@@ -1152,23 +2194,267 @@ export class ProcessingContext {
1152
2194
  });
1153
2195
  case "text_to_video":
1154
2196
  return provider.textToVideo({
2197
+ signal: params.signal,
1155
2198
  prompt: String(params.prompt ?? ""),
2199
+ entities: await coerceEntityList(params, this),
1156
2200
  model: { id: req.model, name: req.model, provider: req.provider },
1157
2201
  negativePrompt: params.negative_prompt,
1158
2202
  numFrames: params.num_frames,
1159
2203
  durationSeconds: params.duration_seconds,
1160
2204
  aspectRatio: params.aspect_ratio,
1161
- resolution: params.resolution
2205
+ resolution: params.resolution,
2206
+ timeoutSeconds: params.timeout_seconds
1162
2207
  });
1163
2208
  case "image_to_video":
1164
- return provider.imageToVideo(params.image, {
2209
+ return provider.imageToVideo(firstNonEmptyByteList(coerceImageList(params)), {
2210
+ signal: params.signal,
1165
2211
  prompt: params.prompt,
2212
+ entities: await coerceEntityList(params, this),
1166
2213
  model: { id: req.model, name: req.model, provider: req.provider },
1167
2214
  negativePrompt: params.negative_prompt,
1168
2215
  numFrames: params.num_frames,
1169
2216
  durationSeconds: params.duration_seconds,
1170
2217
  aspectRatio: params.aspect_ratio,
1171
- resolution: params.resolution
2218
+ resolution: params.resolution,
2219
+ timeoutSeconds: params.timeout_seconds
2220
+ });
2221
+ case "reference_to_video": {
2222
+ const images = coerceByteList(params.reference_images);
2223
+ const videos = coerceByteList(params.reference_videos);
2224
+ if (images.length === 0 && videos.length === 0) {
2225
+ throw new Error("reference_to_video requires at least one reference image or video");
2226
+ }
2227
+ return provider.referenceToVideo({ images, videos }, {
2228
+ model: { id: req.model, name: req.model, provider: req.provider },
2229
+ prompt: isString(params.prompt) ? params.prompt : "",
2230
+ entities: await coerceEntityList(params, this),
2231
+ useReferenceVideoAudio: isBoolean(params.use_reference_video_audio)
2232
+ ? params.use_reference_video_audio
2233
+ : undefined,
2234
+ negativePrompt: isString(params.negative_prompt)
2235
+ ? params.negative_prompt
2236
+ : undefined,
2237
+ numFrames: isNumber(params.num_frames)
2238
+ ? params.num_frames
2239
+ : undefined,
2240
+ durationSeconds: isNumber(params.duration_seconds)
2241
+ ? params.duration_seconds
2242
+ : undefined,
2243
+ aspectRatio: isString(params.aspect_ratio)
2244
+ ? params.aspect_ratio
2245
+ : undefined,
2246
+ resolution: isString(params.resolution)
2247
+ ? params.resolution
2248
+ : undefined,
2249
+ timeoutSeconds: isNumber(params.timeout_seconds)
2250
+ ? params.timeout_seconds
2251
+ : undefined,
2252
+ signal: isAbortSignal(params.signal) ? params.signal : undefined
2253
+ });
2254
+ }
2255
+ case "outpaint_image":
2256
+ return provider.outpaintImage(coerceImageList(params), {
2257
+ signal: params.signal,
2258
+ prompt: params.prompt,
2259
+ entities: await coerceEntityList(params, this),
2260
+ model: { id: req.model, name: req.model, provider: req.provider },
2261
+ negativePrompt: params.negative_prompt,
2262
+ padding: coerceOutpaintPadding(params.padding),
2263
+ aspectRatio: params.aspect_ratio,
2264
+ seed: params.seed
2265
+ });
2266
+ case "upscale_image":
2267
+ return provider.upscaleImage(params.image, {
2268
+ model: { id: req.model, name: req.model, provider: req.provider },
2269
+ scale: params.scale,
2270
+ prompt: params.prompt,
2271
+ creativity: params.creativity,
2272
+ seed: params.seed
2273
+ });
2274
+ case "remove_background":
2275
+ return provider.removeBackground(params.image, {
2276
+ model: { id: req.model, name: req.model, provider: req.provider }
2277
+ });
2278
+ case "relight_image":
2279
+ return provider.relightImage(params.image, {
2280
+ model: { id: req.model, name: req.model, provider: req.provider },
2281
+ prompt: params.prompt,
2282
+ negativePrompt: params.negative_prompt,
2283
+ seed: params.seed
2284
+ });
2285
+ case "segment_image":
2286
+ return provider.segmentImage(params.image, {
2287
+ signal: params.signal,
2288
+ model: { id: req.model, name: req.model, provider: req.provider },
2289
+ prompt: params.prompt,
2290
+ points: params.points,
2291
+ box: params.box,
2292
+ maxMasks: params.max_masks,
2293
+ minConfidence: params.min_confidence
2294
+ });
2295
+ case "vectorize_image":
2296
+ return provider.vectorizeImage(params.image, {
2297
+ model: { id: req.model, name: req.model, provider: req.provider }
2298
+ });
2299
+ case "estimate_depth":
2300
+ return provider.estimateDepth(params.image, {
2301
+ model: { id: req.model, name: req.model, provider: req.provider },
2302
+ signal: isAbortSignal(params.signal) ? params.signal : this.signal
2303
+ });
2304
+ case "rerank":
2305
+ return provider.rerank({
2306
+ model: req.model,
2307
+ query: isString(params.query) ? params.query : "",
2308
+ // Every entry stays, so result indices point into the caller's list.
2309
+ documents: Array.isArray(params.documents)
2310
+ ? params.documents.map((doc) => (isString(doc) ? doc : String(doc)))
2311
+ : [],
2312
+ topK: isNumber(params.top_k) ? params.top_k : undefined,
2313
+ signal: isAbortSignal(params.signal) ? params.signal : this.signal
2314
+ });
2315
+ case "track_object": {
2316
+ const request = objectTrackingRequestSchema.parse(params);
2317
+ if (!provider.getCapabilities().includes("track_object")) {
2318
+ throw new Error("The selected provider does not support object tracking.");
2319
+ }
2320
+ const signal = params.signal instanceof AbortSignal ? params.signal : this.signal;
2321
+ signal.throwIfAborted();
2322
+ const video = await loadMediaRefBytes({ type: "video", asset_id: request.sourceAssetId }, this);
2323
+ if (!video?.byteLength) {
2324
+ throw new Error("The tracking source video could not be loaded.");
2325
+ }
2326
+ signal.throwIfAborted();
2327
+ const output = await provider.trackObject(video, {
2328
+ ...request,
2329
+ model: req.model,
2330
+ signal
2331
+ });
2332
+ signal.throwIfAborted();
2333
+ return parseObjectTrackingResult(output, request);
2334
+ }
2335
+ case "video_to_video": {
2336
+ const signal = isAbortSignal(params.signal)
2337
+ ? params.signal
2338
+ : this.signal;
2339
+ const referenceImages = coerceByteList(params.reference_images ?? params.referenceImages);
2340
+ const referenceAssetIds = coerceStringList(params.reference_asset_ids ?? params.referenceAssetIds);
2341
+ signal.throwIfAborted();
2342
+ return provider.videoToVideo(params.video, {
2343
+ model: { id: req.model, name: req.model, provider: req.provider },
2344
+ prompt: params.prompt,
2345
+ referenceImages: referenceImages.length > 0 ? referenceImages : undefined,
2346
+ referenceAssetIds: referenceAssetIds.length > 0 ? referenceAssetIds : undefined,
2347
+ signal,
2348
+ entities: await coerceEntityList(params, this),
2349
+ negativePrompt: params.negative_prompt,
2350
+ strength: params.strength,
2351
+ durationSeconds: params.duration_seconds,
2352
+ resolution: params.resolution,
2353
+ seed: params.seed
2354
+ });
2355
+ }
2356
+ case "video_to_audio": {
2357
+ const source = isRecord(params.source)
2358
+ ? {
2359
+ assetId: params.source.assetId ?? params.source.asset_id,
2360
+ durationSeconds: params.source.durationSeconds ?? params.source.duration_seconds,
2361
+ startSeconds: params.source.startSeconds ?? params.source.start_seconds,
2362
+ endSeconds: params.source.endSeconds ?? params.source.end_seconds
2363
+ }
2364
+ : params.source;
2365
+ const request = VideoToAudioRequest.parse({
2366
+ task: VIDEO_TO_AUDIO_TASK,
2367
+ model: {
2368
+ id: req.model,
2369
+ provider: req.provider,
2370
+ supportedTasks: [VIDEO_TO_AUDIO_TASK]
2371
+ },
2372
+ source,
2373
+ sceneContext: params.sceneContext ?? params.scene_context
2374
+ });
2375
+ const video = params.video instanceof Uint8Array
2376
+ ? params.video
2377
+ : await loadMediaRefBytes({ type: "video", asset_id: request.source.assetId }, this);
2378
+ if (!video?.byteLength) {
2379
+ throw new Error("video_to_audio requires nonempty source video bytes");
2380
+ }
2381
+ return requestVideoToAudio(provider, video, request, {
2382
+ signal: params.signal
2383
+ });
2384
+ }
2385
+ case "extend_video": {
2386
+ if ((params.mode !== "start" && params.mode !== "end") ||
2387
+ typeof params.duration_seconds !== "number" ||
2388
+ !Number.isFinite(params.duration_seconds) ||
2389
+ params.duration_seconds <= 0) {
2390
+ throw new Error("extend_video requires start/end mode and positive duration_seconds");
2391
+ }
2392
+ return provider.extendVideo(params.video, {
2393
+ model: { id: req.model, name: req.model, provider: req.provider },
2394
+ prompt: typeof params.prompt === "string" ? params.prompt : "",
2395
+ mode: params.mode,
2396
+ durationSeconds: params.duration_seconds
2397
+ });
2398
+ }
2399
+ case "upscale_video":
2400
+ return provider.upscaleVideo(params.video, {
2401
+ signal: params.signal,
2402
+ model: { id: req.model, name: req.model, provider: req.provider },
2403
+ scale: params.scale,
2404
+ targetResolution: params.target_resolution,
2405
+ prompt: params.prompt,
2406
+ creativity: params.creativity,
2407
+ seed: params.seed,
2408
+ timeoutSeconds: params.timeout_seconds
2409
+ });
2410
+ case "interpolate_video":
2411
+ return provider.interpolateVideo(params.video, {
2412
+ signal: params.signal,
2413
+ model: { id: req.model, name: req.model, provider: req.provider },
2414
+ targetFps: params.target_fps,
2415
+ factor: params.factor,
2416
+ seed: params.seed,
2417
+ timeoutSeconds: params.timeout_seconds
2418
+ });
2419
+ case "outpaint_video":
2420
+ return provider.outpaintVideo(params.video, {
2421
+ signal: params.signal,
2422
+ model: { id: req.model, name: req.model, provider: req.provider },
2423
+ prompt: params.prompt,
2424
+ entities: await coerceEntityList(params, this),
2425
+ negativePrompt: params.negative_prompt,
2426
+ padding: coerceOutpaintPadding(params.padding),
2427
+ expandRatio: params.expand_ratio,
2428
+ aspectRatio: params.aspect_ratio,
2429
+ resolution: params.resolution,
2430
+ seed: params.seed,
2431
+ timeoutSeconds: params.timeout_seconds
2432
+ });
2433
+ case "lip_sync":
2434
+ return provider.lipSync(params.video, {
2435
+ model: { id: req.model, name: req.model, provider: req.provider },
2436
+ audio: params.audio,
2437
+ seed: params.seed
2438
+ });
2439
+ case "text_to_music":
2440
+ return provider.textToMusic({
2441
+ prompt: String(params.prompt ?? ""),
2442
+ model: { id: req.model, name: req.model, provider: req.provider },
2443
+ lyrics: params.lyrics,
2444
+ durationSeconds: params.duration_seconds,
2445
+ seed: params.seed,
2446
+ audioFormat: params.audioFormat,
2447
+ timeoutSeconds: params.timeout_seconds
2448
+ });
2449
+ case "audio_to_audio":
2450
+ return provider.audioToAudio(params.audio, {
2451
+ model: { id: req.model, name: req.model, provider: req.provider },
2452
+ prompt: params.prompt,
2453
+ voice: params.voice,
2454
+ strength: params.strength,
2455
+ seed: params.seed,
2456
+ audioFormat: params.audioFormat,
2457
+ timeoutSeconds: params.timeout_seconds
1172
2458
  });
1173
2459
  case "automatic_speech_recognition":
1174
2460
  return provider.automaticSpeechRecognition({
@@ -1184,30 +2470,605 @@ export class ProcessingContext {
1184
2470
  model: req.model,
1185
2471
  dimensions: params.dimensions
1186
2472
  });
2473
+ case "text_to_3d":
2474
+ return provider.textTo3D({
2475
+ model: model3dOf(params, req),
2476
+ prompt: String(params.prompt ?? ""),
2477
+ negativePrompt: params.negative_prompt ?? null,
2478
+ artStyle: params.art_style ?? null,
2479
+ outputFormat: params.output_format,
2480
+ seed: params.seed ?? null,
2481
+ timeoutSeconds: params.timeout_seconds ?? null,
2482
+ enableTextures: params.enable_textures
2483
+ });
2484
+ case "image_to_3d":
2485
+ return provider.imageTo3D(coerceImageList(params)[0] ?? new Uint8Array(), {
2486
+ model: model3dOf(params, req),
2487
+ prompt: params.prompt ?? null,
2488
+ outputFormat: params.output_format,
2489
+ seed: params.seed ?? null,
2490
+ timeoutSeconds: params.timeout_seconds ?? null
2491
+ });
1187
2492
  default:
1188
2493
  throw new Error(`Capability '${req.capability}' requires streaming API`);
1189
2494
  }
1190
2495
  }
2496
+ /**
2497
+ * HOLDOUT (anti-slop/no-unknown-returns): the honest return type is
2498
+ * {@link ProviderPredictionResult} (see {@link dispatchCapability}), but the
2499
+ * node and capability call sites assert their own capability's result off it
2500
+ * (`as Uint8Array`, `as number[][]`, …), and those assertions do not compile
2501
+ * against the union. Typing it means editing every one of them. `text-nodes`
2502
+ * is done — it parses each result with a predicate instead.
2503
+ */
1191
2504
  async runProviderPrediction(req) {
1192
- const id = randomUUID();
2505
+ return (await this.runGeneration(req)).output;
2506
+ }
2507
+ /**
2508
+ * Run one generation: open the record, call the provider inside a receipt
2509
+ * scope, persist what came back, close the record. Every terminal state is
2510
+ * a message — `completed`, `failed`, `cancelled` — and the tracker turns
2511
+ * those into the row. Design: docs/media-generation-tracking-design.md § 5.
2512
+ */
2513
+ async runGeneration(req) {
2514
+ return this.runGenerationWith(req, (provider, signal) => {
2515
+ if (!provider) {
2516
+ throw new Error("Provider generation requires a provider.");
2517
+ }
2518
+ const dispatchReq = {
2519
+ ...req,
2520
+ params: { ...(req.params ?? {}), signal }
2521
+ };
2522
+ return this.retryContentFilterRefusal(req, () => this.dispatchCapability(provider, dispatchReq));
2523
+ });
2524
+ }
2525
+ /**
2526
+ * The generation envelope around an arbitrary provider call: the same
2527
+ * record, receipt scope, persistence and terminal messages as
2528
+ * {@link runGeneration}, for a caller whose call shape the capability
2529
+ * switch has no case for — N images in one request, streamed PCM wrapped
2530
+ * into a WAV. `call` receives the provider and the generation's abort
2531
+ * signal; what it returns is the output, and its media buffers are what
2532
+ * `persist` saves.
2533
+ */
2534
+ async runGenerationWith(req, call, opts) {
2535
+ const id = req.id ?? randomUUID();
2536
+ return withSpan("generation", { "generation.id": id, "generation.provider": req.provider, "generation.kind": req.capability }, async () => {
2537
+ const startedAt = Date.now();
2538
+ const origin = this.generationOrigin(req, req.origin);
2539
+ const controller = new AbortController();
2540
+ const signals = [controller.signal, this.signal];
2541
+ if (req.signal)
2542
+ signals.push(req.signal);
2543
+ const signal = AbortSignal.any(signals);
2544
+ const accept = opts?.onGenerationAccepted ??
2545
+ this._generationLifecycle?.onGenerationAccepted;
2546
+ const terminal = opts?.onGenerationTerminal ??
2547
+ this._generationLifecycle?.onGenerationTerminal;
2548
+ let durableAccepted = false;
2549
+ let terminalAttempted = false;
2550
+ try {
2551
+ const accepted = await accept?.({ generationId: id, request: req });
2552
+ if (accepted?.skipProvider) {
2553
+ throw new GenerationAlreadyAcceptedError(accepted.existingGenerationId ?? id);
2554
+ }
2555
+ durableAccepted = accepted?.durable === true;
2556
+ generationRegistry.register(id, {
2557
+ userId: this.userId,
2558
+ abort: () => controller.abort(new Error("Generation cancelled"))
2559
+ });
2560
+ this.emitPrediction("running", req, id, null, undefined, startedAt, {
2561
+ origin
2562
+ });
2563
+ const provider = opts?.withoutProvider
2564
+ ? null
2565
+ : await this.getProvider(req.provider);
2566
+ const { value: output, receipt: recorded, providerResult } = await runWithGenerationReceipt(() => call(provider, signal), {
2567
+ providerRequestOptions: accepted ?? undefined,
2568
+ onProviderRequestAccepted: opts?.onProviderRequestAccepted ??
2569
+ this._generationLifecycle?.onProviderRequestAccepted,
2570
+ onProviderRequestBound: opts?.onProviderRequestBound ??
2571
+ this._generationLifecycle?.onProviderRequestBound
2572
+ });
2573
+ const encoded = encodedAudioOf(output);
2574
+ const persistedAssets = req.persist
2575
+ ? await this.persistGenerationBytes(id, req, encoded ? [encoded.data] : mediaBuffers(output), encoded?.mimeType)
2576
+ : [];
2577
+ const assets = persistedAssets.filter((asset) => asset !== null);
2578
+ let delivery;
2579
+ if (opts?.finalizeOutput) {
2580
+ try {
2581
+ delivery = await opts.finalizeOutput(output, assets);
2582
+ }
2583
+ catch (error) {
2584
+ delivery = {
2585
+ status: "failed",
2586
+ error: error instanceof Error ? error.message : String(error)
2587
+ };
2588
+ }
2589
+ }
2590
+ const deliveryFields = {};
2591
+ if (delivery) {
2592
+ deliveryFields.delivery = delivery;
2593
+ }
2594
+ const late = opts?.receiptAfterPersist?.() ?? null;
2595
+ const receipt = late || recorded ? { ...(recorded ?? {}), ...(late ?? {}) } : null;
2596
+ const assetIds = assets
2597
+ .map((asset) => asset.asset_id)
2598
+ .filter((assetId) => typeof assetId === "string");
2599
+ const durableAssetIds = persistedAssets.map((asset) => asset?.asset_id ?? null);
2600
+ try {
2601
+ terminalAttempted = true;
2602
+ await terminal?.({
2603
+ generationId: id,
2604
+ request: req,
2605
+ status: "completed",
2606
+ output: providerResult ?? output,
2607
+ receipt,
2608
+ assetIds: durableAssetIds,
2609
+ ...deliveryFields
2610
+ });
2611
+ }
2612
+ catch (terminalError) {
2613
+ this.emit({
2614
+ type: "log_update",
2615
+ node_id: req.nodeId ?? "",
2616
+ node_name: req.nodeId ?? "",
2617
+ workflow_id: req.workflowId ?? this.workflowId ?? null,
2618
+ severity: "warning",
2619
+ content: `Generation ${id} completed but durable finalization will be recovered: ${terminalError instanceof Error ? terminalError.message : String(terminalError)}`
2620
+ });
2621
+ }
2622
+ this.emitPrediction("completed", req, id, delivery ? { delivery } : generationResultData(req.capability, output), undefined, startedAt, { origin, asset_ids: assetIds, receipt });
2623
+ generationRegistry.settle(id, {
2624
+ status: "completed",
2625
+ asset_ids: assetIds,
2626
+ receipt,
2627
+ ...deliveryFields
2628
+ });
2629
+ return {
2630
+ id,
2631
+ output,
2632
+ assets,
2633
+ receipt,
2634
+ duration_ms: Date.now() - startedAt,
2635
+ ...deliveryFields
2636
+ };
2637
+ }
2638
+ catch (error) {
2639
+ const cause = error instanceof GenerationScopeError ? error.cause : error;
2640
+ const receipt = error instanceof GenerationScopeError ? error.receipt : null;
2641
+ const providerResult = error instanceof GenerationScopeError
2642
+ ? error.providerResult
2643
+ : undefined;
2644
+ const message = cause instanceof Error ? cause.message : String(cause);
2645
+ if (cause instanceof GenerationAlreadyAcceptedError) {
2646
+ throw cause;
2647
+ }
2648
+ const status = signal.aborted
2649
+ ? durableAccepted
2650
+ ? "running"
2651
+ : "cancelled"
2652
+ : durableAccepted && !isAuthoritativeProviderTerminalError(cause)
2653
+ ? "running"
2654
+ : "failed";
2655
+ if (durableAccepted && !terminalAttempted) {
2656
+ try {
2657
+ terminalAttempted = true;
2658
+ await terminal?.({
2659
+ generationId: id,
2660
+ request: req,
2661
+ status: isAuthoritativeProviderTerminalError(cause)
2662
+ ? "failed"
2663
+ : "recovering",
2664
+ output: providerResult,
2665
+ error: message,
2666
+ receipt,
2667
+ assetIds: []
2668
+ });
2669
+ }
2670
+ catch {
2671
+ // The provider observation remains authoritative. The durable row
2672
+ // can be finalized by the recovery worker.
2673
+ }
2674
+ }
2675
+ if (this.appRunContext && !durableAccepted && !terminalAttempted) {
2676
+ try {
2677
+ terminalAttempted = true;
2678
+ await terminal?.({
2679
+ generationId: id,
2680
+ request: req,
2681
+ status: signal.aborted ? "cancelled" : "failed",
2682
+ error: message,
2683
+ receipt,
2684
+ assetIds: []
2685
+ });
2686
+ }
2687
+ catch {
2688
+ // Preserve the provider failure; the live ledger can retry its write.
2689
+ }
2690
+ }
2691
+ this.emitPrediction(status, req, id, null, message, startedAt, {
2692
+ origin,
2693
+ receipt
2694
+ });
2695
+ generationRegistry.settle(id, {
2696
+ status,
2697
+ error: message,
2698
+ asset_ids: [],
2699
+ receipt
2700
+ });
2701
+ throw cause;
2702
+ }
2703
+ });
2704
+ }
2705
+ /**
2706
+ * Save a generation's media output as assets, stamping the generation id
2707
+ * on each. Bytes only: a segmentation result, a transcript or an embedding
2708
+ * is a result and stays on the message.
2709
+ */
2710
+ async persistGenerationBytes(id, req, buffers, mimeOverride) {
2711
+ if (!this.hasModelInterface("createAsset")) {
2712
+ return buffers.map(() => null);
2713
+ }
2714
+ const assets = [];
2715
+ // The prompt and settings ride on the asset, not only on the ledger row:
2716
+ // the row has its own retention and lives in another table, while the
2717
+ // asset is what somebody opens weeks later to make a variant.
2718
+ const provenance = buildAssetGenerationMetadata({
2719
+ prompt: req.params?.prompt,
2720
+ provider: req.provider,
2721
+ model: req.model,
2722
+ capability: req.capability,
2723
+ params: req.params
2724
+ });
2725
+ const metadata = { generation_id: id, ...provenance };
2726
+ for (const [index, bytes] of buffers.entries()) {
2727
+ const mime = req.persist?.mime ??
2728
+ mimeOverride ??
2729
+ generationMime(req.capability, bytes);
2730
+ const ext = extForGenerationMime(mime);
2731
+ const name = buffers.length === 1 || !req.persist?.name
2732
+ ? (req.persist?.name ?? `${req.capability}-${startedName(id)}.${ext}`)
2733
+ : req.persist.name.replace(/(\.[a-z0-9]+)?$/i, `-${index + 1}$1`);
2734
+ try {
2735
+ const created = await this.createAsset({
2736
+ name,
2737
+ contentType: mime,
2738
+ content: bytes,
2739
+ parentId: req.persist?.parentId ?? null,
2740
+ nodeId: req.nodeId ?? null,
2741
+ metadata: { ...metadata }
2742
+ });
2743
+ const assetId = isRecord(created) && typeof created.id === "string"
2744
+ ? created.id
2745
+ : null;
2746
+ if (!assetId) {
2747
+ assets.push(null);
2748
+ continue;
2749
+ }
2750
+ assets.push({
2751
+ type: assetRefType(mime),
2752
+ uri: `asset://${assetId}.${ext}`,
2753
+ asset_id: assetId,
2754
+ metadata: { ...metadata }
2755
+ });
2756
+ }
2757
+ catch (error) {
2758
+ assets.push(null);
2759
+ // The generation happened and was billed; a failed save must not turn
2760
+ // it into a failed generation. The caller still gets the bytes.
2761
+ this.emit({
2762
+ type: "log_update",
2763
+ node_id: req.nodeId ?? "",
2764
+ node_name: req.nodeId ?? "",
2765
+ workflow_id: req.workflowId ?? this.workflowId ?? null,
2766
+ severity: "warning",
2767
+ content: `Generated ${mime} could not be saved as an asset: ${error instanceof Error ? error.message : String(error)}`
2768
+ });
2769
+ }
2770
+ }
2771
+ return assets;
2772
+ }
2773
+ /**
2774
+ * Run a prediction, retrying a content-filter refusal a bounded number of
2775
+ * times. The filter is non-deterministic — Veo refused one shot of a
2776
+ * five-shot trailer on an ordinary cinematic prompt and passed the same
2777
+ * prompt on retry — and a refused take is not charged, so the cheap answer to
2778
+ * a refusal is to ask again before failing the node that asked.
2779
+ *
2780
+ * A refusal that survives the retries is rethrown as-is, so the kernel can
2781
+ * still drop the item rather than the run.
2782
+ */
2783
+ async retryContentFilterRefusal(req, attemptFn) {
2784
+ for (let attempt = 1;; attempt++) {
2785
+ try {
2786
+ return await attemptFn();
2787
+ }
2788
+ catch (error) {
2789
+ if (!isContentFilterRefusal(error) ||
2790
+ attempt > CONTENT_FILTER_MAX_RETRIES) {
2791
+ throw error;
2792
+ }
2793
+ const message = error instanceof Error ? error.message : String(error);
2794
+ this.emit({
2795
+ type: "log_update",
2796
+ node_id: req.nodeId ?? "",
2797
+ node_name: req.nodeId ?? "",
2798
+ workflow_id: req.workflowId ?? this.workflowId ?? null,
2799
+ severity: "warning",
2800
+ content: `${req.provider}/${req.model} content-filtered this request (attempt ${attempt} of ${CONTENT_FILTER_MAX_RETRIES + 1}); retrying. ${message}`
2801
+ });
2802
+ await waitForRetry(contentFilterRetryDelayMs(attempt), this.signal);
2803
+ }
2804
+ }
2805
+ }
2806
+ /**
2807
+ * Whether a provider streams raw PCM for text-to-speech. When false, the
2808
+ * caller should use {@link textToSpeechEncoded} (the provider returns an
2809
+ * encoded audio file rather than samples).
2810
+ */
2811
+ async providerSupportsStreamingTTS(providerId) {
2812
+ const provider = await this.getProvider(providerId);
2813
+ return provider.supportsStreamingTextToSpeech();
2814
+ }
2815
+ /**
2816
+ * Whether a provider's chat API accepts a `video` content part. Lets a
2817
+ * caller refuse before paying for a prediction that the provider would
2818
+ * reject while resolving the message, which is where the video bytes have
2819
+ * already been fetched and inlined.
2820
+ */
2821
+ async providerSupportsVideoInput(providerId) {
2822
+ const provider = await this.getProvider(providerId);
2823
+ return provider.supportsVideoInput;
2824
+ }
2825
+ /**
2826
+ * Generate speech as an encoded audio file (mp3/wav/…). Returns null when the
2827
+ * provider doesn't implement the encoded path. Used for providers (FAL, KIE,
2828
+ * ElevenLabs) that return audio files instead of streaming PCM.
2829
+ */
2830
+ async textToSpeechEncoded(req) {
2831
+ return this.runEncodedGeneration(req, async (provider) => {
2832
+ const params = req.params ?? {};
2833
+ return provider.textToSpeechEncoded({
2834
+ text: String(params.text ?? ""),
2835
+ model: req.model,
2836
+ voice: params.voice,
2837
+ speed: params.speed,
2838
+ referenceAudio: params.reference_audio,
2839
+ referenceText: params.reference_text,
2840
+ language: params.language,
2841
+ instructions: params.instructions,
2842
+ audioFormat: params.audioFormat
2843
+ });
2844
+ });
2845
+ }
2846
+ /**
2847
+ * The generation lifecycle for the encoded-audio paths: same id on every
2848
+ * message, origin on `running`, the receipt on the terminal message,
2849
+ * `cancelled` on abort. These return an encoded result rather than bytes,
2850
+ * so the caller persists.
2851
+ */
2852
+ async runEncodedGeneration(req, call) {
2853
+ const id = req.id ?? randomUUID();
1193
2854
  const startedAt = Date.now();
1194
- this.emitPrediction("running", req, id, null, undefined, startedAt);
2855
+ const origin = this.generationOrigin(req, req.origin);
2856
+ const controller = new AbortController();
2857
+ const signals = [controller.signal, this.signal];
2858
+ if (req.signal)
2859
+ signals.push(req.signal);
2860
+ const signal = AbortSignal.any(signals);
2861
+ const accept = this._generationLifecycle?.onGenerationAccepted;
2862
+ const terminal = this._generationLifecycle?.onGenerationTerminal;
2863
+ let durableAccepted = false;
2864
+ let terminalAttempted = false;
2865
+ let accepted;
1195
2866
  try {
2867
+ accepted = await accept?.({ generationId: id, request: req });
2868
+ if (accepted?.skipProvider) {
2869
+ throw new GenerationAlreadyAcceptedError(accepted.existingGenerationId ?? id);
2870
+ }
2871
+ durableAccepted = accepted?.durable === true;
2872
+ generationRegistry.register(id, {
2873
+ userId: this.userId,
2874
+ abort: () => controller.abort(new Error("Generation cancelled"))
2875
+ });
2876
+ this.emitPrediction("running", req, id, null, undefined, startedAt, {
2877
+ origin
2878
+ });
1196
2879
  const provider = await this.getProvider(req.provider);
1197
- const output = await this.dispatchCapability(provider, req);
1198
- this.emitPrediction("completed", req, id, output, undefined, startedAt);
1199
- return output;
2880
+ const { value, receipt, providerResult } = await runWithGenerationReceipt(() => call(provider, signal), {
2881
+ providerRequestOptions: accepted ?? undefined,
2882
+ onProviderRequestAccepted: this._generationLifecycle?.onProviderRequestAccepted,
2883
+ onProviderRequestBound: this._generationLifecycle?.onProviderRequestBound
2884
+ });
2885
+ const encoded = encodedAudioOf(value);
2886
+ const persistedAssets = req.persist && encoded
2887
+ ? await this.persistGenerationBytes(id, req, [encoded.data], encoded.mimeType)
2888
+ : [];
2889
+ const assets = persistedAssets.filter((asset) => asset !== null);
2890
+ const assetIds = assets
2891
+ .map((asset) => asset.asset_id)
2892
+ .filter((assetId) => typeof assetId === "string");
2893
+ const durableAssetIds = persistedAssets.map((asset) => asset?.asset_id ?? null);
2894
+ try {
2895
+ terminalAttempted = true;
2896
+ await terminal?.({
2897
+ generationId: id,
2898
+ request: req,
2899
+ status: "completed",
2900
+ output: providerResult ?? value,
2901
+ receipt,
2902
+ assetIds: durableAssetIds
2903
+ });
2904
+ }
2905
+ catch {
2906
+ // Provider success remains authoritative even when durable finalization
2907
+ // needs a later recovery attempt.
2908
+ }
2909
+ this.emitPrediction("completed", req, id, null, undefined, startedAt, {
2910
+ origin,
2911
+ asset_ids: assetIds,
2912
+ receipt
2913
+ });
2914
+ generationRegistry.settle(id, {
2915
+ status: "completed",
2916
+ asset_ids: assetIds,
2917
+ receipt
2918
+ });
2919
+ return value;
1200
2920
  }
1201
2921
  catch (error) {
1202
- const message = error instanceof Error ? error.message : String(error);
1203
- this.emitPrediction("failed", req, id, null, message, startedAt);
1204
- throw error;
2922
+ const cause = error instanceof GenerationScopeError ? error.cause : error;
2923
+ const receipt = error instanceof GenerationScopeError ? error.receipt : null;
2924
+ const providerResult = error instanceof GenerationScopeError
2925
+ ? error.providerResult
2926
+ : undefined;
2927
+ const message = cause instanceof Error ? cause.message : String(cause);
2928
+ if (cause instanceof GenerationAlreadyAcceptedError) {
2929
+ throw cause;
2930
+ }
2931
+ const status = signal.aborted
2932
+ ? durableAccepted
2933
+ ? "running"
2934
+ : "cancelled"
2935
+ : durableAccepted && !isAuthoritativeProviderTerminalError(cause)
2936
+ ? "running"
2937
+ : "failed";
2938
+ if (durableAccepted && !terminalAttempted) {
2939
+ try {
2940
+ terminalAttempted = true;
2941
+ await terminal?.({
2942
+ generationId: id,
2943
+ request: req,
2944
+ status: isAuthoritativeProviderTerminalError(cause)
2945
+ ? "failed"
2946
+ : "recovering",
2947
+ output: providerResult,
2948
+ error: message,
2949
+ receipt,
2950
+ assetIds: []
2951
+ });
2952
+ }
2953
+ catch {
2954
+ // Keep the provider outcome visible while durable finalization is
2955
+ // retried by the recovery worker.
2956
+ }
2957
+ }
2958
+ if (this.appRunContext && !durableAccepted && !terminalAttempted) {
2959
+ try {
2960
+ terminalAttempted = true;
2961
+ await terminal?.({
2962
+ generationId: id,
2963
+ request: req,
2964
+ status: signal.aborted ? "cancelled" : "failed",
2965
+ error: message,
2966
+ receipt,
2967
+ assetIds: []
2968
+ });
2969
+ }
2970
+ catch {
2971
+ // Preserve the provider failure; the live ledger can retry its write.
2972
+ }
2973
+ }
2974
+ this.emitPrediction(status, req, id, null, message, startedAt, {
2975
+ origin,
2976
+ receipt
2977
+ });
2978
+ generationRegistry.settle(id, {
2979
+ status,
2980
+ error: message,
2981
+ asset_ids: [],
2982
+ receipt
2983
+ });
2984
+ throw cause;
1205
2985
  }
1206
2986
  }
2987
+ /**
2988
+ * Generate music as an encoded audio file (mp3/wav/flac). Mirrors
2989
+ * {@link textToSpeechEncoded} for the `text_to_music` capability — the
2990
+ * provider returns a fully-encoded audio file rather than streaming PCM.
2991
+ */
2992
+ async textToMusic(req) {
2993
+ return this.runEncodedGeneration(req, async (provider) => {
2994
+ const params = req.params ?? {};
2995
+ return provider.textToMusic({
2996
+ prompt: String(params.prompt ?? ""),
2997
+ model: { id: req.model, name: req.model, provider: req.provider },
2998
+ lyrics: params.lyrics,
2999
+ durationSeconds: params.duration_seconds,
3000
+ seed: params.seed,
3001
+ audioFormat: params.audioFormat,
3002
+ timeoutSeconds: params.timeout_seconds
3003
+ });
3004
+ });
3005
+ }
3006
+ /**
3007
+ * Rewrite a recording into another one. Mirrors {@link textToMusic} for the
3008
+ * `audio_to_audio` capability: the provider hands back an encoded file, and
3009
+ * the source bytes ride in `params.audio`.
3010
+ */
3011
+ async audioToAudio(req) {
3012
+ return this.runEncodedGeneration(req, async (provider) => {
3013
+ const params = req.params ?? {};
3014
+ const audio = params.audio;
3015
+ if (!(audio instanceof Uint8Array) || audio.length === 0) {
3016
+ throw new Error("audio_to_audio needs source audio bytes");
3017
+ }
3018
+ return provider.audioToAudio(audio, {
3019
+ model: { id: req.model, name: req.model, provider: req.provider },
3020
+ prompt: params.prompt,
3021
+ voice: params.voice,
3022
+ strength: params.strength,
3023
+ seed: params.seed,
3024
+ audioFormat: params.audioFormat,
3025
+ timeoutSeconds: params.timeout_seconds
3026
+ });
3027
+ });
3028
+ }
3029
+ /**
3030
+ * Generate an audio result conditioned on a captured video window and scene
3031
+ * context. The encoded-audio lifecycle persists the result without touching
3032
+ * the source video's soundtrack or timeline placement.
3033
+ */
3034
+ async videoToAudio(req) {
3035
+ return this.runEncodedGeneration(req, async (provider, signal) => {
3036
+ const params = req.params ?? {};
3037
+ const source = isRecord(params.source)
3038
+ ? {
3039
+ assetId: params.source.assetId ?? params.source.asset_id,
3040
+ durationSeconds: params.source.durationSeconds ?? params.source.duration_seconds,
3041
+ startSeconds: params.source.startSeconds ?? params.source.start_seconds,
3042
+ endSeconds: params.source.endSeconds ?? params.source.end_seconds
3043
+ }
3044
+ : params.source;
3045
+ const request = VideoToAudioRequest.parse({
3046
+ task: VIDEO_TO_AUDIO_TASK,
3047
+ model: {
3048
+ id: req.model,
3049
+ provider: req.provider,
3050
+ supportedTasks: [VIDEO_TO_AUDIO_TASK]
3051
+ },
3052
+ source,
3053
+ sceneContext: params.sceneContext ?? params.scene_context
3054
+ });
3055
+ const video = params.video instanceof Uint8Array
3056
+ ? params.video
3057
+ : await loadMediaRefBytes({ type: "video", asset_id: request.source.assetId }, this);
3058
+ if (!video?.byteLength) {
3059
+ throw new Error("video_to_audio requires nonempty source video bytes");
3060
+ }
3061
+ return requestVideoToAudio(provider, video, request, { signal });
3062
+ });
3063
+ }
1207
3064
  async *streamProviderPrediction(req) {
1208
- const id = randomUUID();
3065
+ const id = req.id ?? randomUUID();
1209
3066
  const startedAt = Date.now();
1210
- this.emitPrediction("running", req, id, null, undefined, startedAt);
3067
+ const origin = this.generationOrigin(req, req.origin);
3068
+ this.emitPrediction("running", req, id, null, undefined, startedAt, {
3069
+ origin
3070
+ });
3071
+ let terminalStatusEmitted = false;
1211
3072
  try {
1212
3073
  const provider = await this.getProvider(req.provider);
1213
3074
  const params = req.params ?? {};
@@ -1232,7 +3093,11 @@ export class ProcessingContext {
1232
3093
  text: String(params.text ?? ""),
1233
3094
  model: req.model,
1234
3095
  voice: params.voice,
1235
- speed: params.speed
3096
+ speed: params.speed,
3097
+ referenceAudio: params.reference_audio,
3098
+ referenceText: params.reference_text,
3099
+ language: params.language,
3100
+ instructions: params.instructions
1236
3101
  })) {
1237
3102
  yield item;
1238
3103
  }
@@ -1240,13 +3105,29 @@ export class ProcessingContext {
1240
3105
  else {
1241
3106
  throw new Error(`Capability '${req.capability}' is not streamable`);
1242
3107
  }
1243
- this.emitPrediction("completed", req, id, null, undefined, startedAt);
3108
+ this.emitPrediction("completed", req, id, null, undefined, startedAt, {
3109
+ origin,
3110
+ asset_ids: []
3111
+ });
3112
+ terminalStatusEmitted = true;
1244
3113
  }
1245
3114
  catch (error) {
1246
3115
  const message = error instanceof Error ? error.message : String(error);
1247
- this.emitPrediction("failed", req, id, null, message, startedAt);
3116
+ const status = this.signal.aborted || req.signal?.aborted ? "cancelled" : "failed";
3117
+ this.emitPrediction(status, req, id, null, message, startedAt, {
3118
+ origin
3119
+ });
3120
+ terminalStatusEmitted = true;
1248
3121
  throw error;
1249
3122
  }
3123
+ finally {
3124
+ if (!terminalStatusEmitted) {
3125
+ this.emitPrediction("completed", req, id, null, undefined, startedAt, {
3126
+ origin,
3127
+ asset_ids: []
3128
+ });
3129
+ }
3130
+ }
1250
3131
  }
1251
3132
  // -----------------------------------------------------------------------
1252
3133
  // Output normalization
@@ -1257,6 +3138,8 @@ export class ProcessingContext {
1257
3138
  *
1258
3139
  * Port of sanitize_memory_uris_for_client() from types.py.
1259
3140
  */
3141
+ // HOLDOUT (anti-slop/no-unknown-returns): same value domain as
3142
+ // `normalizeOutputValue` — an arbitrary node output, rewritten in place.
1260
3143
  static sanitizeForClient(value) {
1261
3144
  if (value === null || value === undefined)
1262
3145
  return value;
@@ -1266,11 +3149,11 @@ export class ProcessingContext {
1266
3149
  if (Array.isArray(value)) {
1267
3150
  return value.map((v) => ProcessingContext.sanitizeForClient(v));
1268
3151
  }
1269
- if (typeof value !== "object")
3152
+ if (!isRecord(value))
1270
3153
  return value;
1271
3154
  const obj = value;
1272
3155
  const uri = obj.uri;
1273
- const isAssetLike = "type" in obj && typeof uri === "string";
3156
+ const isAssetLike = "type" in obj && isString(uri);
1274
3157
  if (isAssetLike && uri.startsWith("memory://")) {
1275
3158
  const sanitized = { ...obj };
1276
3159
  if (sanitized.data !== undefined && sanitized.data !== null) {
@@ -1320,13 +3203,22 @@ export class ProcessingContext {
1320
3203
  const v = value;
1321
3204
  return "type" in v && ("uri" in v || "data" in v || "asset_id" in v);
1322
3205
  }
1323
- static guessAssetMime(asset) {
1324
- const explicit = asset.mime_type ?? asset.content_type;
1325
- if (typeof explicit === "string" && explicit)
1326
- return explicit;
3206
+ /**
3207
+ * `mimeType` comes first because it is the field `ImageRef` declares — this
3208
+ * used to read only `mime_type`, which nothing sets, so a node's sniffed
3209
+ * `image/webp` was discarded and every provider's WebP stored as PNG.
3210
+ * `image/unknown` is `image-nodes` reporting its own sniff found nothing, so
3211
+ * it counts as undeclared here rather than as a type to forward.
3212
+ */
3213
+ static guessAssetMime(asset, bytes) {
3214
+ const declared = asset.mimeType ?? asset.mime_type ?? asset.content_type;
3215
+ if (isNonEmptyString(declared) && declared !== "image/unknown") {
3216
+ return declared;
3217
+ }
1327
3218
  const type = String(asset.type ?? "").toLowerCase();
1328
- if (type.includes("image"))
1329
- return "image/png";
3219
+ if (type.includes("image")) {
3220
+ return (bytes && sniffImageMime(bytes)) ?? "image/png";
3221
+ }
1330
3222
  if (type.includes("audio"))
1331
3223
  return "audio/wav";
1332
3224
  if (type.includes("video"))
@@ -1338,10 +3230,10 @@ export class ProcessingContext {
1338
3230
  return "application/octet-stream";
1339
3231
  }
1340
3232
  static extForMime(mime) {
3233
+ const imageExt = extForImageMime(mime);
3234
+ if (imageExt)
3235
+ return imageExt;
1341
3236
  const map = {
1342
- "image/png": "png",
1343
- "image/jpeg": "jpg",
1344
- "image/webp": "webp",
1345
3237
  "audio/wav": "wav",
1346
3238
  "audio/mpeg": "mp3",
1347
3239
  "video/mp4": "mp4",
@@ -1359,33 +3251,87 @@ export class ProcessingContext {
1359
3251
  if (Array.isArray(data) && data.every((v) => Number.isInteger(v))) {
1360
3252
  return new Uint8Array(data);
1361
3253
  }
1362
- if (typeof data === "string") {
3254
+ if (isString(data)) {
1363
3255
  return Uint8Array.from(Buffer.from(data, "base64"));
1364
3256
  }
1365
3257
  return null;
1366
3258
  }
3259
+ static isClientFetchableResolvedAssetUri(sourceUri, resolvedUri) {
3260
+ return (resolvedUri.startsWith("/api/storage/") ||
3261
+ resolvedUri.startsWith("api/storage/") ||
3262
+ (/^(memory|file|s3|supabase):\/\//.test(sourceUri) &&
3263
+ resolvedUri !== sourceUri &&
3264
+ /^https?:\/\//.test(resolvedUri)));
3265
+ }
1367
3266
  async getAssetBytes(asset) {
1368
3267
  const decoded = ProcessingContext.decodeAssetData(asset.data);
1369
3268
  if (decoded)
1370
3269
  return decoded;
1371
3270
  const uri = asset.uri;
1372
- if (typeof uri !== "string" || !this.storage)
1373
- return null;
1374
- return this.storage.retrieve(uri);
3271
+ if (isString(uri) && this.storage) {
3272
+ const stored = await this.storage.retrieve(uri);
3273
+ if (stored)
3274
+ return stored;
3275
+ }
3276
+ if (isString(uri) &&
3277
+ (uri.startsWith("asset://") ||
3278
+ uri.startsWith("/api/storage/") ||
3279
+ uri.startsWith("api/storage/"))) {
3280
+ const { bytes } = await this.resolveAssetBytes(uri);
3281
+ if (bytes)
3282
+ return bytes;
3283
+ }
3284
+ // No usable uri but an asset_id is present: resolve directly by id.
3285
+ // resolveAssetBytes has its own HTTP fallback, so this works even without a
3286
+ // storage adapter (which the early `!this.storage` return used to preclude).
3287
+ const assetId = asset.asset_id;
3288
+ if (isNonEmptyString(assetId)) {
3289
+ const { bytes } = await this.resolveAssetBytes(`asset://${assetId}`);
3290
+ if (bytes)
3291
+ return bytes;
3292
+ }
3293
+ return null;
1375
3294
  }
1376
- async materializeAsset(asset, mode) {
1377
- if (mode === "python" || mode === "raw") {
1378
- return asset;
3295
+ async materializeAsset(rawAsset, mode) {
3296
+ if (mode === "native" || mode === "raw") {
3297
+ // In-process consumers handle the raw asset as-is (incl. raw RGBA).
3298
+ return rawAsset;
3299
+ }
3300
+ // A fetchable reference with no inline bytes is already materialized for
3301
+ // an HTTP client. Reuse it instead of downloading the asset into this
3302
+ // process and writing an identical second temp object. This is especially
3303
+ // important for SDK pass-through workflows: their large input was already
3304
+ // uploaded once and should not be copied again on output.
3305
+ if (mode === "temp_url") {
3306
+ const uri = isString(rawAsset.uri) ? rawAsset.uri : "";
3307
+ const hasInlineBytes = ProcessingContext.decodeAssetData(rawAsset.data) !== null;
3308
+ const isStorageRoute = uri.startsWith("/api/storage/") || uri.startsWith("api/storage/");
3309
+ const isAdapterUri = /^(file|s3|supabase):\/\//.test(uri);
3310
+ if (uri && !hasInlineBytes && (isStorageRoute || isAdapterUri)) {
3311
+ const resolvedUri = this._tempUrlResolver
3312
+ ? await this._tempUrlResolver(uri)
3313
+ : uri;
3314
+ if (ProcessingContext.isClientFetchableResolvedAssetUri(uri, resolvedUri)) {
3315
+ return {
3316
+ ...rawAsset,
3317
+ uri: resolvedUri,
3318
+ data: undefined
3319
+ };
3320
+ }
3321
+ }
1379
3322
  }
3323
+ // Raw in-flight RGBA → encode to PNG up front so every downstream mode
3324
+ // treats it as an ordinary image (correct mime, extension, and bytes).
3325
+ const asset = (await encodeRawImageRef(rawAsset));
1380
3326
  const bytes = await this.getAssetBytes(asset);
1381
3327
  if (!bytes)
1382
3328
  return asset;
1383
- const mime = ProcessingContext.guessAssetMime(asset);
3329
+ const mime = ProcessingContext.guessAssetMime(asset, bytes);
1384
3330
  if (mode === "data_uri") {
1385
- const encoded = Buffer.from(bytes).toString("base64");
3331
+ const base64 = Buffer.from(bytes).toString("base64");
1386
3332
  return {
1387
3333
  ...asset,
1388
- uri: `data:${mime};base64,${encoded}`
3334
+ uri: `data:${mime};base64,${base64}`
1389
3335
  };
1390
3336
  }
1391
3337
  if (mode === "storage_url") {
@@ -1404,12 +3350,9 @@ export class ProcessingContext {
1404
3350
  return asset;
1405
3351
  const key = `temp/${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
1406
3352
  const storedUri = await this.storage.store(key, bytes, mime);
1407
- const resolvedUri = this._tempUrlResolver
1408
- ? await this._tempUrlResolver(storedUri)
1409
- : storedUri;
1410
3353
  return {
1411
3354
  ...asset,
1412
- uri: resolvedUri,
3355
+ uri: await this.resolveTempUrl(storedUri),
1413
3356
  data: undefined
1414
3357
  };
1415
3358
  }
@@ -1417,6 +3360,10 @@ export class ProcessingContext {
1417
3360
  if (!this.workspaceDir) {
1418
3361
  throw new Error("workspace_dir is required for workspace asset output");
1419
3362
  }
3363
+ // Imported here rather than at module scope: this module also loads in
3364
+ // browser / Edge runtimes, and `@nodetool-ai/storage` reaches `node:fs`.
3365
+ // Only this branch, which already requires a workspace directory, needs it.
3366
+ const { FileStorageAdapter } = await import("@nodetool-ai/storage");
1420
3367
  const workspaceAssets = new FileStorageAdapter(resolveWorkspacePath(this.workspaceDir, "assets"));
1421
3368
  const key = `${randomUUID()}.${ProcessingContext.extForMime(mime)}`;
1422
3369
  const uri = await workspaceAssets.store(key, bytes, mime);
@@ -1432,6 +3379,9 @@ export class ProcessingContext {
1432
3379
  * Recursively normalize workflow outputs, materializing asset-like values
1433
3380
  * according to the selected output mode.
1434
3381
  */
3382
+ // HOLDOUT (anti-slop/no-unknown-returns): the input and the result are an
3383
+ // arbitrary workflow value; naming that domain is a bigger change than a
3384
+ // typing pass.
1435
3385
  async normalizeOutputValue(value, mode = this.assetOutputMode) {
1436
3386
  if (value === null || value === undefined)
1437
3387
  return value;
@@ -1442,9 +3392,27 @@ export class ProcessingContext {
1442
3392
  return Promise.all(value.map((item) => this.normalizeOutputValue(item, mode)));
1443
3393
  }
1444
3394
  if (ProcessingContext.isAssetLike(value)) {
1445
- return this.materializeAsset(value, mode);
3395
+ let byMode = this._normalizedOutputAssets.get(value);
3396
+ if (!byMode) {
3397
+ byMode = new Map();
3398
+ this._normalizedOutputAssets.set(value, byMode);
3399
+ }
3400
+ let normalized = byMode.get(mode);
3401
+ if (!normalized) {
3402
+ normalized = this.materializeAsset(value, mode);
3403
+ byMode.set(mode, normalized);
3404
+ }
3405
+ try {
3406
+ return await normalized;
3407
+ }
3408
+ catch (error) {
3409
+ // A transient storage failure must remain retryable.
3410
+ if (byMode.get(mode) === normalized)
3411
+ byMode.delete(mode);
3412
+ throw error;
3413
+ }
1446
3414
  }
1447
- if (typeof value === "object") {
3415
+ if (isRecord(value)) {
1448
3416
  const entries = Object.entries(value);
1449
3417
  const normalized = await Promise.all(entries.map(async ([k, v]) => [k, await this.normalizeOutputValue(v, mode)]));
1450
3418
  return Object.fromEntries(normalized);
@@ -1452,4 +3420,155 @@ export class ProcessingContext {
1452
3420
  return value;
1453
3421
  }
1454
3422
  }
3423
+ /**
3424
+ * The media resolver to hand `BaseProvider.generateLoop`, or undefined when
3425
+ * this context cannot dereference one.
3426
+ *
3427
+ * `view_image` answers with an `asset://` reference and the loop turns it into
3428
+ * pixels for the one request that needs them, so every host that drives a tool
3429
+ * loop passes this. A partial context — a test double, a host object built
3430
+ * from a subset of the interface — has no method to call, and the loop then
3431
+ * sends the reference through untouched rather than throwing mid-turn.
3432
+ */
3433
+ export function mediaResolverFor(context) {
3434
+ if (!context || !isCallable(context.resolveMessageMediaUris))
3435
+ return undefined;
3436
+ return (messages) => context.resolveMessageMediaUris(messages);
3437
+ }
3438
+ // ---------------------------------------------------------------------------
3439
+ // Generation seam helpers
3440
+ // ---------------------------------------------------------------------------
3441
+ /**
3442
+ * The 3D model a request names: the full `Model3D` the node resolved (with
3443
+ * its supported tasks and formats) when it passed one, else the minimal ref.
3444
+ */
3445
+ function model3dOf(params, req) {
3446
+ const given = params.model;
3447
+ if (isRecord(given) &&
3448
+ typeof given.id === "string" &&
3449
+ typeof given.provider === "string") {
3450
+ return given;
3451
+ }
3452
+ return {
3453
+ id: req.model,
3454
+ name: req.model,
3455
+ provider: req.provider
3456
+ };
3457
+ }
3458
+ /** The media buffers a capability result carries, if any. */
3459
+ function mediaBuffers(output) {
3460
+ if (output instanceof Uint8Array)
3461
+ return [output];
3462
+ if (Array.isArray(output) &&
3463
+ output.length > 0 &&
3464
+ output.every((item) => item instanceof Uint8Array)) {
3465
+ return output;
3466
+ }
3467
+ return [];
3468
+ }
3469
+ /** An encoded-audio result (`{data, mimeType}`), or null for anything else. */
3470
+ function encodedAudioOf(value) {
3471
+ if (!isRecord(value))
3472
+ return null;
3473
+ const data = value.data;
3474
+ if (!(data instanceof Uint8Array) || data.length === 0)
3475
+ return null;
3476
+ const mimeType = value.mimeType;
3477
+ return {
3478
+ data,
3479
+ mimeType: typeof mimeType === "string" ? mimeType : undefined
3480
+ };
3481
+ }
3482
+ /** What a `completed` prediction message carries as `data`: never bytes. */
3483
+ function generationResultData(capability, output) {
3484
+ if (output instanceof Uint8Array)
3485
+ return null;
3486
+ if (encodedAudioOf(output))
3487
+ return null;
3488
+ if (Array.isArray(output)) {
3489
+ if (output.every((item) => item instanceof Uint8Array)) {
3490
+ return { count: output.length };
3491
+ }
3492
+ if (capability === "segment_image") {
3493
+ return output.map((item) => {
3494
+ if (!isRecord(item))
3495
+ return item;
3496
+ const { mask: _mask, ...rest } = item;
3497
+ return rest;
3498
+ });
3499
+ }
3500
+ if (capability === "generate_embedding") {
3501
+ const first = output[0];
3502
+ return {
3503
+ count: output.length,
3504
+ dimensions: Array.isArray(first) ? first.length : null
3505
+ };
3506
+ }
3507
+ return { count: output.length };
3508
+ }
3509
+ return output ?? null;
3510
+ }
3511
+ const VIDEO_CAPABILITIES = new Set([
3512
+ "text_to_video",
3513
+ "image_to_video",
3514
+ "reference_to_video",
3515
+ "video_to_video",
3516
+ "extend_video",
3517
+ "lip_sync"
3518
+ ]);
3519
+ const AUDIO_CAPABILITIES = new Set([
3520
+ "text_to_speech",
3521
+ "text_to_music",
3522
+ "audio_to_audio",
3523
+ "video_to_audio"
3524
+ ]);
3525
+ const MODEL3D_CAPABILITIES = new Set([
3526
+ "text_to_3d",
3527
+ "image_to_3d"
3528
+ ]);
3529
+ /** The MIME a generation's bytes carry when the caller stated none. */
3530
+ function generationMime(capability, bytes) {
3531
+ if (VIDEO_CAPABILITIES.has(capability))
3532
+ return "video/mp4";
3533
+ if (AUDIO_CAPABILITIES.has(capability))
3534
+ return "audio/mpeg";
3535
+ if (MODEL3D_CAPABILITIES.has(capability))
3536
+ return "model/gltf-binary";
3537
+ if (capability === "vectorize_image")
3538
+ return "image/svg+xml";
3539
+ return sniffImageMime(bytes) ?? "image/png";
3540
+ }
3541
+ const GENERATION_MIME_EXT = {
3542
+ "video/mp4": "mp4",
3543
+ "video/webm": "webm",
3544
+ "audio/mpeg": "mp3",
3545
+ "audio/mp3": "mp3",
3546
+ "audio/wav": "wav",
3547
+ "audio/x-wav": "wav",
3548
+ "audio/flac": "flac",
3549
+ "audio/ogg": "ogg",
3550
+ "model/gltf-binary": "glb",
3551
+ "model/gltf+json": "gltf",
3552
+ "image/svg+xml": "svg"
3553
+ };
3554
+ function extForGenerationMime(mime) {
3555
+ if (mime.startsWith("image/") && mime !== "image/svg+xml") {
3556
+ return extForImageMime(mime) ?? "bin";
3557
+ }
3558
+ return GENERATION_MIME_EXT[mime] ?? "bin";
3559
+ }
3560
+ function assetRefType(mime) {
3561
+ if (mime.startsWith("image/"))
3562
+ return "image";
3563
+ if (mime.startsWith("video/"))
3564
+ return "video";
3565
+ if (mime.startsWith("audio/"))
3566
+ return "audio";
3567
+ return "asset";
3568
+ }
3569
+ /** A short, sortable name fragment for an unnamed generation asset. */
3570
+ function startedName(id) {
3571
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
3572
+ return `${stamp}-${id.slice(0, 8)}`;
3573
+ }
1455
3574
  //# sourceMappingURL=context.js.map