@tanstack/ai 0.42.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) 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 +5 -36
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
  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 -21
  11. package/dist/esm/activities/chat/index.js +2100 -1813
  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 +24 -6
  134. package/dist/esm/index.js +30 -98
  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.d.ts +3 -1
  157. package/dist/esm/middlewares/otel.js +599 -732
  158. package/dist/esm/middlewares/otel.js.map +1 -1
  159. package/dist/esm/middlewares/usage-attributes.js +47 -40
  160. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  161. package/dist/esm/realtime/event-emitter.js +24 -25
  162. package/dist/esm/realtime/event-emitter.js.map +1 -1
  163. package/dist/esm/realtime/index.d.ts +5 -9
  164. package/dist/esm/realtime/index.js +29 -6
  165. package/dist/esm/realtime/index.js.map +1 -1
  166. package/dist/esm/scope.d.ts +47 -0
  167. package/dist/esm/stream-durability.d.ts +171 -0
  168. package/dist/esm/stream-durability.js +295 -0
  169. package/dist/esm/stream-durability.js.map +1 -0
  170. package/dist/esm/stream-to-response.d.ts +178 -13
  171. package/dist/esm/stream-to-response.js +663 -115
  172. package/dist/esm/stream-to-response.js.map +1 -1
  173. package/dist/esm/strip-to-spec-middleware.js +30 -16
  174. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  175. package/dist/esm/system-prompts.js +27 -21
  176. package/dist/esm/system-prompts.js.map +1 -1
  177. package/dist/esm/tool-registry.js +72 -45
  178. package/dist/esm/tool-registry.js.map +1 -1
  179. package/dist/esm/tools/provider-tool.js +14 -5
  180. package/dist/esm/tools/provider-tool.js.map +1 -1
  181. package/dist/esm/types.d.ts +321 -42
  182. package/dist/esm/types.js +2 -0
  183. package/dist/esm/utilities/ag-ui-wire.js +79 -93
  184. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  185. package/dist/esm/utilities/chat-params.d.ts +26 -4
  186. package/dist/esm/utilities/chat-params.js +218 -92
  187. package/dist/esm/utilities/chat-params.js.map +1 -1
  188. package/dist/esm/utilities/errors.js +28 -18
  189. package/dist/esm/utilities/errors.js.map +1 -1
  190. package/dist/esm/utilities/media-prompt.js +46 -41
  191. package/dist/esm/utilities/media-prompt.js.map +1 -1
  192. package/dist/esm/utilities/numbers.js +13 -10
  193. package/dist/esm/utilities/numbers.js.map +1 -1
  194. package/dist/esm/utilities/provider-executed.js +20 -11
  195. package/dist/esm/utilities/provider-executed.js.map +1 -1
  196. package/dist/esm/utilities/sampling-keys.js +31 -19
  197. package/dist/esm/utilities/sampling-keys.js.map +1 -1
  198. package/dist/esm/utilities/tool-result.js +42 -30
  199. package/dist/esm/utilities/tool-result.js.map +1 -1
  200. package/dist/esm/utilities/usage.js +27 -9
  201. package/dist/esm/utilities/usage.js.map +1 -1
  202. package/dist/esm/utils.js +26 -18
  203. package/dist/esm/utils.js.map +1 -1
  204. package/package.json +10 -6
  205. package/skills/ai-core/SKILL.md +69 -18
  206. package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
  207. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
  208. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
  209. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
  210. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
  211. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
  212. package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
  213. package/skills/ai-core/chat-experience/SKILL.md +98 -11
  214. package/skills/ai-core/client-persistence/SKILL.md +277 -0
  215. package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
  216. package/skills/ai-core/debug-logging/SKILL.md +1 -1
  217. package/skills/ai-core/locks/SKILL.md +143 -0
  218. package/skills/ai-core/media-generation/SKILL.md +144 -12
  219. package/skills/ai-core/middleware/SKILL.md +258 -33
  220. package/skills/ai-core/structured-outputs/SKILL.md +1 -1
  221. package/skills/ai-core/tool-calling/SKILL.md +54 -61
  222. package/src/activities/chat/agent-loop-strategies.ts +5 -39
  223. package/src/activities/chat/cancel.ts +81 -0
  224. package/src/activities/chat/index.ts +1091 -200
  225. package/src/activities/chat/mcp/manager.ts +4 -4
  226. package/src/activities/chat/mcp/types.ts +2 -2
  227. package/src/activities/chat/messages.ts +5 -3
  228. package/src/activities/chat/middleware/builder.ts +1 -1
  229. package/src/activities/chat/middleware/compose.ts +186 -9
  230. package/src/activities/chat/middleware/index.ts +26 -0
  231. package/src/activities/chat/middleware/locks.ts +102 -0
  232. package/src/activities/chat/middleware/pending-turn.ts +47 -0
  233. package/src/activities/chat/middleware/run-disconnect.ts +62 -0
  234. package/src/activities/chat/middleware/run-store.ts +412 -0
  235. package/src/activities/chat/middleware/types.ts +62 -1
  236. package/src/activities/chat/stream/processor.ts +189 -5
  237. package/src/activities/chat/tools/approval-schema.ts +205 -0
  238. package/src/activities/chat/tools/tool-calls.ts +106 -13
  239. package/src/activities/chat/tools/tool-definition.ts +210 -39
  240. package/src/activities/generateAudio/index.ts +20 -3
  241. package/src/activities/generateImage/index.ts +20 -3
  242. package/src/activities/generateSpeech/index.ts +25 -3
  243. package/src/activities/generateTranscription/index.ts +26 -3
  244. package/src/activities/generateVideo/index.ts +345 -82
  245. package/src/activities/middleware/index.ts +2 -0
  246. package/src/activities/middleware/run.ts +31 -0
  247. package/src/activities/middleware/types.ts +49 -5
  248. package/src/activities/stream-generation-result.ts +30 -2
  249. package/src/activities/summarize/chat-stream-summarize.ts +5 -0
  250. package/src/activities/summarize/index.ts +200 -10
  251. package/src/adapter-internals.ts +10 -1
  252. package/src/client.ts +244 -0
  253. package/src/custom-events.ts +107 -0
  254. package/src/delivery-detach.ts +72 -0
  255. package/src/delivery-disconnect.ts +84 -0
  256. package/src/index.ts +138 -1
  257. package/src/interrupt-resume.ts +824 -0
  258. package/src/interrupt-serialization.ts +183 -0
  259. package/src/interrupts.ts +146 -0
  260. package/src/locks.ts +17 -0
  261. package/src/logger/types.ts +1 -1
  262. package/src/middlewares/otel.ts +23 -5
  263. package/src/realtime/index.ts +5 -9
  264. package/src/scope.ts +47 -0
  265. package/src/stream-durability.ts +598 -0
  266. package/src/stream-to-response.ts +1051 -95
  267. package/src/strip-to-spec-middleware.ts +3 -3
  268. package/src/types.ts +405 -45
  269. package/src/utilities/chat-params.ts +245 -55
  270. package/dist/esm/activities/index.js.map +0 -1
  271. package/dist/esm/adapter-internals.js.map +0 -1
  272. package/dist/esm/index.js.map +0 -1
  273. package/dist/esm/middlewares/index.js.map +0 -1
