@ably/ai-transport 0.2.0 → 0.3.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 (166) hide show
  1. package/README.md +10 -19
  2. package/dist/ably-ai-transport.js +1790 -1091
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +2 -2
  7. package/dist/core/agent.d.ts +20 -5
  8. package/dist/core/channel-options.d.ts +57 -0
  9. package/dist/core/codec/codec-event.d.ts +9 -0
  10. package/dist/core/codec/decoder.d.ts +4 -1
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +2 -7
  13. package/dist/core/codec/field-bag.d.ts +85 -0
  14. package/dist/core/codec/fields.d.ts +141 -0
  15. package/dist/core/codec/index.d.ts +8 -1
  16. package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
  17. package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
  18. package/dist/core/codec/input-descriptors.d.ts +281 -0
  19. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  20. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  21. package/dist/core/codec/output-descriptors.d.ts +237 -0
  22. package/dist/core/codec/types.d.ts +95 -36
  23. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  24. package/dist/core/transport/agent-view.d.ts +296 -0
  25. package/dist/core/transport/decode-fold.d.ts +40 -32
  26. package/dist/core/transport/headers.d.ts +30 -1
  27. package/dist/core/transport/index.d.ts +1 -1
  28. package/dist/core/transport/invocation.d.ts +1 -1
  29. package/dist/core/transport/load-history-pages.d.ts +71 -0
  30. package/dist/core/transport/load-history.d.ts +21 -16
  31. package/dist/core/transport/run-manager.d.ts +9 -11
  32. package/dist/core/transport/session-support.d.ts +55 -0
  33. package/dist/core/transport/tree.d.ts +165 -15
  34. package/dist/core/transport/types/agent.d.ts +120 -98
  35. package/dist/core/transport/types/client.d.ts +45 -12
  36. package/dist/core/transport/types/tree.d.ts +52 -10
  37. package/dist/core/transport/types/view.d.ts +55 -28
  38. package/dist/core/transport/view.d.ts +176 -58
  39. package/dist/core/transport/wire-log.d.ts +102 -0
  40. package/dist/errors.d.ts +10 -4
  41. package/dist/index.d.ts +6 -5
  42. package/dist/react/ably-ai-transport-react.js +784 -415
  43. package/dist/react/ably-ai-transport-react.js.map +1 -1
  44. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  45. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  46. package/dist/react/contexts/client-session-context.d.ts +2 -1
  47. package/dist/react/contexts/client-session-provider.d.ts +3 -0
  48. package/dist/react/index.d.ts +2 -1
  49. package/dist/react/internal/skipped-session.d.ts +8 -0
  50. package/dist/react/use-view.d.ts +3 -3
  51. package/dist/utils.d.ts +22 -54
  52. package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
  53. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  55. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  56. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  57. package/dist/vercel/codec/events.d.ts +1 -2
  58. package/dist/vercel/codec/fields.d.ts +44 -0
  59. package/dist/vercel/codec/fold-content.d.ts +16 -0
  60. package/dist/vercel/codec/fold-data.d.ts +16 -0
  61. package/dist/vercel/codec/fold-input.d.ts +67 -0
  62. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  63. package/dist/vercel/codec/fold-text.d.ts +16 -0
  64. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  65. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  66. package/dist/vercel/codec/index.d.ts +5 -30
  67. package/dist/vercel/codec/inputs.d.ts +11 -0
  68. package/dist/vercel/codec/outputs.d.ts +11 -0
  69. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  70. package/dist/vercel/codec/reducer.d.ts +20 -102
  71. package/dist/vercel/codec/tool-transitions.d.ts +0 -6
  72. package/dist/vercel/codec/wire-data.d.ts +34 -0
  73. package/dist/vercel/index.d.ts +1 -0
  74. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
  75. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  76. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
  77. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  78. package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
  79. package/dist/vercel/run-end-reason.d.ts +66 -11
  80. package/dist/vercel/tool-part.d.ts +21 -0
  81. package/dist/vercel/transport/chat-transport.d.ts +0 -2
  82. package/dist/vercel/transport/index.d.ts +1 -1
  83. package/dist/vercel/transport/run-output-stream.d.ts +6 -8
  84. package/dist/version.d.ts +1 -1
  85. package/package.json +2 -2
  86. package/src/constants.ts +2 -2
  87. package/src/core/agent.ts +43 -19
  88. package/src/core/channel-options.ts +89 -0
  89. package/src/core/codec/codec-event.ts +27 -0
  90. package/src/core/codec/decoder.ts +145 -21
  91. package/src/core/codec/define-codec.ts +432 -0
  92. package/src/core/codec/encoder.ts +13 -54
  93. package/src/core/codec/field-bag.ts +142 -0
  94. package/src/core/codec/fields.ts +193 -0
  95. package/src/core/codec/index.ts +43 -0
  96. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  97. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  98. package/src/core/codec/input-descriptors.ts +373 -0
  99. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  100. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  101. package/src/core/codec/output-descriptors.ts +307 -0
  102. package/src/core/codec/types.ts +99 -36
  103. package/src/core/codec/well-known-inputs.ts +96 -0
  104. package/src/core/transport/agent-session.ts +330 -589
  105. package/src/core/transport/agent-view.ts +738 -0
  106. package/src/core/transport/client-session.ts +74 -69
  107. package/src/core/transport/decode-fold.ts +57 -47
  108. package/src/core/transport/headers.ts +57 -4
  109. package/src/core/transport/index.ts +2 -1
  110. package/src/core/transport/invocation.ts +1 -1
  111. package/src/core/transport/load-history-pages.ts +220 -0
  112. package/src/core/transport/load-history.ts +63 -61
  113. package/src/core/transport/pipe-stream.ts +10 -1
  114. package/src/core/transport/run-manager.ts +25 -31
  115. package/src/core/transport/session-support.ts +96 -0
  116. package/src/core/transport/tree.ts +414 -47
  117. package/src/core/transport/types/agent.ts +129 -102
  118. package/src/core/transport/types/client.ts +49 -13
  119. package/src/core/transport/types/tree.ts +61 -12
  120. package/src/core/transport/types/view.ts +57 -28
  121. package/src/core/transport/view.ts +520 -172
  122. package/src/core/transport/wire-log.ts +189 -0
  123. package/src/errors.ts +10 -3
  124. package/src/index.ts +44 -11
  125. package/src/react/contexts/client-session-context.ts +1 -1
  126. package/src/react/contexts/client-session-provider.tsx +38 -2
  127. package/src/react/index.ts +2 -1
  128. package/src/react/internal/skipped-session.ts +62 -0
  129. package/src/react/use-client-session.ts +7 -30
  130. package/src/react/use-view.ts +3 -3
  131. package/src/utils.ts +31 -97
  132. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  133. package/src/vercel/codec/events.ts +1 -3
  134. package/src/vercel/codec/fields.ts +58 -0
  135. package/src/vercel/codec/fold-content.ts +54 -0
  136. package/src/vercel/codec/fold-data.ts +46 -0
  137. package/src/vercel/codec/fold-input.ts +255 -0
  138. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  139. package/src/vercel/codec/fold-text.ts +55 -0
  140. package/src/vercel/codec/fold-tool-input.ts +86 -0
  141. package/src/vercel/codec/fold-tool-output.ts +79 -0
  142. package/src/vercel/codec/index.ts +23 -63
  143. package/src/vercel/codec/inputs.ts +116 -0
  144. package/src/vercel/codec/outputs.ts +207 -0
  145. package/src/vercel/codec/reducer-state.ts +169 -0
  146. package/src/vercel/codec/reducer.ts +52 -838
  147. package/src/vercel/codec/tool-transitions.ts +1 -12
  148. package/src/vercel/codec/wire-data.ts +64 -0
  149. package/src/vercel/index.ts +1 -0
  150. package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
  151. package/src/vercel/react/use-chat-transport.ts +8 -28
  152. package/src/vercel/react/use-message-sync.ts +5 -10
  153. package/src/vercel/run-end-reason.ts +95 -16
  154. package/src/vercel/tool-part.ts +25 -0
  155. package/src/vercel/transport/chat-transport.ts +10 -22
  156. package/src/vercel/transport/index.ts +1 -1
  157. package/src/vercel/transport/run-output-stream.ts +7 -8
  158. package/src/version.ts +1 -1
  159. package/dist/core/transport/branch-chain.d.ts +0 -43
  160. package/dist/core/transport/load-conversation.d.ts +0 -128
  161. package/dist/vercel/codec/decoder.d.ts +0 -9
  162. package/dist/vercel/codec/encoder.d.ts +0 -11
  163. package/src/core/transport/branch-chain.ts +0 -58
  164. package/src/core/transport/load-conversation.ts +0 -355
  165. package/src/vercel/codec/decoder.ts +0 -696
  166. package/src/vercel/codec/encoder.ts +0 -548
