@tanstack/ai 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/activities/chat/adapter.js +23 -16
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agent-loop-strategies.d.ts +10 -4
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -17
  6. package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
  7. package/dist/esm/activities/chat/cancel.d.ts +40 -0
  8. package/dist/esm/activities/chat/cancel.js +54 -0
  9. package/dist/esm/activities/chat/cancel.js.map +1 -0
  10. package/dist/esm/activities/chat/index.d.ts +28 -16
  11. package/dist/esm/activities/chat/index.js +2100 -1744
  12. package/dist/esm/activities/chat/index.js.map +1 -1
  13. package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
  14. package/dist/esm/activities/chat/mcp/manager.js +90 -77
  15. package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
  16. package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
  17. package/dist/esm/activities/chat/messages.js +397 -346
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/builder.js +17 -15
  20. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  21. package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
  22. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
  23. package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
  24. package/dist/esm/activities/chat/middleware/compose.js +623 -531
  25. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  26. package/dist/esm/activities/chat/middleware/define.js +12 -5
  27. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  28. package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
  29. package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
  30. package/dist/esm/activities/chat/middleware/locks.js +71 -0
  31. package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
  32. package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
  33. package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
  34. package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
  35. package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
  36. package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
  37. package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
  38. package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
  39. package/dist/esm/activities/chat/middleware/run-store.js +176 -0
  40. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
  41. package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
  42. package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
  43. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
  44. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
  45. package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
  46. package/dist/esm/activities/chat/middleware/validate.js +23 -28
  47. package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
  48. package/dist/esm/activities/chat/stream/json-parser.js +39 -25
  49. package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
  50. package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
  51. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  52. package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
  53. package/dist/esm/activities/chat/stream/processor.js +1341 -1542
  54. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  55. package/dist/esm/activities/chat/stream/strategies.js +69 -53
  56. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  57. package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
  58. package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
  59. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
  60. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
  61. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  62. package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
  63. package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
  64. package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
  65. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  66. package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
  67. package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
  68. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  69. package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
  70. package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
  71. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  72. package/dist/esm/activities/error-payload.js +85 -47
  73. package/dist/esm/activities/error-payload.js.map +1 -1
  74. package/dist/esm/activities/generateAudio/adapter.js +22 -15
  75. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  76. package/dist/esm/activities/generateAudio/index.d.ts +4 -0
  77. package/dist/esm/activities/generateAudio/index.js +141 -105
  78. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  79. package/dist/esm/activities/generateImage/adapter.js +22 -15
  80. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  81. package/dist/esm/activities/generateImage/index.d.ts +4 -0
  82. package/dist/esm/activities/generateImage/index.js +155 -111
  83. package/dist/esm/activities/generateImage/index.js.map +1 -1
  84. package/dist/esm/activities/generateSpeech/adapter.js +22 -15
  85. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  86. package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
  87. package/dist/esm/activities/generateSpeech/index.js +159 -110
  88. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  89. package/dist/esm/activities/generateTranscription/adapter.js +22 -15
  90. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  91. package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
  92. package/dist/esm/activities/generateTranscription/index.js +159 -100
  93. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  94. package/dist/esm/activities/generateVideo/adapter.js +36 -29
  95. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  96. package/dist/esm/activities/generateVideo/index.d.ts +143 -19
  97. package/dist/esm/activities/generateVideo/index.js +456 -279
  98. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  99. package/dist/esm/activities/generateVideo/snap.js +60 -48
  100. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  101. package/dist/esm/activities/index.js +8 -34
  102. package/dist/esm/activities/middleware/index.d.ts +1 -1
  103. package/dist/esm/activities/middleware/run.d.ts +10 -0
  104. package/dist/esm/activities/middleware/run.js +53 -29
  105. package/dist/esm/activities/middleware/run.js.map +1 -1
  106. package/dist/esm/activities/middleware/types.d.ts +44 -6
  107. package/dist/esm/activities/stream-generation-result.d.ts +4 -1
  108. package/dist/esm/activities/stream-generation-result.js +79 -44
  109. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  110. package/dist/esm/activities/summarize/adapter.js +22 -15
  111. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  112. package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
  113. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  114. package/dist/esm/activities/summarize/index.d.ts +27 -0
  115. package/dist/esm/activities/summarize/index.js +268 -102
  116. package/dist/esm/activities/summarize/index.js.map +1 -1
  117. package/dist/esm/adapter-internals.d.ts +2 -1
  118. package/dist/esm/adapter-internals.js +4 -11
  119. package/dist/esm/client.d.ts +25 -3
  120. package/dist/esm/client.js +131 -64
  121. package/dist/esm/client.js.map +1 -1
  122. package/dist/esm/custom-events.d.ts +76 -0
  123. package/dist/esm/custom-events.js +37 -0
  124. package/dist/esm/custom-events.js.map +1 -0
  125. package/dist/esm/delivery-detach.d.ts +50 -0
  126. package/dist/esm/delivery-detach.js +71 -0
  127. package/dist/esm/delivery-detach.js.map +1 -0
  128. package/dist/esm/delivery-disconnect.d.ts +62 -0
  129. package/dist/esm/delivery-disconnect.js +81 -0
  130. package/dist/esm/delivery-disconnect.js.map +1 -0
  131. package/dist/esm/extend-adapter.js +19 -17
  132. package/dist/esm/extend-adapter.js.map +1 -1
  133. package/dist/esm/index.d.ts +23 -5
  134. package/dist/esm/index.js +30 -97
  135. package/dist/esm/interrupt-resume.d.ts +71 -0
  136. package/dist/esm/interrupt-resume.js +438 -0
  137. package/dist/esm/interrupt-resume.js.map +1 -0
  138. package/dist/esm/interrupt-serialization.d.ts +12 -0
  139. package/dist/esm/interrupt-serialization.js +178 -0
  140. package/dist/esm/interrupt-serialization.js.map +1 -0
  141. package/dist/esm/interrupts.d.ts +84 -0
  142. package/dist/esm/interrupts.js +31 -0
  143. package/dist/esm/interrupts.js.map +1 -0
  144. package/dist/esm/locks.d.ts +10 -0
  145. package/dist/esm/locks.js +2 -0
  146. package/dist/esm/logger/console-logger.js +101 -78
  147. package/dist/esm/logger/console-logger.js.map +1 -1
  148. package/dist/esm/logger/internal-logger.js +104 -89
  149. package/dist/esm/logger/internal-logger.js.map +1 -1
  150. package/dist/esm/logger/resolve.js +54 -49
  151. package/dist/esm/logger/resolve.js.map +1 -1
  152. package/dist/esm/logger/types.d.ts +1 -1
  153. package/dist/esm/middlewares/content-guard.js +142 -148
  154. package/dist/esm/middlewares/content-guard.js.map +1 -1
  155. package/dist/esm/middlewares/index.js +2 -6
  156. package/dist/esm/middlewares/otel.js +598 -732
  157. package/dist/esm/middlewares/otel.js.map +1 -1
  158. package/dist/esm/middlewares/usage-attributes.js +47 -40
  159. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  160. package/dist/esm/realtime/event-emitter.js +24 -25
  161. package/dist/esm/realtime/event-emitter.js.map +1 -1
  162. package/dist/esm/realtime/index.d.ts +5 -9
  163. package/dist/esm/realtime/index.js +29 -6
  164. package/dist/esm/realtime/index.js.map +1 -1
  165. package/dist/esm/scope.d.ts +47 -0
  166. package/dist/esm/stream-durability.d.ts +171 -0
  167. package/dist/esm/stream-durability.js +295 -0
  168. package/dist/esm/stream-durability.js.map +1 -0
  169. package/dist/esm/stream-to-response.d.ts +178 -13
  170. package/dist/esm/stream-to-response.js +663 -115
  171. package/dist/esm/stream-to-response.js.map +1 -1
  172. package/dist/esm/strip-to-spec-middleware.js +30 -16
  173. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  174. package/dist/esm/system-prompts.js +27 -21
  175. package/dist/esm/system-prompts.js.map +1 -1
  176. package/dist/esm/tool-registry.js +72 -45
  177. package/dist/esm/tool-registry.js.map +1 -1
  178. package/dist/esm/tools/provider-tool.js +14 -5
  179. package/dist/esm/tools/provider-tool.js.map +1 -1
  180. package/dist/esm/types.d.ts +332 -21
  181. package/dist/esm/types.js +2 -0
  182. package/dist/esm/utilities/ag-ui-wire.js +79 -93
  183. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  184. package/dist/esm/utilities/chat-params.d.ts +26 -4
  185. package/dist/esm/utilities/chat-params.js +218 -92
  186. package/dist/esm/utilities/chat-params.js.map +1 -1
  187. package/dist/esm/utilities/errors.js +28 -18
  188. package/dist/esm/utilities/errors.js.map +1 -1
  189. package/dist/esm/utilities/media-prompt.js +46 -41
  190. package/dist/esm/utilities/media-prompt.js.map +1 -1
  191. package/dist/esm/utilities/numbers.js +13 -10
  192. package/dist/esm/utilities/numbers.js.map +1 -1
  193. package/dist/esm/utilities/provider-executed.js +20 -11
  194. package/dist/esm/utilities/provider-executed.js.map +1 -1
  195. package/dist/esm/utilities/sampling-keys.js +31 -19
  196. package/dist/esm/utilities/sampling-keys.js.map +1 -1
  197. package/dist/esm/utilities/tool-result.js +42 -30
  198. package/dist/esm/utilities/tool-result.js.map +1 -1
  199. package/dist/esm/utilities/usage.js +27 -9
  200. package/dist/esm/utilities/usage.js.map +1 -1
  201. package/dist/esm/utils.js +26 -18
  202. package/dist/esm/utils.js.map +1 -1
  203. package/package.json +10 -6
  204. package/skills/ai-core/SKILL.md +69 -18
  205. package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
  206. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
  207. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
  208. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
  209. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
  210. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
  211. package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
  212. package/skills/ai-core/chat-experience/SKILL.md +156 -11
  213. package/skills/ai-core/client-persistence/SKILL.md +277 -0
  214. package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
  215. package/skills/ai-core/debug-logging/SKILL.md +1 -1
  216. package/skills/ai-core/locks/SKILL.md +143 -0
  217. package/skills/ai-core/media-generation/SKILL.md +144 -12
  218. package/skills/ai-core/middleware/SKILL.md +258 -33
  219. package/skills/ai-core/structured-outputs/SKILL.md +1 -1
  220. package/skills/ai-core/tool-calling/SKILL.md +54 -59
  221. package/src/activities/chat/agent-loop-strategies.ts +10 -4
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1152 -153
  224. package/src/activities/chat/mcp/manager.ts +4 -4
  225. package/src/activities/chat/mcp/types.ts +2 -2
  226. package/src/activities/chat/messages.ts +5 -3
  227. package/src/activities/chat/middleware/builder.ts +1 -1
  228. package/src/activities/chat/middleware/compose.ts +186 -9
  229. package/src/activities/chat/middleware/index.ts +26 -0
  230. package/src/activities/chat/middleware/locks.ts +102 -0
  231. package/src/activities/chat/middleware/pending-turn.ts +47 -0
  232. package/src/activities/chat/middleware/run-disconnect.ts +62 -0
  233. package/src/activities/chat/middleware/run-store.ts +412 -0
  234. package/src/activities/chat/middleware/types.ts +62 -1
  235. package/src/activities/chat/stream/processor.ts +189 -5
  236. package/src/activities/chat/tools/approval-schema.ts +205 -0
  237. package/src/activities/chat/tools/tool-calls.ts +106 -13
  238. package/src/activities/chat/tools/tool-definition.ts +210 -39
  239. package/src/activities/generateAudio/index.ts +20 -3
  240. package/src/activities/generateImage/index.ts +20 -3
  241. package/src/activities/generateSpeech/index.ts +25 -3
  242. package/src/activities/generateTranscription/index.ts +26 -3
  243. package/src/activities/generateVideo/index.ts +345 -82
  244. package/src/activities/middleware/index.ts +2 -0
  245. package/src/activities/middleware/run.ts +31 -0
  246. package/src/activities/middleware/types.ts +49 -5
  247. package/src/activities/stream-generation-result.ts +30 -2
  248. package/src/activities/summarize/chat-stream-summarize.ts +5 -0
  249. package/src/activities/summarize/index.ts +200 -10
  250. package/src/adapter-internals.ts +10 -1
  251. package/src/client.ts +244 -0
  252. package/src/custom-events.ts +107 -0
  253. package/src/delivery-detach.ts +72 -0
  254. package/src/delivery-disconnect.ts +84 -0
  255. package/src/index.ts +138 -0
  256. package/src/interrupt-resume.ts +824 -0
  257. package/src/interrupt-serialization.ts +183 -0
  258. package/src/interrupts.ts +146 -0
  259. package/src/locks.ts +17 -0
  260. package/src/logger/types.ts +1 -1
  261. package/src/middlewares/otel.ts +1 -0
  262. package/src/realtime/index.ts +5 -9
  263. package/src/scope.ts +47 -0
  264. package/src/stream-durability.ts +598 -0
  265. package/src/stream-to-response.ts +1051 -95
  266. package/src/strip-to-spec-middleware.ts +3 -3
  267. package/src/types.ts +416 -24
  268. package/src/utilities/chat-params.ts +245 -55
  269. package/dist/esm/activities/index.js.map +0 -1
  270. package/dist/esm/adapter-internals.js.map +0 -1
  271. package/dist/esm/index.js.map +0 -1
  272. package/dist/esm/middlewares/index.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"stream-to-response.js","sources":["../../src/stream-to-response.ts"],"sourcesContent":["import { toRunErrorPayload } from './activities/error-payload'\nimport type { StreamChunk } from './types'\n\n/**\n * Collect all text content from a StreamChunk async iterable and return as a string.\n *\n * This function consumes the entire stream, accumulating content from TEXT_MESSAGE_CONTENT events,\n * and returns the final concatenated text.\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @returns Promise<string> - The accumulated text content\n *\n * @example\n * ```typescript\n * const stream = chat({\n * adapter: openaiText(),\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: 'Hello!' }]\n * });\n * const text = await streamToText(stream);\n * console.log(text); // \"Hello! How can I help you today?\"\n * ```\n */\nexport async function streamToText(\n stream: AsyncIterable<StreamChunk>,\n): Promise<string> {\n let accumulatedContent = ''\n\n for await (const chunk of stream) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {\n accumulatedContent += chunk.delta\n }\n }\n\n return accumulatedContent\n}\n\n/**\n * Convert a StreamChunk async iterable to a ReadableStream in Server-Sent Events format\n *\n * This creates a ReadableStream that emits chunks in SSE format:\n * - Each chunk is prefixed with \"data: \"\n * - Each chunk is followed by \"\\n\\n\"\n * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param abortController - Optional AbortController to abort when stream is cancelled\n * @returns ReadableStream in Server-Sent Events format\n */\nexport function toServerSentEventsStream(\n stream: AsyncIterable<StreamChunk>,\n abortController?: AbortController,\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder()\n\n return new ReadableStream({\n async start(controller) {\n try {\n for await (const chunk of stream) {\n // Check if stream was cancelled/aborted\n if (abortController?.signal.aborted) {\n break\n }\n\n // Send each chunk as Server-Sent Events format\n controller.enqueue(\n encoder.encode(`data: ${JSON.stringify(chunk)}\\n\\n`),\n )\n }\n\n controller.close()\n } catch (error: unknown) {\n // Don't send error if aborted\n if (abortController?.signal.aborted) {\n controller.close()\n return\n }\n\n // Send error event (AG-UI RUN_ERROR)\n controller.enqueue(\n encoder.encode(\n `data: ${JSON.stringify({\n type: 'RUN_ERROR',\n timestamp: Date.now(),\n error: toRunErrorPayload(error),\n })}\\n\\n`,\n ),\n )\n controller.close()\n }\n },\n cancel() {\n // When the ReadableStream is cancelled (e.g., client disconnects),\n // abort the underlying stream\n if (abortController) {\n abortController.abort()\n }\n },\n })\n}\n\n/**\n * Convert a StreamChunk async iterable to a Response in Server-Sent Events format\n *\n * This creates a Response that emits chunks in SSE format:\n * - Each chunk is prefixed with \"data: \"\n * - Each chunk is followed by \"\\n\\n\"\n * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param init - Optional Response initialization options (including `abortController`)\n * @returns Response in Server-Sent Events format\n *\n * @example\n * ```typescript\n * const stream = chat({ adapter: openaiText(), model: \"gpt-4o\", messages: [...] });\n * return toServerSentEventsResponse(stream, { abortController });\n * ```\n */\nexport function toServerSentEventsResponse(\n stream: AsyncIterable<StreamChunk>,\n init?: ResponseInit & { abortController?: AbortController },\n): Response {\n const { headers, abortController, ...responseInit } = init ?? {}\n\n // Start with default SSE headers\n const mergedHeaders = new Headers({\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n })\n\n // Override with user headers if provided, handling all HeadersInit forms:\n // Headers instance, string[][], or plain object\n if (headers) {\n const userHeaders = new Headers(headers)\n userHeaders.forEach((value, key) => {\n mergedHeaders.set(key, value)\n })\n }\n\n return new Response(toServerSentEventsStream(stream, abortController), {\n ...responseInit,\n headers: mergedHeaders,\n })\n}\n\n/**\n * Convert a StreamChunk async iterable to a ReadableStream in HTTP stream format (newline-delimited JSON)\n *\n * This creates a ReadableStream that emits chunks as newline-delimited JSON:\n * - Each chunk is JSON.stringify'd and followed by \"\\n\"\n * - No SSE formatting (no \"data: \" prefix)\n *\n * This format is compatible with `fetchHttpStream` connection adapter.\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param abortController - Optional AbortController to abort when stream is cancelled\n * @returns ReadableStream in HTTP stream format (newline-delimited JSON)\n *\n * @example\n * ```typescript\n * const stream = chat({ adapter: openaiText(), model: \"gpt-4o\", messages: [...] });\n * const readableStream = toHttpStream(stream);\n * // Use with Response for HTTP streaming (not SSE)\n * return new Response(readableStream, {\n * headers: { 'Content-Type': 'application/x-ndjson' }\n * });\n * ```\n */\nexport function toHttpStream(\n stream: AsyncIterable<StreamChunk>,\n abortController?: AbortController,\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder()\n\n return new ReadableStream({\n async start(controller) {\n try {\n for await (const chunk of stream) {\n // Check if stream was cancelled/aborted\n if (abortController?.signal.aborted) {\n break\n }\n\n // Send each chunk as newline-delimited JSON\n controller.enqueue(encoder.encode(`${JSON.stringify(chunk)}\\n`))\n }\n\n controller.close()\n } catch (error: unknown) {\n // Don't send error if aborted\n if (abortController?.signal.aborted) {\n controller.close()\n return\n }\n\n // Send error event (AG-UI RUN_ERROR)\n controller.enqueue(\n encoder.encode(\n `${JSON.stringify({\n type: 'RUN_ERROR',\n timestamp: Date.now(),\n error: toRunErrorPayload(error),\n })}\\n`,\n ),\n )\n controller.close()\n }\n },\n cancel() {\n // When the ReadableStream is cancelled (e.g., client disconnects),\n // abort the underlying stream\n if (abortController) {\n abortController.abort()\n }\n },\n })\n}\n\n/**\n * Convert a StreamChunk async iterable to a Response in HTTP stream format (newline-delimited JSON)\n *\n * This creates a Response that emits chunks in HTTP stream format:\n * - Each chunk is JSON.stringify'd and followed by \"\\n\"\n * - No SSE formatting (no \"data: \" prefix)\n *\n * This format is compatible with `fetchHttpStream` connection adapter.\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param init - Optional Response initialization options (including `abortController`)\n * @returns Response in HTTP stream format (newline-delimited JSON)\n *\n * @example\n * ```typescript\n * const stream = chat({ adapter: openaiText(), model: \"gpt-4o\", messages: [...] });\n * return toHttpResponse(stream, { abortController });\n * ```\n */\nexport function toHttpResponse(\n stream: AsyncIterable<StreamChunk>,\n init?: ResponseInit & { abortController?: AbortController },\n): Response {\n return new Response(toHttpStream(stream, init?.abortController), {\n ...init,\n })\n}\n"],"names":[],"mappings":";AAuBA,eAAsB,aACpB,QACiB;AACjB,MAAI,qBAAqB;AAEzB,mBAAiB,SAAS,QAAQ;AAChC,QAAI,MAAM,SAAS,0BAA0B,MAAM,OAAO;AACxD,4BAAsB,MAAM;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AACT;AAcO,SAAS,yBACd,QACA,iBAC4B;AAC5B,QAAM,UAAU,IAAI,YAAA;AAEpB,SAAO,IAAI,eAAe;AAAA,IACxB,MAAM,MAAM,YAAY;AACtB,UAAI;AACF,yBAAiB,SAAS,QAAQ;AAEhC,cAAI,iBAAiB,OAAO,SAAS;AACnC;AAAA,UACF;AAGA,qBAAW;AAAA,YACT,QAAQ,OAAO,SAAS,KAAK,UAAU,KAAK,CAAC;AAAA;AAAA,CAAM;AAAA,UAAA;AAAA,QAEvD;AAEA,mBAAW,MAAA;AAAA,MACb,SAAS,OAAgB;AAEvB,YAAI,iBAAiB,OAAO,SAAS;AACnC,qBAAW,MAAA;AACX;AAAA,QACF;AAGA,mBAAW;AAAA,UACT,QAAQ;AAAA,YACN,SAAS,KAAK,UAAU;AAAA,cACtB,MAAM;AAAA,cACN,WAAW,KAAK,IAAA;AAAA,cAChB,OAAO,kBAAkB,KAAK;AAAA,YAAA,CAC/B,CAAC;AAAA;AAAA;AAAA,UAAA;AAAA,QACJ;AAEF,mBAAW,MAAA;AAAA,MACb;AAAA,IACF;AAAA,IACA,SAAS;AAGP,UAAI,iBAAiB;AACnB,wBAAgB,MAAA;AAAA,MAClB;AAAA,IACF;AAAA,EAAA,CACD;AACH;AAoBO,SAAS,2BACd,QACA,MACU;AACV,QAAM,EAAE,SAAS,iBAAiB,GAAG,aAAA,IAAiB,QAAQ,CAAA;AAG9D,QAAM,gBAAgB,IAAI,QAAQ;AAAA,IAChC,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,YAAY;AAAA,EAAA,CACb;AAID,MAAI,SAAS;AACX,UAAM,cAAc,IAAI,QAAQ,OAAO;AACvC,gBAAY,QAAQ,CAAC,OAAO,QAAQ;AAClC,oBAAc,IAAI,KAAK,KAAK;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,SAAS,yBAAyB,QAAQ,eAAe,GAAG;AAAA,IACrE,GAAG;AAAA,IACH,SAAS;AAAA,EAAA,CACV;AACH;AAyBO,SAAS,aACd,QACA,iBAC4B;AAC5B,QAAM,UAAU,IAAI,YAAA;AAEpB,SAAO,IAAI,eAAe;AAAA,IACxB,MAAM,MAAM,YAAY;AACtB,UAAI;AACF,yBAAiB,SAAS,QAAQ;AAEhC,cAAI,iBAAiB,OAAO,SAAS;AACnC;AAAA,UACF;AAGA,qBAAW,QAAQ,QAAQ,OAAO,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI,CAAC;AAAA,QACjE;AAEA,mBAAW,MAAA;AAAA,MACb,SAAS,OAAgB;AAEvB,YAAI,iBAAiB,OAAO,SAAS;AACnC,qBAAW,MAAA;AACX;AAAA,QACF;AAGA,mBAAW;AAAA,UACT,QAAQ;AAAA,YACN,GAAG,KAAK,UAAU;AAAA,cAChB,MAAM;AAAA,cACN,WAAW,KAAK,IAAA;AAAA,cAChB,OAAO,kBAAkB,KAAK;AAAA,YAAA,CAC/B,CAAC;AAAA;AAAA,UAAA;AAAA,QACJ;AAEF,mBAAW,MAAA;AAAA,MACb;AAAA,IACF;AAAA,IACA,SAAS;AAGP,UAAI,iBAAiB;AACnB,wBAAgB,MAAA;AAAA,MAClB;AAAA,IACF;AAAA,EAAA,CACD;AACH;AAqBO,SAAS,eACd,QACA,MACU;AACV,SAAO,IAAI,SAAS,aAAa,QAAQ,MAAM,eAAe,GAAG;AAAA,IAC/D,GAAG;AAAA,EAAA,CACJ;AACH;"}
