@ably/ai-transport 0.1.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 (221) hide show
  1. package/README.md +93 -111
  2. package/dist/ably-ai-transport.js +2401 -1387
  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 +116 -42
  7. package/dist/core/agent.d.ts +44 -0
  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 +24 -24
  11. package/dist/core/codec/define-codec.d.ts +100 -0
  12. package/dist/core/codec/encoder.d.ts +10 -12
  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 -2
  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/lifecycle-tracker.d.ts +10 -9
  20. package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
  21. package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
  22. package/dist/core/codec/output-descriptors.d.ts +237 -0
  23. package/dist/core/codec/types.d.ts +470 -119
  24. package/dist/core/codec/well-known-inputs.d.ts +52 -0
  25. package/dist/core/transport/agent-session.d.ts +10 -0
  26. package/dist/core/transport/agent-view.d.ts +296 -0
  27. package/dist/core/transport/client-session.d.ts +13 -0
  28. package/dist/core/transport/decode-fold.d.ts +55 -0
  29. package/dist/core/transport/headers.d.ts +121 -14
  30. package/dist/core/transport/index.d.ts +5 -6
  31. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  32. package/dist/core/transport/invocation.d.ts +74 -0
  33. package/dist/core/transport/load-history-pages.d.ts +71 -0
  34. package/dist/core/transport/load-history.d.ts +44 -0
  35. package/dist/core/transport/pipe-stream.d.ts +9 -9
  36. package/dist/core/transport/run-manager.d.ts +76 -0
  37. package/dist/core/transport/session-support.d.ts +55 -0
  38. package/dist/core/transport/tree.d.ts +523 -109
  39. package/dist/core/transport/types/agent.d.ts +375 -0
  40. package/dist/core/transport/types/client.d.ts +201 -0
  41. package/dist/core/transport/types/shared.d.ts +24 -0
  42. package/dist/core/transport/types/tree.d.ts +357 -0
  43. package/dist/core/transport/types/view.d.ts +249 -0
  44. package/dist/core/transport/types.d.ts +13 -553
  45. package/dist/core/transport/view.d.ts +390 -84
  46. package/dist/core/transport/wire-log.d.ts +102 -0
  47. package/dist/errors.d.ts +27 -10
  48. package/dist/index.d.ts +8 -9
  49. package/dist/logger.d.ts +12 -0
  50. package/dist/react/ably-ai-transport-react.js +1365 -1010
  51. package/dist/react/ably-ai-transport-react.js.map +1 -1
  52. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  53. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  54. package/dist/react/contexts/client-session-context.d.ts +37 -0
  55. package/dist/react/contexts/client-session-provider.d.ts +56 -0
  56. package/dist/react/create-session-hooks.d.ts +116 -0
  57. package/dist/react/index.d.ts +13 -12
  58. package/dist/react/internal/skipped-session.d.ts +8 -0
  59. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  60. package/dist/react/use-ably-messages.d.ts +17 -14
  61. package/dist/react/use-client-session.d.ts +81 -0
  62. package/dist/react/use-create-view.d.ts +14 -13
  63. package/dist/react/use-tree.d.ts +30 -15
  64. package/dist/react/use-view.d.ts +81 -50
  65. package/dist/utils.d.ts +48 -71
  66. package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
  67. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  68. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  69. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  70. package/dist/vercel/codec/decode-lifecycle.d.ts +9 -0
  71. package/dist/vercel/codec/events.d.ts +50 -0
  72. package/dist/vercel/codec/fields.d.ts +44 -0
  73. package/dist/vercel/codec/fold-content.d.ts +16 -0
  74. package/dist/vercel/codec/fold-data.d.ts +16 -0
  75. package/dist/vercel/codec/fold-input.d.ts +67 -0
  76. package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
  77. package/dist/vercel/codec/fold-text.d.ts +16 -0
  78. package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
  79. package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
  80. package/dist/vercel/codec/index.d.ts +7 -20
  81. package/dist/vercel/codec/inputs.d.ts +11 -0
  82. package/dist/vercel/codec/outputs.d.ts +11 -0
  83. package/dist/vercel/codec/reducer-state.d.ts +121 -0
  84. package/dist/vercel/codec/reducer.d.ts +62 -0
  85. package/dist/vercel/codec/tool-transitions.d.ts +2 -8
  86. package/dist/vercel/codec/wire-data.d.ts +34 -0
  87. package/dist/vercel/index.d.ts +5 -5
  88. package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
  89. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  90. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -45
  91. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  92. package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
  93. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  94. package/dist/vercel/react/index.d.ts +1 -2
  95. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  96. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  97. package/dist/vercel/run-end-reason.d.ts +84 -0
  98. package/dist/vercel/tool-part.d.ts +21 -0
  99. package/dist/vercel/transport/chat-transport.d.ts +41 -24
  100. package/dist/vercel/transport/index.d.ts +24 -20
  101. package/dist/vercel/transport/run-output-stream.d.ts +54 -0
  102. package/dist/version.d.ts +2 -0
  103. package/package.json +31 -24
  104. package/src/constants.ts +124 -51
  105. package/src/core/agent.ts +92 -0
  106. package/src/core/channel-options.ts +89 -0
  107. package/src/core/codec/codec-event.ts +27 -0
  108. package/src/core/codec/decoder.ts +202 -105
  109. package/src/core/codec/define-codec.ts +432 -0
  110. package/src/core/codec/encoder.ts +114 -107
  111. package/src/core/codec/field-bag.ts +142 -0
  112. package/src/core/codec/fields.ts +193 -0
  113. package/src/core/codec/index.ts +56 -6
  114. package/src/core/codec/input-descriptor-decoder.ts +97 -0
  115. package/src/core/codec/input-descriptor-encoder.ts +150 -0
  116. package/src/core/codec/input-descriptors.ts +373 -0
  117. package/src/core/codec/lifecycle-tracker.ts +10 -9
  118. package/src/core/codec/output-descriptor-decoder.ts +139 -0
  119. package/src/core/codec/output-descriptor-encoder.ts +101 -0
  120. package/src/core/codec/output-descriptors.ts +307 -0
  121. package/src/core/codec/types.ts +505 -126
  122. package/src/core/codec/well-known-inputs.ts +96 -0
  123. package/src/core/transport/agent-session.ts +1085 -0
  124. package/src/core/transport/agent-view.ts +738 -0
  125. package/src/core/transport/client-session.ts +780 -0
  126. package/src/core/transport/decode-fold.ts +101 -0
  127. package/src/core/transport/headers.ts +234 -22
  128. package/src/core/transport/index.ts +27 -27
  129. package/src/core/transport/internal/bounded-map.ts +27 -0
  130. package/src/core/transport/invocation.ts +98 -0
  131. package/src/core/transport/load-history-pages.ts +220 -0
  132. package/src/core/transport/load-history.ts +271 -0
  133. package/src/core/transport/pipe-stream.ts +63 -39
  134. package/src/core/transport/run-manager.ts +243 -0
  135. package/src/core/transport/session-support.ts +96 -0
  136. package/src/core/transport/tree.ts +1293 -308
  137. package/src/core/transport/types/agent.ts +434 -0
  138. package/src/core/transport/types/client.ts +247 -0
  139. package/src/core/transport/types/shared.ts +27 -0
  140. package/src/core/transport/types/tree.ts +393 -0
  141. package/src/core/transport/types/view.ts +288 -0
  142. package/src/core/transport/types.ts +13 -706
  143. package/src/core/transport/view.ts +1229 -450
  144. package/src/core/transport/wire-log.ts +189 -0
  145. package/src/errors.ts +29 -9
  146. package/src/event-emitter.ts +3 -2
  147. package/src/index.ts +86 -42
  148. package/src/logger.ts +14 -1
  149. package/src/react/contexts/client-session-context.ts +41 -0
  150. package/src/react/contexts/client-session-provider.tsx +222 -0
  151. package/src/react/create-session-hooks.ts +141 -0
  152. package/src/react/index.ts +24 -13
  153. package/src/react/internal/skipped-session.ts +62 -0
  154. package/src/react/internal/use-resolved-session.ts +63 -0
  155. package/src/react/use-ably-messages.ts +32 -22
  156. package/src/react/use-client-session.ts +178 -0
  157. package/src/react/use-create-view.ts +33 -29
  158. package/src/react/use-tree.ts +61 -30
  159. package/src/react/use-view.ts +138 -96
  160. package/src/utils.ts +83 -131
  161. package/src/vercel/codec/decode-lifecycle.ts +70 -0
  162. package/src/vercel/codec/events.ts +85 -0
  163. package/src/vercel/codec/fields.ts +58 -0
  164. package/src/vercel/codec/fold-content.ts +54 -0
  165. package/src/vercel/codec/fold-data.ts +46 -0
  166. package/src/vercel/codec/fold-input.ts +255 -0
  167. package/src/vercel/codec/fold-lifecycle.ts +85 -0
  168. package/src/vercel/codec/fold-text.ts +55 -0
  169. package/src/vercel/codec/fold-tool-input.ts +86 -0
  170. package/src/vercel/codec/fold-tool-output.ts +79 -0
  171. package/src/vercel/codec/index.ts +28 -21
  172. package/src/vercel/codec/inputs.ts +116 -0
  173. package/src/vercel/codec/outputs.ts +207 -0
  174. package/src/vercel/codec/reducer-state.ts +169 -0
  175. package/src/vercel/codec/reducer.ts +191 -0
  176. package/src/vercel/codec/tool-transitions.ts +3 -14
  177. package/src/vercel/codec/wire-data.ts +64 -0
  178. package/src/vercel/index.ts +7 -19
  179. package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
  180. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  181. package/src/vercel/react/index.ts +3 -5
  182. package/src/vercel/react/use-chat-transport.ts +44 -66
  183. package/src/vercel/react/use-message-sync.ts +75 -39
  184. package/src/vercel/run-end-reason.ts +157 -0
  185. package/src/vercel/tool-part.ts +25 -0
  186. package/src/vercel/transport/chat-transport.ts +380 -98
  187. package/src/vercel/transport/index.ts +38 -37
  188. package/src/vercel/transport/run-output-stream.ts +169 -0
  189. package/src/version.ts +2 -0
  190. package/dist/core/transport/client-transport.d.ts +0 -10
  191. package/dist/core/transport/decode-history.d.ts +0 -43
  192. package/dist/core/transport/server-transport.d.ts +0 -7
  193. package/dist/core/transport/stream-router.d.ts +0 -29
  194. package/dist/core/transport/turn-manager.d.ts +0 -37
  195. package/dist/react/contexts/transport-context.d.ts +0 -31
  196. package/dist/react/contexts/transport-provider.d.ts +0 -49
  197. package/dist/react/create-transport-hooks.d.ts +0 -124
  198. package/dist/react/use-active-turns.d.ts +0 -12
  199. package/dist/react/use-client-transport.d.ts +0 -80
  200. package/dist/vercel/codec/accumulator.d.ts +0 -21
  201. package/dist/vercel/codec/decoder.d.ts +0 -22
  202. package/dist/vercel/codec/encoder.d.ts +0 -41
  203. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  204. package/dist/vercel/tool-approvals.d.ts +0 -124
  205. package/dist/vercel/tool-events.d.ts +0 -26
  206. package/src/core/transport/client-transport.ts +0 -977
  207. package/src/core/transport/decode-history.ts +0 -485
  208. package/src/core/transport/server-transport.ts +0 -612
  209. package/src/core/transport/stream-router.ts +0 -136
  210. package/src/core/transport/turn-manager.ts +0 -165
  211. package/src/react/contexts/transport-context.ts +0 -37
  212. package/src/react/contexts/transport-provider.tsx +0 -164
  213. package/src/react/create-transport-hooks.ts +0 -144
  214. package/src/react/use-active-turns.ts +0 -72
  215. package/src/react/use-client-transport.ts +0 -197
  216. package/src/vercel/codec/accumulator.ts +0 -588
  217. package/src/vercel/codec/decoder.ts +0 -618
  218. package/src/vercel/codec/encoder.ts +0 -410
  219. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  220. package/src/vercel/tool-approvals.ts +0 -380
  221. package/src/vercel/tool-events.ts +0 -53
