@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
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Vercel output (`ai-output`) event descriptors — the single source of truth for
3
+ * encoding/decoding `UIMessageChunk` outputs. `defineCodec` injects the
4
+ * direction-scoped `{ event, stream }` builder; the generic drivers consume the
5
+ * returned array. Adding an ordinary output event is one entry here.
6
+ *
7
+ * Author-facing acceptance gate: this file contains **zero `as` casts**. The
8
+ * injected `event`/`stream` builders narrow each chunk member, so the
9
+ * `data`/`encode`/`decode` callbacks are fully typed.
10
+ */
11
+
12
+ import * as Ably from 'ably';
13
+
14
+ import type { OutputBuilder, OutputDescriptor } from '../../core/codec/index.js';
15
+ import { boolField, jsonField, strField } from '../../core/codec/index.js';
16
+ import { ErrorCode, errorInfoIs } from '../../errors.js';
17
+ import { parseJsonOrString, stripUndefined } from '../../utils.js';
18
+ import type { VercelOutput } from './events.js';
19
+ import {
20
+ fDynamic,
21
+ fFinishReason,
22
+ fId,
23
+ fMediaType,
24
+ fMessageId,
25
+ fMeta,
26
+ fProviderExecuted,
27
+ fSourceId,
28
+ fTitle,
29
+ fToolCallId,
30
+ fToolName,
31
+ } from './fields.js';
32
+ import {
33
+ asString,
34
+ isAgentToolOutputErrorWireData,
35
+ isToolInputErrorWireData,
36
+ isToolOutputAvailableWireData,
37
+ } from './wire-data.js';
38
+
39
+ /**
40
+ * The Vercel codec's `ai-output` descriptors, built from the injected
41
+ * direction-scoped builder.
42
+ * @param builder - The `{ event, stream }` builder curried on `VercelOutput`.
43
+ * @param builder.event - Define a single discrete output event.
44
+ * @param builder.stream - Define a streamed output family (start / delta / end).
45
+ * @returns The output descriptor table the generic output drivers consume.
46
+ */
47
+ export const outputs = ({ event, stream }: OutputBuilder<VercelOutput>): readonly OutputDescriptor<VercelOutput>[] => [
48
+ // --- streamed families -----------------------------------------------------
49
+
50
+ stream('text', {
51
+ start: 'text-start',
52
+ delta: 'text-delta',
53
+ end: 'text-end',
54
+ idField: 'id',
55
+ deltaField: 'delta',
56
+ fields: [fId, fMeta],
57
+ }),
58
+
59
+ stream('reasoning', {
60
+ start: 'reasoning-start',
61
+ delta: 'reasoning-delta',
62
+ end: 'reasoning-end',
63
+ idField: 'id',
64
+ deltaField: 'delta',
65
+ fields: [fId, fMeta],
66
+ }),
67
+
68
+ // tool-input streams; the close step is a close-or-discrete fallback, the end
69
+ // chunk reconstructs `input` from the accumulated text, and the family also
70
+ // decodes from a non-streamed discrete publish.
71
+ stream('tool-input', {
72
+ start: 'tool-input-start',
73
+ delta: 'tool-input-delta',
74
+ end: 'tool-input-available',
75
+ idField: 'toolCallId',
76
+ deltaField: 'inputTextDelta',
77
+ fields: [fToolCallId, fToolName, fDynamic, fTitle, fProviderExecuted, fMeta],
78
+ onEnd: async (c, core, { h, name }) => {
79
+ try {
80
+ await core.closeStream(c.toolCallId, {
81
+ name,
82
+ data: '',
83
+ codecHeaders: h(c, ['toolCallId', 'toolName', 'providerMetadata']),
84
+ });
85
+ } catch (error: unknown) {
86
+ // closeStream raises InvalidArgument when there is no active stream for
87
+ // this id; fall through to a discrete publish, rethrow anything else.
88
+ if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
89
+ throw error;
90
+ }
91
+ await core.publishDiscrete({ name, data: c.input, codecHeaders: h(c) });
92
+ }
93
+ },
94
+ decodeEnd: ({ streamId, accumulated, codecHeaders, closingCodecHeaders }) => [
95
+ stripUndefined({
96
+ type: 'tool-input-available' as const,
97
+ toolCallId: streamId,
98
+ toolName: fToolName.read(closingCodecHeaders) || fToolName.read(codecHeaders),
99
+ input: parseJsonOrString(accumulated),
100
+ providerMetadata: fMeta.read(closingCodecHeaders),
101
+ }),
102
+ ],
103
+ decodeDiscrete: ({ codecHeaders, data }) => [
104
+ stripUndefined({
105
+ type: 'tool-input-start' as const,
106
+ toolCallId: fToolCallId.read(codecHeaders),
107
+ toolName: fToolName.read(codecHeaders),
108
+ dynamic: fDynamic.read(codecHeaders),
109
+ title: fTitle.read(codecHeaders),
110
+ providerExecuted: fProviderExecuted.read(codecHeaders),
111
+ providerMetadata: fMeta.read(codecHeaders),
112
+ }),
113
+ stripUndefined({
114
+ type: 'tool-input-available' as const,
115
+ toolCallId: fToolCallId.read(codecHeaders),
116
+ toolName: fToolName.read(codecHeaders),
117
+ input: data,
118
+ providerMetadata: fMeta.read(codecHeaders),
119
+ }),
120
+ ],
121
+ }),
122
+
123
+ // --- discrete lifecycle events ---------------------------------------------
124
+
125
+ // `start` injects the encoder's configured messageId as a fallback, so it
126
+ // builds its headers through a hatch rather than a pure descriptor.
127
+ event('start', {
128
+ fields: [fMessageId, jsonField('messageMetadata')],
129
+ encode: async (c, core, { h, name, messageId, opts }) => {
130
+ await core.publishDiscrete(
131
+ { name, data: '', codecHeaders: h({ ...c, messageId: c.messageId ?? messageId }) },
132
+ opts,
133
+ );
134
+ },
135
+ }),
136
+ event('start-step'),
137
+ event('finish-step'),
138
+ event('finish', {
139
+ fields: [fFinishReason, jsonField('messageMetadata')],
140
+ }),
141
+ event('message-metadata', { fields: [jsonField('messageMetadata')] }),
142
+ event('error', {
143
+ data: { encode: (c) => c.errorText, decode: (data) => ({ errorText: asString(data) }) },
144
+ }),
145
+
146
+ // abort: an ordinary discrete output carrying its reason as wire data. The
147
+ // agent's own stream emits it on abort; run cancellation closes in-flight
148
+ // streams via the encoder's cancelStreams() and terminates via the transport
149
+ // ai-run-end event — this chunk is content, not the run terminator.
150
+ event('abort', {
151
+ data: {
152
+ encode: (c) => c.reason ?? '',
153
+ decode: (data) => (typeof data === 'string' && data ? { reason: data } : {}),
154
+ },
155
+ }),
156
+
157
+ // --- content parts ---------------------------------------------------------
158
+
159
+ event('file', {
160
+ fields: [fMediaType, fMeta],
161
+ data: { encode: (c) => c.url, decode: (data) => ({ url: asString(data) }) },
162
+ }),
163
+ event('source-url', {
164
+ fields: [fSourceId, fTitle, fMeta],
165
+ data: { encode: (c) => c.url, decode: (data) => ({ url: asString(data) }) },
166
+ }),
167
+ event('source-document', {
168
+ fields: [fSourceId, fMediaType, strField('title', ''), strField('filename'), fMeta],
169
+ }),
170
+
171
+ // --- tool lifecycle (discrete) ---------------------------------------------
172
+
173
+ event('tool-input-error', {
174
+ fields: [fToolCallId, fToolName, fDynamic, fTitle, fProviderExecuted, fMeta],
175
+ data: {
176
+ encode: (c) => ({ errorText: c.errorText, input: c.input }),
177
+ decode: (data) =>
178
+ isToolInputErrorWireData(data) ? { errorText: data.errorText ?? '', input: data.input } : { errorText: '' },
179
+ },
180
+ }),
181
+ event('tool-output-available', {
182
+ fields: [fToolCallId, fDynamic, fProviderExecuted, boolField('preliminary')],
183
+ data: {
184
+ encode: (c) => ({ output: c.output }),
185
+ decode: (data) => (isToolOutputAvailableWireData(data) ? { output: data.output } : {}),
186
+ },
187
+ }),
188
+ event('tool-output-error', {
189
+ fields: [fToolCallId, fDynamic, fProviderExecuted],
190
+ data: {
191
+ encode: (c) => ({ errorText: c.errorText }),
192
+ decode: (data) => ({ errorText: isAgentToolOutputErrorWireData(data) ? (data.errorText ?? '') : '' }),
193
+ },
194
+ }),
195
+ event('tool-approval-request', {
196
+ fields: [fToolCallId, strField('approvalId', '')],
197
+ }),
198
+ event('tool-output-denied', { fields: [fToolCallId] }),
199
+
200
+ // --- data-* wildcard -------------------------------------------------------
201
+
202
+ event('data-*', {
203
+ fields: [fId, boolField('transient')],
204
+ ephemeral: (c) => c.transient === true,
205
+ data: { encode: (c) => c.data, decode: (data) => ({ data }) },
206
+ }),
207
+ ];
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Shared reducer state: the projection shape, its internal tracker types,
3
+ * `init`, and the message/tracker lookup helpers the per-concern fold modules
4
+ * build on. This module is the base of the reducer's import DAG — the fold
5
+ * modules depend on it; it depends on none of them.
6
+ */
7
+
8
+ import type * as AI from 'ai';
9
+
10
+ import type { CodecMessage } from '../../core/codec/index.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Internal tracker state
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Tracks an in-progress tool part within a UIMessage. Text and reasoning
18
+ * parts don't need this — we write to them directly via partIndex. Tool
19
+ * parts need an extra `inputText` buffer because deltas arrive as raw
20
+ * JSON fragments that must be accumulated before parsing.
21
+ */
22
+ export interface ToolPartTracker {
23
+ /** Index in the message's parts array. */
24
+ partIndex: number;
25
+ /** Accumulated streaming input text (for JSON parsing on completion). */
26
+ inputText: string;
27
+ }
28
+
29
+ /** Per-codecMessageId tracking state for in-progress streams within a UIMessage. */
30
+ export interface MessageTrackers {
31
+ /** Text stream id → partIndex. */
32
+ text: Map<string, number>;
33
+ /** Reasoning stream id → partIndex. */
34
+ reasoning: Map<string, number>;
35
+ /** Tool call id → tracker. */
36
+ tools: Map<string, ToolPartTracker>;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Projection
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * The per-Run state produced by the Vercel codec's reducer.
45
+ *
46
+ * The SDK reads only `messages` (via `Codec.getMessages`). The remaining
47
+ * fields are internal to the reducer; they happen to live on the
48
+ * projection because the projection is the only thing the reducer can
49
+ * carry from fold to fold (it has no instance state).
50
+ */
51
+ export interface VercelProjection {
52
+ /**
53
+ * UIMessages produced or modified in this Run, in publication order,
54
+ * each paired with its codec-message-id. The reducer correlates strictly
55
+ * on `codecMessageId`; `message.id` is preserved verbatim from the source
56
+ * (the AI SDK stream's `start.messageId` for assistants, the caller's id
57
+ * for user messages) and is never used as an identity key.
58
+ */
59
+ messages: CodecMessage<AI.UIMessage>[];
60
+ /** Per-codecMessageId tracker state for streamed parts. Internal — do not access. */
61
+ trackers: Map<string, MessageTrackers>;
62
+ /**
63
+ * Tool-resolution events that arrived before any assistant in this
64
+ * projection had a matching `toolCallId`. Re-evaluated on every
65
+ * subsequent fold so that an out-of-order tool output is folded as
66
+ * soon as the corresponding assistant lands.
67
+ */
68
+ pendingToolResolutions: PendingToolResolution[];
69
+ }
70
+
71
+ /**
72
+ * A buffered tool resolution waiting for its assistant message to arrive.
73
+ * The reducer scans pending entries after every successful fold so an
74
+ * out-of-order tool output is promoted as soon as the matching assistant
75
+ * is added to the projection.
76
+ */
77
+ export interface PendingToolResolution {
78
+ /** The codec-message-id of the assistant the resolution targets. */
79
+ targetCodecMessageId: string;
80
+ /** Tool call this resolution targets. */
81
+ toolCallId: string;
82
+ /** Variant of the tool-resolution used to transition the assistant's tool part. */
83
+ resolution:
84
+ | { kind: 'tool-result'; output: unknown }
85
+ | { kind: 'tool-result-error'; message: string }
86
+ | { kind: 'tool-approval-response'; approved: boolean; reason?: string };
87
+ }
88
+
89
+ /** A located `dynamic-tool` part with its owning message and tracker. */
90
+ export interface OwnerLookup {
91
+ /** The message owning the tool part. */
92
+ message: AI.UIMessage;
93
+ /** The tracker pointing at the part's index. */
94
+ tracker: ToolPartTracker;
95
+ /** The resolved `dynamic-tool` part itself. */
96
+ part: AI.DynamicToolUIPart;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // init
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /**
104
+ * Build an empty initial projection.
105
+ * @returns A fresh VercelProjection with no messages and no tracker state.
106
+ */
107
+ export const init = (): VercelProjection => ({
108
+ messages: [],
109
+ trackers: new Map(),
110
+ pendingToolResolutions: [],
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Message + tracker helpers
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Resolve the assistant message for a codec-message-id, creating an empty
119
+ * placeholder when none exists yet.
120
+ * @param state - Projection to read or extend.
121
+ * @param codecMessageId - The codec-message-id to resolve.
122
+ * @returns The existing or newly-seeded UIMessage for that id.
123
+ */
124
+ export const ensureMessage = (state: VercelProjection, codecMessageId: string): AI.UIMessage => {
125
+ let entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
126
+ if (!entry) {
127
+ // No source id seen yet — seed the domain `message.id` with the
128
+ // codec-message-id as a fallback. The `start` chunk overwrites it with
129
+ // the stream's `messageId` when the stream provides one.
130
+ entry = { codecMessageId, message: { id: codecMessageId, role: 'assistant', parts: [] } };
131
+ state.messages.push(entry);
132
+ }
133
+ return entry.message;
134
+ };
135
+
136
+ /**
137
+ * Resolve the stream trackers for a codec-message-id, creating empty maps
138
+ * when none exist yet.
139
+ * @param state - Projection to read or extend.
140
+ * @param messageId - The codec-message-id whose trackers to resolve.
141
+ * @returns The existing or newly-created tracker maps for that id.
142
+ */
143
+ export const ensureTrackers = (state: VercelProjection, messageId: string): MessageTrackers => {
144
+ let trackers = state.trackers.get(messageId);
145
+ if (!trackers) {
146
+ trackers = { text: new Map(), reasoning: new Map(), tools: new Map() };
147
+ state.trackers.set(messageId, trackers);
148
+ }
149
+ return trackers;
150
+ };
151
+
152
+ /**
153
+ * Resolve the `dynamic-tool` part tracked for a toolCallId within a message.
154
+ * @param message - The message whose parts to read.
155
+ * @param trackers - The message's tracker maps.
156
+ * @param toolCallId - The tool call to resolve.
157
+ * @returns The tracker and part, or `undefined` if untracked or the part is not a dynamic-tool.
158
+ */
159
+ export const getToolPart = (
160
+ message: AI.UIMessage,
161
+ trackers: MessageTrackers,
162
+ toolCallId: string,
163
+ ): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined => {
164
+ const tracker = trackers.tools.get(toolCallId);
165
+ if (!tracker) return undefined;
166
+ const part = message.parts[tracker.partIndex];
167
+ if (part?.type !== 'dynamic-tool') return undefined;
168
+ return { tracker, part };
169
+ };