1
+ {"version":3,"file":"stream-to-response.js","names":[],"sources":["../../src/stream-to-response.ts"],"sourcesContent":["import { toRunErrorPayload } from './activities/error-payload'\nimport { isCancelRequestedReason } from './activities/chat/cancel'\nimport {\n isRunStatus,\n isTerminalRunStatus,\n} from './activities/chat/middleware/run-store'\nimport { wasRunDetached } from './delivery-detach'\nimport { notifyRunDisconnected } from './delivery-disconnect'\nimport { resolveResumeRunId } from './stream-durability'\nimport { EventType } from './types'\nimport { resolveDebugOption } from './logger/resolve'\nimport type { LockStore } from './activities/chat/middleware/locks'\nimport type {\n RunRecord,\n RunStore,\n} from './activities/chat/middleware/run-store'\nimport type { InternalLogger } from './logger/internal-logger'\nimport type { DebugOption } from './logger/types'\nimport type { StreamDurability } from './stream-durability'\nimport type { StreamChunk } from './types'\n\nexport { resolveResumeRunId } from './stream-durability'\n\n/**\n * Collect all text content from a StreamChunk async iterable and return as a string.\n *\n * This function consumes the entire stream, accumulating content from TEXT_MESSAGE_CONTENT events,\n * and returns the final concatenated text.\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @returns Promise<string> - The accumulated text content\n *\n * @example\n * ```typescript\n * const stream = chat({\n * adapter: openaiText('gpt-5.5'),\n * messages: [{ role: 'user', content: 'Hello!' }]\n * });\n * const text = await streamToText(stream);\n * console.log(text); // \"Hello! How can I help you today?\"\n * ```\n */\nexport async function streamToText(\n stream: AsyncIterable<StreamChunk>,\n): Promise<string> {\n let accumulatedContent = ''\n\n for await (const chunk of stream) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {\n accumulatedContent += chunk.delta\n }\n }\n\n return accumulatedContent\n}\n\ninterface RecordedFailure {\n error: unknown\n}\n\nfunction errorMessage(error: unknown): string {\n return toRunErrorPayload(error).message\n}\n\nfunction combineFailures(\n primary: unknown,\n secondary: unknown,\n phase: string,\n): unknown {\n if (primary === secondary) return primary\n const errors =\n primary instanceof AggregateError\n ? [...primary.errors, secondary]\n : [primary, secondary]\n return new AggregateError(\n errors,\n `${errorMessage(primary)}; ${phase}: ${errorMessage(secondary)}`,\n )\n}\n\nfunction runErrorChunk(\n error: unknown,\n): Extract<StreamChunk, { type: 'RUN_ERROR' }> {\n const payload = toRunErrorPayload(error)\n return {\n type: EventType.RUN_ERROR,\n timestamp: Date.now(),\n message: payload.message,\n ...(payload.code === undefined ? {} : { code: payload.code }),\n error: payload,\n }\n}\n\nfunction isAborted(signal: AbortSignal): boolean {\n return signal.aborted\n}\n\n/**\n * Whether this abort is an EXPLICIT in-process cancel — the caller aborted with\n * {@link RUN_CANCEL_REASON} rather than the socket going away.\n *\n * Core's own guard, independent of any middleware verdict: a user pressing Stop\n * must always get a closed, terminal log, so the sink refuses to treat that abort\n * as a detach even if the run's middleware published one. A reason-less abort\n * carries a `DOMException`, never a string, so a non-string reason is \"no\n * explicit intent\" — exactly how `resolveAbortReason` reads it in `chat()`.\n */\nfunction isExplicitCancel(signal: AbortSignal): boolean {\n const reason: unknown = signal.reason\n return typeof reason === 'string' && isCancelRequestedReason(reason)\n}\n\nfunction needsTerminalPersistence(\n terminalPersisted: boolean,\n cancelled: boolean,\n failed: boolean,\n): boolean {\n return !terminalPersisted && (cancelled || failed)\n}\n\nfunction toEncodedStream(\n stream: AsyncIterable<StreamChunk>,\n abortController: AbortController | undefined,\n encodeChunk: (chunk: StreamChunk, index: number) => Uint8Array,\n encodeError: (error: unknown) => Uint8Array,\n detachOnCancel = false,\n /**\n * Called once when the response body is cancelled on the detach path, BEFORE\n * returning. The durability branch uses it to tell the run its viewer is gone\n * (see `./delivery-disconnect`) without aborting it.\n */\n onDetachedCancel?: () => void,\n): ReadableStream<Uint8Array> {\n const cancellation = abortController ?? new AbortController()\n let iterator: AsyncIterator<StreamChunk> | undefined\n let iteratorCleanup: Promise<void> | undefined\n let pumpPromise: Promise<void> = Promise.resolve()\n let pumpFailure: RecordedFailure | undefined\n let cancelled = false\n\n const recordPumpFailure = (error: unknown, phase: string): void => {\n pumpFailure = {\n error:\n pumpFailure === undefined\n ? error\n : combineFailures(pumpFailure.error, error, phase),\n }\n }\n\n const closeIterator = (): Promise<void> => {\n iteratorCleanup ??= (async () => {\n if (iterator?.return) await iterator.return()\n })()\n return iteratorCleanup\n }\n\n return new ReadableStream({\n start(controller) {\n iterator = stream[Symbol.asyncIterator]()\n pumpPromise = (async () => {\n let index = 0\n let iteratorDone = false\n\n try {\n while (!isAborted(cancellation.signal)) {\n const result = await iterator.next()\n if (result.done) {\n iteratorDone = true\n break\n }\n if (isAborted(cancellation.signal)) break\n // After a detached cancel the reader is gone but we keep pulling to\n // drain the producer into the durable log; skip enqueuing to the\n // closed controller.\n if (!cancelled) controller.enqueue(encodeChunk(result.value, index))\n index += 1\n }\n } catch (error) {\n recordPumpFailure(error, 'stream iteration failed')\n } finally {\n if (!iteratorDone) {\n try {\n await closeIterator()\n } catch (error) {\n recordPumpFailure(error, 'iterator cleanup failed')\n }\n }\n\n if (\n !cancelled &&\n !isAborted(cancellation.signal) &&\n pumpFailure !== undefined\n ) {\n controller.enqueue(encodeError(pumpFailure.error))\n }\n if (!cancelled) controller.close()\n }\n })().catch((error: unknown) => {\n recordPumpFailure(error, 'stream pump failed')\n })\n },\n async cancel(reason) {\n cancelled = true\n // Detached durable delivery: the client is gone (e.g. a page reload), but\n // the run must finish into the durable log so a rejoining client can tail\n // it to the real terminal. Do NOT abort the producer (that would kill the\n // run and seal the log with RUN_ERROR) and do NOT await the pump — it\n // keeps draining `stream` → the log in the background and terminates\n // normally on its own. A genuine caller-driven stop aborts the producer's\n // own AbortController instead, which this path never touches.\n //\n // Notify the run FIRST, and synchronously. This is the only moment the\n // socket-closed fact exists anywhere, and the run cannot observe it on its\n // own: it holds no handle on this response. That notification is what lets a\n // durable run record itself as detached while it KEEPS RUNNING — the\n // alternative applications were driven to (mirroring `request.signal` into\n // `chat()`'s abortController) reaches the middleware only by killing the run,\n // which for a sandboxed run means the agent is never even launched.\n if (detachOnCancel) {\n onDetachedCancel?.()\n return\n }\n\n if (!isAborted(cancellation.signal)) cancellation.abort(reason)\n\n let cancellationFailure: RecordedFailure | undefined\n try {\n await closeIterator()\n } catch (error) {\n cancellationFailure = { error }\n }\n await pumpPromise\n\n if (pumpFailure !== undefined && cancellationFailure !== undefined) {\n throw combineFailures(\n pumpFailure.error,\n cancellationFailure.error,\n 'iterator cancellation failed',\n )\n }\n if (pumpFailure !== undefined) throw pumpFailure.error\n if (cancellationFailure !== undefined) throw cancellationFailure.error\n },\n })\n}\n\n/**\n * Convert a StreamChunk async iterable to a ReadableStream in Server-Sent Events format\n *\n * This creates a ReadableStream that emits chunks in SSE format:\n * - Each chunk is prefixed with \"data: \"\n * - Each chunk is followed by \"\\n\\n\"\n * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param abortController - Optional AbortController to abort when stream is cancelled\n * @param getId - Optional per-chunk durability offset; when present, each event gets an `id:` line\n * @returns ReadableStream in Server-Sent Events format\n */\nexport function toServerSentEventsStream(\n stream: AsyncIterable<StreamChunk>,\n abortController?: AbortController,\n getId?: (chunk: StreamChunk, index: number) => string | undefined,\n): ReadableStream<Uint8Array> {\n const { encodeChunk, encodeError } = sseEncoders(getId)\n return toEncodedStream(stream, abortController, encodeChunk, encodeError)\n}\n\n/**\n * SSE chunk/error encoders. Shared by the public {@link toServerSentEventsStream}\n * and the internal durability branch (which additionally needs `toEncodedStream`'s\n * private `detachOnCancel`), so the wire format stays identical for both.\n */\nfunction sseEncoders(\n getId?: (chunk: StreamChunk, index: number) => string | undefined,\n): {\n encodeChunk: (chunk: StreamChunk, index: number) => Uint8Array\n encodeError: (error: unknown) => Uint8Array\n} {\n const encoder = new TextEncoder()\n return {\n encodeChunk: (chunk, index) => {\n const id = getId?.(chunk, index)\n const idLine = id === undefined ? '' : `id: ${id}\\n`\n return encoder.encode(`${idLine}data: ${JSON.stringify(chunk)}\\n\\n`)\n },\n encodeError: (error) =>\n encoder.encode(`data: ${JSON.stringify(runErrorChunk(error))}\\n\\n`),\n }\n}\n\n/** Default number of chunks buffered before a durability `append`. */\nconst DEFAULT_DURABILITY_BATCH = 32\n\n/**\n * Resolve and validate the durability batch size. A non-positive-integer (0,\n * negative, fractional, or `NaN`) is rejected rather than clamped: silently\n * `Math.max(1, …)`-ing a `NaN` used to disable size-based flushing entirely\n * (`length >= NaN` is always false), which is a subtle footgun.\n */\nfunction resolveBatchSize(batch: number | undefined): number {\n if (batch === undefined) return DEFAULT_DURABILITY_BATCH\n if (!Number.isInteger(batch) || batch <= 0) {\n throw new Error(\n `Invalid durability batch size: ${batch}. Must be a positive integer.`,\n )\n }\n return batch\n}\n\n/**\n * Boundaries at which the batching producer flushes early, regardless of the\n * batch size — the run-start marker, terminal events, and tool-call ends.\n * Flushing here keeps the durability log promptly consistent at semantically\n * meaningful points.\n *\n * `RUN_STARTED` matters especially for one-shot activities (image, speech,\n * transcription, summarize): they emit `RUN_STARTED`, then await the provider\n * for seconds, then a terminal. Without flushing `RUN_STARTED` the log stays\n * empty for the whole run, so a mount-time `joinRun` finds nothing and its\n * empty-log deadline fast-fails as \"run gone\" — even though the run is alive.\n * Flushing it immediately makes the run resumable from the instant it starts.\n */\nfunction isDurabilityFlushBoundary(chunk: StreamChunk): boolean {\n return (\n chunk.type === 'RUN_STARTED' ||\n chunk.type === 'RUN_FINISHED' ||\n chunk.type === 'RUN_ERROR' ||\n chunk.type === 'TOOL_CALL_END'\n )\n}\n\n/**\n * Name of the synthetic `CUSTOM` chunk a fresh durable producer appends to its\n * log before pulling the first real chunk.\n *\n * Flushing `RUN_STARTED` (above) makes a run joinable from the instant the\n * stream EMITS something — but a `chat()` whose middleware boots a sandbox\n * (create a container, install a CLI) legitimately emits nothing for minutes,\n * and during that window the log is empty. Every joiner's empty-log fail-fast\n * (`memoryStream`'s first-chunk deadline, the client's rejoin connect deadline)\n * then reads the run as gone — and the client clears its resume pointer, so a\n * reload during the boot window permanently orphans a run that is still going.\n *\n * This marker closes the window: it is appended (and flushed) before the\n * producer stream is first pulled, so a join always finds a first chunk within\n * milliseconds of the run being accepted. Takeover alignment is unaffected — a\n * journal replay cannot reproduce the marker, and alignment already skips\n * stored `CUSTOM` chunks as out-of-band for exactly that reason (see\n * `isBridgeCustomChunk` in `@tanstack/ai-sandbox`).\n */\nexport const RUN_ACCEPTED_EVENT = 'run.accepted'\n\n/**\n * Build the delivery-durable source iterable for a transport helper.\n *\n * - **Resume** (`resumeFrom()` non-null): replay strictly after the offset,\n * reading only from the durability log. The input `stream` is NEVER iterated,\n * so `chat()`'s lazy iterator never fires the provider — the untouched\n * generator is simply GC'd. This is what makes resume free of re-invocation.\n * - **Fresh** (`resumeFrom()` null): iterate `stream`, buffering up to `batch`\n * chunks (flushing early at terminal / tool-call boundaries), `append` each\n * batch to the log, then forward. Appending BEFORE forwarding guarantees a\n * reconnecting client can always replay exactly what it already saw.\n *\n * The returned `getId` maps each forwarded chunk to the exact opaque offset\n * returned by the durability adapter for the SSE `id:` line.\n */\nfunction durableStreamSource<TOffset extends string>(\n stream: AsyncIterable<StreamChunk>,\n durability: StreamDurability<TOffset>,\n options: {\n abortController: AbortController\n batch?: number\n logger?: InternalLogger\n },\n): {\n source: AsyncIterable<StreamChunk>\n getId: (chunk: StreamChunk) => string | undefined\n} {\n const resumeOffset = durability.resumeFrom()\n const batchSize = resolveBatchSize(options.batch)\n const abortController = options.abortController\n const logger = options.logger\n const idByChunk = new WeakMap<object, string>()\n const seenOffsets = new Set<string>()\n const getId = (chunk: StreamChunk): string | undefined => idByChunk.get(chunk)\n\n const validateOffset = (offset: TOffset): void => {\n // Reject NUL/CR/LF (would corrupt the SSE `id:` line) and any offset that\n // is not invariant under the wire round-trip. The SSE client reads the id\n // with `.trim()`, so an offset with leading/trailing whitespace would come\n // back changed and no longer match on reconnect — fail loud here rather\n // than silently mis-resuming. (NDJSON carries the offset inside the JSON\n // envelope and is unaffected, but the contract must hold for both wires.)\n if (\n offset.length === 0 ||\n offset.includes('\\0') ||\n offset.includes('\\r') ||\n offset.includes('\\n') ||\n offset !== offset.trim()\n ) {\n throw new Error(\n `Invalid durability offset for SSE id: ${JSON.stringify(offset)}`,\n )\n }\n if (seenOffsets.has(offset)) {\n throw new Error(\n `Durability adapter must return a unique offset per chunk: ${JSON.stringify(offset)}`,\n )\n }\n seenOffsets.add(offset)\n }\n\n async function* produce(): AsyncIterable<StreamChunk> {\n let batch: Array<StreamChunk> = []\n let terminalPersisted = false\n // Whether a terminal event was actually delivered LIVE to the consumer (as\n // opposed to only appended to the log). Distinguishes \"the run already ended\n // on the wire\" from \"the log has a terminal but the consumer never saw one\",\n // which governs whether a late durability-cleanup failure may be rethrown.\n // Only ever assigned inside the nested flush() closure, which TS's\n // control-flow analysis can't observe (see the disable at the read site).\n let terminalForwarded = false\n let failure: RecordedFailure | undefined\n let terminalCause: unknown\n let hasTerminalCause = false\n\n const recordFailure = (error: unknown, phase: string): void => {\n failure = {\n error:\n failure === undefined\n ? error\n : combineFailures(failure.error, error, phase),\n }\n }\n\n async function* flush(): AsyncIterable<StreamChunk> {\n if (batch.length === 0) return\n const toForward = batch\n batch = []\n // Tag each chunk with the exact backend offset. Requiring one opaque\n // token per chunk preserves exact-once resume at any batch size.\n const offsets = await durability.append(toForward)\n if (offsets.length !== toForward.length) {\n throw new Error(\n `Durability append returned ${offsets.length} offsets for ${toForward.length} chunks`,\n )\n }\n toForward.forEach((chunk, i) => {\n const offset = offsets[i]\n if (offset === undefined) {\n throw new Error(`Durability append omitted offset at index ${i}`)\n }\n validateOffset(offset)\n idByChunk.set(chunk, offset)\n })\n if (\n toForward.some(\n (chunk) =>\n chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR',\n )\n ) {\n terminalPersisted = true\n }\n for (const chunk of toForward) {\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n terminalForwarded = true\n }\n yield chunk\n }\n }\n\n try {\n if (isAborted(abortController.signal)) return\n // Make the run joinable BEFORE the producer is first pulled — the pull\n // is what runs the middleware chain, and middleware may take minutes to\n // yield a first chunk. See {@link RUN_ACCEPTED_EVENT}.\n batch.push({\n type: 'CUSTOM',\n name: RUN_ACCEPTED_EVENT,\n value: {},\n timestamp: Date.now(),\n })\n yield* flush()\n for await (const chunk of stream) {\n if (isAborted(abortController.signal)) break\n batch.push(chunk)\n if (batch.length >= batchSize || isDurabilityFlushBoundary(chunk)) {\n yield* flush()\n }\n }\n if (!isAborted(abortController.signal)) yield* flush()\n } catch (error) {\n terminalCause = error\n hasTerminalCause = true\n recordFailure(error, 'producer failed')\n // The provider stream threw. Persist a terminal RUN_ERROR to the\n // durability log so a resumer / joiner learns the run failed (otherwise\n // the log ends with no terminal and they wait forever). Flush any\n // buffered chunks first, then append the terminal WITHOUT forwarding it\n // live — the transport layer synthesizes the live RUN_ERROR on rethrow,\n // so forwarding here too would double-emit.\n if (!isAborted(abortController.signal)) {\n try {\n yield* flush()\n } catch (flushError) {\n recordFailure(flushError, 'flushing buffered chunks failed')\n }\n }\n } finally {\n // The PRODUCER was stopped, which is deliberately not the same question as\n // \"did the delivery socket go away\". A disconnect alone must leave this\n // false: the run survives it and terminalizes this log itself on its way\n // out, and treating the disconnect as a cancel here would make `detached`\n // true for a run that had already finished — skipping `close()` and parking\n // every later tailer forever on a log nobody will ever continue.\n const cancelled = isAborted(abortController.signal)\n\n // Persist any buffered-but-unflushed chunks before terminalizing, so a\n // joiner replaying the log sees everything produced up to a disconnect\n // rather than a truncated prefix. On the abort path the streaming loop\n // broke before its trailing flush; drain flush() here for its persistence\n // side effect only (the delivery socket is gone, so the yielded chunks are\n // discarded). The normal and provider-throw paths already flushed, so\n // `batch` is empty for them and this is a no-op.\n if (batch.length > 0) {\n try {\n for await (const _chunk of flush()) {\n // persist-only: nothing consumes these\n }\n } catch (flushError) {\n recordFailure(flushError, 'flushing buffered chunks on exit failed')\n }\n }\n\n // Was this abort a DETACH? Only the run's own middleware can say — it is\n // the only actor that has resolved both out-of-band cancel bands and\n // `detachOnDisconnect` — and it says so on the stream itself (see\n // `./delivery-detach`). Read only AFTER the try block above has exited,\n // which is what awaits the chat generator's `return()` and therefore the\n // whole `onAbort` chain that publishes the verdict.\n //\n // Every conjunct is load bearing. `cancelled` keeps a normal finish on\n // today's path. `!isExplicitCancel` is core's own belt-and-braces refusal to\n // spare a run the user deliberately stopped, whatever a middleware claims.\n // `!hasTerminalCause` keeps a GENUINE provider failure\n // terminal even if the socket died too, so a real error is never mistaken\n // for a detach. And `wasRunDetached` is false for an\n // explicit cancel (either band), for a non-detachable disconnect, and for\n // every app that has not wired durability — all of which keep terminalizing\n // and closing exactly as before.\n //\n // What is ALREADY IN THE LOG is deliberately NOT a conjunct. An agent-loop\n // run emits one `RUN_FINISHED` PER ITERATION — the intermediate\n // `finishReason: 'tool_calls'` terminal is flushed at its boundary\n // mid-run — so `terminalPersisted` means \"some terminal is in the log\",\n // never \"the run ended\". Gating on it terminalized the log of a healthy,\n // still-running agent for every tool-calling run. Nor can the sink\n // distinguish a final terminal from an intermediate one by its\n // `finishReason`: only the run's middleware knows, and that is exactly\n // what the verdict reports. So a published detach verdict WINS — it\n // already means \"the agent is alive and a successor will terminalize this\n // log\".\n const detached =\n cancelled &&\n !isExplicitCancel(abortController.signal) &&\n !hasTerminalCause &&\n wasRunDetached(stream)\n\n if (\n !detached &&\n needsTerminalPersistence(terminalPersisted, cancelled, hasTerminalCause)\n ) {\n // Prefer the real provider error even when the delivery socket was also\n // aborted: if the run genuinely failed, a joiner should see that cause,\n // not a generic AbortError that masks it. AbortError is only used for a\n // pure cancellation with no underlying failure.\n const cause = hasTerminalCause ? terminalCause : { name: 'AbortError' }\n try {\n await durability.append([runErrorChunk(cause)])\n terminalPersisted = true\n } catch (terminalError) {\n // Rethrown to the live consumer below, but a joiner replaying the log\n // only ever sees a generic incomplete error — so record the real\n // cause server-side where an operator can act on it.\n logger?.errors('persisting terminal RUN_ERROR failed', {\n error: terminalError,\n })\n recordFailure(terminalError, 'persisting terminal RUN_ERROR failed')\n }\n }\n\n // A detached run's log is deliberately left OPEN: the run is still going,\n // and `close()` would terminalize the log the takeover has to continue —\n // a tailing attach would stop at the prefix, and a stored synthetic\n // `RUN_ERROR` would additionally diverge the takeover's journal replay and\n // record a healthy run as failed.\n //\n // This is NOT the general \"fence the close\" that `ai-sandbox`'s claim.ts\n // rules out. That fence would suppress `close()` for a run nobody will ever\n // drive again, wedging the record at `'running'` with every tailer parked\n // forever. The skip here is conditional on a verdict that means the exact\n // opposite: the agent is alive and a successor WILL terminalize this log\n // (its own producer exit runs this same `finally`). Keep that distinction —\n // widening this condition to \"any abort\" re-introduces the wedge.\n if (!detached) {\n try {\n await durability.close()\n } catch (closeError) {\n // A failed close leaves the durable log unterminated for joiners; the\n // live consumer gets the rethrow, but log it for the joiner's sake.\n logger?.errors('closing durability stream failed', {\n error: closeError,\n })\n recordFailure(closeError, 'closing durability stream failed')\n }\n }\n\n // Rethrow a terminalization/close failure to the live consumer ONLY when\n // no terminal reached it yet — the transport then synthesizes a live\n // RUN_ERROR so the consumer isn't left without a terminal. If a terminal\n // was already forwarded (the run ended on the wire), a late failure is a\n // server-side cleanup issue; rethrowing it would append a contradictory\n // second terminal (RUN_ERROR after RUN_FINISHED) on the wire. Suppress the\n // rethrow, but never let the cause vanish — record it server-side, the\n // same as the close / terminal-append failures above. (This also covers a\n // provider that throws AFTER emitting its own terminal, whose error is\n // otherwise neither delivered nor logged.)\n if (failure !== undefined) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- terminalForwarded is set only inside the flush() closure, which TS CFA narrows away here\n if (!terminalForwarded) {\n // eslint-disable-next-line no-unsafe-finally\n throw failure.error\n }\n logger?.errors(\n 'durability failure after a terminal event was forwarded',\n {\n error: failure.error,\n },\n )\n }\n }\n }\n\n async function* replay(offset: TOffset): AsyncIterable<StreamChunk> {\n // Thread the consumer's abort signal into the read so a live-tailing join\n // (a mid-stream reconnect) that is aborted — or that hit a runId with no\n // in-process producer — stops parking and ends instead of hanging forever.\n for await (const { offset: eventOffset, chunk } of durability.read(\n offset,\n abortController.signal,\n )) {\n if (isAborted(abortController.signal)) break\n validateOffset(eventOffset)\n idByChunk.set(chunk, eventOffset)\n yield chunk\n }\n }\n\n return {\n source: resumeOffset !== null ? replay(resumeOffset) : produce(),\n getId,\n }\n}\n\n/**\n * Convert a StreamChunk async iterable to a Response in Server-Sent Events format\n *\n * This creates a Response that emits chunks in SSE format:\n * - Each chunk is prefixed with \"data: \"\n * - Each chunk is followed by \"\\n\\n\"\n * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)\n *\n * Pass a `durability` sink (`memoryStream(request)` / `durableStream(request)`)\n * to make the stream resumable: fresh runs are appended to the log and each SSE\n * event is tagged with an `id:` offset; a reconnect (native `Last-Event-ID`) or\n * a `?offset` join replays from the log without re-running the producer. `batch`\n * controls how many chunks are buffered per `append` (default 32).\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param init - Optional Response initialization options (including `abortController`, `durability` with its optional `batch`, and `debug`)\n * @returns Response in Server-Sent Events format\n *\n * @example\n * ```typescript\n * export async function POST(request: Request) {\n * const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });\n * return toServerSentEventsResponse(stream, { durability: { adapter: memoryStream(request) } });\n * }\n * ```\n */\nexport function toServerSentEventsResponse<TOffset extends string = string>(\n stream: AsyncIterable<StreamChunk>,\n init?: ResponseInit & {\n abortController?: AbortController\n durability?: { adapter: StreamDurability<TOffset>; batch?: number }\n /**\n * Customize logging for durability failure paths (terminal-append and\n * close). These failures are always logged server-side by default (the\n * `errors` category is on even without `debug`, via a `ConsoleLogger`);\n * pass `debug` to route them to a custom `Logger` or raise verbosity. A\n * joiner replaying the log only ever sees a generic incomplete error, so\n * server-side logging is where the real cause is recoverable.\n */\n debug?: DebugOption\n },\n): Response {\n const { headers, abortController, durability, debug, ...responseInit } =\n init ?? {}\n\n // Start with default SSE headers\n const mergedHeaders = new Headers({\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n })\n\n // Override with user headers if provided, handling all HeadersInit forms:\n // Headers instance, string[][], or plain object\n if (headers) {\n const userHeaders = new Headers(headers)\n userHeaders.forEach((value, key) => {\n mergedHeaders.set(key, value)\n })\n }\n\n let body: ReadableStream<Uint8Array>\n if (durability) {\n // A fresh run (not a resume/replay) drains into the durable log under its\n // OWN producer controller, decoupled from the HTTP response: a response\n // cancel (page reload) detaches and keeps draining in the background so a\n // rejoining client tails the log to the real terminal, rather than killing\n // the run and sealing the log with RUN_ERROR. The producer is aborted only\n // by a caller-supplied `abortController` (a genuine stop()). On the resume\n // path the response IS a reader, so a cancel should stop the read normally.\n const isFresh = durability.adapter.resumeFrom() === null\n const producerAbortController = abortController ?? new AbortController()\n const deliveryAbortController = isFresh\n ? new AbortController()\n : producerAbortController\n const { source, getId } = durableStreamSource(stream, durability.adapter, {\n abortController: producerAbortController,\n batch: durability.batch,\n // `errors` category is on by default even when `debug` is undefined, so\n // durability terminal-append / close failures always surface server-side —\n // including on the client-disconnect path where there is no live consumer.\n logger: resolveDebugOption(debug),\n })\n const { encodeChunk, encodeError } = sseEncoders(getId)\n body = toEncodedStream(\n source,\n deliveryAbortController,\n encodeChunk,\n encodeError,\n isFresh,\n // Fresh runs only: a resume response IS a reader, so its cancel is an\n // ordinary read being stopped, not a producer losing its viewer.\n isFresh ? () => notifyRunDisconnected(stream) : undefined,\n )\n } else {\n body = toServerSentEventsStream(stream, abortController)\n }\n\n return new Response(body, {\n ...responseInit,\n headers: mergedHeaders,\n })\n}\n\n/**\n * A resume is served entirely from the durability log, so there is no producer\n * to iterate. This empty source satisfies the response helpers' signature; on a\n * resume `durableStreamSource` replays from the log and never touches it.\n */\nfunction emptyDurableSource(): AsyncIterable<StreamChunk> {\n return (async function* () {})()\n}\n\n/**\n * Everything the resume helpers need to take a run over as a side effect of\n * serving its log.\n *\n * `claim` and `pipe` are **injected**, not imported. The two mechanisms a\n * takeover needs (`withRunClaim` and `pipeToRunLog`) live in\n * `@tanstack/ai-sandbox`, and `@tanstack/ai` must not depend on that package —\n * that layering inversion is exactly what moving `LockStore` into core was meant\n * to prevent, and it would make core depend on the sandbox package to serve a\n * plain chat run. Injecting them keeps only the *shape* of a takeover in core\n * (parse the run id, read the record, skip if terminal, claim, drive) and lets a\n * background-worker-driven run supply its own pair.\n * `@tanstack/ai-sandbox`'s `sandboxRunDriver` fills both in.\n */\nexport interface RunDriverOptions {\n /** The attach request; its run id is read with {@link resolveResumeRunId}. */\n request: Request\n runs: RunStore\n locks: LockStore\n /** Produce the run's remaining events. Called only once the claim is held. */\n drive: (input: {\n runId: string\n threadId: string\n signal: AbortSignal\n }) => AsyncIterable<StreamChunk>\n /** Run `fn` under exclusive ownership of the run, or reject if refused. */\n claim: <T>(\n input: { runs: RunStore; locks: LockStore; runId: string },\n fn: (claim: {\n runId: string\n epoch: number\n signal: AbortSignal\n }) => Promise<T>,\n ) => Promise<T>\n /** Persist the driven stream to the run's producer-side durability log. */\n pipe: (\n stream: AsyncIterable<StreamChunk>,\n input: { runId: string; threadId: string; signal: AbortSignal },\n ) => Promise<unknown>\n /** Platform keep-alive (e.g. `ctx.waitUntil`) for the background drive. */\n waitUntil?: (promise: Promise<unknown>) => void\n logger?: InternalLogger\n}\n\n/** Shared options for the resume-only response helpers. */\ntype ResumeResponseOptions<TOffset extends string> = ResponseInit & {\n adapter: StreamDurability<TOffset>\n batch?: number\n debug?: DebugOption\n /**\n * Take the run over while serving its log. Omit to serve the log only —\n * the response is byte-identical either way.\n */\n driver?: RunDriverOptions\n}\n\n/**\n * Take over an in-flight run as a side effect of serving its log.\n *\n * The response itself is unchanged: it still replays from the durability log via\n * `emptyDurableSource()`. The drive runs BESIDE it, appending to the run's own\n * producer-side log through the injected `pipe`, and the response tails what\n * lands. That separation is what lets a taken-over run keep `chat()`'s normal\n * middleware path — `withPersistence.onFinish` is what saves the transcript, so a\n * parallel translation path would lose the history of any run that completed\n * while detached.\n *\n * TOTAL BY CONSTRUCTION. Every failure is logged and swallowed:\n *\n * - No run id, no record, or a terminal record → serve the log, drive nothing.\n * A second tab attaching to a finished run must still see the transcript.\n * - The claim is refused (another host is already driving) → serve the log,\n * drive nothing. That is the documented \"two hosts attach at once: one wins\n * the lease and drives, the other tails the log\" behavior.\n * - The drive throws → logged. It cannot be reported to this response, which is\n * already streaming the log; the run's own `RUN_ERROR` event is the channel.\n *\n * A rejection escaping here would be an unhandled rejection with nobody to\n * report it to — process-fatal on modern Node and instance-fatal inside a\n * Durable Object.\n */\nfunction startRunDriver(driver: RunDriverOptions): void {\n const logger = driver.logger\n const promise = (async () => {\n const runId = resolveResumeRunId(driver.request)\n if (runId === null) return\n let record: RunRecord | null = null\n try {\n record = await driver.runs.get(runId)\n } catch (error) {\n logger?.errors('resume driver: reading the run record failed', {\n runId,\n error,\n })\n return\n }\n // Validated, not trusted: `record.status` is typed `RunStatus` but comes off\n // a user-implemented `RunStore`, so the type is a claim about a storage\n // column and nothing checked it. An unrecognized value means the run cannot\n // be reasoned about at all — the record says nothing trustworthy about\n // whether an agent is already driving it — so refuse the drive the same way\n // a terminal record does, and still serve the log so a corrupt row does not\n // also blank the transcript.\n if (record !== null && !isRunStatus(record.status)) {\n logger?.errors(\n 'resume driver: the run record has an unrecognized status',\n {\n runId,\n status: record.status,\n },\n )\n return\n }\n if (record === null || isTerminalRunStatus(record.status)) return\n // A recorded cancel is NOT a status. `requestRunCancel` deliberately writes\n // only `cancelRequested`, so a run cancelled out of band while its driving\n // host had already died stays `'running'` — and the status gate above waves\n // it straight through. Driving it resurrects a run the user explicitly\n // stopped and burns tokens until the TTL expires. The log is still served, so\n // an attaching tab sees the transcript; only the drive is refused.\n //\n // This is the \"don't START one\" half. Aborting a drive that is ALREADY live\n // when a cancel lands afterwards is a separate, still-open concern.\n if (record.cancelRequested === true) return\n // Captured after narrowing so the closure below sees a definite record\n // rather than the re-widened `let`.\n const active = record\n\n try {\n await driver.claim(\n { runs: driver.runs, locks: driver.locks, runId },\n async (claim) => {\n // A viewer is attached again, so the detached clock stops. Cleared\n // under the claim so it cannot race the reaper's read.\n //\n // THE REAPER: do NOT reuse `startRunDriver` for reclaiming detached\n // runs. `@tanstack/ai-sandbox`'s `reapDetachedRuns` deliberately does\n // the opposite of this line — it ACTS ON `detachedSince` and must\n // leave the marker intact for its own TTL accounting — so borrowing\n // this path would erase the very evidence the reaper selected the run\n // on, resetting the TTL on every sweep so a detached run could never\n // expire. That is why the reaper has its own drive path.\n //\n // LOG AND CONTINUE. This write is BOOKKEEPING for the reaper's TTL\n // accounting; the claim is already held and the takeover is the\n // valuable part. Letting a rejection propagate would land in the catch\n // below — the channel reserved for the normal \"someone else won the\n // lease\" case — so one transient store error would silently cost the\n // whole drive, logged only on the `provider` debug channel and\n // therefore invisible at default log levels. The worst case of\n // continuing is a stale `detachedSince` the reaper may act on later;\n // the worst case of vetoing is a run nobody drives at all.\n try {\n await driver.runs.update(runId, { detachedSince: undefined })\n } catch (error) {\n logger?.errors('resume driver: clearing detachedSince failed', {\n runId,\n error,\n })\n }\n await driver.pipe(\n driver.drive({\n runId,\n threadId: active.threadId,\n signal: claim.signal,\n }),\n { runId, threadId: active.threadId, signal: claim.signal },\n )\n },\n )\n } catch (error) {\n // Includes RunClaimNotAcquiredError (someone else is driving) and\n // RunClaimLostError (we were superseded mid-drive). Both are normal.\n logger?.provider('resume driver: not driving this run', { runId, error })\n }\n })()\n\n if (driver.waitUntil) {\n driver.waitUntil(promise)\n } else {\n // No platform keep-alive: at least ensure the rejection is handled. The\n // async body above already catches everything, so this is belt-and-braces.\n void promise.catch(() => {})\n }\n}\n\n/**\n * The single wiring point both resume helpers call, so the SSE and NDJSON\n * halves cannot drift: a fix here applies to both. Called AFTER each helper's\n * `resumeFrom() === null` 400 check — an attach with no offset has nothing to\n * replay, and driving a run whose response will 400 would start an agent\n * nobody is watching.\n */\nfunction maybeStartRunDriver(driver: RunDriverOptions | undefined): void {\n if (driver) startRunDriver(driver)\n}\n\nconst NO_RESUME_OFFSET =\n 'No resume offset provided (expected a Last-Event-ID header or an ?offset query parameter).'\n\n/**\n * Serve a resumable run from its durability log over Server-Sent Events, without\n * re-running the model. Use this in a `GET` handler so a reload or a second tab\n * can re-attach to an in-flight or finished run.\n *\n * The adapter (`memoryStream(request)` / `durableStream(request)`) captures the\n * resume offset from the request. If there is none (no `Last-Event-ID` header\n * and no `?offset`), there is nothing to replay and this returns a 400.\n *\n * @example\n * ```typescript\n * export async function GET(request: Request) {\n * return resumeServerSentEventsResponse({ adapter: memoryStream(request) });\n * }\n * ```\n */\nexport function resumeServerSentEventsResponse<TOffset extends string = string>(\n options: ResumeResponseOptions<TOffset>,\n): Response {\n // `driver` MUST be destructured out: `responseInit` is spread into\n // `new Response(body, init)`, so leaving it in would leak the driver object\n // (and its Request) into the response init.\n const { adapter, batch, debug, driver, ...responseInit } = options\n if (adapter.resumeFrom() === null) {\n return new Response(NO_RESUME_OFFSET, { status: 400 })\n }\n maybeStartRunDriver(driver)\n return toServerSentEventsResponse(emptyDurableSource(), {\n ...responseInit,\n durability: { adapter, batch },\n debug,\n })\n}\n\n/**\n * Convert a StreamChunk async iterable to a ReadableStream in HTTP stream format (newline-delimited JSON)\n *\n * This creates a ReadableStream that emits chunks as newline-delimited JSON:\n * - Each chunk is JSON.stringify'd and followed by \"\\n\"\n * - No SSE formatting (no \"data: \" prefix)\n *\n * This format is compatible with `fetchHttpStream` connection adapter.\n *\n * When `getId` is supplied (delivery durability), each chunk is emitted as an\n * envelope `{\"id\":\"<offset>\",\"chunk\":{…}}` instead of a bare chunk. NDJSON has\n * no native event-id field like SSE's `id:` line, so the resumable offset rides\n * inside the payload. Untagged chunks (no id) stay bare, so a non-durable\n * stream is byte-identical to before and the client auto-detects either form.\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param abortController - Optional AbortController to abort when stream is cancelled\n * @param getId - Optional per-chunk durability offset; when present, chunks are envelope-encoded\n * @returns ReadableStream in HTTP stream format (newline-delimited JSON)\n *\n * @example\n * ```typescript\n * const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });\n * const readableStream = toHttpStream(stream);\n * // Use with Response for HTTP streaming (not SSE)\n * return new Response(readableStream, {\n * headers: { 'Content-Type': 'application/x-ndjson' }\n * });\n * ```\n */\nexport function toHttpStream(\n stream: AsyncIterable<StreamChunk>,\n abortController?: AbortController,\n getId?: (chunk: StreamChunk, index: number) => string | undefined,\n): ReadableStream<Uint8Array> {\n const { encodeChunk, encodeError } = ndjsonEncoders(getId)\n return toEncodedStream(stream, abortController, encodeChunk, encodeError)\n}\n\n/**\n * NDJSON chunk/error encoders. Shared by {@link toHttpStream} and the internal\n * durability branch (see {@link sseEncoders}).\n */\nfunction ndjsonEncoders(\n getId?: (chunk: StreamChunk, index: number) => string | undefined,\n): {\n encodeChunk: (chunk: StreamChunk, index: number) => Uint8Array\n encodeError: (error: unknown) => Uint8Array\n} {\n const encoder = new TextEncoder()\n return {\n encodeChunk: (chunk, index) => {\n const id = getId?.(chunk, index)\n const line =\n id === undefined ? JSON.stringify(chunk) : JSON.stringify({ id, chunk })\n return encoder.encode(`${line}\\n`)\n },\n encodeError: (error) =>\n encoder.encode(`${JSON.stringify(runErrorChunk(error))}\\n`),\n }\n}\n\n/**\n * Convert a StreamChunk async iterable to a Response in HTTP stream format (newline-delimited JSON)\n *\n * This creates a Response that emits chunks in HTTP stream format:\n * - Each chunk is JSON.stringify'd and followed by \"\\n\"\n * - No SSE formatting (no \"data: \" prefix)\n *\n * This format is compatible with `fetchHttpStream` connection adapter.\n *\n * Pass a `durability` sink (`memoryStream(request)` / `durableStream(request)`)\n * to make the stream resumable: fresh runs are appended to the log and each\n * NDJSON line is emitted as an `{ id, chunk }` envelope carrying an opaque\n * offset; a reconnect (native `Last-Event-ID` header) or a `?offset` join\n * replays from the log without re-running the producer. `batch` controls how\n * many chunks are buffered per `append` (default 32). This shares the exact\n * `durableStreamSource` used by `toServerSentEventsResponse` — only the wire\n * encoding differs.\n *\n * @param stream - AsyncIterable of StreamChunks from chat()\n * @param init - Optional Response initialization options (including `abortController`, `durability` with its optional `batch`, and `debug`)\n * @returns Response in HTTP stream format (newline-delimited JSON)\n *\n * @example\n * ```typescript\n * export async function POST(request: Request) {\n * const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });\n * return toHttpResponse(stream, { durability: { adapter: memoryStream(request) } });\n * }\n * ```\n */\nexport function toHttpResponse<TOffset extends string = string>(\n stream: AsyncIterable<StreamChunk>,\n init?: ResponseInit & {\n abortController?: AbortController\n durability?: { adapter: StreamDurability<TOffset>; batch?: number }\n /**\n * Customize logging for durability failure paths (terminal-append and\n * close). These failures are always logged server-side by default (the\n * `errors` category is on even without `debug`, via a `ConsoleLogger`);\n * pass `debug` to route them to a custom `Logger` or raise verbosity. A\n * joiner replaying the log only ever sees a generic incomplete error, so\n * server-side logging is where the real cause is recoverable.\n */\n debug?: DebugOption\n },\n): Response {\n const { abortController, durability, debug, headers, ...responseInit } =\n init ?? {}\n\n // Default to a streaming NDJSON content type (with no-cache), overridable by\n // user headers. Without an explicit streaming type some intermediaries buffer\n // the response, defeating incremental delivery. Mirrors the SSE helper.\n const mergedHeaders = new Headers({\n 'Content-Type': 'application/x-ndjson',\n 'Cache-Control': 'no-cache',\n })\n if (headers) {\n const userHeaders = new Headers(headers)\n userHeaders.forEach((value, key) => {\n mergedHeaders.set(key, value)\n })\n }\n\n let body: ReadableStream<Uint8Array>\n if (durability) {\n // See toServerSentEventsResponse: a fresh run drains into the durable log\n // under its own producer controller, so a response cancel (reload) detaches\n // and keeps draining in the background instead of killing the run; a resume\n // response is a reader whose cancel stops the read normally.\n const isFresh = durability.adapter.resumeFrom() === null\n const producerAbortController = abortController ?? new AbortController()\n const deliveryAbortController = isFresh\n ? new AbortController()\n : producerAbortController\n const { source, getId } = durableStreamSource(stream, durability.adapter, {\n abortController: producerAbortController,\n batch: durability.batch,\n // Errors-on-by-default logger (see toServerSentEventsResponse).\n logger: resolveDebugOption(debug),\n })\n const { encodeChunk, encodeError } = ndjsonEncoders(getId)\n body = toEncodedStream(\n source,\n deliveryAbortController,\n encodeChunk,\n encodeError,\n isFresh,\n // See the SSE helper: fresh runs only.\n isFresh ? () => notifyRunDisconnected(stream) : undefined,\n )\n } else {\n body = toHttpStream(stream, abortController)\n }\n\n return new Response(body, {\n ...responseInit,\n headers: mergedHeaders,\n })\n}\n\n/**\n * Serve a resumable run from its durability log over NDJSON, without re-running\n * the model. The NDJSON counterpart of {@link resumeServerSentEventsResponse};\n * pair it with a `toHttpResponse` producer. Returns a 400 when the request\n * carries no resume offset (no `Last-Event-ID` header and no `?offset`).\n *\n * @example\n * ```typescript\n * export async function GET(request: Request) {\n * return resumeHttpResponse({ adapter: memoryStream(request) });\n * }\n * ```\n */\nexport function resumeHttpResponse<TOffset extends string = string>(\n options: ResumeResponseOptions<TOffset>,\n): Response {\n // See `resumeServerSentEventsResponse`: `driver` must not reach `responseInit`.\n const { adapter, batch, debug, driver, ...responseInit } = options\n if (adapter.resumeFrom() === null) {\n return new Response(NO_RESUME_OFFSET, { status: 400 })\n }\n maybeStartRunDriver(driver)\n return toHttpResponse(emptyDurableSource(), {\n ...responseInit,\n durability: { adapter, batch },\n debug,\n })\n}\n"],"mappings":"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"}
@@ -1,20 +1,34 @@
1
+ //#region src/strip-to-spec-middleware.ts
2
+ /**
3
+ * Strip only the deprecated nested `error` object from RUN_ERROR events.
4
+ * The flat `message`/`code` fields are the spec-compliant form.
5
+ *
6
+ * All other fields pass through unchanged. @ag-ui/core's BaseEventSchema
7
+ * uses `.passthrough()`, so extra fields (model, content, usage,
8
+ * finishReason, toolName, stepId, etc.) are allowed and won't break
9
+ * spec validation or verifyEvents.
10
+ */
1
11
  function stripToSpec(chunk) {
2
- if (chunk.type === "RUN_ERROR" && "error" in chunk) {
3
- const { error: _deprecated, ...rest } = chunk;
4
- return rest;
5
- }
6
- return chunk;
12
+ if (chunk.type === "RUN_ERROR" && "error" in chunk) {
13
+ const { error: _deprecated, ...rest } = chunk;
14
+ return rest;
15
+ }
16
+ return chunk;
7
17
  }