@@ -1,410 +0,0 @@
1
- /**
2
- * Vercel AI SDK Encoder
3
- *
4
- * Maps UIMessageChunk events and complete UIMessage objects to Ably channel
5
- * operations (publish, appendMessage, updateMessage).
6
- *
7
- * Delegates the message append lifecycle (publish, append, close, abort,
8
- * flush/recover) to the encoder core. This file contains only the
9
- * Vercel-specific event-to-operation mapping.
10
- *
11
- * Domain-specific headers use the `x-domain-` prefix to distinguish them
12
- * from transport-level `x-ably-` headers.
13
- *
14
- * ## Core operations and domain headers
15
- *
16
- * Each UIMessageChunk maps to exactly one encoder core operation. Domain
17
- * headers are passed to every operation that accepts them — the core handles
18
- * merging, persistence, and deduplication:
19
- *
20
- * - **`startStream`**: Opens a message stream. Domain headers become
21
- * "persistent headers" — the core repeats them on every subsequent append.
22
- * - **`appendStream`**: Appends a text delta. Data only, no headers parameter.
23
- * The core automatically carries persistent headers from start.
24
- * - **`closeStream`**: Closes the stream. Pass all domain headers from the
25
- * chunk — the core merges them on top of persistent headers, so changed
26
- * values (e.g. updated providerMetadata) are picked up and unchanged
27
- * values are harmlessly deduplicated.
28
- * - **`publishDiscrete`**: Publishes a standalone message. All domain headers
29
- * for the chunk are passed directly.
30
- */
31
-
32
- import * as Ably from 'ably';
33
- import type * as AI from 'ai';
34
- import { isDataUIPart } from 'ai';
35
-
36
- import { HEADER_STATUS } from '../../constants.js';
37
- import type { EncoderCore, EncoderCoreOptions } from '../../core/codec/encoder.js';
38
- import { createEncoderCore } from '../../core/codec/encoder.js';
39
- import type { ChannelWriter, MessagePayload, StreamEncoder, WriteOptions } from '../../core/codec/types.js';
40
- import { ErrorCode, errorInfoIs } from '../../errors.js';
41
- import { headerWriter } from '../../utils.js';
42
-
43
- // ---------------------------------------------------------------------------
44
- // Discrete event payload builder
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Build a MessagePayload for discrete (non-streaming) event types.
49
- * Used by both `writeEvent` and `appendEvent` for tool output events,
50
- * content parts, and data-* custom chunks.
51
- * @param chunk - The UI message chunk to encode.
52
- * @returns The message payload for publishing to the channel.
53
- */
54
- const buildDiscretePayload = (chunk: AI.UIMessageChunk): MessagePayload => {
55
- switch (chunk.type) {
56
- case 'tool-output-available': {
57
- const h = headerWriter()
58
- .str('toolCallId', chunk.toolCallId)
59
- .bool('dynamic', chunk.dynamic)
60
- .bool('providerExecuted', chunk.providerExecuted)
61
- .bool('preliminary', chunk.preliminary)
62
- .build();
63
- return { name: 'tool-output-available', data: { output: chunk.output }, headers: h };
64
- }
65
-
66
- case 'tool-output-error': {
67
- const h = headerWriter()
68
- .str('toolCallId', chunk.toolCallId)
69
- .bool('dynamic', chunk.dynamic)
70
- .bool('providerExecuted', chunk.providerExecuted)
71
- .build();
72
- return { name: 'tool-output-error', data: { errorText: chunk.errorText }, headers: h };
73
- }
74
-
75
- case 'tool-approval-request': {
76
- const h = headerWriter().str('toolCallId', chunk.toolCallId).str('approvalId', chunk.approvalId).build();
77
- return { name: 'tool-approval-request', data: '', headers: h };
78
- }
79
-
80
- case 'tool-output-denied': {
81
- const h = headerWriter().str('toolCallId', chunk.toolCallId).build();
82
- return { name: 'tool-output-denied', data: '', headers: h };
83
- }
84
-
85
- default: {
86
- if (chunk.type.startsWith('data-')) {
87
- // CAST: data-* chunks always have id, transient, and data fields per AI SDK types.
88
- // TypeScript can't narrow the template literal union in a default case.
89
- const dataChunk = chunk as Extract<AI.UIMessageChunk, { type: `data-${string}` }>;
90
- const h = headerWriter().str('id', dataChunk.id).bool('transient', dataChunk.transient).build();
91
- const ephemeral = dataChunk.transient === true;
92
- return { name: chunk.type, data: dataChunk.data, headers: h, ephemeral };
93
- }
94
- throw new Ably.ErrorInfo(
95
- `unable to write event; unsupported chunk type '${chunk.type}'`,
96
- ErrorCode.InvalidArgument,
97
- 400,
98
- );
99
- }
100
- }
101
- };
102
-
103
- // ---------------------------------------------------------------------------
104
- // Default implementation
105
- // ---------------------------------------------------------------------------
106
-
107
- class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIMessage> {
108
- private readonly _core: EncoderCore;
109
- private readonly _messageId: string | undefined;
110
- private _aborted = false;
111
-
112
- constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
113
- this._core = createEncoderCore(writer, options);
114
- this._messageId = options.messageId;
115
- }
116
-
117
- async appendEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
118
- switch (chunk.type) {
119
- // -- Stream start: open a message stream with persistent headers -------
120
-
121
- case 'text-start': {
122
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
123
- await this._core.startStream(chunk.id, { name: 'text', data: '', headers: h }, perWrite);
124
- break;
125
- }
126
-
127
- case 'reasoning-start': {
128
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
129
- await this._core.startStream(chunk.id, { name: 'reasoning', data: '', headers: h }, perWrite);
130
- break;
131
- }
132
-
133
- case 'tool-input-start': {
134
- const h = headerWriter()
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: 'tool-input', data: '', headers: h }, perWrite);
143
- break;
144
- }
145
-
146
- // -- Stream append: data only, core carries persistent headers --------
147
-
148
- case 'text-delta': {
149
- this._core.appendStream(chunk.id, chunk.delta);
150
- break;
151
- }
152
-
153
- case 'reasoning-delta': {
154
- this._core.appendStream(chunk.id, chunk.delta);
155
- break;
156
- }
157
-
158
- case 'tool-input-delta': {
159
- this._core.appendStream(chunk.toolCallId, chunk.inputTextDelta);
160
- break;
161
- }
162
-
163
- // -- Stream close: pass all chunk headers, core merges with persistent
164
-
165
- case 'text-end': {
166
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
167
- await this._core.closeStream(chunk.id, { name: 'text', data: '', headers: h });
168
- break;
169
- }
170
-
171
- case 'reasoning-end': {
172
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
173
- await this._core.closeStream(chunk.id, { name: 'reasoning', data: '', headers: h });
174
- break;
175
- }
176
-
177
- case 'tool-input-available': {
178
- // If a stream tracker exists, this tool call was streamed — close it.
179
- // Otherwise it's a non-streaming tool call — publish discrete.
180
- try {
181
- const h = headerWriter()
182
- .str('toolCallId', chunk.toolCallId)
183
- .str('toolName', chunk.toolName)
184
- .json('providerMetadata', chunk.providerMetadata)
185
- .build();
186
- await this._core.closeStream(chunk.toolCallId, { name: 'tool-input', data: '', headers: h });
187
- } catch (error: unknown) {
188
- // Only fall through to discrete for "no active stream" — rethrow real failures
189
- if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
190
- throw error;
191
- }
192
- const h = headerWriter()
193
- .str('toolCallId', chunk.toolCallId)
194
- .str('toolName', chunk.toolName)
195
- .bool('dynamic', chunk.dynamic)
196
- .str('title', chunk.title)
197
- .bool('providerExecuted', chunk.providerExecuted)
198
- .json('providerMetadata', chunk.providerMetadata)
199
- .build();
200
- await this._core.publishDiscrete({ name: 'tool-input', data: chunk.input, headers: h });
201
- }
202
- break;
203
- }
204
-
205
- // -- Discrete: lifecycle events ---------------------------------------
206
-
207
- case 'start': {
208
- const h = headerWriter()
209
- .str('messageId', chunk.messageId ?? this._messageId)
210
- .json('messageMetadata', chunk.messageMetadata)
211
- .build();
212
- await this._core.publishDiscrete({ name: 'start', data: '', headers: h }, perWrite);
213
- break;
214
- }
215
-
216
- case 'start-step': {
217
- await this._core.publishDiscrete({ name: 'start-step', data: '' }, perWrite);
218
- break;
219
- }
220
-
221
- case 'finish-step': {
222
- await this._core.publishDiscrete({ name: 'finish-step', data: '' }, perWrite);
223
- break;
224
- }
225
-
226
- case 'finish': {
227
- const h = headerWriter()
228
- .str('finishReason', chunk.finishReason)
229
- .json('messageMetadata', chunk.messageMetadata)
230
- .build();
231
- await this._core.publishDiscrete({ name: 'finish', data: '', headers: h }, perWrite);
232
- break;
233
- }
234
-
235
- case 'error': {
236
- await this._core.publishDiscrete({ name: 'error', data: chunk.errorText }, perWrite);
237
- break;
238
- }
239
-
240
- case 'abort': {
241
- this._aborted = true;
242
- await this._core.abortAllStreams(perWrite);
243
- await this._core.publishDiscrete(
244
- { name: 'abort', data: chunk.reason ?? '', headers: { [HEADER_STATUS]: 'aborted' } },
245
- perWrite,
246
- );
247
- break;
248
- }
249
-
250
- // -- Discrete: tool lifecycle events ----------------------------------
251
-
252
- case 'tool-input-error': {
253
- const h = headerWriter()
254
- .str('toolCallId', chunk.toolCallId)
255
- .str('toolName', chunk.toolName)
256
- .bool('dynamic', chunk.dynamic)
257
- .str('title', chunk.title)
258
- .bool('providerExecuted', chunk.providerExecuted)
259
- .json('providerMetadata', chunk.providerMetadata)
260
- .build();
261
- await this._core.publishDiscrete({
262
- name: 'tool-input-error',
263
- data: { errorText: chunk.errorText, input: chunk.input },
264
- headers: h,
265
- });
266
- break;
267
- }
268
-
269
- case 'tool-output-available':
270
- case 'tool-output-error':
271
- case 'tool-approval-request':
272
- case 'tool-output-denied': {
273
- await this._core.publishDiscrete(buildDiscretePayload(chunk), perWrite);
274
- break;
275
- }
276
-
277
- // -- Discrete: content parts ------------------------------------------
278
-
279
- case 'file': {
280
- const h = headerWriter()
281
- .str('mediaType', chunk.mediaType)
282
- .json('providerMetadata', chunk.providerMetadata)
283
- .build();
284
- await this._core.publishDiscrete({ name: 'file', data: chunk.url, headers: h }, perWrite);
285
- break;
286
- }
287
-
288
- case 'source-url': {
289
- const h = headerWriter()
290
- .str('sourceId', chunk.sourceId)
291
- .str('title', chunk.title)
292
- .json('providerMetadata', chunk.providerMetadata)
293
- .build();
294
- await this._core.publishDiscrete({ name: 'source-url', data: chunk.url, headers: h }, perWrite);
295
- break;
296
- }
297
-
298
- case 'source-document': {
299
- const h = headerWriter()
300
- .str('sourceId', chunk.sourceId)
301
- .str('mediaType', chunk.mediaType)
302
- .str('title', chunk.title)
303
- .str('filename', chunk.filename)
304
- .json('providerMetadata', chunk.providerMetadata)
305
- .build();
306
- await this._core.publishDiscrete({ name: 'source-document', data: '', headers: h }, perWrite);
307
- break;
308
- }
309
-
310
- case 'message-metadata': {
311
- const h = headerWriter().json('messageMetadata', chunk.messageMetadata).build();
312
- await this._core.publishDiscrete({ name: 'message-metadata', data: '', headers: h }, perWrite);
313
- break;
314
- }
315
-
316
- // -- Discrete: data-* custom chunks -----------------------------------
317
-
318
- default: {
319
- if (chunk.type.startsWith('data-')) {
320
- const h = headerWriter().str('id', chunk.id).bool('transient', chunk.transient).build();
321
- const ephemeral = chunk.transient === true;
322
- await this._core.publishDiscrete({ name: chunk.type, data: chunk.data, headers: h, ephemeral }, perWrite);
323
- }
324
- break;
325
- }
326
- }
327
- }
328
-
329
- async writeEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<Ably.PublishResult> {
330
- return this._core.publishDiscrete(buildDiscretePayload(chunk), perWrite);
331
- }
332
-
333
- async writeMessages(messages: AI.UIMessage[], perWrite?: WriteOptions): Promise<Ably.PublishResult> {
334
- const payloads = messages.flatMap((msg) => encodeMessagePayloads(msg));
335
- return this._core.publishDiscreteBatch(payloads, perWrite);
336
- }
337
-
338
- async abort(reason?: string): Promise<void> {
339
- if (this._aborted) return;
340
- this._aborted = true;
341
- await this._core.abortAllStreams();
342
- await this._core.publishDiscrete({
343
- name: 'abort',
344
- data: reason ?? '',
345
- headers: { [HEADER_STATUS]: 'aborted' },
346
- });
347
- }
348
-
349
- async close(): Promise<void> {
350
- await this._core.close();
351
- }
352
- }
353
-
354
- // ---------------------------------------------------------------------------
355
- // Message payload encoding (stateless helper)
356
- // ---------------------------------------------------------------------------
357
-
358
- const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
359
- const messageId = message.id;
360
- const payloads: MessagePayload[] = [];
361
-
362
- for (const part of message.parts) {
363
- switch (part.type) {
364
- case 'text': {
365
- payloads.push({ name: 'text', data: part.text, headers: headerWriter().str('messageId', messageId).build() });
366
- break;
367
- }
368
- case 'file': {
369
- payloads.push({
370
- name: 'file',
371
- data: part.url,
372
- headers: headerWriter().str('messageId', messageId).str('mediaType', part.mediaType).build(),
373
- });
374
- break;
375
- }
376
- default: {
377
- if (isDataUIPart(part)) {
378
- payloads.push({
379
- name: part.type,
380
- data: part.data,
381
- headers: headerWriter().str('messageId', messageId).str('id', part.id).build(),
382
- });
383
- }
384
- break;
385
- }
386
- }
387
- }
388
-
389
- if (payloads.length === 0) {
390
- payloads.push({ name: 'text', data: '', headers: headerWriter().str('messageId', messageId).build() });
391
- }
392
-
393
- return payloads;
394
- };
395
-
396
- // ---------------------------------------------------------------------------
397
- // Factory
398
- // ---------------------------------------------------------------------------
399
-
400
- /**
401
- * Create a Vercel AI SDK encoder that maps UIMessageChunk events to Ably
402
- * channel operations via the encoder core.
403
- * @param writer - The channel writer to publish messages through.
404
- * @param options - Encoder configuration (clientId, extras, hooks, logger).
405
- * @returns A {@link StreamEncoder} for UIMessageChunk/UIMessage.
406
- */
407
- export const createEncoder = (
408
- writer: ChannelWriter,
409
- options: EncoderCoreOptions = {},
410
- ): StreamEncoder<AI.UIMessageChunk, AI.UIMessage> => new DefaultUIMessageEncoder(writer, options);
@@ -1,87 +0,0 @@
1
- /**
2
- * useStagedAddToolApprovalResponse — wrap useChat's `addToolApprovalResponse`
3
- * so the approval response is also applied to the transport tree
4
- * synchronously at click time.
5
- *
6
- * Patching the tree at click time eliminates the useChat↔tree divergence
7
- * the ChatTransport would otherwise have to reconcile via a history
8
- * overlay, and closes the observer-turn race that could wipe the
9
- * approval state between `addToolApprovalResponse` and
10
- * `sendAutomaticallyWhen`'s evaluation.
11
- *
12
- * Use this in place of useChat's raw `addToolApprovalResponse` wherever
13
- * you wire Approve / Deny buttons.
14
- */
15
-
16
- import type * as AI from 'ai';
17
- import type { ChatAddToolApproveResponseFunction } from 'ai';
18
- import { useCallback } from 'react';
19
-
20
- import type { ClientTransport } from '../../core/transport/types.js';
21
-
22
- /**
23
- * Returns a function with the same signature as useChat's
24
- * `addToolApprovalResponse`, but additionally applies the approval
25
- * response to the transport tree via `stageMessage` before delegating.
26
- *
27
- * If the tool call identified by `opts.id` isn't found in the tree,
28
- * the tree update is skipped and the raw function is still called —
29
- * matches useChat's tolerant behavior for stale approval ids.
30
- * @param transport - The client transport whose tree to patch.
31
- * @param addToolApprovalResponse - The raw function from `useChat()`.
32
- * @returns A drop-in replacement that patches the tree then delegates.
33
- */
34
- export const useStagedAddToolApprovalResponse = (
35
- transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
36
- addToolApprovalResponse: ChatAddToolApproveResponseFunction,
37
- ): ChatAddToolApproveResponseFunction =>
38
- useCallback<ChatAddToolApproveResponseFunction>(
39
- (opts) => {
40
- stageApprovalResponseOnTree(transport, opts);
41
- return addToolApprovalResponse(opts);
42
- },
43
- [transport, addToolApprovalResponse],
44
- );
45
-
46
- /**
47
- * Locate the assistant message whose `dynamic-tool` part carries the
48
- * given `approval.id`, build a patched copy with the part transitioned
49
- * to `approval-responded`, and stage the patched message on the tree.
50
- * @param transport - The transport whose tree to patch.
51
- * @param opts - The approval response being applied.
52
- * @param opts.id - The approval id matching a dynamic-tool part in the tree.
53
- * @param opts.approved - Whether the user approved or denied.
54
- * @param opts.reason - Optional reason accompanying the response.
55
- */
56
- const stageApprovalResponseOnTree = (
57
- transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
58
- opts: { id: string; approved: boolean; reason?: string },
59
- ): void => {
60
- const nodes = transport.view.flattenNodes();
61
- for (const node of nodes) {
62
- const partIndex = node.message.parts.findIndex((p) => p.type === 'dynamic-tool' && p.approval?.id === opts.id);
63
- if (partIndex === -1) continue;
64
-
65
- // CAST: findIndex predicate above narrows this to a dynamic-tool part
66
- // with a non-undefined approval.
67
- const part = node.message.parts[partIndex] as AI.DynamicToolUIPart;
68
-
69
- // Build the approval-responded variant directly rather than spreading
70
- // `part`, which TypeScript narrows to whichever source-state variant
71
- // the union discriminator inferred and then rejects when we change
72
- // `state` to a variant with different approval/output constraints.
73
- const patchedPart: AI.DynamicToolUIPart = {
74
- type: 'dynamic-tool',
75
- toolName: part.toolName,
76
- toolCallId: part.toolCallId,
77
- state: 'approval-responded',
78
- input: part.input,
79
- approval: { id: opts.id, approved: opts.approved, reason: opts.reason },
80
- };
81
-
82
- const patchedParts = [...node.message.parts];
83
- patchedParts[partIndex] = patchedPart;
84
- transport.stageMessage(node.msgId, { ...node.message, parts: patchedParts });
85
- return;
86
- }
87
- };