@nodetool-ai/runtime 0.7.0-rc.22 → 0.8.1

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