@@ -11,11 +11,12 @@
11
11
  * with no instance state. Mutation in place is allowed — the projection
12
12
  * is single-owner.
13
13
  *
14
- * Idempotency is **per conflict key**, not stream-wide: when two events
15
- * compete for the same logical state (e.g. two `tool-output-available`
16
- * for the same `toolCallId`), the higher-serial one wins and the other
17
- * is dropped. Unrelated events arrive freely in any order. See
18
- * `_conflictKeyOf` for the per-variant key derivation.
14
+ * The reducer does not dedup or reorder. The transport sequences events
15
+ * canonically ascending by wire serial across messages, in decode order
16
+ * within a wire and delivers each exactly once, so the reducer folds
17
+ * unconditionally. Last-writer-wins for events competing over the same
18
+ * logical state (e.g. two `tool-output-available` for one `toolCallId`)
19
+ * falls out of fold order: the highest-serial event folds last.
19
20
  *
20
21
  * Client-published tool resolutions (`ToolResult`, `ToolResultError`,
21
22
  * `ToolApprovalResponse`) carry `codecMessageId` targeting the assistant
@@ -23,129 +24,31 @@
23
24
  * `dynamic-tool` part directly. If the assistant has not yet arrived in
24
25
  * the projection (out-of-order delivery), the resolution is buffered in
25
26
  * `pendingToolResolutions` and re-evaluated on each subsequent fold.
