@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
@@ -0,0 +1,598 @@
1
+ import type { StreamChunk } from './types'
2
+
3
+ /**
4
+ * A pluggable delivery-durability backend.
5
+ *
6
+ * Offsets are owned by the adapter and opaque to the transport. The generic
7
+ * parameter lets an adapter retain a branded string type across append, read,
8
+ * and resume without requiring core to understand its cursor format.
9
+ */
10
+ export interface StreamDurability<TOffset extends string = string> {
11
+ /** Return the adapter offset captured from the request, or null for a producer. */
12
+ resumeFrom: () => TOffset | null
13
+ /**
14
+ * Persist a batch before it is delivered and return exactly one resumable
15
+ * offset for each chunk, in the same order.
16
+ */
17
+ append: (chunks: Array<StreamChunk>) => Promise<Array<TOffset>>
18
+ /** Replay chunks strictly after the supplied adapter-owned offset. */
19
+ read: (
20
+ offset: TOffset,
21
+ signal?: AbortSignal,
22
+ ) => AsyncIterable<{ offset: TOffset; chunk: StreamChunk }>
23
+ /**
24
+ * Terminalize the producer log and unblock live readers. Core awaits this
25
+ * for every producer exit, including completion, cancellation, and failure.
26
+ */
27
+ close: () => Promise<void>
28
+ /**
29
+ * Everything stored for this run **at the moment of the call**, in append
30
+ * order, then resolve.
31
+ *
32
+ * This is the bounded counterpart to {@link StreamDurability.read}. `read`
33
+ * tails: it parks until the log is terminalized or the caller aborts, so it
34
+ * cannot be used to inspect a log whose producer died without calling
35
+ * `close` — that log stays open forever and a `for await` over it never
36
+ * finishes. `snapshot` exists for exactly that case: a producer resuming a
37
+ * run needs to see the prefix a previous host already stored so it can line
38
+ * its own output up against it, and it needs that read to *return*.
39
+ *
40
+ * Implementations MUST:
41
+ *
42
+ * - never wait for more entries — resolve with what is stored, including
43
+ * while the log is still open and still being appended to;
44
+ * - resolve to an empty array for a run with nothing stored, rather than
45
+ * throwing. In particular an implementation must not reuse the
46
+ * unknown-run failure path a from-start `read` join takes (`read('-1')` on
47
+ * an empty log is allowed to fail; `snapshot()` is not). A backend over a
48
+ * network may of course still reject on a transport, protocol, or
49
+ * authorization failure — that is a failed call, not an empty run;
50
+ * - return a fresh array the caller can keep or mutate without reaching the
51
+ * stored log through it.
52
+ *
53
+ * The result is a point-in-time view and carries no lock: a concurrent
54
+ * `append` may land immediately after the snapshot is taken, so a caller
55
+ * must not treat the last returned offset as the permanent tail.
56
+ */
57
+ snapshot: () => Promise<Array<{ offset: TOffset; chunk: StreamChunk }>>
58
+ }
59
+
60
+ /**
61
+ * A {@link StreamDurability} that can re-persist an already-stored range
62
+ * idempotently.
63
+ *
64
+ * A run driver resuming after a crash re-derives the same offsets from its
65
+ * source position, so replaying an overlapping range must be a no-op rather
66
+ * than producing duplicates. That capability is deliberately a **separate,
67
+ * optional method** instead of an optional parameter on `append`:
68
+ *
69
+ * - Only adapters that actually support it return this type, so a consumer
70
+ * requiring the capability asks for `UpsertableStreamDurability` and a
71
+ * mismatch is a compile error rather than a runtime failure buried in a
72
+ * run log.
73
+ * - Pairing each chunk with its offset structurally makes a length mismatch
74
+ * and an unpaired chunk unrepresentable. A sparse hole is still
75
+ * representable, so implementations must reject one explicitly.
76
+ *
77
+ * Implementations MUST validate the entire batch before mutating any stored
78
+ * state (so a rejected call never partially applies), MUST reject an offset
79
+ * they did not mint themselves (every accepted offset is resumable by
80
+ * definition), MUST reject an offset repeated within one batch, and MUST
81
+ * reject a hole in the entries array.
82
+ */
83
+ export interface UpsertableStreamDurability<
84
+ TOffset extends string = string,
85
+ > extends StreamDurability<TOffset> {
86
+ /**
87
+ * Persist a batch at caller-supplied offsets, replacing any entry already
88
+ * stored at the same offset. Returns the offsets in the order supplied.
89
+ */
90
+ upsert: (
91
+ entries: Array<{ chunk: StreamChunk; offset: TOffset }>,
92
+ ) => Promise<Array<TOffset>>
93
+ }
94
+
95
+ const MEMORY_OFFSET_PREFIX = 'memory:v1:'
96
+
97
+ interface MemoryOffset {
98
+ runId: string
99
+ seq: number
100
+ }
101
+
102
+ function encodeMemoryOffset(runId: string, seq: number): string {
103
+ return `${MEMORY_OFFSET_PREFIX}${encodeURIComponent(runId)}:${seq}`
104
+ }
105
+
106
+ function decodeMemoryOffset(offset: string): MemoryOffset {
107
+ if (!offset.startsWith(MEMORY_OFFSET_PREFIX)) {
108
+ throw new Error(`Invalid memory stream offset: ${offset}`)
109
+ }
110
+ const encoded = offset.slice(MEMORY_OFFSET_PREFIX.length)
111
+ const separator = encoded.lastIndexOf(':')
112
+ if (separator === -1) {
113
+ throw new Error(`Invalid memory stream offset: ${offset}`)
114
+ }
115
+ const runId = decodeURIComponent(encoded.slice(0, separator))
116
+ const seq = Number(encoded.slice(separator + 1))
117
+ if (!Number.isSafeInteger(seq) || seq < 1) {
118
+ throw new Error(`Invalid memory stream offset: ${offset}`)
119
+ }
120
+ return { runId, seq }
121
+ }
122
+
123
+ function readResumeOffset(request: Request): string | null {
124
+ const header = request.headers.get('Last-Event-ID')
125
+ if (header) return header
126
+ try {
127
+ return new URL(request.url).searchParams.get('offset')
128
+ } catch {
129
+ return null
130
+ }
131
+ }
132
+
133
+ /**
134
+ * The run id a request names: `X-Run-Id` header first, then `?runId`.
135
+ *
136
+ * The single implementation of that precedence, shared by the durability
137
+ * adapters below and by the resume response helpers' run driver
138
+ * (`stream-to-response.ts`), so the helper and the adapter can never disagree
139
+ * about which run a request is talking about.
140
+ */
141
+ export function resolveResumeRunId(request: Request): string | null {
142
+ // A POST producer carries its client-chosen run id in the X-Run-Id header so
143
+ // the request URL stays byte-identical to a plain, non-durable request; the
144
+ // GET join path carries it in the ?runId query instead. Prefer the header,
145
+ // fall back to the query.
146
+ const header = request.headers.get('X-Run-Id')
147
+ if (header) return header
148
+ try {
149
+ return new URL(request.url).searchParams.get('runId')
150
+ } catch {
151
+ return null
152
+ }
153
+ }
154
+
155
+ function assertValidRunId(runId: string): string {
156
+ if (runId.length === 0 || /[\r\n]/.test(runId)) {
157
+ throw new Error(
158
+ `Invalid runId (must be non-empty and contain no CR/LF): ${JSON.stringify(runId)}`,
159
+ )
160
+ }
161
+ return runId
162
+ }
163
+
164
+ function resolveMemoryRunId(
165
+ request: Request,
166
+ resumeOffset: string | null,
167
+ ): string {
168
+ if (
169
+ resumeOffset !== null &&
170
+ resumeOffset !== '-1' &&
171
+ resumeOffset !== 'now'
172
+ ) {
173
+ return assertValidRunId(decodeMemoryOffset(resumeOffset).runId)
174
+ }
175
+ const requestedRunId = resolveResumeRunId(request)
176
+ return requestedRunId === null
177
+ ? crypto.randomUUID()
178
+ : assertValidRunId(requestedRunId)
179
+ }
180
+
181
+ function memoryThreshold(offset: string, runId: string, tail: number): number {
182
+ if (offset === '-1') return -1
183
+ if (offset === 'now') return tail
184
+ const decoded = decodeMemoryOffset(offset)
185
+ if (decoded.runId !== runId) {
186
+ throw new Error(
187
+ `Memory stream offset belongs to run ${JSON.stringify(decoded.runId)}, not ${JSON.stringify(runId)}`,
188
+ )
189
+ }
190
+ return decoded.seq
191
+ }
192
+
193
+ interface MemoryEntry {
194
+ seq: number
195
+ offset: string
196
+ chunk: StreamChunk
197
+ }
198
+
199
+ /**
200
+ * One validated action from an `upsert` batch. Building the whole plan before
201
+ * applying any of it is what keeps a rejected `upsert` from partially mutating
202
+ * the log.
203
+ */
204
+ type UpsertStep =
205
+ | { kind: 'replace'; existing: MemoryEntry; chunk: StreamChunk }
206
+ | { kind: 'push'; seq: number; offset: string; chunk: StreamChunk }
207
+
208
+ interface MemoryLog {
209
+ entries: Array<MemoryEntry>
210
+ complete: boolean
211
+ /** Epoch ms when the log was terminalized; undefined while still producing. */
212
+ completedAt: number | undefined
213
+ waiters: Array<() => void>
214
+ }
215
+
216
+ /**
217
+ * Bounds for the in-process log store. `memoryStream` is the dev/single-process
218
+ * backend; without eviction its module-global Map would grow without bound on a
219
+ * long-lived server (one retained chunk buffer per run, forever). Completed logs
220
+ * are swept after a grace window — late resumers/joiners still work briefly —
221
+ * and a hard cap drops the oldest completed logs under pressure. Active
222
+ * (incomplete) logs are never evicted, so an in-flight run is never dropped.
223
+ */
224
+ const MAX_MEMORY_RUNS = 1024
225
+ const COMPLETED_LOG_TTL_MS = 5 * 60_000
226
+
227
+ /**
228
+ * How long a from-start join (`-1` / `now`) waits for a run's first chunk before
229
+ * failing. Bounds the "joined a run that never produces" case so a consumer
230
+ * gets a surfaced error instead of an indefinitely-open, event-less connection.
231
+ *
232
+ * Defaults short: the common from-start join is a reload rejoining a run whose
233
+ * producer ran in a PRIOR request, so an in-flight run's log already holds
234
+ * chunks (it streams immediately, deadline never applies) and an empty log means
235
+ * the run is gone — failing fast lets the client re-enable input near-instantly
236
+ * instead of hanging. Raise `firstChunkDeadlineMs` for backends where a producer
237
+ * legitimately starts well after a joiner attaches (a queued/deferred job).
238
+ */
239
+ const DEFAULT_FIRST_CHUNK_DEADLINE_MS = 100
240
+
241
+ /** Options for the in-process delivery-durability backend. */
242
+ export interface MemoryStreamOptions {
243
+ /**
244
+ * Milliseconds a from-start join waits for the run's first chunk before
245
+ * throwing. Defaults to {@link DEFAULT_FIRST_CHUNK_DEADLINE_MS} (100ms) —
246
+ * raise it if a producer can legitimately start long after a joiner attaches.
247
+ */
248
+ firstChunkDeadlineMs?: number
249
+ }
250
+
251
+ const memoryLogs = new Map<string, MemoryLog>()
252
+
253
+ /**
254
+ * Evict completed logs past their grace window, then, if still over the cap,
255
+ * drop the oldest completed logs (the Map preserves insertion order) until back
256
+ * under the cap. Never touches an incomplete (in-flight) log.
257
+ */
258
+ function sweepMemoryLogs(now: number): void {
259
+ for (const [id, log] of memoryLogs) {
260
+ if (
261
+ log.complete &&
262
+ log.completedAt !== undefined &&
263
+ now - log.completedAt > COMPLETED_LOG_TTL_MS
264
+ ) {
265
+ memoryLogs.delete(id)
266
+ }
267
+ }
268
+ if (memoryLogs.size <= MAX_MEMORY_RUNS) return
269
+ for (const [id, log] of memoryLogs) {
270
+ if (memoryLogs.size <= MAX_MEMORY_RUNS) break
271
+ if (log.complete) memoryLogs.delete(id)
272
+ }
273
+ }
274
+
275
+ function getOrCreateLog(id: string): MemoryLog {
276
+ let log = memoryLogs.get(id)
277
+ if (!log) {
278
+ sweepMemoryLogs(Date.now())
279
+ log = { entries: [], complete: false, completedAt: undefined, waiters: [] }
280
+ memoryLogs.set(id, log)
281
+ }
282
+ return log
283
+ }
284
+
285
+ function markComplete(log: MemoryLog): void {
286
+ if (!log.complete) {
287
+ log.complete = true
288
+ log.completedAt = Date.now()
289
+ }
290
+ }
291
+
292
+ function wakeWaiters(log: MemoryLog): void {
293
+ const waiters = log.waiters
294
+ log.waiters = []
295
+ for (const wake of waiters) wake()
296
+ }
297
+
298
+ /**
299
+ * Explicit construction for {@link memoryStream}, for callers that don't have
300
+ * the incoming `Request` — e.g. a TanStack Start server function implementing
301
+ * a `joinRun` replay for a run id it received as call data:
302
+ *
303
+ * ```ts
304
+ * const durability = memoryStream({ runId })
305
+ * for await (const chunk of replayRunStream(durability)) yield chunk
306
+ * ```
307
+ */
308
+ export interface MemoryStreamInit {
309
+ /** The run this durability adapter attaches to. */
310
+ runId: string
311
+ /**
312
+ * Resume offset captured by the consumer (`resumeFrom()` returns it).
313
+ * Defaults to `null` (a producer / from-start reader).
314
+ */
315
+ offset?: string | null
316
+ }
317
+
318
+ /**
319
+ * The zero-infrastructure delivery-durability backend. Its versioned cursor is
320
+ * deliberately private: callers and core only pass the returned string back.
321
+ *
322
+ * Construct from the incoming `Request` (HTTP transports) or from an explicit
323
+ * {@link MemoryStreamInit} (server functions / direct calls that already know
324
+ * the run id).
325
+ *
326
+ * Logs live in a process-global map, so this backend is for development, tests,
327
+ * and single-process deployments only. Completed runs are evicted after a grace
328
+ * window (see {@link COMPLETED_LOG_TTL_MS}); a resume of an evicted or unknown
329
+ * run fails loudly rather than hanging.
330
+ */
331
+ export function memoryStream(
332
+ source: Request | MemoryStreamInit,
333
+ options: MemoryStreamOptions = {},
334
+ ): UpsertableStreamDurability {
335
+ const resumeOffset =
336
+ source instanceof Request
337
+ ? readResumeOffset(source)
338
+ : (source.offset ?? null)
339
+ const runId =
340
+ source instanceof Request
341
+ ? resolveMemoryRunId(source, resumeOffset)
342
+ : assertValidRunId(source.runId)
343
+ const firstChunkDeadlineMs =
344
+ options.firstChunkDeadlineMs ?? DEFAULT_FIRST_CHUNK_DEADLINE_MS
345
+
346
+ return {
347
+ resumeFrom: () => resumeOffset,
348
+ // `async` so every failure surfaces as a rejected promise rather than a
349
+ // synchronous throw at the call site — `append` is declared to return a
350
+ // Promise, so callers must be able to `.catch()` every failure mode.
351
+ append: async (chunks) => {
352
+ const log = getOrCreateLog(runId)
353
+ const firstSeq = (log.entries.at(-1)?.seq ?? 0) + 1
354
+ const offsets = chunks.map((chunk, index) => {
355
+ const seq = firstSeq + index
356
+ const offset = encodeMemoryOffset(runId, seq)
357
+ log.entries.push({ seq, offset, chunk })
358
+ return offset
359
+ })
360
+ wakeWaiters(log)
361
+ return offsets
362
+ },
363
+ // `async` for the same reason as `append`: every validation failure below
364
+ // must be observable via `.catch()`, never as a synchronous throw.
365
+ upsert: async (entries) => {
366
+ const log = getOrCreateLog(runId)
367
+ const tailSeq = log.entries.at(-1)?.seq ?? 0
368
+
369
+ // Validate the WHOLE batch before touching `log.entries`, so a rejected
370
+ // upsert never partially applies and a caller that catches and retries
371
+ // can be sure no prefix landed.
372
+ const seen = new Set<string>()
373
+ // Tail as it will stand once every push planned so far has been applied,
374
+ // so intra-batch ordering is validated up front too.
375
+ let plannedTailSeq = tailSeq
376
+ // `Array.from` rather than `entries.map`: `map` SKIPS holes in a sparse
377
+ // array, which would leave the plan short and make the apply loop below
378
+ // read `undefined` partway through, after earlier steps had already
379
+ // mutated the log. `Array.from` invokes this callback for every index,
380
+ // so a hole is rejected here, before anything is touched.
381
+ const plan = Array.from(entries, (entry, index): UpsertStep => {
382
+ if (entry === undefined) {
383
+ throw new Error(
384
+ `memoryStream: entries[${index}] is missing; entries must be dense`,
385
+ )
386
+ }
387
+ const { chunk, offset } = entry
388
+ let decoded: MemoryOffset
389
+ try {
390
+ decoded = decodeMemoryOffset(offset)
391
+ } catch (cause) {
392
+ throw new Error(
393
+ `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is not a resumable memory stream offset: ${cause instanceof Error ? cause.message : String(cause)}`,
394
+ )
395
+ }
396
+ if (decoded.runId !== runId) {
397
+ throw new Error(
398
+ `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} belongs to run ${JSON.stringify(decoded.runId)}, not ${JSON.stringify(runId)}`,
399
+ )
400
+ }
401
+ const seq = decoded.seq
402
+ if (seen.has(offset)) {
403
+ throw new Error(
404
+ `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is repeated within the batch; each offset may appear at most once`,
405
+ )
406
+ }
407
+ seen.add(offset)
408
+ const existing = log.entries.find((stored) => stored.offset === offset)
409
+ if (existing) return { kind: 'replace', existing, chunk }
410
+ // A not-yet-stored offset must sit strictly after the current tail.
411
+ // `read()` walks `entries` in array order and filters `seq > threshold`,
412
+ // so a pushed entry has to keep the seqs monotonically increasing;
413
+ // reusing the offset's own decoded seq also keeps a returned offset's
414
+ // threshold exactly consistent with the entry it names. Gaps are fine —
415
+ // nothing depends on seqs being contiguous, only on them increasing —
416
+ // so do NOT "fix" this to renumber densely.
417
+ if (seq <= plannedTailSeq) {
418
+ throw new Error(
419
+ `memoryStream: entries[${index}].offset ${JSON.stringify(offset)} is not stored yet but claims position ${seq}, at or before the tail ${plannedTailSeq}; a new offset must come after every stored and preceding entry`,
420
+ )
421
+ }
422
+ plannedTailSeq = seq
423
+ return { kind: 'push', seq, offset, chunk }
424
+ })
425
+
426
+ // Validation passed for every entry — mutation below cannot fail.
427
+ for (const step of plan) {
428
+ if (step.kind === 'replace') {
429
+ step.existing.chunk = step.chunk
430
+ } else {
431
+ log.entries.push({
432
+ seq: step.seq,
433
+ offset: step.offset,
434
+ chunk: step.chunk,
435
+ })
436
+ }
437
+ }
438
+ wakeWaiters(log)
439
+ return plan.map((step) =>
440
+ step.kind === 'replace' ? step.existing.offset : step.offset,
441
+ )
442
+ },
443
+ snapshot: () => {
444
+ // Peek, never getOrCreateLog: an unknown run must resolve to `[]`, and
445
+ // inserting an empty, never-completed log here would leave a permanent
446
+ // entry the sweep cannot reclaim (it only reclaims complete logs).
447
+ const log = memoryLogs.get(runId)
448
+ if (log === undefined) return Promise.resolve([])
449
+ // Fresh outer array AND fresh pair objects, so a caller that mutates the
450
+ // result cannot reach `log.entries` or the stored entries through it.
451
+ // Never touches `log.waiters` — a snapshot is a point-in-time read and
452
+ // returns even while the log is open and still being appended to.
453
+ return Promise.resolve(
454
+ log.entries.map((entry) => ({
455
+ offset: entry.offset,
456
+ chunk: entry.chunk,
457
+ })),
458
+ )
459
+ },
460
+ close: () => {
461
+ const log = getOrCreateLog(runId)
462
+ markComplete(log)
463
+ wakeWaiters(log)
464
+ return Promise.resolve()
465
+ },
466
+ read: async function* (offset, signal) {
467
+ const isFromStartJoin = offset === '-1' || offset === 'now'
468
+
469
+ // Peek, never getOrCreateLog. A concrete resume offset for an absent run
470
+ // means the run was evicted (or never lived in this process) and will not
471
+ // reappear — fail WITHOUT inserting a log. Inserting here would leave a
472
+ // permanent empty, never-completed log (sweep only reclaims complete
473
+ // ones), so client-supplied offsets could grow the map without bound and
474
+ // defeat the eviction this backend relies on.
475
+ let log = memoryLogs.get(runId)
476
+ if (log === undefined || (log.entries.length === 0 && !log.complete)) {
477
+ if (!isFromStartJoin) {
478
+ throw new Error(
479
+ `Unknown or expired memory stream run: ${JSON.stringify(runId)}`,
480
+ )
481
+ }
482
+ // A from-start join may legitimately attach before the producer creates
483
+ // the log (second-tab race); create it so a later append reuses the
484
+ // same entry. If no producer ever arrives, the first-chunk deadline
485
+ // below deletes this phantom before rejecting.
486
+ log = getOrCreateLog(runId)
487
+ }
488
+
489
+ const threshold = memoryThreshold(
490
+ offset,
491
+ runId,
492
+ log.entries.at(-1)?.seq ?? 0,
493
+ )
494
+ let index = 0
495
+
496
+ for (;;) {
497
+ while (index < log.entries.length) {
498
+ const entry = log.entries[index]
499
+ index += 1
500
+ if (entry && entry.seq > threshold) {
501
+ yield { offset: entry.offset, chunk: entry.chunk }
502
+ }
503
+ }
504
+ // A terminal chunk (RUN_FINISHED / RUN_ERROR) does NOT end the read: an
505
+ // agent-loop run emits one per iteration (finishReason "tool_calls" then
506
+ // "stop"), so stopping on the first would truncate a tool-calling run at
507
+ // its first tool call. The producer signals true completion by calling
508
+ // `close()` (it does so on every exit — see StreamDurability.close), which
509
+ // sets `log.complete`. Read tails until then, or until the caller aborts.
510
+ if (log.complete || signal?.aborted) return
511
+
512
+ // Bound only the wait for the very first chunk: once a run has produced
513
+ // anything, its producer owns termination and a caught-up reader may
514
+ // legitimately park indefinitely between chunks.
515
+ const deadlineForFirstChunk =
516
+ log.entries.length === 0 ? firstChunkDeadlineMs : undefined
517
+
518
+ await new Promise<void>((resolve, reject) => {
519
+ let timer: ReturnType<typeof setTimeout> | undefined
520
+ const cleanup = () => {
521
+ if (timer !== undefined) clearTimeout(timer)
522
+ signal?.removeEventListener('abort', onAbort)
523
+ const waiterIndex = log.waiters.indexOf(wake)
524
+ if (waiterIndex !== -1) log.waiters.splice(waiterIndex, 1)
525
+ }
526
+ const onAbort = () => {
527
+ cleanup()
528
+ resolve()
529
+ }
530
+ const wake = () => {
531
+ cleanup()
532
+ resolve()
533
+ }
534
+ log.waiters.push(wake)
535
+ signal?.addEventListener('abort', onAbort, { once: true })
536
+ if (deadlineForFirstChunk !== undefined) {
537
+ timer = setTimeout(() => {
538
+ cleanup()
539
+ // No producer ever created data for this joined run. Drop the
540
+ // phantom log we created above so it does not linger uncollected
541
+ // (it is empty and will never be marked complete).
542
+ if (
543
+ log.entries.length === 0 &&
544
+ !log.complete &&
545
+ memoryLogs.get(runId) === log
546
+ ) {
547
+ memoryLogs.delete(runId)
548
+ }
549
+ reject(
550
+ new Error(
551
+ `Memory stream run produced no data within ${deadlineForFirstChunk}ms: ${JSON.stringify(runId)}`,
552
+ ),
553
+ )
554
+ }, deadlineForFirstChunk)
555
+ }
556
+ })
557
+ }
558
+ },
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Replay a run's delivery-durability log as a bare stream of chunks, for
564
+ * callers that serve a `joinRun` handler without an HTTP `Response` — e.g. a
565
+ * TanStack Start server function returning an async iterable:
566
+ *
567
+ * ```ts
568
+ * async function* joinImageRun({ data: runId }: { data: string }) {
569
+ * yield* replayRunStream(memoryStream({ runId }))
570
+ * }
571
+ *
572
+ * // Serve it from a server function whose handler is the generator above
573
+ * // (`createServerFn({ method: 'GET' }).inputValidator(...)`).
574
+ * ```
575
+ *
576
+ * NOTE: the example deliberately declares the generator separately instead of
577
+ * inlining it into the server-fn builder chain. TanStack Start's server-fn
578
+ * Vite plugin decides whether a module needs compiling by regex-matching the
579
+ * SOURCE for a dotted `handler(` call, and JSDoc survives into `dist` — an
580
+ * inlined chain here would make every Start app treat this package as a
581
+ * server-fn module and try to resolve its framework's `@tanstack/*-start`
582
+ * package, failing the build wherever that framework is not the one installed.
583
+ *
584
+ * Reads from `offset` (default `'-1'` — from the start) and tails until the
585
+ * producer closes the log or `signal` aborts, exactly like the HTTP
586
+ * `resumeServerSentEventsResponse` path.
587
+ */
588
+ export async function* replayRunStream<TOffset extends string>(
589
+ durability: StreamDurability<TOffset>,
590
+ offset?: TOffset,
591
+ signal?: AbortSignal,
592
+ ): AsyncGenerator<StreamChunk> {
593
+ // '-1' is the from-start replay sentinel every shipped backend honors.
594
+ const from = offset ?? ('-1' as TOffset)
595
+ for await (const { chunk } of durability.read(from, signal)) {
596
+ yield chunk
597
+ }
598
+ }