18
+ /**
19
+ * Middleware that ensures events are AG-UI spec compliant.
20
+ * Currently only strips the deprecated nested `error` object from RUN_ERROR.
21
+ * All other fields pass through unchanged (passthrough allowed by spec).
22
+ */
8
23
  function stripToSpecMiddleware() {
9
- return {
10
- name: "strip-to-spec",
11
- onChunk(_ctx, chunk) {
12
- return stripToSpec(chunk);
13
- }
14
- };
24
+ return {
25
+ name: "strip-to-spec",
26
+ onChunk(_ctx, chunk) {
27
+ return stripToSpec(chunk);
28
+ }
29
+ };
15
30
  }
16
- export {
17
- stripToSpec,
18
- stripToSpecMiddleware
19
- };
20
- //# sourceMappingURL=strip-to-spec-middleware.js.map
31
+ //#endregion
32
+ export { stripToSpec, stripToSpecMiddleware };
33
+
34
+ //# sourceMappingURL=strip-to-spec-middleware.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"strip-to-spec-middleware.js","sources":["../../src/strip-to-spec-middleware.ts"],"sourcesContent":["import type { ChatMiddleware } from './activities/chat/middleware/types'\nimport type { StreamChunk } from './types'\n\n/**\n * Strip only the deprecated nested `error` object from RUN_ERROR events.\n * The flat `message`/`code` fields are the spec-compliant form.\n *\n * All other fields pass through unchanged. @ag-ui/core's BaseEventSchema\n * uses `.passthrough()`, so extra fields (model, content, usage,\n * finishReason, toolName, stepId, etc.) are allowed and won't break\n * spec validation or verifyEvents.\n */\nexport function stripToSpec(chunk: StreamChunk): StreamChunk {\n // Only strip the deprecated nested error object from RUN_ERROR\n if (chunk.type === 'RUN_ERROR' && 'error' in chunk) {\n const { error: _deprecated, ...rest } = chunk as Record<string, unknown>\n return rest as StreamChunk\n }\n return chunk\n}\n\n/**\n * Middleware that ensures events are AG-UI spec compliant.\n * Currently only strips the deprecated nested `error` object from RUN_ERROR.\n * All other fields pass through unchanged (passthrough allowed by spec).\n */\nexport function stripToSpecMiddleware(): ChatMiddleware {\n return {\n name: 'strip-to-spec',\n onChunk(_ctx, chunk) {\n return stripToSpec(chunk)\n },\n }\n}\n"],"names":[],"mappings":"AAYO,SAAS,YAAY,OAAiC;AAE3D,MAAI,MAAM,SAAS,eAAe,WAAW,OAAO;AAClD,UAAM,EAAE,OAAO,aAAa,GAAG,SAAS;AACxC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,wBAAwC;AACtD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,MAAM,OAAO;AACnB,aAAO,YAAY,KAAK;AAAA,IAC1B;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"strip-to-spec-middleware.js","names":[],"sources":["../../src/strip-to-spec-middleware.ts"],"sourcesContent":["import type { ChatMiddleware } from './activities/chat/middleware/types'\nimport type { StreamChunk } from './types'\n\n/**\n * Strip only the deprecated nested `error` object from RUN_ERROR events.\n * The flat `message`/`code` fields are the spec-compliant form.\n *\n * All other fields pass through unchanged. @ag-ui/core's BaseEventSchema\n * uses `.passthrough()`, so extra fields (model, content, usage,\n * finishReason, toolName, stepId, etc.) are allowed and won't break\n * spec validation or verifyEvents.\n */\nexport function stripToSpec(chunk: StreamChunk): StreamChunk {\n // Only strip the deprecated nested error object from RUN_ERROR.\n if (chunk.type === 'RUN_ERROR' && 'error' in chunk) {\n const { error: _deprecated, ...rest } = chunk\n return rest\n }\n return chunk\n}\n\n/**\n * Middleware that ensures events are AG-UI spec compliant.\n * Currently only strips the deprecated nested `error` object from RUN_ERROR.\n * All other fields pass through unchanged (passthrough allowed by spec).\n */\nexport function stripToSpecMiddleware(): ChatMiddleware {\n return {\n name: 'strip-to-spec',\n onChunk(_ctx, chunk) {\n return stripToSpec(chunk)\n },\n }\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,YAAY,OAAiC;CAE3D,IAAI,MAAM,SAAS,eAAe,WAAW,OAAO;EAClD,MAAM,EAAE,OAAO,aAAa,GAAG,SAAS;EACxC,OAAO;CACT;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,wBAAwC;CACtD,OAAO;EACL,MAAM;EACN,QAAQ,MAAM,OAAO;GACnB,OAAO,YAAY,KAAK;EAC1B;CACF;AACF"}
@@ -1,23 +1,29 @@
1
+ //#region src/system-prompts.ts
2
+ /**
3
+ * Normalise the public `systemPrompts` shape (`Array<string | { content, metadata? }>`)
4
+ * to a homogenous `Array<{ content, metadata? }>`. Adapters use this so they
5
+ * don't have to type-narrow string vs object inline.
6
+ *
7
+ * Returns an empty array (never `undefined`) so callers can chain `.map` /
8
+ * `.join` without an extra null check.
9
+ *
10
+ * Throws a `TypeError` (naming the offending index) if an object-form entry's
11
+ * `content` isn't a string. Public API boundary — callers reaching this
12
+ * function through `as any` / external JS would otherwise stream a literal
13
+ * `"undefined"` into the model's system prompt with no signal.
14
+ */
1
15
  function normalizeSystemPrompts(prompts) {
2
- if (!prompts || prompts.length === 0) return [];
3
- return prompts.map((p, i) => {
4
- if (typeof p === "string") return { content: p };
5
- const candidate = p;
6
- if (candidate === null || typeof candidate !== "object") {
7
- throw new TypeError(
8
- `systemPrompts[${i}]: expected a string or { content, metadata? }, got ${candidate === null ? "null" : typeof candidate}`
9
- );
10
- }
11
- const { content } = candidate;
12
- if (typeof content !== "string") {
13
- throw new TypeError(
14
- `systemPrompts[${i}]: content must be a string, got ${typeof content}`
15
- );
16
- }
17
- return p;
18
- });
16
+ if (!prompts || prompts.length === 0) return [];
17
+ return prompts.map((p, i) => {
18
+ if (typeof p === "string") return { content: p };
19
+ const candidate = p;
20
+ if (candidate === null || typeof candidate !== "object") throw new TypeError(`systemPrompts[${i}]: expected a string or { content, metadata? }, got ${candidate === null ? "null" : typeof candidate}`);
21
+ const { content } = candidate;
22
+ if (typeof content !== "string") throw new TypeError(`systemPrompts[${i}]: content must be a string, got ${typeof content}`);
23
+ return p;
24
+ });
19
25
  }