@@ -1,121 +1,669 @@
1
1
  import { toRunErrorPayload } from "./activities/error-payload.js";
2
+ import { isCancelRequestedReason } from "./activities/chat/cancel.js";
3
+ import { isRunStatus, isTerminalRunStatus } from "./activities/chat/middleware/run-store.js";
4
+ import { wasRunDetached } from "./delivery-detach.js";
5
+ import { notifyRunDisconnected } from "./delivery-disconnect.js";
6
+ import { resolveResumeRunId } from "./stream-durability.js";
7
+ import { EventType } from "./types.js";
8
+ import { resolveDebugOption } from "./logger/resolve.js";
9
+ //#region src/stream-to-response.ts
10
+ /**
11
+ * Collect all text content from a StreamChunk async iterable and return as a string.
12
+ *
13
+ * This function consumes the entire stream, accumulating content from TEXT_MESSAGE_CONTENT events,
14
+ * and returns the final concatenated text.
15
+ *
16
+ * @param stream - AsyncIterable of StreamChunks from chat()
17
+ * @returns Promise<string> - The accumulated text content
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const stream = chat({
22
+ * adapter: openaiText('gpt-5.5'),
23
+ * messages: [{ role: 'user', content: 'Hello!' }]
24
+ * });
25
+ * const text = await streamToText(stream);
26
+ * console.log(text); // "Hello! How can I help you today?"
27
+ * ```
28
+ */
2
29
  async function streamToText(stream) {
3
- let accumulatedContent = "";
4
- for await (const chunk of stream) {
5
- if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) {
6
- accumulatedContent += chunk.delta;
7
- }
8
- }
9
- return accumulatedContent;
10
- }
11
- function toServerSentEventsStream(stream, abortController) {
12
- const encoder = new TextEncoder();
13
- return new ReadableStream({
14
- async start(controller) {
15
- try {
16
- for await (const chunk of stream) {
17
- if (abortController?.signal.aborted) {
18
- break;
19
- }
20
- controller.enqueue(
21
- encoder.encode(`data: ${JSON.stringify(chunk)}
22
-
23
- `)
24
- );
25
- }
26
- controller.close();
27
- } catch (error) {
28
- if (abortController?.signal.aborted) {
29
- controller.close();
30
- return;
31
- }
32
- controller.enqueue(
33
- encoder.encode(
34
- `data: ${JSON.stringify({
35
- type: "RUN_ERROR",
36
- timestamp: Date.now(),
37
- error: toRunErrorPayload(error)
38
- })}
39
-
40
- `
41
- )
42
- );
43
- controller.close();
44
- }
45
- },
46
- cancel() {
47
- if (abortController) {
48
- abortController.abort();
49
- }
50
- }
51
- });
30
+ let accumulatedContent = "";
31
+ for await (const chunk of stream) if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) accumulatedContent += chunk.delta;
32
+ return accumulatedContent;
33
+ }
34
+ function errorMessage(error) {
35
+ return toRunErrorPayload(error).message;
36
+ }
37
+ function combineFailures(primary, secondary, phase) {
38
+ if (primary === secondary) return primary;
39
+ const errors = primary instanceof AggregateError ? [...primary.errors, secondary] : [primary, secondary];
40
+ return new AggregateError(errors, `${errorMessage(primary)}; ${phase}: ${errorMessage(secondary)}`);
41
+ }
42
+ function runErrorChunk(error) {
43
+ const payload = toRunErrorPayload(error);
44
+ return {
45
+ type: EventType.RUN_ERROR,
46
+ timestamp: Date.now(),
47
+ message: payload.message,
48
+ ...payload.code === void 0 ? {} : { code: payload.code },
49
+ error: payload
50
+ };
51
+ }
52
+ function isAborted(signal) {
53
+ return signal.aborted;
54
+ }
55
+ /**
56
+ * Whether this abort is an EXPLICIT in-process cancel — the caller aborted with
57
+ * {@link RUN_CANCEL_REASON} rather than the socket going away.
58
+ *
59
+ * Core's own guard, independent of any middleware verdict: a user pressing Stop
60
+ * must always get a closed, terminal log, so the sink refuses to treat that abort
61
+ * as a detach even if the run's middleware published one. A reason-less abort
62
+ * carries a `DOMException`, never a string, so a non-string reason is "no
63
+ * explicit intent" — exactly how `resolveAbortReason` reads it in `chat()`.
64
+ */
65
+ function isExplicitCancel(signal) {
66
+ const reason = signal.reason;
67
+ return typeof reason === "string" && isCancelRequestedReason(reason);
68
+ }
69
+ function needsTerminalPersistence(terminalPersisted, cancelled, failed) {
70
+ return !terminalPersisted && (cancelled || failed);
71
+ }
72
+ function toEncodedStream(stream, abortController, encodeChunk, encodeError, detachOnCancel = false, onDetachedCancel) {
73
+ const cancellation = abortController ?? new AbortController();
74
+ let iterator;
75
+ let iteratorCleanup;
76
+ let pumpPromise = Promise.resolve();
77
+ let pumpFailure;
78
+ let cancelled = false;
79
+ const recordPumpFailure = (error, phase) => {
80
+ pumpFailure = { error: pumpFailure === void 0 ? error : combineFailures(pumpFailure.error, error, phase) };
81
+ };
82
+ const closeIterator = () => {
83
+ iteratorCleanup ??= (async () => {
84
+ if (iterator?.return) await iterator.return();
85
+ })();
86
+ return iteratorCleanup;
87
+ };
88
+ return new ReadableStream({
89
+ start(controller) {
90
+ iterator = stream[Symbol.asyncIterator]();
91
+ pumpPromise = (async () => {
92
+ let index = 0;
93
+ let iteratorDone = false;
94
+ try {
95
+ while (!isAborted(cancellation.signal)) {
96
+ const result = await iterator.next();
97
+ if (result.done) {
98
+ iteratorDone = true;
99
+ break;
100
+ }
101
+ if (isAborted(cancellation.signal)) break;
102
+ if (!cancelled) controller.enqueue(encodeChunk(result.value, index));
103
+ index += 1;
104
+ }
105
+ } catch (error) {
106
+ recordPumpFailure(error, "stream iteration failed");
107
+ } finally {
108
+ if (!iteratorDone) try {
109
+ await closeIterator();
110
+ } catch (error) {
111
+ recordPumpFailure(error, "iterator cleanup failed");
112
+ }
113
+ if (!cancelled && !isAborted(cancellation.signal) && pumpFailure !== void 0) controller.enqueue(encodeError(pumpFailure.error));
114
+ if (!cancelled) controller.close();
115
+ }
116
+ })().catch((error) => {
117
+ recordPumpFailure(error, "stream pump failed");
118
+ });
119
+ },
120
+ async cancel(reason) {
121
+ cancelled = true;
122
+ if (detachOnCancel) {
123
+ onDetachedCancel?.();
124
+ return;
125
+ }
126
+ if (!isAborted(cancellation.signal)) cancellation.abort(reason);
127
+ let cancellationFailure;
128
+ try {
129
+ await closeIterator();
130
+ } catch (error) {
131
+ cancellationFailure = { error };
132
+ }
133
+ await pumpPromise;
134
+ if (pumpFailure !== void 0 && cancellationFailure !== void 0) throw combineFailures(pumpFailure.error, cancellationFailure.error, "iterator cancellation failed");
135
+ if (pumpFailure !== void 0) throw pumpFailure.error;
136
+ if (cancellationFailure !== void 0) throw cancellationFailure.error;
137
+ }
138
+ });
139
+ }
140
+ /**
141
+ * Convert a StreamChunk async iterable to a ReadableStream in Server-Sent Events format
142
+ *
143
+ * This creates a ReadableStream that emits chunks in SSE format:
144
+ * - Each chunk is prefixed with "data: "
145
+ * - Each chunk is followed by "\n\n"
146
+ * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)
147
+ *
148
+ * @param stream - AsyncIterable of StreamChunks from chat()
149
+ * @param abortController - Optional AbortController to abort when stream is cancelled
150
+ * @param getId - Optional per-chunk durability offset; when present, each event gets an `id:` line
151
+ * @returns ReadableStream in Server-Sent Events format
152
+ */
153
+ function toServerSentEventsStream(stream, abortController, getId) {
154
+ const { encodeChunk, encodeError } = sseEncoders(getId);
155
+ return toEncodedStream(stream, abortController, encodeChunk, encodeError);
156
+ }
157
+ /**
158
+ * SSE chunk/error encoders. Shared by the public {@link toServerSentEventsStream}
159
+ * and the internal durability branch (which additionally needs `toEncodedStream`'s
160
+ * private `detachOnCancel`), so the wire format stays identical for both.
161
+ */
162
+ function sseEncoders(getId) {
163
+ const encoder = new TextEncoder();
164
+ return {
165
+ encodeChunk: (chunk, index) => {
166
+ const id = getId?.(chunk, index);
167
+ const idLine = id === void 0 ? "" : `id: ${id}\n`;
168
+ return encoder.encode(`${idLine}data: ${JSON.stringify(chunk)}\n\n`);
169
+ },
170
+ encodeError: (error) => encoder.encode(`data: ${JSON.stringify(runErrorChunk(error))}\n\n`)
171
+ };
172
+ }
173
+ /** Default number of chunks buffered before a durability `append`. */
174
+ var DEFAULT_DURABILITY_BATCH = 32;
175
+ /**
176
+ * Resolve and validate the durability batch size. A non-positive-integer (0,
177
+ * negative, fractional, or `NaN`) is rejected rather than clamped: silently
178
+ * `Math.max(1, …)`-ing a `NaN` used to disable size-based flushing entirely
179
+ * (`length >= NaN` is always false), which is a subtle footgun.
180
+ */
181
+ function resolveBatchSize(batch) {
182
+ if (batch === void 0) return DEFAULT_DURABILITY_BATCH;
183
+ if (!Number.isInteger(batch) || batch <= 0) throw new Error(`Invalid durability batch size: ${batch}. Must be a positive integer.`);
184
+ return batch;
52
185
  }