26
- */
27
-
28
- import type * as AI from 'ai';
29
-
30
- import type {
31
- CodecMessage,
32
- ReducerMeta,
33
- ToolApprovalResponse,
34
- ToolResult,
35
- ToolResultError,
36
- } from '../../core/codec/types.js';
37
- import { stripUndefined } from '../../utils.js';
38
- import type {
39
- VercelInput,
40
- VercelOutput,
41
- VercelToolApprovalResponsePayload,
42
- VercelToolResultErrorPayload,
43
- VercelToolResultPayload,
44
- } from './events.js';
45
- import { toolBase, transitionToolPart } from './tool-transitions.js';
46
-
47
- // ---------------------------------------------------------------------------
48
- // Internal tracker state
49
- // ---------------------------------------------------------------------------
50
-
51
- /**
52
- * Tracks an in-progress tool part within a UIMessage. Text and reasoning
53
- * parts don't need this — we write to them directly via partIndex. Tool
54
- * parts need an extra `inputText` buffer because deltas arrive as raw
55
- * JSON fragments that must be accumulated before parsing.
56
- */
57
- interface ToolPartTracker {
58
- /** Index in the message's parts array. */
59
- partIndex: number;
60
- /** Accumulated streaming input text (for JSON parsing on completion). */
61
- inputText: string;
62
- }
63
-
64
- /** Per-codecMessageId tracking state for in-progress streams within a UIMessage. */
65
- interface MessageTrackers {
66
- /** Text stream id → partIndex. */
67
- text: Map<string, number>;
68
- /** Reasoning stream id → partIndex. */
69
- reasoning: Map<string, number>;
70
- /** Tool call id → tracker. */
71
- tools: Map<string, ToolPartTracker>;
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // Projection
76
- // ---------------------------------------------------------------------------
77
-
78
- /**
79
- * The per-Run state produced by the Vercel codec's reducer.
80
27
  *
81
- * The SDK reads only `messages` (via `Codec.getMessages`). The remaining
82
- * fields are internal to the reducer; they happen to live on the
83
- * projection because the projection is the only thing the reducer can
84
- * carry from fold to fold (it has no instance state).
28
+ * This file is the reducer's public facade and dispatch: `init`,
29
+ * `getMessages`, `fold`, and the output-chunk router. The per-concern fold
30
+ * logic lives in the sibling `fold-*` modules over a shared `reducer-state`
31
+ * base; the import graph is an acyclic DAG rooted here.
85
32
  */
86
- export interface VercelProjection {
87
- /**
88
- * UIMessages produced or modified in this Run, in publication order,
89
- * each paired with its codec-message-id. The reducer correlates strictly
90
- * on `codecMessageId`; `message.id` is preserved verbatim from the source
91
- * (the AI SDK stream's `start.messageId` for assistants, the caller's id
92
- * for user messages) and is never used as an identity key.
93
- */
94
- messages: CodecMessage<AI.UIMessage>[];
95
- /**
96
- * Per-conflict-key high-water-marks. Maps a codec-derived conflict key
97
- * (see `_conflictKeyOf`) to the highest `meta.serial` already folded for
98
- * that key. Events whose serial is `<=` the stored value are dropped as
99
- * duplicates of an already-incorporated operation. Events that have no
100
- * conflict key (additive content, lifecycle markers) are folded
101
- * unconditionally.
102
- */
103
- conflictSerials: Map<string, string>;
104
- /** Per-codecMessageId tracker state for streamed parts. Internal — do not access. */
105
- trackers: Map<string, MessageTrackers>;
106
- /**
107
- * Tool-resolution events that arrived before any assistant in this
108
- * projection had a matching `toolCallId`. Re-evaluated on every
109
- * subsequent fold so that an out-of-order tool output is folded as
110
- * soon as the corresponding assistant lands.
111
- */
112
- pendingToolResolutions: PendingToolResolution[];
113
- }
114
33
 
115
- /**
116
- * A buffered tool resolution waiting for its assistant message to arrive.
117
- * The reducer scans pending entries after every successful fold so an
118
- * out-of-order tool output is promoted as soon as the matching assistant
119
- * is added to the projection.
120
- */
121
- interface PendingToolResolution {
122
- /** The codec-message-id of the assistant the resolution targets. */
123
- targetCodecMessageId: string;
124
- /** Tool call this resolution targets. */
125
- toolCallId: string;
126
- /** Serial of the wire message — used by the conflict-key check on promotion. */
127
- serial: string;
128
- /** Variant of the tool-resolution used to transition the assistant's tool part. */
129
- resolution:
130
- | { kind: 'tool-result'; output: unknown }
131
- | { kind: 'tool-result-error'; message: string }
132
- | { kind: 'tool-approval-response'; approved: boolean; reason?: string };
133
- }
34
+ import type * as AI from 'ai';
134
35
 
135
- // ---------------------------------------------------------------------------
136
- // init
137
- // ---------------------------------------------------------------------------
138
-
139
- /**
140
- * Build an empty initial projection.
141
- * @returns A fresh VercelProjection with no messages and no tracker state.
142
- */
143
- export const init = (): VercelProjection => ({
144
- messages: [],
145
- conflictSerials: new Map(),
146
- trackers: new Map(),
147
- pendingToolResolutions: [],
148
- });
36
+ import type { CodecEvent, CodecMessage, ReducerMeta } from '../../core/codec/index.js';
37
+ import type { VercelInput, VercelOutput } from './events.js';
38
+ import { foldContentPart } from './fold-content.js';
39
+ import { foldDataPart } from './fold-data.js';
40
+ import {
41
+ foldClientToolResult,
42
+ foldClientToolResultError,
43
+ foldToolApprovalResponse,
44
+ foldUserMessage,
45
+ retryPendingResolutions,
46
+ } from './fold-input.js';
47
+ import { foldLifecycle } from './fold-lifecycle.js';
48
+ import { foldTextOrReasoning } from './fold-text.js';
49
+ import { foldToolInput } from './fold-tool-input.js';
50
+ import { foldToolOutput } from './fold-tool-output.js';
51
+ import type { VercelProjection } from './reducer-state.js';
149
52
 
150
53
  // ---------------------------------------------------------------------------
151
54
  // fold
@@ -155,12 +58,11 @@ export const init = (): VercelProjection => ({
155
58
  * Fold one input or output event into the projection. Mutates and returns
156
59
  * `state`.
157
60
  *
158
- * Idempotency is per conflict key (see `_conflictKeyOf`): if the event has
159
- * a conflict key and the projection has already folded an event for that
160
- * key at a higher-or-equal serial, this call is a no-op. Events without a
161
- * conflict key (additive content, lifecycle markers) are folded
162
- * unconditionally. Orphan events (e.g. tool-output for an unknown
163
- * toolCallId) are dropped silently inside the per-variant fold helpers.
61
+ * The transport invokes `fold` exactly once per event, in canonical order,
62
+ * so the reducer folds unconditionally no dedup or high-water-mark here.
63
+ * Competing events resolve by order (the highest-serial event folds last
64
+ * and wins). Orphan events (e.g. tool-output for an unknown toolCallId) are
65
+ * dropped silently inside the per-variant fold helpers.
164
66
  * @param state - Projection to fold into (may be mutated in place).
165
67
  * @param event - Input or output event to fold.
166
68
  * @param meta - Transport-derived metadata (serial, optional messageId).
@@ -168,24 +70,14 @@ export const init = (): VercelProjection => ({
168
70
  */
169
71
  export const fold = (
170
72
  state: VercelProjection,
171
- event: VercelInput | VercelOutput,
73
+ event: CodecEvent<VercelInput, VercelOutput>,
172
74
  meta: ReducerMeta,
173
75
  ): VercelProjection => {
174
- if (meta.serial) {
175
- const key = _conflictKeyOf(event, meta);
176
- if (key !== undefined) {
177
- const seen = state.conflictSerials.get(key);
178
- if (seen !== undefined && meta.serial <= seen) {
179
- return state;
180
- }
181
- state.conflictSerials.set(key, meta.serial);
182
- }
183
- }
184
-
185
- if (_isInput(event)) {
186
- switch (event.kind) {
76
+ if (event.direction === 'input') {
77
+ const input = event.event;
78
+ switch (input.kind) {
187
79
  case 'user-message': {
188
- _foldUserMessage(state, event.message, meta);
80
+ foldUserMessage(state, input.message, meta);
189
81
  break;
190
82
  }
191
83
  case 'regenerate': {
@@ -195,377 +87,37 @@ export const fold = (
195
87
  break;
196
88
  }
197
89
  case 'tool-result': {
198
- _foldClientToolResult(state, event, meta);
90
+ foldClientToolResult(state, input);
199
91
  break;
200
92
  }
201
93
  case 'tool-result-error': {
202
- _foldClientToolResultError(state, event, meta);
94
+ foldClientToolResultError(state, input);
203
95
  break;
204
96
  }
205
97
  case 'tool-approval-response': {
206
- _foldToolApprovalResponse(state, event, meta);
98
+ foldToolApprovalResponse(state, input);
207
99
  break;
208
100
  }
209
101
  }
210
102
  } else {
211
- _foldChunk(state, event, meta);
103
+ foldChunk(state, event.event, meta);
212
104
  }
213
105
 
214
106
  // Re-evaluate pending tool resolutions in case the just-folded event
215
107
  // produced the assistant they were waiting on. Cheap when the list is
216
108
  // empty (the common case).
217
109
  if (state.pendingToolResolutions.length > 0) {
218
- _retryPendingResolutions(state);
110
+ retryPendingResolutions(state);
219
111
  }
220
112
 
221
113
  return state;
222
114
  };
223
115
 
224
- /**
225
- * Narrow the union to TInput vs TOutput by the discriminator field name.
226
- * VercelInput variants carry `kind`; VercelOutput variants carry `type`.
227
- * @param event - The event to narrow.
228
- * @returns True when the event is a VercelInput, false for VercelOutput.
229
- */
230
- const _isInput = (event: VercelInput | VercelOutput): event is VercelInput => 'kind' in event;
231
-
232
- // ---------------------------------------------------------------------------
233
- // Conflict-key derivation
234
- // ---------------------------------------------------------------------------
235
-
236
- /**
237
- * Derive a per-event conflict key, or `undefined` if the event doesn't
238
- * compete with any other event for shared state. Used by `fold` to scope
239
- * the high-water-mark check to genuine conflicts (e.g. two
240
- * `tool-output-available` for the same `toolCallId`) rather than to every
241
- * event in the stream.
242
- * @param event - The event being folded.
243
- * @param meta - Transport-derived metadata (used for events keyed by codec-message-id).
244
- * @returns The conflict key, or `undefined` if the event is additive / independent.
245
- */
246
- const _conflictKeyOf = (event: VercelInput | VercelOutput, meta: ReducerMeta): string | undefined => {
247
- if (_isInput(event)) {
248
- switch (event.kind) {
249
- case 'user-message': {
250
- // Dedup re-publishes of the same user message by its wire
251
- // codec-message-id, never by the domain `message.id`. Without a
252
- // codec-message-id there is nothing to correlate on, so the fold
253
- // is left unconditional.
254
- return meta.messageId === undefined ? undefined : `user-msg:${meta.messageId}`;
255
- }
256
- case 'tool-approval-response': {
257
- return `tool-approval:${event.payload.toolCallId}`;
258
- }
259
- // Client tool results compete for the same final state of the tool
260
- // call (against agent-side `tool-output-available`/`tool-output-error`
261
- // chunks and against `tool-output-denied`/`tool-approval-request`).
262
- // Highest serial wins. Shares the `tool-output:` namespace with the
263
- // agent-side chunks below.
264
- case 'tool-result':
265
- case 'tool-result-error': {
266
- return `tool-output:${event.payload.toolCallId}`;
267
- }
268
- case 'regenerate': {
269
- return undefined;
270
- }
271
- }
272
- }
273
-
274
- switch (event.type) {
275
- // Tool-input state machine, keyed by toolCallId.
276
- case 'tool-input-start':
277
- case 'tool-input-available':
278
- case 'tool-input-error': {
279
- return `${event.type}:${event.toolCallId}`;
280
- }
281
-
282
- // All "tool-output-ish" output variants compete for the same final
283
- // state of the tool call. Shares the `tool-output:` namespace with
284
- // the client-published input variants above.
285
- case 'tool-output-available':
286
- case 'tool-output-error':
287
- case 'tool-output-denied':
288
- case 'tool-approval-request': {
289
- return `tool-output:${event.toolCallId}`;
290
- }
291
-
292
- // Per-stream start/end markers: duplicates would create phantom parts
293
- // or wipe accumulated text. Keyed by (codec-message-id, stream-id).
294
- case 'text-start':
295
- case 'text-end':
296
- case 'reasoning-start':
297
- case 'reasoning-end': {
298
- return `${event.type}:${meta.messageId ?? ''}:${event.id}`;
299
- }
300
-
301
- // Message-level markers, keyed by codec-message-id.
302
- case 'finish':
303
- case 'message-metadata': {
304
- return `${event.type}:${meta.messageId ?? ''}`;
305
- }
306
-
307
- // Purely additive or independent — never dedup:
308
- // text-delta / reasoning-delta / tool-input-delta (additive content)
309
- // start / start-step / finish-step / abort / error (lifecycle)
310
- // file / source-url / source-document (independent attachments)
311
- // data-* (opaque to the reducer)
312
- default: {
313
- return undefined;
314
- }
315
- }
316
- };
317
-
318
116
  // ---------------------------------------------------------------------------
319
- // Input folds
117
+ // UIMessageChunk dispatch
320
118
  // ---------------------------------------------------------------------------
321
119
 
322
- const _foldUserMessage = (state: VercelProjection, message: AI.UIMessage, meta: ReducerMeta): VercelProjection => {
323
- // Correlate the projection entry on the wire codec-message-id; the
324
- // caller-supplied `message.id` is preserved verbatim and surfaced to the
325
- // application unchanged. Without a codec-message-id the message has no
326
- // identity to key on, so it is appended as a fresh entry.
327
- const codecMessageId = meta.messageId;
328
- if (codecMessageId === undefined) {
329
- state.messages.push({ codecMessageId: message.id, message });
330
- return state;
331
- }
332
- const existingIdx = state.messages.findIndex((e) => e.codecMessageId === codecMessageId);
333
- if (existingIdx === -1) {
334
- state.messages.push({ codecMessageId, message });
335
- } else {
336
- state.messages[existingIdx] = { codecMessageId, message };
337
- }
338
- return state;
339
- };
340
-
341
- /**
342
- * Fold a client-published `ToolResult`. The input carries
343
- * `codecMessageId` pointing at the assistant whose `dynamic-tool` part
344
- * holds the matching `toolCallId`. If the assistant and its matching
345
- * `dynamic-tool` part are both present, fold directly; otherwise pend
346
- * until that tool part arrives.
347
- * @param state - Projection to fold into.
348
- * @param event - The tool-result input (codecMessageId + domain payload).
349
- * @param meta - Transport-derived metadata.
350
- * @returns The same projection reference.
351
- */
352
- const _foldClientToolResult = (
353
- state: VercelProjection,
354
- event: ToolResult<VercelToolResultPayload>,
355
- meta: ReducerMeta,
356
- ): VercelProjection => {
357
- const { toolCallId, output } = event.payload;
358
- const owner = _findOwner(state, event.codecMessageId, toolCallId);
359
- if (owner) {
360
- owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
361
- type: 'tool-output-available',
362
- toolCallId,
363
- output,
364
- });
365
- return state;
366
- }
367
-
368
- state.pendingToolResolutions.push({
369
- targetCodecMessageId: event.codecMessageId,
370
- toolCallId,
371
- serial: meta.serial,
372
- resolution: { kind: 'tool-result', output },
373
- });
374
- return state;
375
- };
376
-
377
- /**
378
- * Fold a client-published `ToolResultError`. Mirrors
379
- * {@link _foldClientToolResult} but with the error transition.
380
- * @param state - Projection to fold into.
381
- * @param event - The tool-result-error input (codecMessageId + domain payload).
382
- * @param meta - Transport-derived metadata.
383
- * @returns The same projection reference.
384
- */
385
- const _foldClientToolResultError = (
386
- state: VercelProjection,
387
- event: ToolResultError<VercelToolResultErrorPayload>,
388
- meta: ReducerMeta,
389
- ): VercelProjection => {
390
- const { toolCallId, message } = event.payload;
391
- const owner = _findOwner(state, event.codecMessageId, toolCallId);
392
- if (owner) {
393
- owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
394
- type: 'tool-output-error',
395
- toolCallId,
396
- errorText: message,
397
- });
398
- return state;
399
- }
400
-
401
- state.pendingToolResolutions.push({
402
- targetCodecMessageId: event.codecMessageId,
403
- toolCallId,
404
- serial: meta.serial,
405
- resolution: { kind: 'tool-result-error', message },
406
- });
407
- return state;
408
- };
409
-
410
- /**
411
- * Fold a client-published `ToolApprovalResponse`. The input carries
412
- * `codecMessageId` pointing at the assistant whose `dynamic-tool` part
413
- * holds the matching `toolCallId`. Approval → `approval-responded`;
414
- * denial → `output-denied` via {@link transitionToolPart}.
415
- * @param state - Projection to fold into.
416
- * @param event - The approval-response input.
417
- * @param meta - Transport-derived metadata.
418
- * @returns The same projection reference.
419
- */
420
- const _foldToolApprovalResponse = (
421
- state: VercelProjection,
422
- event: ToolApprovalResponse<VercelToolApprovalResponsePayload>,
423
- meta: ReducerMeta,
424
- ): VercelProjection => {
425
- const { toolCallId, approved, reason } = event.payload;
426
- const owner = _findOwner(state, event.codecMessageId, toolCallId);
427
- if (owner) {
428
- owner.message.parts[owner.tracker.partIndex] = _approvalTransition(owner.part, approved, reason);
429
- return state;
430
- }
431
-
432
- state.pendingToolResolutions.push({
433
- targetCodecMessageId: event.codecMessageId,
434
- toolCallId,
435
- serial: meta.serial,
436
- resolution: {
437
- kind: 'tool-approval-response',
438
- approved,
439
- ...(reason === undefined ? {} : { reason }),
440
- },
441
- });
442
- return state;
443
- };
444
-
445
- interface OwnerLookup {
446
- message: AI.UIMessage;
447
- tracker: ToolPartTracker;
448
- part: AI.DynamicToolUIPart;
449
- }
450
-
451
- const _findOwner = (state: VercelProjection, codecMessageId: string, toolCallId: string): OwnerLookup | undefined => {
452
- const entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
453
- if (!entry) return undefined;
454
- const trackers = _ensureTrackers(state, codecMessageId);
455
- const found = _getToolPart(entry.message, trackers, toolCallId);
456
- if (!found) return undefined;
457
- return { message: entry.message, tracker: found.tracker, part: found.part };
458
- };
459
-
460
- /**
461
- * Locate the `dynamic-tool` part for a `toolCallId` anywhere in the projection.
462
- * Agent-emitted second-pass tool outputs (after an approved tool runs) are
463
- * stamped with a fresh codec-message-id that differs from the assistant holding
464
- * the tool call, so they can't be found via `meta.messageId` — they fold onto
465
- * whichever message holds the matching tool call (created in the first pass or
466
- * by an approval response).
467
- * @param state - Projection to scan.
468
- * @param toolCallId - The tool call to locate.
469
- * @returns The owning message, tracker, and part, or `undefined` if absent.
470
- */
471
- const _findToolPartOwner = (state: VercelProjection, toolCallId: string): OwnerLookup | undefined => {
472
- for (const entry of state.messages) {
473
- const trackers = state.trackers.get(entry.codecMessageId);
474
- if (!trackers) continue;
475
- const found = _getToolPart(entry.message, trackers, toolCallId);
476
- if (found) return { message: entry.message, tracker: found.tracker, part: found.part };
477
- }
478
- return undefined;
479
- };
480
-
481
- /**
482
- * Build the next `dynamic-tool` part shape for an approval response.
483
- *
484
- * For `approved=true`, transition to `approval-responded` so the AI SDK's
485
- * multi-step loop will auto-run the tool on the next step.
486
- * `transitionToolPart` has no shape for this transition, so we synthesize
487
- * the part directly.
488
- *
489
- * For `approved=false`, delegate to `transitionToolPart` with a synthetic
490
- * `tool-output-denied` chunk so denial mirrors the chunk-driven path.
491
- * @param part - The existing `dynamic-tool` part being transitioned.
492
- * @param approved - Whether the user approved the tool execution.
493
- * @param reason - Optional human-readable reason.
494
- * @returns The replacement `dynamic-tool` part.
495
- */
496
- const _approvalTransition = (
497
- part: AI.DynamicToolUIPart,
498
- approved: boolean,
499
- reason: string | undefined,
500
- ): AI.DynamicToolUIPart => {
501
- if (approved) {
502
- return {
503
- ...toolBase(part),
504
- state: 'approval-responded',
505
- input: 'input' in part ? part.input : undefined,
506
- approval: {
507
- id: 'approval' in part && part.approval ? part.approval.id : '',
508
- approved: true,
509
- ...(reason === undefined ? {} : { reason }),
510
- },
511
- };
512
- }
513
- return transitionToolPart(part, {
514
- type: 'tool-output-denied',
515
- toolCallId: part.toolCallId,
516
- ...(reason === undefined ? {} : { reason }),
517
- });
518
- };
519
-
520
- /**
521
- * Re-attempt every pending tool resolution against the current projection.
522
- * Successfully promoted entries are removed from the pending list. Cheap:
523
- * bounded by the number of pending entries.
524
- * @param state - Projection to walk and mutate.
525
- */
526
- const _retryPendingResolutions = (state: VercelProjection): void => {
527
- const next: PendingToolResolution[] = [];
528
- for (const pending of state.pendingToolResolutions) {
529
- const owner = _findOwner(state, pending.targetCodecMessageId, pending.toolCallId);
530
- if (!owner) {
531
- next.push(pending);
532
- continue;
533
- }
534
- switch (pending.resolution.kind) {
535
- case 'tool-result': {
536
- owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
537
- type: 'tool-output-available',
538
- toolCallId: pending.toolCallId,
539
- output: pending.resolution.output,
540
- });
541
- break;
542
- }
543
- case 'tool-result-error': {
544
- owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, {
545
- type: 'tool-output-error',
546
- toolCallId: pending.toolCallId,
547
- errorText: pending.resolution.message,
548
- });
549
- break;
550
- }
551
- case 'tool-approval-response': {
552
- owner.message.parts[owner.tracker.partIndex] = _approvalTransition(
553
- owner.part,
554
- pending.resolution.approved,
555
- pending.resolution.reason,
556
- );
557
- break;
558
- }
559
- }
560
- }
561
- state.pendingToolResolutions = next;
562
- };
563
-
564
- // ---------------------------------------------------------------------------
565
- // UIMessageChunk fold
566
- // ---------------------------------------------------------------------------
567
-
568
- const _foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerMeta): VercelProjection => {
120
+ const foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerMeta): VercelProjection => {
569
121
  const messageId = meta.messageId;
570
122
  if (messageId === undefined) {
571
123
  // Without a target codec-message-id, a chunk has nowhere to land. Drop.
@@ -580,7 +132,7 @@ const _foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerM
580
132
  case 'abort':
581
133
  case 'error':
582
134
  case 'message-metadata': {
583
- return _foldLifecycle(state, chunk, messageId);
135
+ return foldLifecycle(state, chunk, messageId);
584
136
  }
585
137
 
586
138
  case 'text-start':
@@ -589,378 +141,38 @@ const _foldChunk = (state: VercelProjection, chunk: VercelOutput, meta: ReducerM
589
141
  case 'reasoning-start':
590
142
  case 'reasoning-delta':
591
143
  case 'reasoning-end': {
592
- return _foldTextOrReasoning(state, chunk, messageId);
144
+ return foldTextOrReasoning(state, chunk, messageId);
593
145
  }
594
146
 
595
147
  case 'tool-input-start':
596
148
  case 'tool-input-delta':
597
149
  case 'tool-input-available':
598
150
  case 'tool-input-error': {
599
- return _foldToolInput(state, chunk, messageId);
151
+ return foldToolInput(state, chunk, messageId);
600
152
  }
601
153
 
602
154
  case 'tool-output-available':
603
155
  case 'tool-output-error':
604
156
  case 'tool-output-denied':
605
157
  case 'tool-approval-request': {
606
- return _foldToolOutput(state, chunk, messageId);
158
+ return foldToolOutput(state, chunk, messageId);
607
159
  }
608
160
 
609
161
  case 'file':
610
162
  case 'source-url':
611
163
  case 'source-document': {
612
- return _foldContentPart(state, chunk, messageId);
164
+ return foldContentPart(state, chunk, messageId);
613
165
  }
614
166
 
615
167
  default: {
616
168
  if (chunk.type.startsWith('data-')) {
617
- return _foldDataPart(state, chunk, messageId);
169
+ return foldDataPart(state, chunk, messageId);
618
170
  }
619
171
  return state;
620
172
  }
621
173
  }
622
174
  };
623
175
 
624
- // ---------------------------------------------------------------------------
625
- // Message + tracker helpers
626
- // ---------------------------------------------------------------------------
627
-
628
- const _ensureMessage = (state: VercelProjection, codecMessageId: string): AI.UIMessage => {
629
- let entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
630
- if (!entry) {
631
- // No source id seen yet — seed the domain `message.id` with the
632
- // codec-message-id as a fallback. The `start` chunk overwrites it with
633
- // the stream's `messageId` when the stream provides one.
634
- entry = { codecMessageId, message: { id: codecMessageId, role: 'assistant', parts: [] } };
635
- state.messages.push(entry);
636
- }
637
- return entry.message;
638
- };
639
-
640
- const _ensureTrackers = (state: VercelProjection, messageId: string): MessageTrackers => {
641
- let trackers = state.trackers.get(messageId);
642
- if (!trackers) {
643
- trackers = { text: new Map(), reasoning: new Map(), tools: new Map() };
644
- state.trackers.set(messageId, trackers);
645
- }
646
- return trackers;
647
- };
648
-
649
- const _getToolPart = (
650
- message: AI.UIMessage,
651
- trackers: MessageTrackers,
652
- toolCallId: string,
653
- ): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined => {
654
- const tracker = trackers.tools.get(toolCallId);
655
- if (!tracker) return undefined;
656
- const part = message.parts[tracker.partIndex];
657
- if (part?.type !== 'dynamic-tool') return undefined;
658
- return { tracker, part };
659
- };
660
-
661
- // ---------------------------------------------------------------------------
662
- // Lifecycle events
663
- // ---------------------------------------------------------------------------
664
-
665
- const _foldLifecycle = (
666
- state: VercelProjection,
667
- chunk: Extract<
668
- AI.UIMessageChunk,
669
- { type: 'start' | 'start-step' | 'finish-step' | 'finish' | 'abort' | 'error' | 'message-metadata' }
670
- >,
671
- messageId: string,
672
- ): VercelProjection => {
673
- switch (chunk.type) {
674
- case 'start': {
675
- // The projection entry is keyed on the wire codec-message-id
676
- // (`messageId`); every subsequent chunk for this message correlates on
677
- // that, independent of `message.id`. So we faithfully reproduce the
678
- // stream's own `messageId` on the reconstructed `UIMessage.id` (the
679
- // value surfaced to the application) without risk of orphaning later
680
- // chunks. When the stream omits it, the codec-message-id seeded by
681
- // `_ensureMessage` stands as the fallback id.
682
- const message = _ensureMessage(state, messageId);
683
- if (chunk.messageId !== undefined) message.id = chunk.messageId;
684
- if (chunk.messageMetadata !== undefined) message.metadata = chunk.messageMetadata;
685
- return state;
686
- }
687
- case 'start-step': {
688
- const message = _ensureMessage(state, messageId);
689
- message.parts.push({ type: 'step-start' });
690
- return state;
691
- }
692
- case 'finish-step': {
693
- // Reset text/reasoning stream trackers so a follow-up step can start
694
- // new parts with potentially-reused stream ids.
695
- const trackers = state.trackers.get(messageId);
696
- if (trackers) {
697
- trackers.text.clear();
698
- trackers.reasoning.clear();
699
- }
700
- return state;
701
- }
702
- case 'finish': {
703
- const message = state.messages.find((e) => e.codecMessageId === messageId)?.message;
704
- if (message && chunk.messageMetadata !== undefined) {
705
- message.metadata = chunk.messageMetadata;
706
- }
707
- // Tracker state retained — late events still resolvable; cleanup happens at Run end.
708
- return state;
709
- }
710
- case 'abort':
711
- case 'error': {
712
- // No state mutation — run termination is observed via the wire run-end
713
- // event, not the projection.
714
- return state;
715
- }
716
- case 'message-metadata': {
717
- const message = state.messages.find((e) => e.codecMessageId === messageId)?.message;
718
- if (message && chunk.messageMetadata !== undefined) {
719
- message.metadata = chunk.messageMetadata;
720
- }
721
- return state;
722
- }
723
- }
724
- };
725
-
726
- // ---------------------------------------------------------------------------
727
- // Text and reasoning streaming
728
- // ---------------------------------------------------------------------------
729
-
730
- const _foldTextOrReasoning = (
731
- state: VercelProjection,
732
- chunk: Extract<
733
- AI.UIMessageChunk,
734
- { type: 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' }
735
- >,
736
- messageId: string,
737
- ): VercelProjection => {
738
- const message = _ensureMessage(state, messageId);
739
- const trackers = _ensureTrackers(state, messageId);
740
-
741
- const isText = chunk.type.startsWith('text-');
742
- const partType = isText ? 'text' : 'reasoning';
743
- const activeMap = isText ? trackers.text : trackers.reasoning;
744
-
745
- switch (chunk.type) {
746
- case 'text-start':
747
- case 'reasoning-start': {
748
- activeMap.set(chunk.id, message.parts.length);
749
- message.parts.push({ type: partType, text: '' });
750
- return state;
751
- }
752
- case 'text-delta':
753
- case 'reasoning-delta': {
754
- const idx = activeMap.get(chunk.id);
755
- if (idx === undefined) return state;
756
- const part = message.parts[idx];
757
- if (part?.type === partType) {
758
- part.text += chunk.delta;
759
- }
760
- return state;
761
- }
762
- case 'text-end':
763
- case 'reasoning-end': {
764
- activeMap.delete(chunk.id);
765
- return state;
766
- }
767
- }
768
- };
769
-
770
- // ---------------------------------------------------------------------------
771
- // Tool input streaming
772
- // ---------------------------------------------------------------------------
773
-
774
- const _foldToolInput = (
775
- state: VercelProjection,
776
- chunk: Extract<
777
- AI.UIMessageChunk,
778
- { type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-available' | 'tool-input-error' }
779
- >,
780
- messageId: string,
781
- ): VercelProjection => {
782
- const message = _ensureMessage(state, messageId);
783
- const trackers = _ensureTrackers(state, messageId);
784
-
785
- switch (chunk.type) {
786
- case 'tool-input-start': {
787
- const partIndex = message.parts.length;
788
- message.parts.push({ ...toolBase(chunk), state: 'input-streaming', input: undefined });
789
- trackers.tools.set(chunk.toolCallId, { partIndex, inputText: '' });
790
- return state;
791
- }
792
- case 'tool-input-delta': {
793
- const tracker = trackers.tools.get(chunk.toolCallId);
794
- if (!tracker) return state;
795
- tracker.inputText += chunk.inputTextDelta;
796
-
797
- let parsedInput: unknown;
798
- try {
799
- // CAST: JSON.parse returns any; unknown is the safe trust-boundary type.
800
- parsedInput = JSON.parse(tracker.inputText) as unknown;
801
- } catch {
802
- parsedInput = undefined;
803
- }
804
-
805
- const found = _getToolPart(message, trackers, chunk.toolCallId);
806
- if (!found) return state;
807
- message.parts[found.tracker.partIndex] = {
808
- ...toolBase(found.part),
809
- state: 'input-streaming',
810
- input: parsedInput,
811
- };
812
- return state;
813
- }
814
- case 'tool-input-available': {
815
- const found = _getToolPart(message, trackers, chunk.toolCallId);
816
- if (!found) return state;
817
- message.parts[found.tracker.partIndex] = {
818
- ...toolBase(found.part),
819
- state: 'input-available',
820
- input: chunk.input,
821
- };
822
- return state;
823
- }
824
- case 'tool-input-error': {
825
- const found = _getToolPart(message, trackers, chunk.toolCallId);
826
- if (found) {
827
- message.parts[found.tracker.partIndex] = {
828
- ...toolBase(found.part),
829
- state: 'output-error',
830
- input: chunk.input,
831
- errorText: chunk.errorText,
832
- };
833
- } else {
834
- const partIndex = message.parts.length;
835
- message.parts.push({
836
- ...toolBase(chunk),
837
- state: 'output-error',
838
- input: chunk.input,
839
- errorText: chunk.errorText,
840
- });
841
- trackers.tools.set(chunk.toolCallId, { partIndex, inputText: '' });
842
- }
843
- return state;
844
- }
845
- }
846
- };
847
-
848
- // ---------------------------------------------------------------------------
849
- // Tool output transitions (agent-published chunks)
850
- // ---------------------------------------------------------------------------
851
-
852
- const _foldToolOutput = (
853
- state: VercelProjection,
854
- chunk: Extract<
855
- AI.UIMessageChunk,
856
- { type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
857
- >,
858
- messageId: string,
859
- ): VercelProjection => {
860
- // `tool-output-available` / `tool-output-error` after an approved tool runs
861
- // are emitted by streamText's continuation pass under a fresh
862
- // codec-message-id that differs from the assistant holding the tool call.
863
- // Resolve the owning part by toolCallId across the whole projection so the
864
- // output folds onto the original message. Deliberately do NOT materialise
865
- // `messageId` first — that would leave a phantom empty message behind the
866
- // fresh id. Drop on miss: a tool output with no matching tool call has no
867
- // anchor to attach to.
868
- if (chunk.type === 'tool-output-available' || chunk.type === 'tool-output-error') {
869
- const owner = _findToolPartOwner(state, chunk.toolCallId);
870
- if (!owner) return state;
871
- owner.message.parts[owner.tracker.partIndex] = transitionToolPart(owner.part, chunk);
872
- return state;
873
- }
874
-
875
- // `tool-approval-request` (first pass) creates the part on the run's own
876
- // message; `tool-output-denied` transitions that same part. Both key on the
877
- // stamped messageId.
878
- const message = _ensureMessage(state, messageId);
879
- const trackers = _ensureTrackers(state, messageId);
880
-
881
- const found = _getToolPart(message, trackers, chunk.toolCallId);
882
- if (!found) return state;
883
-
884
- message.parts[found.tracker.partIndex] = transitionToolPart(found.part, chunk);
885
- return state;
886
- };
887
-
888
- // ---------------------------------------------------------------------------
889
- // File / source content parts
890
- // ---------------------------------------------------------------------------
891
-
892
- const _foldContentPart = (
893
- state: VercelProjection,
894
- chunk: Extract<AI.UIMessageChunk, { type: 'file' | 'source-url' | 'source-document' }>,
895
- messageId: string,
896
- ): VercelProjection => {
897
- const message = _ensureMessage(state, messageId);
898
-
899
- switch (chunk.type) {
900
- case 'file': {
901
- message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url });
902
- return state;
903
- }
904
- case 'source-url': {
905
- message.parts.push(
906
- stripUndefined({
907
- type: 'source-url' as const,
908
- sourceId: chunk.sourceId,
909
- url: chunk.url,
910
- title: chunk.title,
911
- }),
912
- );
913
- return state;
914
- }
915
- case 'source-document': {
916
- message.parts.push(
917
- stripUndefined({
918
- type: 'source-document' as const,
919
- sourceId: chunk.sourceId,
920
- mediaType: chunk.mediaType,
921
- title: chunk.title,
922
- filename: chunk.filename,
923
- }),
924
- );
925
- return state;
926
- }
927
- }
928
- };
929
-
930
- // ---------------------------------------------------------------------------
931
- // data-* parts
932
- // ---------------------------------------------------------------------------
933
-
934
- const _foldDataPart = (
935
- state: VercelProjection,
936
- chunk: Extract<AI.UIMessageChunk, { type: `data-${string}` }>,
937
- messageId: string,
938
- ): VercelProjection => {
939
- if (chunk.transient) return state;
940
-
941
- const message = _ensureMessage(state, messageId);
942
-
943
- // CAST: chunk.type is `data-${string}` which satisfies DataUIPart, but
944
- // TypeScript cannot verify the template literal matches a specific
945
- // UIMessagePart variant at the type level.
946
- const dataPart = stripUndefined({
947
- type: chunk.type,
948
- id: chunk.id,
949
- data: chunk.data,
950
- }) as AI.UIMessage['parts'][number];
951
-
952
- if (chunk.id !== undefined) {
953
- const idx = message.parts.findIndex((p) => p.type === chunk.type && 'id' in p && p.id === chunk.id);
954
- if (idx !== -1) {
955
- message.parts[idx] = dataPart;
956
- return state;
957
- }
958
- }
959
-
960
- message.parts.push(dataPart);
961
- return state;
962
- };
963
-
964
176
  // ---------------------------------------------------------------------------
965
177
  // getMessages
966
178
  // ---------------------------------------------------------------------------
@@ -975,3 +187,5 @@ const _foldDataPart = (
975
187
  * @returns The visible messages with their codec-message-ids, in publication order.
976
188
  */
977
189
  export const getMessages = (projection: VercelProjection): CodecMessage<AI.UIMessage>[] => projection.messages;
190
+
191
+ export { init, type VercelProjection } from './reducer-state.js';