20
- export {
21
- normalizeSystemPrompts
22
- };
23
- //# sourceMappingURL=system-prompts.js.map
26
+ //#endregion
27
+ export { normalizeSystemPrompts };
28
+
29
+ //# sourceMappingURL=system-prompts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompts.js","sources":["../../src/system-prompts.ts"],"sourcesContent":["/**\n * A single entry in `chat({ systemPrompts: [...] })`.\n *\n * Accepts a plain string (the common case) or a structured object that lets\n * providers attach typed metadata to the prompt — e.g. Anthropic\n * `cache_control` for prompt caching, future per-prompt safety overrides for\n * Gemini, etc.\n *\n * At the chat call site, `metadata` is narrowed by the adapter via\n * `~types['systemPromptMetadata']`. Providers that don't declare one inherit\n * the default `never`, which makes the field carry no meaningful value: TS\n * only accepts `undefined` there, and provider-foreign metadata that reaches\n * an adapter via JS / `as any` is silently dropped, never written to the\n * wire. For type-safe per-provider metadata, refer to the provider's\n * `<Provider>SystemPromptMetadata` interface (e.g. `AnthropicSystemPromptMetadata`).\n *\n * @example\n * // The 90% case — plain strings work everywhere.\n * systemPrompts: ['Be concise.', 'Cite sources.']\n *\n * @example\n * // Provider-specific metadata via the object form. No `satisfies` cast\n * // is needed — the adapter narrows the `metadata` field's type at the\n * // call site so users get autocomplete and structural checking\n * // automatically.\n * import { anthropicText } from '@tanstack/ai-anthropic'\n *\n * chat({\n * adapter: anthropicText(),\n * systemPrompts: [\n * {\n * content: 'Stable instructions — cache me.',\n * metadata: { cache_control: { type: 'ephemeral' } },\n * },\n * 'Volatile per-request instruction.',\n * ],\n * })\n */\nexport type SystemPrompt<TMetadata = unknown> =\n | string\n | {\n content: string\n metadata?: TMetadata\n }\n\n/**\n * Normalised shape adapters see after the chat layer turns string entries\n * into `{ content }` objects. Adapters call `normalizeSystemPrompts` once at\n * the top of their option-mapping pipeline so the rest of the code only has\n * to handle one shape.\n */\nexport interface NormalizedSystemPrompt<TMetadata = unknown> {\n content: string\n metadata?: TMetadata\n}\n\n/**\n * Normalise the public `systemPrompts` shape (`Array<string | { content, metadata? }>`)\n * to a homogenous `Array<{ content, metadata? }>`. Adapters use this so they\n * don't have to type-narrow string vs object inline.\n *\n * Returns an empty array (never `undefined`) so callers can chain `.map` /\n * `.join` without an extra null check.\n *\n * Throws a `TypeError` (naming the offending index) if an object-form entry's\n * `content` isn't a string. Public API boundary — callers reaching this\n * function through `as any` / external JS would otherwise stream a literal\n * `\"undefined\"` into the model's system prompt with no signal.\n */\nexport function normalizeSystemPrompts<TMetadata = unknown>(\n // Accept the wide public shape (`SystemPrompt<unknown>`) regardless of the\n // caller's `TMetadata`. Adapters know their own metadata shape; the\n // generic narrows the *output* so adapter code can read `p.metadata.X`\n // without an additional cast.\n prompts: ReadonlyArray<SystemPrompt> | undefined,\n): Array<NormalizedSystemPrompt<TMetadata>> {\n if (!prompts || prompts.length === 0) return []\n return prompts.map((p, i) => {\n if (typeof p === 'string') return { content: p }\n // Defence in depth: TypeScript narrows `p` to the object arm here, but\n // this function is a public API boundary that callers can reach via\n // plain JS or `as any`. Re-validate at runtime so we never stream a\n // literal `\"undefined\"` into the model.\n const candidate = p as unknown\n if (candidate === null || typeof candidate !== 'object') {\n throw new TypeError(\n `systemPrompts[${i}]: expected a string or { content, metadata? }, got ${candidate === null ? 'null' : typeof candidate}`,\n )\n }\n const { content } = candidate as { content?: unknown }\n if (typeof content !== 'string') {\n throw new TypeError(\n `systemPrompts[${i}]: content must be a string, got ${typeof content}`,\n )\n }\n return p as NormalizedSystemPrompt<TMetadata>\n })\n}\n"],"names":[],"mappings":"AAqEO,SAAS,uBAKd,SAC0C;AAC1C,MAAI,CAAC,WAAW,QAAQ,WAAW,UAAU,CAAA;AAC7C,SAAO,QAAQ,IAAI,CAAC,GAAG,MAAM;AAC3B,QAAI,OAAO,MAAM,SAAU,QAAO,EAAE,SAAS,EAAA;AAK7C,UAAM,YAAY;AAClB,QAAI,cAAc,QAAQ,OAAO,cAAc,UAAU;AACvD,YAAM,IAAI;AAAA,QACR,iBAAiB,CAAC,uDAAuD,cAAc,OAAO,SAAS,OAAO,SAAS;AAAA,MAAA;AAAA,IAE3H;AACA,UAAM,EAAE,YAAY;AACpB,QAAI,OAAO,YAAY,UAAU;AAC/B,YAAM,IAAI;AAAA,QACR,iBAAiB,CAAC,oCAAoC,OAAO,OAAO;AAAA,MAAA;AAAA,IAExE;AACA,WAAO;AAAA,EACT,CAAC;AACH;"}
1
+ {"version":3,"file":"system-prompts.js","names":[],"sources":["../../src/system-prompts.ts"],"sourcesContent":["/**\n * A single entry in `chat({ systemPrompts: [...] })`.\n *\n * Accepts a plain string (the common case) or a structured object that lets\n * providers attach typed metadata to the prompt — e.g. Anthropic\n * `cache_control` for prompt caching, future per-prompt safety overrides for\n * Gemini, etc.\n *\n * At the chat call site, `metadata` is narrowed by the adapter via\n * `~types['systemPromptMetadata']`. Providers that don't declare one inherit\n * the default `never`, which makes the field carry no meaningful value: TS\n * only accepts `undefined` there, and provider-foreign metadata that reaches\n * an adapter via JS / `as any` is silently dropped, never written to the\n * wire. For type-safe per-provider metadata, refer to the provider's\n * `<Provider>SystemPromptMetadata` interface (e.g. `AnthropicSystemPromptMetadata`).\n *\n * @example\n * // The 90% case — plain strings work everywhere.\n * systemPrompts: ['Be concise.', 'Cite sources.']\n *\n * @example\n * // Provider-specific metadata via the object form. No `satisfies` cast\n * // is needed — the adapter narrows the `metadata` field's type at the\n * // call site so users get autocomplete and structural checking\n * // automatically.\n * import { anthropicText } from '@tanstack/ai-anthropic'\n *\n * chat({\n * adapter: anthropicText(),\n * systemPrompts: [\n * {\n * content: 'Stable instructions — cache me.',\n * metadata: { cache_control: { type: 'ephemeral' } },\n * },\n * 'Volatile per-request instruction.',\n * ],\n * })\n */\nexport type SystemPrompt<TMetadata = unknown> =\n | string\n | {\n content: string\n metadata?: TMetadata\n }\n\n/**\n * Normalised shape adapters see after the chat layer turns string entries\n * into `{ content }` objects. Adapters call `normalizeSystemPrompts` once at\n * the top of their option-mapping pipeline so the rest of the code only has\n * to handle one shape.\n */\nexport interface NormalizedSystemPrompt<TMetadata = unknown> {\n content: string\n metadata?: TMetadata\n}\n\n/**\n * Normalise the public `systemPrompts` shape (`Array<string | { content, metadata? }>`)\n * to a homogenous `Array<{ content, metadata? }>`. Adapters use this so they\n * don't have to type-narrow string vs object inline.\n *\n * Returns an empty array (never `undefined`) so callers can chain `.map` /\n * `.join` without an extra null check.\n *\n * Throws a `TypeError` (naming the offending index) if an object-form entry's\n * `content` isn't a string. Public API boundary — callers reaching this\n * function through `as any` / external JS would otherwise stream a literal\n * `\"undefined\"` into the model's system prompt with no signal.\n */\nexport function normalizeSystemPrompts<TMetadata = unknown>(\n // Accept the wide public shape (`SystemPrompt<unknown>`) regardless of the\n // caller's `TMetadata`. Adapters know their own metadata shape; the\n // generic narrows the *output* so adapter code can read `p.metadata.X`\n // without an additional cast.\n prompts: ReadonlyArray<SystemPrompt> | undefined,\n): Array<NormalizedSystemPrompt<TMetadata>> {\n if (!prompts || prompts.length === 0) return []\n return prompts.map((p, i) => {\n if (typeof p === 'string') return { content: p }\n // Defence in depth: TypeScript narrows `p` to the object arm here, but\n // this function is a public API boundary that callers can reach via\n // plain JS or `as any`. Re-validate at runtime so we never stream a\n // literal `\"undefined\"` into the model.\n const candidate = p as unknown\n if (candidate === null || typeof candidate !== 'object') {\n throw new TypeError(\n `systemPrompts[${i}]: expected a string or { content, metadata? }, got ${candidate === null ? 'null' : typeof candidate}`,\n )\n }\n const { content } = candidate as { content?: unknown }\n if (typeof content !== 'string') {\n throw new TypeError(\n `systemPrompts[${i}]: content must be a string, got ${typeof content}`,\n )\n }\n return p as NormalizedSystemPrompt<TMetadata>\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAqEA,SAAgB,uBAKd,SAC0C;CAC1C,IAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC;CAC9C,OAAO,QAAQ,KAAK,GAAG,MAAM;EAC3B,IAAI,OAAO,MAAM,UAAU,OAAO,EAAE,SAAS,EAAE;EAK/C,MAAM,YAAY;EAClB,IAAI,cAAc,QAAQ,OAAO,cAAc,UAC7C,MAAM,IAAI,UACR,iBAAiB,EAAE,sDAAsD,cAAc,OAAO,SAAS,OAAO,WAChH;EAEF,MAAM,EAAE,YAAY;EACpB,IAAI,OAAO,YAAY,UACrB,MAAM,IAAI,UACR,iBAAiB,EAAE,mCAAmC,OAAO,SAC/D;EAEF,OAAO;CACT,CAAC;AACH"}
@@ -1,49 +1,76 @@
1
+ //#region src/tool-registry.ts
2
+ /**
3
+ * Create a mutable tool registry for dynamic tool scenarios.
4
+ *
5
+ * Tools can be added and removed during chat execution, and the
6
+ * changes will be reflected in subsequent agent loop iterations.
7
+ *
8
+ * @param initialTools - Optional initial set of tools
9
+ * @returns A mutable ToolRegistry
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const registry = createToolRegistry([toolA, toolB])
14
+ *
15
+ * const stream = chat({
16
+ * adapter,
17
+ * messages,
18
+ * toolRegistry: registry,
19
+ * })
20
+ *
21
+ * // Later, during tool execution:
22
+ * registry.add(newTool) // Immediately available to LLM
23
+ * ```
24
+ */
1
25
  function createToolRegistry(initialTools = []) {
2
- const tools = /* @__PURE__ */ new Map();
3
- for (const tool of initialTools) {
4
- tools.set(tool.name, tool);
5
- }
6
- return {
7
- getTools: () => Array.from(tools.values()),
8
- add: (tool) => {
9
- tools.set(tool.name, tool);
10
- },
11
- remove: (name) => {
12
- return tools.delete(name);
13
- },
14
- has: (name) => {
15
- return tools.has(name);
16
- },
17
- get: (name) => {
18
- return tools.get(name);
19
- },
20
- isFrozen: false
21
- };
26
+ const tools = /* @__PURE__ */ new Map();
27
+ for (const tool of initialTools) tools.set(tool.name, tool);
28
+ return {
29
+ getTools: () => Array.from(tools.values()),
30
+ add: (tool) => {
31
+ tools.set(tool.name, tool);
32
+ },
33
+ remove: (name) => {
34
+ return tools.delete(name);
35
+ },
36
+ has: (name) => {
37
+ return tools.has(name);
38
+ },
39
+ get: (name) => {
40
+ return tools.get(name);
41
+ },
42
+ isFrozen: false
43
+ };
22
44
  }
45
+ /**
46
+ * Create a frozen (immutable) tool registry from a tools array.
47
+ *
48
+ * This is used internally to wrap static `tools` arrays for backward compatibility.
49
+ * Add and remove operations are no-ops on frozen registries.
50
+ *
51
+ * @param tools - The static array of tools
52
+ * @returns A frozen ToolRegistry
53
+ */
23
54
  function createFrozenRegistry(tools = []) {
24
- const toolMap = /* @__PURE__ */ new Map();
25
- for (const tool of tools) {
26
- toolMap.set(tool.name, tool);
27
- }
28
- const frozenTools = Object.freeze([...tools]);
29
- return {
30
- getTools: () => [...frozenTools],
31
- add: (_tool) => {
32
- },
33
- remove: (_name) => {
34
- return false;
35
- },
36
- has: (name) => {
37
- return toolMap.has(name);
38
- },
39
- get: (name) => {
40
- return toolMap.get(name);
41
- },
42
- isFrozen: true
43
- };
55
+ const toolMap = /* @__PURE__ */ new Map();
56
+ for (const tool of tools) toolMap.set(tool.name, tool);
57
+ const frozenTools = Object.freeze([...tools]);
58
+ return {
59
+ getTools: () => [...frozenTools],
60
+ add: (_tool) => {},
61
+ remove: (_name) => {
62
+ return false;
63
+ },
64
+ has: (name) => {
65
+ return toolMap.has(name);
66
+ },
67
+ get: (name) => {
68
+ return toolMap.get(name);
69
+ },
70
+ isFrozen: true
71
+ };
44
72
  }
45
- export {
46
- createFrozenRegistry,
47
- createToolRegistry
48
- };
49
- //# sourceMappingURL=tool-registry.js.map
73
+ //#endregion
74
+ export { createFrozenRegistry, createToolRegistry };
75
+
76
+ //# sourceMappingURL=tool-registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-registry.js","sources":["../../src/tool-registry.ts"],"sourcesContent":["import type { AnyTool } from './types'\n\n/**\n * A registry that holds tools and allows dynamic tool management.\n *\n * The registry can be either mutable (allowing additions/removals during execution)\n * or frozen (static tool list, for backward compatibility with tools arrays).\n */\nexport interface ToolRegistry<TTool extends AnyTool = AnyTool> {\n /**\n * Get all current tools in the registry.\n * Called each agent loop iteration to get the latest tool list.\n */\n getTools: () => Array<TTool>\n\n /**\n * Add a tool to the registry dynamically.\n * For frozen registries, this is a no-op.\n *\n * @param tool - The tool to add\n */\n add: (tool: TTool) => void\n\n /**\n * Remove a tool from the registry by name.\n * For frozen registries, this always returns false.\n *\n * @param name - The name of the tool to remove\n * @returns true if the tool was removed, false if not found or frozen\n */\n remove: (name: string) => boolean\n\n /**\n * Check if a tool exists in the registry.\n *\n * @param name - The name of the tool to check\n */\n has: (name: string) => boolean\n\n /**\n * Get a tool by name.\n *\n * @param name - The name of the tool to get\n * @returns The tool if found, undefined otherwise\n */\n get: (name: string) => TTool | undefined\n\n /**\n * Whether this registry is frozen (immutable).\n * Frozen registries don't allow add/remove operations.\n */\n readonly isFrozen: boolean\n}\n\n/**\n * Create a mutable tool registry for dynamic tool scenarios.\n *\n * Tools can be added and removed during chat execution, and the\n * changes will be reflected in subsequent agent loop iterations.\n *\n * @param initialTools - Optional initial set of tools\n * @returns A mutable ToolRegistry\n *\n * @example\n * ```typescript\n * const registry = createToolRegistry([toolA, toolB])\n *\n * const stream = chat({\n * adapter,\n * messages,\n * toolRegistry: registry,\n * })\n *\n * // Later, during tool execution:\n * registry.add(newTool) // Immediately available to LLM\n * ```\n */\nexport function createToolRegistry<TTool extends AnyTool = AnyTool>(\n initialTools: Array<TTool> = [],\n): ToolRegistry<TTool> {\n const tools = new Map<string, TTool>()\n\n for (const tool of initialTools) {\n tools.set(tool.name, tool)\n }\n\n return {\n getTools: () => Array.from(tools.values()),\n\n add: (tool: TTool) => {\n tools.set(tool.name, tool)\n },\n\n remove: (name: string) => {\n return tools.delete(name)\n },\n\n has: (name: string) => {\n return tools.has(name)\n },\n\n get: (name: string) => {\n return tools.get(name)\n },\n\n isFrozen: false,\n }\n}\n\n/**\n * Create a frozen (immutable) tool registry from a tools array.\n *\n * This is used internally to wrap static `tools` arrays for backward compatibility.\n * Add and remove operations are no-ops on frozen registries.\n *\n * @param tools - The static array of tools\n * @returns A frozen ToolRegistry\n */\nexport function createFrozenRegistry<TTool extends AnyTool = AnyTool>(\n tools: Array<TTool> = [],\n): ToolRegistry<TTool> {\n const toolMap = new Map<string, TTool>()\n\n for (const tool of tools) {\n toolMap.set(tool.name, tool)\n }\n\n const frozenTools = Object.freeze([...tools])\n\n return {\n getTools: () => [...frozenTools],\n\n add: (_tool: TTool) => {\n // No-op for frozen registry\n },\n\n remove: (_name: string) => {\n // No-op for frozen registry\n return false\n },\n\n has: (name: string) => {\n return toolMap.has(name)\n },\n\n get: (name: string) => {\n return toolMap.get(name)\n },\n\n isFrozen: true,\n }\n}\n"],"names":[],"mappings":"AA6EO,SAAS,mBACd,eAA6B,IACR;AACrB,QAAM,4BAAY,IAAA;AAElB,aAAW,QAAQ,cAAc;AAC/B,UAAM,IAAI,KAAK,MAAM,IAAI;AAAA,EAC3B;AAEA,SAAO;AAAA,IACL,UAAU,MAAM,MAAM,KAAK,MAAM,QAAQ;AAAA,IAEzC,KAAK,CAAC,SAAgB;AACpB,YAAM,IAAI,KAAK,MAAM,IAAI;AAAA,IAC3B;AAAA,IAEA,QAAQ,CAAC,SAAiB;AACxB,aAAO,MAAM,OAAO,IAAI;AAAA,IAC1B;AAAA,IAEA,KAAK,CAAC,SAAiB;AACrB,aAAO,MAAM,IAAI,IAAI;AAAA,IACvB;AAAA,IAEA,KAAK,CAAC,SAAiB;AACrB,aAAO,MAAM,IAAI,IAAI;AAAA,IACvB;AAAA,IAEA,UAAU;AAAA,EAAA;AAEd;AAWO,SAAS,qBACd,QAAsB,IACD;AACrB,QAAM,8BAAc,IAAA;AAEpB,aAAW,QAAQ,OAAO;AACxB,YAAQ,IAAI,KAAK,MAAM,IAAI;AAAA,EAC7B;AAEA,QAAM,cAAc,OAAO,OAAO,CAAC,GAAG,KAAK,CAAC;AAE5C,SAAO;AAAA,IACL,UAAU,MAAM,CAAC,GAAG,WAAW;AAAA,IAE/B,KAAK,CAAC,UAAiB;AAAA,IAEvB;AAAA,IAEA,QAAQ,CAAC,UAAkB;AAEzB,aAAO;AAAA,IACT;AAAA,IAEA,KAAK,CAAC,SAAiB;AACrB,aAAO,QAAQ,IAAI,IAAI;AAAA,IACzB;AAAA,IAEA,KAAK,CAAC,SAAiB;AACrB,aAAO,QAAQ,IAAI,IAAI;AAAA,IACzB;AAAA,IAEA,UAAU;AAAA,EAAA;AAEd;"}
1
+ {"version":3,"file":"tool-registry.js","names":[],"sources":["../../src/tool-registry.ts"],"sourcesContent":["import type { AnyTool } from './types'\n\n/**\n * A registry that holds tools and allows dynamic tool management.\n *\n * The registry can be either mutable (allowing additions/removals during execution)\n * or frozen (static tool list, for backward compatibility with tools arrays).\n */\nexport interface ToolRegistry<TTool extends AnyTool = AnyTool> {\n /**\n * Get all current tools in the registry.\n * Called each agent loop iteration to get the latest tool list.\n */\n getTools: () => Array<TTool>\n\n /**\n * Add a tool to the registry dynamically.\n * For frozen registries, this is a no-op.\n *\n * @param tool - The tool to add\n */\n add: (tool: TTool) => void\n\n /**\n * Remove a tool from the registry by name.\n * For frozen registries, this always returns false.\n *\n * @param name - The name of the tool to remove\n * @returns true if the tool was removed, false if not found or frozen\n */\n remove: (name: string) => boolean\n\n /**\n * Check if a tool exists in the registry.\n *\n * @param name - The name of the tool to check\n */\n has: (name: string) => boolean\n\n /**\n * Get a tool by name.\n *\n * @param name - The name of the tool to get\n * @returns The tool if found, undefined otherwise\n */\n get: (name: string) => TTool | undefined\n\n /**\n * Whether this registry is frozen (immutable).\n * Frozen registries don't allow add/remove operations.\n */\n readonly isFrozen: boolean\n}\n\n/**\n * Create a mutable tool registry for dynamic tool scenarios.\n *\n * Tools can be added and removed during chat execution, and the\n * changes will be reflected in subsequent agent loop iterations.\n *\n * @param initialTools - Optional initial set of tools\n * @returns A mutable ToolRegistry\n *\n * @example\n * ```typescript\n * const registry = createToolRegistry([toolA, toolB])\n *\n * const stream = chat({\n * adapter,\n * messages,\n * toolRegistry: registry,\n * })\n *\n * // Later, during tool execution:\n * registry.add(newTool) // Immediately available to LLM\n * ```\n */\nexport function createToolRegistry<TTool extends AnyTool = AnyTool>(\n initialTools: Array<TTool> = [],\n): ToolRegistry<TTool> {\n const tools = new Map<string, TTool>()\n\n for (const tool of initialTools) {\n tools.set(tool.name, tool)\n }\n\n return {\n getTools: () => Array.from(tools.values()),\n\n add: (tool: TTool) => {\n tools.set(tool.name, tool)\n },\n\n remove: (name: string) => {\n return tools.delete(name)\n },\n\n has: (name: string) => {\n return tools.has(name)\n },\n\n get: (name: string) => {\n return tools.get(name)\n },\n\n isFrozen: false,\n }\n}\n\n/**\n * Create a frozen (immutable) tool registry from a tools array.\n *\n * This is used internally to wrap static `tools` arrays for backward compatibility.\n * Add and remove operations are no-ops on frozen registries.\n *\n * @param tools - The static array of tools\n * @returns A frozen ToolRegistry\n */\nexport function createFrozenRegistry<TTool extends AnyTool = AnyTool>(\n tools: Array<TTool> = [],\n): ToolRegistry<TTool> {\n const toolMap = new Map<string, TTool>()\n\n for (const tool of tools) {\n toolMap.set(tool.name, tool)\n }\n\n const frozenTools = Object.freeze([...tools])\n\n return {\n getTools: () => [...frozenTools],\n\n add: (_tool: TTool) => {\n // No-op for frozen registry\n },\n\n remove: (_name: string) => {\n // No-op for frozen registry\n return false\n },\n\n has: (name: string) => {\n return toolMap.has(name)\n },\n\n get: (name: string) => {\n return toolMap.get(name)\n },\n\n isFrozen: true,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6EA,SAAgB,mBACd,eAA6B,CAAC,GACT;CACrB,MAAM,wBAAQ,IAAI,IAAmB;CAErC,KAAK,MAAM,QAAQ,cACjB,MAAM,IAAI,KAAK,MAAM,IAAI;CAG3B,OAAO;EACL,gBAAgB,MAAM,KAAK,MAAM,OAAO,CAAC;EAEzC,MAAM,SAAgB;GACpB,MAAM,IAAI,KAAK,MAAM,IAAI;EAC3B;EAEA,SAAS,SAAiB;GACxB,OAAO,MAAM,OAAO,IAAI;EAC1B;EAEA,MAAM,SAAiB;GACrB,OAAO,MAAM,IAAI,IAAI;EACvB;EAEA,MAAM,SAAiB;GACrB,OAAO,MAAM,IAAI,IAAI;EACvB;EAEA,UAAU;CACZ;AACF;;;;;;;;;;AAWA,SAAgB,qBACd,QAAsB,CAAC,GACF;CACrB,MAAM,0BAAU,IAAI,IAAmB;CAEvC,KAAK,MAAM,QAAQ,OACjB,QAAQ,IAAI,KAAK,MAAM,IAAI;CAG7B,MAAM,cAAc,OAAO,OAAO,CAAC,GAAG,KAAK,CAAC;CAE5C,OAAO;EACL,gBAAgB,CAAC,GAAG,WAAW;EAE/B,MAAM,UAAiB,CAEvB;EAEA,SAAS,UAAkB;GAEzB,OAAO;EACT;EAEA,MAAM,SAAiB;GACrB,OAAO,QAAQ,IAAI,IAAI;EACzB;EAEA,MAAM,SAAiB;GACrB,OAAO,QAAQ,IAAI,IAAI;EACzB;EAEA,UAAU;CACZ;AACF"}
@@ -1,7 +1,16 @@
1
+ //#region src/tools/provider-tool.ts
2
+ /**
3
+ * Attach the `ProviderTool` phantom brand to a plain `Tool`-shaped object.
4
+ *
5
+ * The brand fields (`'~provider'`, `'~toolKind'`) exist only in the type
6
+ * system and are never assigned at runtime, so this is a single audited
7
+ * type-only assertion. Use it inside adapter `xxxTool()` factories instead
8
+ * of `as unknown as` — the cast collapses to one named site.
9
+ */
1
10
  function brandProviderTool(tool) {
2
- return tool;
11
+ return tool;
3
12
  }
4
- export {
5
- brandProviderTool
6
- };
7
- //# sourceMappingURL=provider-tool.js.map
13
+ //#endregion
14
+ export { brandProviderTool };
15
+
16
+ //# sourceMappingURL=provider-tool.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-tool.js","sources":["../../../src/tools/provider-tool.ts"],"sourcesContent":["import type { Tool } from '../types'\n\n/**\n * A provider-specific tool produced by an adapter-package factory\n * (e.g. `webSearchTool` from `@tanstack/ai-anthropic/tools`).\n *\n * The two `~`-prefixed fields are type-only phantom brands — they are never\n * assigned at runtime. They allow the core type system to match a factory's\n * output against the selected model's `supports.tools` list and surface a\n * compile-time error when the combination is unsupported.\n *\n * User-defined tools (via `toolDefinition()`) remain plain `Tool` and stay\n * assignable to any model.\n *\n * @template TProvider - Provider identifier (e.g. `'anthropic'`, `'openai'`).\n * @template TKind - Canonical tool-kind string matching the provider's\n * `supports.tools` entries (e.g. `'web_search'`, `'code_execution'`).\n */\nexport interface ProviderTool<\n TProvider extends string,\n TKind extends string,\n> extends Tool {\n readonly '~provider': TProvider\n readonly '~toolKind': TKind\n}\n\n/**\n * Attach the `ProviderTool` phantom brand to a plain `Tool`-shaped object.\n *\n * The brand fields (`'~provider'`, `'~toolKind'`) exist only in the type\n * system and are never assigned at runtime, so this is a single audited\n * type-only assertion. Use it inside adapter `xxxTool()` factories instead\n * of `as unknown as` — the cast collapses to one named site.\n */\nexport function brandProviderTool<T extends ProviderTool<string, string>>(\n tool: Omit<T, '~provider' | '~toolKind'>,\n): T {\n return tool as T\n}\n"],"names":[],"mappings":"AAkCO,SAAS,kBACd,MACG;AACH,SAAO;AACT;"}
1
+ {"version":3,"file":"provider-tool.js","names":[],"sources":["../../../src/tools/provider-tool.ts"],"sourcesContent":["import type { Tool } from '../types'\n\n/**\n * A provider-specific tool produced by an adapter-package factory\n * (e.g. `webSearchTool` from `@tanstack/ai-anthropic/tools`).\n *\n * The two `~`-prefixed fields are type-only phantom brands — they are never\n * assigned at runtime. They allow the core type system to match a factory's\n * output against the selected model's `supports.tools` list and surface a\n * compile-time error when the combination is unsupported.\n *\n * User-defined tools (via `toolDefinition()`) remain plain `Tool` and stay\n * assignable to any model.\n *\n * @template TProvider - Provider identifier (e.g. `'anthropic'`, `'openai'`).\n * @template TKind - Canonical tool-kind string matching the provider's\n * `supports.tools` entries (e.g. `'web_search'`, `'code_execution'`).\n */\nexport interface ProviderTool<\n TProvider extends string,\n TKind extends string,\n> extends Tool {\n readonly '~provider': TProvider\n readonly '~toolKind': TKind\n}\n\n/**\n * Attach the `ProviderTool` phantom brand to a plain `Tool`-shaped object.\n *\n * The brand fields (`'~provider'`, `'~toolKind'`) exist only in the type\n * system and are never assigned at runtime, so this is a single audited\n * type-only assertion. Use it inside adapter `xxxTool()` factories instead\n * of `as unknown as` — the cast collapses to one named site.\n */\nexport function brandProviderTool<T extends ProviderTool<string, string>>(\n tool: Omit<T, '~provider' | '~toolKind'>,\n): T {\n return tool as T\n}\n"],"mappings":";;;;;;;;;AAkCA,SAAgB,kBACd,MACG;CACH,OAAO;AACT"}