@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
@@ -1,548 +0,0 @@
1
- /**
2
- * Vercel AI SDK encoder.
3
- *
4
- * Two publish methods enforce direction at the call site:
5
- *
6
- * - {@link DefaultUIMessageEncoder.publishInput} encodes a `VercelInput`
7
- * variant and publishes it on the `ai-input` wire.
8
- * - {@link DefaultUIMessageEncoder.publishOutput} encodes a `VercelOutput`
9
- * (`AI.UIMessageChunk`) and publishes it on the `ai-output` wire,
10
- * driving the underlying stream-tracker for streamed chunks
11
- * (text / reasoning / tool-input) and falling back to discrete
12
- * publishes for everything else.
13
- *
14
- * The codec event's own discriminator (`kind` for inputs, `type` for
15
- * outputs) is carried in the codec tier's `type` header so the
16
- * decoder can dispatch. Stream-tracker state lives inside the encoder
17
- * core; only the output direction (text / reasoning / tool-input chunks)
18
- * drives it — inputs are always published as discrete messages.
19
- */
20
-
21
- import * as Ably from 'ably';
22
- import type * as AI from 'ai';
23
- import { isDataUIPart } from 'ai';
24
-
25
- import { EVENT_AI_INPUT, EVENT_AI_OUTPUT, HEADER_ROLE, HEADER_STATUS } from '../../constants.js';
26
- import type { EncoderCore, EncoderCoreOptions } from '../../core/codec/encoder.js';
27
- import { createEncoderCore } from '../../core/codec/encoder.js';
28
- import type {
29
- ChannelWriter,
30
- Encoder,
31
- MessagePayload,
32
- ToolApprovalResponse,
33
- ToolResult,
34
- ToolResultError,
35
- UserMessage,
36
- WriteOptions,
37
- } from '../../core/codec/types.js';
38
- import { ErrorCode, errorInfoIs } from '../../errors.js';
39
- import { headerWriter } from '../../utils.js';
40
- import type {
41
- VercelInput,
42
- VercelOutput,
43
- VercelToolApprovalResponsePayload,
44
- VercelToolResultErrorPayload,
45
- VercelToolResultPayload,
46
- } from './events.js';
47
-
48
- // ---------------------------------------------------------------------------
49
- // Default implementation
50
- // ---------------------------------------------------------------------------
51
-
52
- class DefaultUIMessageEncoder implements Encoder<VercelInput, VercelOutput> {
53
- private readonly _core: EncoderCore;
54
- private readonly _messageId: string | undefined;
55
- private _cancelled = false;
56
-
57
- constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
58
- this._core = createEncoderCore(writer, options);
59
- this._messageId = options.messageId;
60
- }
61
-
62
- async publishInput(input: VercelInput, options?: WriteOptions): Promise<void> {
63
- switch (input.kind) {
64
- case 'user-message': {
65
- await this._publishUserMessage(input, options);
66
- return;
67
- }
68
- case 'regenerate': {
69
- await this._publishRegenerate(options);
70
- return;
71
- }
72
- case 'tool-result': {
73
- await this._publishToolResult(input, options);
74
- return;
75
- }
76
- case 'tool-result-error': {
77
- await this._publishToolResultError(input, options);
78
- return;
79
- }
80
- case 'tool-approval-response': {
81
- await this._publishToolApprovalResponse(input, options);
82
- return;
83
- }
84
- }
85
- }
86
-
87
- async publishOutput(output: VercelOutput, options?: WriteOptions): Promise<void> {
88
- await this._publishChunk(output, options);
89
- }
90
-
91
- async cancel(reason?: string): Promise<void> {
92
- if (this._cancelled) return;
93
- this._cancelled = true;
94
- await this._core.cancelAllStreams();
95
- await this._core.publishDiscrete({
96
- name: EVENT_AI_OUTPUT,
97
- data: reason ?? '',
98
- codecHeaders: headerWriter().str('type', 'abort').build(),
99
- transportHeaders: { [HEADER_STATUS]: 'cancelled' },
100
- });
101
- }
102
-
103
- async close(): Promise<void> {
104
- await this._core.close();
105
- }
106
-
107
- // -------------------------------------------------------------------------
108
- // VercelOutput routing — UIMessageChunk
109
- // -------------------------------------------------------------------------
110
-
111
- private async _publishChunk(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
112
- switch (chunk.type) {
113
- // -- Stream start -----------------------------------------------------
114
- case 'text-start': {
115
- const h = headerWriter()
116
- .str('type', 'text')
117
- .str('id', chunk.id)
118
- .json('providerMetadata', chunk.providerMetadata)
119
- .build();
120
- await this._core.startStream(chunk.id, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
121
- return;
122
- }
123
- case 'reasoning-start': {
124
- const h = headerWriter()
125
- .str('type', 'reasoning')
126
- .str('id', chunk.id)
127
- .json('providerMetadata', chunk.providerMetadata)
128
- .build();
129
- await this._core.startStream(chunk.id, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
130
- return;
131
- }
132
- case 'tool-input-start': {
133
- const h = headerWriter()
134
- .str('type', 'tool-input')
135
- .str('toolCallId', chunk.toolCallId)
136
- .str('toolName', chunk.toolName)
137
- .bool('dynamic', chunk.dynamic)
138
- .str('title', chunk.title)
139
- .bool('providerExecuted', chunk.providerExecuted)
140
- .json('providerMetadata', chunk.providerMetadata)
141
- .build();
142
- await this._core.startStream(chunk.toolCallId, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
143
- return;
144
- }
145
-
146
- // -- Stream append ----------------------------------------------------
147
- case 'text-delta': {
148
- this._core.appendStream(chunk.id, chunk.delta);
149
- return;
150
- }
151
- case 'reasoning-delta': {
152
- this._core.appendStream(chunk.id, chunk.delta);
153
- return;
154
- }
155
- case 'tool-input-delta': {
156
- this._core.appendStream(chunk.toolCallId, chunk.inputTextDelta);
157
- return;
158
- }
159
-
160
- // -- Stream close -----------------------------------------------------
161
- case 'text-end': {
162
- const h = headerWriter()
163
- .str('type', 'text')
164
- .str('id', chunk.id)
165
- .json('providerMetadata', chunk.providerMetadata)
166
- .build();
167
- await this._core.closeStream(chunk.id, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h });
168
- return;
169
- }
170
- case 'reasoning-end': {
171
- const h = headerWriter()
172
- .str('type', 'reasoning')
173
- .str('id', chunk.id)
174
- .json('providerMetadata', chunk.providerMetadata)
175
- .build();
176
- await this._core.closeStream(chunk.id, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h });
177
- return;
178
- }
179
- case 'tool-input-available': {
180
- try {
181
- const h = headerWriter()
182
- .str('type', 'tool-input')
183
- .str('toolCallId', chunk.toolCallId)
184
- .str('toolName', chunk.toolName)
185
- .json('providerMetadata', chunk.providerMetadata)
186
- .build();
187
- await this._core.closeStream(chunk.toolCallId, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h });
188
- } catch (error: unknown) {
189
- // closeStream raises ErrorCode.InvalidArgument when there is no active stream for this id; fall through to a discrete publish in that case and rethrow any other error.
190
- if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
191
- throw error;
192
- }
193
- const h = headerWriter()
194
- .str('type', 'tool-input')
195
- .str('toolCallId', chunk.toolCallId)
196
- .str('toolName', chunk.toolName)
197
- .bool('dynamic', chunk.dynamic)
198
- .str('title', chunk.title)
199
- .bool('providerExecuted', chunk.providerExecuted)
200
- .json('providerMetadata', chunk.providerMetadata)
201
- .build();
202
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.input, codecHeaders: h });
203
- }
204
- return;
205
- }
206
-
207
- // -- Lifecycle (discrete) ---------------------------------------------
208
- case 'start': {
209
- const h = headerWriter()
210
- .str('type', 'start')
211
- .str('messageId', chunk.messageId ?? this._messageId)
212
- .json('messageMetadata', chunk.messageMetadata)
213
- .build();
214
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
215
- return;
216
- }
217
- case 'start-step': {
218
- const h = headerWriter().str('type', 'start-step').build();
219
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
220
- return;
221
- }
222
- case 'finish-step': {
223
- const h = headerWriter().str('type', 'finish-step').build();
224
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
225
- return;
226
- }
227
- case 'finish': {
228
- const h = headerWriter()
229
- .str('type', 'finish')
230
- .str('finishReason', chunk.finishReason)
231
- .json('messageMetadata', chunk.messageMetadata)
232
- .build();
233
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
234
- return;
235
- }
236
- case 'error': {
237
- const h = headerWriter().str('type', 'error').build();
238
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.errorText, codecHeaders: h }, perWrite);
239
- return;
240
- }
241
- case 'abort': {
242
- this._cancelled = true;
243
- await this._core.cancelAllStreams(perWrite);
244
- await this._core.publishDiscrete(
245
- {
246
- name: EVENT_AI_OUTPUT,
247
- data: chunk.reason ?? '',
248
- codecHeaders: headerWriter().str('type', 'abort').build(),
249
- transportHeaders: { [HEADER_STATUS]: 'cancelled' },
250
- },
251
- perWrite,
252
- );
253
- return;
254
- }
255
- case 'message-metadata': {
256
- const h = headerWriter().str('type', 'message-metadata').json('messageMetadata', chunk.messageMetadata).build();
257
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
258
- return;
259
- }
260
-
261
- // -- Tool lifecycle (discrete) ----------------------------------------
262
- case 'tool-input-error': {
263
- const h = headerWriter()
264
- .str('type', 'tool-input-error')
265
- .str('toolCallId', chunk.toolCallId)
266
- .str('toolName', chunk.toolName)
267
- .bool('dynamic', chunk.dynamic)
268
- .str('title', chunk.title)
269
- .bool('providerExecuted', chunk.providerExecuted)
270
- .json('providerMetadata', chunk.providerMetadata)
271
- .build();
272
- await this._core.publishDiscrete(
273
- { name: EVENT_AI_OUTPUT, data: { errorText: chunk.errorText, input: chunk.input }, codecHeaders: h },
274
- perWrite,
275
- );
276
- return;
277
- }
278
- case 'tool-output-available':
279
- case 'tool-output-error':
280
- case 'tool-approval-request':
281
- case 'tool-output-denied': {
282
- await this._core.publishDiscrete(buildToolOutputPayload(chunk), perWrite);
283
- return;
284
- }
285
-
286
- // -- Content parts (discrete) -----------------------------------------
287
- case 'file': {
288
- const h = headerWriter()
289
- .str('type', 'file')
290
- .str('mediaType', chunk.mediaType)
291
- .json('providerMetadata', chunk.providerMetadata)
292
- .build();
293
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.url, codecHeaders: h }, perWrite);
294
- return;
295
- }
296
- case 'source-url': {
297
- const h = headerWriter()
298
- .str('type', 'source-url')
299
- .str('sourceId', chunk.sourceId)
300
- .str('title', chunk.title)
301
- .json('providerMetadata', chunk.providerMetadata)
302
- .build();
303
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.url, codecHeaders: h }, perWrite);
304
- return;
305
- }
306
- case 'source-document': {
307
- const h = headerWriter()
308
- .str('type', 'source-document')
309
- .str('sourceId', chunk.sourceId)
310
- .str('mediaType', chunk.mediaType)
311
- .str('title', chunk.title)
312
- .str('filename', chunk.filename)
313
- .json('providerMetadata', chunk.providerMetadata)
314
- .build();
315
- await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
316
- return;
317
- }
318
-
319
- // -- data-* (discrete) ------------------------------------------------
320
- default: {
321
- if (chunk.type.startsWith('data-')) {
322
- // CAST: data-* chunks always have id, transient, and data fields per AI SDK types.
323
- // TypeScript can't narrow the template literal union in a default case.
324
- const dataChunk = chunk;
325
- const h = headerWriter()
326
- .str('type', dataChunk.type)
327
- .str('id', dataChunk.id)
328
- .bool('transient', dataChunk.transient)
329
- .build();
330
- const ephemeral = dataChunk.transient === true;
331
- await this._core.publishDiscrete(
332
- { name: EVENT_AI_OUTPUT, data: dataChunk.data, codecHeaders: h, ephemeral },
333
- perWrite,
334
- );
335
- return;
336
- }
337
- throw new Ably.ErrorInfo(
338
- `unable to publish output; unsupported chunk type '${chunk.type}'`,
339
- ErrorCode.InvalidArgument,
340
- 400,
341
- );
342
- }
343
- }
344
- }
345
-
346
- // -------------------------------------------------------------------------
347
- // VercelInput routing
348
- // -------------------------------------------------------------------------
349
-
350
- /**
351
- * Publish a user-message input as a batch of per-part discrete Ably
352
- * messages on the `ai-input` wire. Wire format matches the multi-part
353
- * user-message convention; the receive-side decoder fans the parts back
354
- * out into a single `UserMessage`.
355
- * @param input - The user-message input carrying the UIMessage to encode.
356
- * @param perWrite - Optional per-write overrides.
357
- */
358
- private async _publishUserMessage(input: UserMessage<AI.UIMessage>, perWrite?: WriteOptions): Promise<void> {
359
- const payloads = encodeMessagePayloads(input.message);
360
- // Stamp role (a transport header) on every payload so the decoder can
361
- // reconstruct a `role: 'user'` UIMessage.
362
- for (const payload of payloads) {
363
- payload.transportHeaders = { ...payload.transportHeaders, [HEADER_ROLE]: 'user' };
364
- }
365
- await this._core.publishDiscreteBatch(payloads, perWrite);
366
- }
367
-
368
- /**
369
- * Publish a regenerate input as a discrete `ai-input` Ably message
370
- * carrying codec `type: 'regenerate'`. The wire carries no domain
371
- * payload — `parent` / `target` are stamped on the transport headers by
372
- * the client-session (it reads them off the input directly and builds
373
- * `buildTransportHeaders`).
374
- * @param perWrite - Per-write overrides carrying the transport headers built by client-session.
375
- */
376
- private async _publishRegenerate(perWrite?: WriteOptions): Promise<void> {
377
- const h = headerWriter().str('type', 'regenerate').build();
378
- await this._core.publishDiscrete({ name: EVENT_AI_INPUT, data: '', codecHeaders: h }, perWrite);
379
- }
380
-
381
- /**
382
- * Publish a client-side tool output on the `ai-input` wire. Targets the
383
- * assistant addressed by `input.codecMessageId`; the wire's
384
- * `codec-message-id` is stamped via `perWrite.messageId` by the
385
- * client-session.
386
- * @param input - The tool-output input.
387
- * @param perWrite - Per-write overrides carrying the wire codecMessageId.
388
- */
389
- private async _publishToolResult(input: ToolResult<VercelToolResultPayload>, perWrite?: WriteOptions): Promise<void> {
390
- const h = headerWriter().str('type', 'tool-result').str('toolCallId', input.payload.toolCallId).build();
391
- await this._core.publishDiscrete(
392
- { name: EVENT_AI_INPUT, data: { output: input.payload.output }, codecHeaders: h },
393
- perWrite,
394
- );
395
- }
396
-
397
- /**
398
- * Publish a client-side tool error on the `ai-input` wire. Targets the
399
- * assistant addressed by `input.codecMessageId`.
400
- * @param input - The tool-result-error input.
401
- * @param perWrite - Per-write overrides.
402
- */
403
- private async _publishToolResultError(
404
- input: ToolResultError<VercelToolResultErrorPayload>,
405
- perWrite?: WriteOptions,
406
- ): Promise<void> {
407
- const h = headerWriter().str('type', 'tool-result-error').str('toolCallId', input.payload.toolCallId).build();
408
- await this._core.publishDiscrete(
409
- { name: EVENT_AI_INPUT, data: { message: input.payload.message }, codecHeaders: h },
410
- perWrite,
411
- );
412
- }
413
-
414
- /**
415
- * Publish a client-side tool approval response on the `ai-input` wire.
416
- * Targets the assistant addressed by `input.codecMessageId`.
417
- * @param input - The approval-response input.
418
- * @param perWrite - Per-write overrides.
419
- */
420
- private async _publishToolApprovalResponse(
421
- input: ToolApprovalResponse<VercelToolApprovalResponsePayload>,
422
- perWrite?: WriteOptions,
423
- ): Promise<void> {
424
- const h = headerWriter()
425
- .str('type', 'tool-approval-response')
426
- .str('toolCallId', input.payload.toolCallId)
427
- .bool('approved', input.payload.approved)
428
- .str('reason', input.payload.reason)
429
- .build();
430
- await this._core.publishDiscrete({ name: EVENT_AI_INPUT, data: '', codecHeaders: h }, perWrite);
431
- }
432
- }
433
-
434
- // ---------------------------------------------------------------------------
435
- // Tool output discrete payload builder (agent-side `ai-output` wire)
436
- // ---------------------------------------------------------------------------
437
-
438
- const buildToolOutputPayload = (
439
- chunk: Extract<
440
- AI.UIMessageChunk,
441
- { type: 'tool-output-available' | 'tool-output-error' | 'tool-approval-request' | 'tool-output-denied' }
442
- >,
443
- ): MessagePayload => {
444
- switch (chunk.type) {
445
- case 'tool-output-available': {
446
- const h = headerWriter()
447
- .str('type', 'tool-output-available')
448
- .str('toolCallId', chunk.toolCallId)
449
- .bool('dynamic', chunk.dynamic)
450
- .bool('providerExecuted', chunk.providerExecuted)
451
- .bool('preliminary', chunk.preliminary)
452
- .build();
453
- return { name: EVENT_AI_OUTPUT, data: { output: chunk.output }, codecHeaders: h };
454
- }
455
- case 'tool-output-error': {
456
- const h = headerWriter()
457
- .str('type', 'tool-output-error')
458
- .str('toolCallId', chunk.toolCallId)
459
- .bool('dynamic', chunk.dynamic)
460
- .bool('providerExecuted', chunk.providerExecuted)
461
- .build();
462
- return { name: EVENT_AI_OUTPUT, data: { errorText: chunk.errorText }, codecHeaders: h };
463
- }
464
- case 'tool-approval-request': {
465
- const h = headerWriter()
466
- .str('type', 'tool-approval-request')
467
- .str('toolCallId', chunk.toolCallId)
468
- .str('approvalId', chunk.approvalId)
469
- .build();
470
- return { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h };
471
- }
472
- case 'tool-output-denied': {
473
- const h = headerWriter().str('type', 'tool-output-denied').str('toolCallId', chunk.toolCallId).build();
474
- return { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h };
475
- }
476
- }
477
- };
478
-
479
- // ---------------------------------------------------------------------------
480
- // User-message per-part payload encoding
481
- // ---------------------------------------------------------------------------
482
-
483
- const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
484
- const messageId = message.id;
485
- const payloads: MessagePayload[] = [];
486
-
487
- for (const part of message.parts) {
488
- switch (part.type) {
489
- case 'text': {
490
- payloads.push({
491
- name: EVENT_AI_INPUT,
492
- data: part.text,
493
- codecHeaders: headerWriter().str('type', 'text').str('messageId', messageId).build(),
494
- });
495
- break;
496
- }
497
- case 'file': {
498
- payloads.push({
499
- name: EVENT_AI_INPUT,
500
- data: part.url,
501
- codecHeaders: headerWriter()
502
- .str('type', 'file')
503
- .str('messageId', messageId)
504
- .str('mediaType', part.mediaType)
505
- .build(),
506
- });
507
- break;
508
- }
509
- default: {
510
- if (isDataUIPart(part)) {
511
- payloads.push({
512
- name: EVENT_AI_INPUT,
513
- data: part.data,
514
- codecHeaders: headerWriter().str('type', part.type).str('messageId', messageId).str('id', part.id).build(),
515
- });
516
- }
517
- break;
518
- }
519
- }
520
- }
521
-
522
- if (payloads.length === 0) {
523
- // Always emit at least one part so the decoder can reconstruct the codec-message-id and role from headers, even when the user-message carried no encodable parts.
524
- payloads.push({
525
- name: EVENT_AI_INPUT,
526
- data: '',
527
- codecHeaders: headerWriter().str('type', 'text').str('messageId', messageId).build(),
528
- });
529
- }
530
-
531
- return payloads;
532
- };
533
-
534
- // ---------------------------------------------------------------------------
535
- // Factory
536
- // ---------------------------------------------------------------------------
537
-
538
- /**
539
- * Create a Vercel AI SDK encoder that maps VercelInput / VercelOutput to
540
- * Ably channel operations via the encoder core.
541
- * @param writer - The channel writer to publish messages through.
542
- * @param options - Encoder configuration (clientId, extras, hooks, logger).
543
- * @returns An {@link Encoder} typed in both directions for the Vercel codec.
544
- */
545
- export const createEncoder = (
546
- writer: ChannelWriter,
547
- options: EncoderCoreOptions = {},
548
- ): Encoder<VercelInput, VercelOutput> => new DefaultUIMessageEncoder(writer, options);