186
+ /**
187
+ * Boundaries at which the batching producer flushes early, regardless of the
188
+ * batch size — the run-start marker, terminal events, and tool-call ends.
189
+ * Flushing here keeps the durability log promptly consistent at semantically
190
+ * meaningful points.
191
+ *
192
+ * `RUN_STARTED` matters especially for one-shot activities (image, speech,
193
+ * transcription, summarize): they emit `RUN_STARTED`, then await the provider
194
+ * for seconds, then a terminal. Without flushing `RUN_STARTED` the log stays
195
+ * empty for the whole run, so a mount-time `joinRun` finds nothing and its
196
+ * empty-log deadline fast-fails as "run gone" — even though the run is alive.
197
+ * Flushing it immediately makes the run resumable from the instant it starts.
198
+ */
199
+ function isDurabilityFlushBoundary(chunk) {
200
+ return chunk.type === "RUN_STARTED" || chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR" || chunk.type === "TOOL_CALL_END";
201
+ }
202
+ /**
203
+ * Name of the synthetic `CUSTOM` chunk a fresh durable producer appends to its
204
+ * log before pulling the first real chunk.
205
+ *
206
+ * Flushing `RUN_STARTED` (above) makes a run joinable from the instant the
207
+ * stream EMITS something — but a `chat()` whose middleware boots a sandbox
208
+ * (create a container, install a CLI) legitimately emits nothing for minutes,
209
+ * and during that window the log is empty. Every joiner's empty-log fail-fast
210
+ * (`memoryStream`'s first-chunk deadline, the client's rejoin connect deadline)
211
+ * then reads the run as gone — and the client clears its resume pointer, so a
212
+ * reload during the boot window permanently orphans a run that is still going.
213
+ *
214
+ * This marker closes the window: it is appended (and flushed) before the
215
+ * producer stream is first pulled, so a join always finds a first chunk within
216
+ * milliseconds of the run being accepted. Takeover alignment is unaffected — a
217
+ * journal replay cannot reproduce the marker, and alignment already skips
218
+ * stored `CUSTOM` chunks as out-of-band for exactly that reason (see
219
+ * `isBridgeCustomChunk` in `@tanstack/ai-sandbox`).
220
+ */
221
+ var RUN_ACCEPTED_EVENT = "run.accepted";
222
+ /**
223
+ * Build the delivery-durable source iterable for a transport helper.
224
+ *
225
+ * - **Resume** (`resumeFrom()` non-null): replay strictly after the offset,
226
+ * reading only from the durability log. The input `stream` is NEVER iterated,
227
+ * so `chat()`'s lazy iterator never fires the provider — the untouched
228
+ * generator is simply GC'd. This is what makes resume free of re-invocation.
229
+ * - **Fresh** (`resumeFrom()` null): iterate `stream`, buffering up to `batch`
230
+ * chunks (flushing early at terminal / tool-call boundaries), `append` each
231
+ * batch to the log, then forward. Appending BEFORE forwarding guarantees a
232
+ * reconnecting client can always replay exactly what it already saw.
233
+ *
234
+ * The returned `getId` maps each forwarded chunk to the exact opaque offset
235
+ * returned by the durability adapter for the SSE `id:` line.
236
+ */
237
+ function durableStreamSource(stream, durability, options) {
238
+ const resumeOffset = durability.resumeFrom();
239
+ const batchSize = resolveBatchSize(options.batch);
240
+ const abortController = options.abortController;
241
+ const logger = options.logger;
242
+ const idByChunk = /* @__PURE__ */ new WeakMap();
243
+ const seenOffsets = /* @__PURE__ */ new Set();
244
+ const getId = (chunk) => idByChunk.get(chunk);
245
+ const validateOffset = (offset) => {
246
+ if (offset.length === 0 || offset.includes("\0") || offset.includes("\r") || offset.includes("\n") || offset !== offset.trim()) throw new Error(`Invalid durability offset for SSE id: ${JSON.stringify(offset)}`);
247
+ if (seenOffsets.has(offset)) throw new Error(`Durability adapter must return a unique offset per chunk: ${JSON.stringify(offset)}`);
248
+ seenOffsets.add(offset);
249
+ };
250
+ async function* produce() {
251
+ let batch = [];
252
+ let terminalPersisted = false;
253
+ let terminalForwarded = false;
254
+ let failure;
255
+ let terminalCause;
256
+ let hasTerminalCause = false;
257
+ const recordFailure = (error, phase) => {
258
+ failure = { error: failure === void 0 ? error : combineFailures(failure.error, error, phase) };
259
+ };
260
+ async function* flush() {
261
+ if (batch.length === 0) return;
262
+ const toForward = batch;
263
+ batch = [];
264
+ const offsets = await durability.append(toForward);
265
+ if (offsets.length !== toForward.length) throw new Error(`Durability append returned ${offsets.length} offsets for ${toForward.length} chunks`);
266
+ toForward.forEach((chunk, i) => {
267
+ const offset = offsets[i];
268
+ if (offset === void 0) throw new Error(`Durability append omitted offset at index ${i}`);
269
+ validateOffset(offset);
270
+ idByChunk.set(chunk, offset);
271
+ });
272
+ if (toForward.some((chunk) => chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR")) terminalPersisted = true;
273
+ for (const chunk of toForward) {
274
+ if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") terminalForwarded = true;
275
+ yield chunk;
276
+ }
277
+ }
278
+ try {
279
+ if (isAborted(abortController.signal)) return;
280
+ batch.push({
281
+ type: "CUSTOM",
282
+ name: RUN_ACCEPTED_EVENT,
283
+ value: {},
284
+ timestamp: Date.now()
285
+ });
286
+ yield* flush();
287
+ for await (const chunk of stream) {
288
+ if (isAborted(abortController.signal)) break;
289
+ batch.push(chunk);
290
+ if (batch.length >= batchSize || isDurabilityFlushBoundary(chunk)) yield* flush();
291
+ }
292
+ if (!isAborted(abortController.signal)) yield* flush();
293
+ } catch (error) {
294
+ terminalCause = error;
295
+ hasTerminalCause = true;
296
+ recordFailure(error, "producer failed");
297
+ if (!isAborted(abortController.signal)) try {
298
+ yield* flush();
299
+ } catch (flushError) {
300
+ recordFailure(flushError, "flushing buffered chunks failed");
301
+ }
302
+ } finally {
303
+ const cancelled = isAborted(abortController.signal);
304
+ if (batch.length > 0) try {
305
+ for await (const _chunk of flush());
306
+ } catch (flushError) {
307
+ recordFailure(flushError, "flushing buffered chunks on exit failed");
308
+ }
309
+ const detached = cancelled && !isExplicitCancel(abortController.signal) && !hasTerminalCause && wasRunDetached(stream);
310
+ if (!detached && needsTerminalPersistence(terminalPersisted, cancelled, hasTerminalCause)) {
311
+ const cause = hasTerminalCause ? terminalCause : { name: "AbortError" };
312
+ try {
313
+ await durability.append([runErrorChunk(cause)]);
314
+ terminalPersisted = true;
315
+ } catch (terminalError) {
316
+ logger?.errors("persisting terminal RUN_ERROR failed", { error: terminalError });
317
+ recordFailure(terminalError, "persisting terminal RUN_ERROR failed");
318
+ }
319
+ }
320
+ if (!detached) try {
321
+ await durability.close();
322
+ } catch (closeError) {
323
+ logger?.errors("closing durability stream failed", { error: closeError });
324
+ recordFailure(closeError, "closing durability stream failed");
325
+ }
326
+ if (failure !== void 0) {
327
+ if (!terminalForwarded) throw failure.error;
328
+ logger?.errors("durability failure after a terminal event was forwarded", { error: failure.error });
329
+ }
330
+ }
331
+ }
332
+ async function* replay(offset) {
333
+ for await (const { offset: eventOffset, chunk } of durability.read(offset, abortController.signal)) {
334
+ if (isAborted(abortController.signal)) break;
335
+ validateOffset(eventOffset);
336
+ idByChunk.set(chunk, eventOffset);
337
+ yield chunk;
338
+ }
339
+ }
340
+ return {
341
+ source: resumeOffset !== null ? replay(resumeOffset) : produce(),
342
+ getId
343
+ };
344
+ }
345
+ /**
346
+ * Convert a StreamChunk async iterable to a Response in Server-Sent Events format
347
+ *
348
+ * This creates a Response that emits chunks in SSE format:
349
+ * - Each chunk is prefixed with "data: "
350
+ * - Each chunk is followed by "\n\n"
351
+ * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)
352
+ *
353
+ * Pass a `durability` sink (`memoryStream(request)` / `durableStream(request)`)
354
+ * to make the stream resumable: fresh runs are appended to the log and each SSE
355
+ * event is tagged with an `id:` offset; a reconnect (native `Last-Event-ID`) or
356
+ * a `?offset` join replays from the log without re-running the producer. `batch`
357
+ * controls how many chunks are buffered per `append` (default 32).
358
+ *
359
+ * @param stream - AsyncIterable of StreamChunks from chat()
360
+ * @param init - Optional Response initialization options (including `abortController`, `durability` with its optional `batch`, and `debug`)
361
+ * @returns Response in Server-Sent Events format
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * export async function POST(request: Request) {
366
+ * const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });
367
+ * return toServerSentEventsResponse(stream, { durability: { adapter: memoryStream(request) } });
368
+ * }
369
+ * ```
370
+ */
53
371
  function toServerSentEventsResponse(stream, init) {
54
- const { headers, abortController, ...responseInit } = init ?? {};
55
- const mergedHeaders = new Headers({
56
- "Content-Type": "text/event-stream",
57
- "Cache-Control": "no-cache",
58
- Connection: "keep-alive"
59
- });
60
- if (headers) {
61
- const userHeaders = new Headers(headers);
62
- userHeaders.forEach((value, key) => {
63
- mergedHeaders.set(key, value);
64
- });
65
- }
66
- return new Response(toServerSentEventsStream(stream, abortController), {
67
- ...responseInit,
68
- headers: mergedHeaders
69
- });
70
- }
71
- function toHttpStream(stream, abortController) {
72
- const encoder = new TextEncoder();
73
- return new ReadableStream({
74
- async start(controller) {
75
- try {
76
- for await (const chunk of stream) {
77
- if (abortController?.signal.aborted) {
78
- break;
79
- }
80
- controller.enqueue(encoder.encode(`${JSON.stringify(chunk)}
81
- `));
82
- }
83
- controller.close();
84
- } catch (error) {
85
- if (abortController?.signal.aborted) {
86
- controller.close();
87
- return;
88
- }
89
- controller.enqueue(
90
- encoder.encode(
91
- `${JSON.stringify({
92
- type: "RUN_ERROR",
93
- timestamp: Date.now(),
94
- error: toRunErrorPayload(error)
95
- })}
96
- `
97
- )
98
- );
99
- controller.close();
100
- }
101
- },
102
- cancel() {
103
- if (abortController) {
104
- abortController.abort();
105
- }
106
- }
107
- });
372
+ const { headers, abortController, durability, debug, ...responseInit } = init ?? {};
373
+ const mergedHeaders = new Headers({
374
+ "Content-Type": "text/event-stream",
375
+ "Cache-Control": "no-cache",
376
+ Connection: "keep-alive"
377
+ });
378
+ if (headers) new Headers(headers).forEach((value, key) => {
379
+ mergedHeaders.set(key, value);
380
+ });
381
+ let body;
382
+ if (durability) {
383
+ const isFresh = durability.adapter.resumeFrom() === null;
384
+ const producerAbortController = abortController ?? new AbortController();
385
+ const deliveryAbortController = isFresh ? new AbortController() : producerAbortController;
386
+ const { source, getId } = durableStreamSource(stream, durability.adapter, {
387
+ abortController: producerAbortController,
388
+ batch: durability.batch,
389
+ logger: resolveDebugOption(debug)
390
+ });
391
+ const { encodeChunk, encodeError } = sseEncoders(getId);
392
+ body = toEncodedStream(source, deliveryAbortController, encodeChunk, encodeError, isFresh, isFresh ? () => notifyRunDisconnected(stream) : void 0);
393
+ } else body = toServerSentEventsStream(stream, abortController);
394
+ return new Response(body, {
395
+ ...responseInit,
396
+ headers: mergedHeaders
397
+ });
398
+ }
399
+ /**
400
+ * A resume is served entirely from the durability log, so there is no producer
401
+ * to iterate. This empty source satisfies the response helpers' signature; on a
402
+ * resume `durableStreamSource` replays from the log and never touches it.
403
+ */
404
+ function emptyDurableSource() {
405
+ return (async function* () {})();
406
+ }
407
+ /**
408
+ * Take over an in-flight run as a side effect of serving its log.
409
+ *
410
+ * The response itself is unchanged: it still replays from the durability log via
411
+ * `emptyDurableSource()`. The drive runs BESIDE it, appending to the run's own
412
+ * producer-side log through the injected `pipe`, and the response tails what
413
+ * lands. That separation is what lets a taken-over run keep `chat()`'s normal
414
+ * middleware path — `withPersistence.onFinish` is what saves the transcript, so a
415
+ * parallel translation path would lose the history of any run that completed
416
+ * while detached.
417
+ *
418
+ * TOTAL BY CONSTRUCTION. Every failure is logged and swallowed:
419
+ *
420
+ * - No run id, no record, or a terminal record → serve the log, drive nothing.
421
+ * A second tab attaching to a finished run must still see the transcript.
422
+ * - The claim is refused (another host is already driving) → serve the log,
423
+ * drive nothing. That is the documented "two hosts attach at once: one wins
424
+ * the lease and drives, the other tails the log" behavior.
425
+ * - The drive throws → logged. It cannot be reported to this response, which is
426
+ * already streaming the log; the run's own `RUN_ERROR` event is the channel.
427
+ *
428
+ * A rejection escaping here would be an unhandled rejection with nobody to
429
+ * report it to — process-fatal on modern Node and instance-fatal inside a
430
+ * Durable Object.
431
+ */
432
+ function startRunDriver(driver) {
433
+ const logger = driver.logger;
434
+ const promise = (async () => {
435
+ const runId = resolveResumeRunId(driver.request);
436
+ if (runId === null) return;
437
+ let record = null;
438
+ try {
439
+ record = await driver.runs.get(runId);
440
+ } catch (error) {
441
+ logger?.errors("resume driver: reading the run record failed", {
442
+ runId,
443
+ error
444
+ });
445
+ return;
446
+ }
447
+ if (record !== null && !isRunStatus(record.status)) {
448
+ logger?.errors("resume driver: the run record has an unrecognized status", {
449
+ runId,
450
+ status: record.status
451
+ });
452
+ return;
453
+ }
454
+ if (record === null || isTerminalRunStatus(record.status)) return;
455
+ if (record.cancelRequested === true) return;
456
+ const active = record;
457
+ try {
458
+ await driver.claim({
459
+ runs: driver.runs,
460
+ locks: driver.locks,
461
+ runId
462
+ }, async (claim) => {
463
+ try {
464
+ await driver.runs.update(runId, { detachedSince: void 0 });
465
+ } catch (error) {
466
+ logger?.errors("resume driver: clearing detachedSince failed", {
467
+ runId,
468
+ error
469
+ });
470
+ }
471
+ await driver.pipe(driver.drive({
472
+ runId,
473
+ threadId: active.threadId,
474
+ signal: claim.signal
475
+ }), {
476
+ runId,
477
+ threadId: active.threadId,
478
+ signal: claim.signal
479
+ });
480
+ });
481
+ } catch (error) {
482
+ logger?.provider("resume driver: not driving this run", {
483
+ runId,
484
+ error
485
+ });
486
+ }
487
+ })();
488
+ if (driver.waitUntil) driver.waitUntil(promise);
489
+ else promise.catch(() => {});
490
+ }
491
+ /**
492
+ * The single wiring point both resume helpers call, so the SSE and NDJSON
493
+ * halves cannot drift: a fix here applies to both. Called AFTER each helper's
494
+ * `resumeFrom() === null` 400 check — an attach with no offset has nothing to
495
+ * replay, and driving a run whose response will 400 would start an agent
496
+ * nobody is watching.
497
+ */
498
+ function maybeStartRunDriver(driver) {
499
+ if (driver) startRunDriver(driver);
500
+ }
501
+ var NO_RESUME_OFFSET = "No resume offset provided (expected a Last-Event-ID header or an ?offset query parameter).";
502
+ /**
503
+ * Serve a resumable run from its durability log over Server-Sent Events, without
504
+ * re-running the model. Use this in a `GET` handler so a reload or a second tab
505
+ * can re-attach to an in-flight or finished run.
506
+ *
507
+ * The adapter (`memoryStream(request)` / `durableStream(request)`) captures the
508
+ * resume offset from the request. If there is none (no `Last-Event-ID` header
509
+ * and no `?offset`), there is nothing to replay and this returns a 400.
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * export async function GET(request: Request) {
514
+ * return resumeServerSentEventsResponse({ adapter: memoryStream(request) });
515
+ * }
516
+ * ```
517
+ */
518
+ function resumeServerSentEventsResponse(options) {
519
+ const { adapter, batch, debug, driver, ...responseInit } = options;
520
+ if (adapter.resumeFrom() === null) return new Response(NO_RESUME_OFFSET, { status: 400 });
521
+ maybeStartRunDriver(driver);
522
+ return toServerSentEventsResponse(emptyDurableSource(), {
523
+ ...responseInit,
524
+ durability: {
525
+ adapter,
526
+ batch
527
+ },
528
+ debug
529
+ });
108
530
  }
531
+ /**
532
+ * Convert a StreamChunk async iterable to a ReadableStream in HTTP stream format (newline-delimited JSON)
533
+ *
534
+ * This creates a ReadableStream that emits chunks as newline-delimited JSON:
535
+ * - Each chunk is JSON.stringify'd and followed by "\n"
536
+ * - No SSE formatting (no "data: " prefix)
537
+ *
538
+ * This format is compatible with `fetchHttpStream` connection adapter.
539
+ *
540
+ * When `getId` is supplied (delivery durability), each chunk is emitted as an
541
+ * envelope `{"id":"<offset>","chunk":{…}}` instead of a bare chunk. NDJSON has
542
+ * no native event-id field like SSE's `id:` line, so the resumable offset rides
543
+ * inside the payload. Untagged chunks (no id) stay bare, so a non-durable
544
+ * stream is byte-identical to before and the client auto-detects either form.
545
+ *
546
+ * @param stream - AsyncIterable of StreamChunks from chat()
547
+ * @param abortController - Optional AbortController to abort when stream is cancelled
548
+ * @param getId - Optional per-chunk durability offset; when present, chunks are envelope-encoded
549
+ * @returns ReadableStream in HTTP stream format (newline-delimited JSON)
550
+ *
551
+ * @example
552
+ * ```typescript
553
+ * const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });
554
+ * const readableStream = toHttpStream(stream);
555
+ * // Use with Response for HTTP streaming (not SSE)
556
+ * return new Response(readableStream, {
557
+ * headers: { 'Content-Type': 'application/x-ndjson' }
558
+ * });
559
+ * ```
560
+ */
561
+ function toHttpStream(stream, abortController, getId) {
562
+ const { encodeChunk, encodeError } = ndjsonEncoders(getId);
563
+ return toEncodedStream(stream, abortController, encodeChunk, encodeError);
564
+ }
565
+ /**
566
+ * NDJSON chunk/error encoders. Shared by {@link toHttpStream} and the internal
567
+ * durability branch (see {@link sseEncoders}).
568
+ */
569
+ function ndjsonEncoders(getId) {
570
+ const encoder = new TextEncoder();
571
+ return {
572
+ encodeChunk: (chunk, index) => {
573
+ const id = getId?.(chunk, index);
574
+ const line = id === void 0 ? JSON.stringify(chunk) : JSON.stringify({
575
+ id,
576
+ chunk
577
+ });
578
+ return encoder.encode(`${line}\n`);
579
+ },
580
+ encodeError: (error) => encoder.encode(`${JSON.stringify(runErrorChunk(error))}\n`)
581
+ };
582
+ }
583
+ /**
584
+ * Convert a StreamChunk async iterable to a Response in HTTP stream format (newline-delimited JSON)
585
+ *
586
+ * This creates a Response that emits chunks in HTTP stream format:
587
+ * - Each chunk is JSON.stringify'd and followed by "\n"
588
+ * - No SSE formatting (no "data: " prefix)
589
+ *
590
+ * This format is compatible with `fetchHttpStream` connection adapter.
591
+ *
592
+ * Pass a `durability` sink (`memoryStream(request)` / `durableStream(request)`)
593
+ * to make the stream resumable: fresh runs are appended to the log and each
594
+ * NDJSON line is emitted as an `{ id, chunk }` envelope carrying an opaque
595
+ * offset; a reconnect (native `Last-Event-ID` header) or a `?offset` join
596
+ * replays from the log without re-running the producer. `batch` controls how
597
+ * many chunks are buffered per `append` (default 32). This shares the exact
598
+ * `durableStreamSource` used by `toServerSentEventsResponse` — only the wire
599
+ * encoding differs.
600
+ *
601
+ * @param stream - AsyncIterable of StreamChunks from chat()
602
+ * @param init - Optional Response initialization options (including `abortController`, `durability` with its optional `batch`, and `debug`)
603
+ * @returns Response in HTTP stream format (newline-delimited JSON)
604
+ *
605
+ * @example
606
+ * ```typescript
607
+ * export async function POST(request: Request) {
608
+ * const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });
609
+ * return toHttpResponse(stream, { durability: { adapter: memoryStream(request) } });
610
+ * }
611
+ * ```
612
+ */
109
613
  function toHttpResponse(stream, init) {
110
- return new Response(toHttpStream(stream, init?.abortController), {
111
- ...init
112
- });
113
- }
114
- export {
115
- streamToText,
116
- toHttpResponse,
117
- toHttpStream,
118
- toServerSentEventsResponse,
119
- toServerSentEventsStream
120
- };
121
- //# sourceMappingURL=stream-to-response.js.map
614
+ const { abortController, durability, debug, headers, ...responseInit } = init ?? {};
615
+ const mergedHeaders = new Headers({
616
+ "Content-Type": "application/x-ndjson",
617
+ "Cache-Control": "no-cache"
618
+ });
619
+ if (headers) new Headers(headers).forEach((value, key) => {
620
+ mergedHeaders.set(key, value);
621
+ });
622
+ let body;
623
+ if (durability) {
624
+ const isFresh = durability.adapter.resumeFrom() === null;
625
+ const producerAbortController = abortController ?? new AbortController();
626
+ const deliveryAbortController = isFresh ? new AbortController() : producerAbortController;
627
+ const { source, getId } = durableStreamSource(stream, durability.adapter, {
628
+ abortController: producerAbortController,
629
+ batch: durability.batch,
630
+ logger: resolveDebugOption(debug)
631
+ });
632
+ const { encodeChunk, encodeError } = ndjsonEncoders(getId);
633
+ body = toEncodedStream(source, deliveryAbortController, encodeChunk, encodeError, isFresh, isFresh ? () => notifyRunDisconnected(stream) : void 0);
634
+ } else body = toHttpStream(stream, abortController);
635
+ return new Response(body, {
636
+ ...responseInit,
637
+ headers: mergedHeaders
638
+ });
639
+ }
640
+ /**
641
+ * Serve a resumable run from its durability log over NDJSON, without re-running
642
+ * the model. The NDJSON counterpart of {@link resumeServerSentEventsResponse};
643
+ * pair it with a `toHttpResponse` producer. Returns a 400 when the request
644
+ * carries no resume offset (no `Last-Event-ID` header and no `?offset`).
645
+ *
646
+ * @example
647
+ * ```typescript
648
+ * export async function GET(request: Request) {
649
+ * return resumeHttpResponse({ adapter: memoryStream(request) });
650
+ * }
651
+ * ```
652
+ */
653
+ function resumeHttpResponse(options) {
654
+ const { adapter, batch, debug, driver, ...responseInit } = options;
655
+ if (adapter.resumeFrom() === null) return new Response(NO_RESUME_OFFSET, { status: 400 });
656
+ maybeStartRunDriver(driver);
657
+ return toHttpResponse(emptyDurableSource(), {
658
+ ...responseInit,
659
+ durability: {
660
+ adapter,
661
+ batch
662
+ },
663
+ debug
664
+ });
665
+ }
666
+ //#endregion
667
+ export { RUN_ACCEPTED_EVENT, resolveResumeRunId, resumeHttpResponse, resumeServerSentEventsResponse, streamToText, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream };
668
+
669
+ //# sourceMappingURL=stream-to-response.js.map