@ably/ai-transport 0.0.1 → 0.2.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 (167) hide show
  1. package/README.md +114 -116
  2. package/dist/ably-ai-transport.js +1743 -961
  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 +117 -39
  7. package/dist/core/agent.d.ts +29 -0
  8. package/dist/core/codec/decoder.d.ts +20 -23
  9. package/dist/core/codec/encoder.d.ts +11 -8
  10. package/dist/core/codec/index.d.ts +1 -2
  11. package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
  12. package/dist/core/codec/types.d.ts +410 -101
  13. package/dist/core/transport/agent-session.d.ts +10 -0
  14. package/dist/core/transport/branch-chain.d.ts +43 -0
  15. package/dist/core/transport/client-session.d.ts +13 -0
  16. package/dist/core/transport/decode-fold.d.ts +47 -0
  17. package/dist/core/transport/headers.d.ts +97 -17
  18. package/dist/core/transport/index.d.ts +5 -3
  19. package/dist/core/transport/internal/bounded-map.d.ts +20 -0
  20. package/dist/core/transport/invocation.d.ts +74 -0
  21. package/dist/core/transport/load-conversation.d.ts +128 -0
  22. package/dist/core/transport/load-history.d.ts +39 -0
  23. package/dist/core/transport/pipe-stream.d.ts +9 -8
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +435 -0
  26. package/dist/core/transport/types/agent.d.ts +353 -0
  27. package/dist/core/transport/types/client.d.ts +168 -0
  28. package/dist/core/transport/types/shared.d.ts +24 -0
  29. package/dist/core/transport/types/tree.d.ts +315 -0
  30. package/dist/core/transport/types/view.d.ts +222 -0
  31. package/dist/core/transport/types.d.ts +13 -402
  32. package/dist/core/transport/view.d.ts +354 -0
  33. package/dist/errors.d.ts +37 -9
  34. package/dist/index.d.ts +6 -6
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +1164 -645
  37. package/dist/react/ably-ai-transport-react.js.map +1 -1
  38. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  39. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  40. package/dist/react/contexts/client-session-context.d.ts +36 -0
  41. package/dist/react/contexts/client-session-provider.d.ts +53 -0
  42. package/dist/react/create-session-hooks.d.ts +116 -0
  43. package/dist/react/index.d.ts +16 -10
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +20 -11
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +23 -0
  48. package/dist/react/use-tree.d.ts +35 -0
  49. package/dist/react/use-view.d.ts +110 -0
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
  52. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  53. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  54. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  55. package/dist/vercel/codec/decoder.d.ts +5 -18
  56. package/dist/vercel/codec/encoder.d.ts +6 -36
  57. package/dist/vercel/codec/events.d.ts +51 -0
  58. package/dist/vercel/codec/index.d.ts +24 -12
  59. package/dist/vercel/codec/reducer.d.ts +144 -0
  60. package/dist/vercel/codec/tool-transitions.d.ts +50 -0
  61. package/dist/vercel/index.d.ts +4 -2
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
  63. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  64. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -1
  65. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  66. package/dist/vercel/react/contexts/chat-transport-context.d.ts +33 -0
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
  68. package/dist/vercel/react/index.d.ts +4 -0
  69. package/dist/vercel/react/use-chat-transport.d.ts +66 -21
  70. package/dist/vercel/react/use-message-sync.d.ts +31 -12
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +71 -30
  73. package/dist/vercel/transport/index.d.ts +25 -18
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +47 -34
  77. package/src/constants.ts +126 -47
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +115 -58
  81. package/src/core/codec/index.ts +13 -6
  82. package/src/core/codec/lifecycle-tracker.ts +10 -9
  83. package/src/core/codec/types.ts +438 -106
  84. package/src/core/transport/agent-session.ts +1344 -0
  85. package/src/core/transport/branch-chain.ts +58 -0
  86. package/src/core/transport/client-session.ts +775 -0
  87. package/src/core/transport/decode-fold.ts +91 -0
  88. package/src/core/transport/headers.ts +182 -19
  89. package/src/core/transport/index.ts +29 -22
  90. package/src/core/transport/internal/bounded-map.ts +27 -0
  91. package/src/core/transport/invocation.ts +98 -0
  92. package/src/core/transport/load-conversation.ts +355 -0
  93. package/src/core/transport/load-history.ts +269 -0
  94. package/src/core/transport/pipe-stream.ts +58 -40
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +1167 -0
  97. package/src/core/transport/types/agent.ts +407 -0
  98. package/src/core/transport/types/client.ts +211 -0
  99. package/src/core/transport/types/shared.ts +27 -0
  100. package/src/core/transport/types/tree.ts +344 -0
  101. package/src/core/transport/types/view.ts +259 -0
  102. package/src/core/transport/types.ts +13 -527
  103. package/src/core/transport/view.ts +1271 -0
  104. package/src/errors.ts +42 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +55 -39
  107. package/src/logger.ts +14 -1
  108. package/src/react/contexts/client-session-context.ts +41 -0
  109. package/src/react/contexts/client-session-provider.tsx +186 -0
  110. package/src/react/create-session-hooks.ts +141 -0
  111. package/src/react/index.ts +27 -10
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +47 -19
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +72 -0
  116. package/src/react/use-tree.ts +84 -0
  117. package/src/react/use-view.ts +275 -0
  118. package/src/react/vite.config.ts +4 -1
  119. package/src/utils.ts +63 -45
  120. package/src/vercel/codec/decoder.ts +336 -255
  121. package/src/vercel/codec/encoder.ts +348 -196
  122. package/src/vercel/codec/events.ts +87 -0
  123. package/src/vercel/codec/index.ts +59 -14
  124. package/src/vercel/codec/reducer.ts +977 -0
  125. package/src/vercel/codec/tool-transitions.ts +122 -0
  126. package/src/vercel/index.ts +7 -3
  127. package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
  128. package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
  129. package/src/vercel/react/index.ts +13 -1
  130. package/src/vercel/react/use-chat-transport.ts +162 -42
  131. package/src/vercel/react/use-message-sync.ts +121 -22
  132. package/src/vercel/react/vite.config.ts +4 -2
  133. package/src/vercel/run-end-reason.ts +78 -0
  134. package/src/vercel/transport/chat-transport.ts +553 -113
  135. package/src/vercel/transport/index.ts +40 -28
  136. package/src/vercel/transport/run-output-stream.ts +170 -0
  137. package/src/version.ts +2 -0
  138. package/dist/core/transport/client-transport.d.ts +0 -10
  139. package/dist/core/transport/conversation-tree.d.ts +0 -9
  140. package/dist/core/transport/decode-history.d.ts +0 -41
  141. package/dist/core/transport/server-transport.d.ts +0 -7
  142. package/dist/core/transport/stream-router.d.ts +0 -19
  143. package/dist/core/transport/turn-manager.d.ts +0 -34
  144. package/dist/react/use-active-turns.d.ts +0 -8
  145. package/dist/react/use-client-transport.d.ts +0 -7
  146. package/dist/react/use-conversation-tree.d.ts +0 -20
  147. package/dist/react/use-edit.d.ts +0 -7
  148. package/dist/react/use-history.d.ts +0 -19
  149. package/dist/react/use-messages.d.ts +0 -7
  150. package/dist/react/use-regenerate.d.ts +0 -7
  151. package/dist/react/use-send.d.ts +0 -7
  152. package/dist/vercel/codec/accumulator.d.ts +0 -21
  153. package/src/core/transport/client-transport.ts +0 -959
  154. package/src/core/transport/conversation-tree.ts +0 -434
  155. package/src/core/transport/decode-history.ts +0 -337
  156. package/src/core/transport/server-transport.ts +0 -458
  157. package/src/core/transport/stream-router.ts +0 -118
  158. package/src/core/transport/turn-manager.ts +0 -147
  159. package/src/react/use-active-turns.ts +0 -61
  160. package/src/react/use-client-transport.ts +0 -37
  161. package/src/react/use-conversation-tree.ts +0 -71
  162. package/src/react/use-edit.ts +0 -24
  163. package/src/react/use-history.ts +0 -111
  164. package/src/react/use-messages.ts +0 -32
  165. package/src/react/use-regenerate.ts +0 -24
  166. package/src/react/use-send.ts +0 -25
  167. package/src/vercel/codec/accumulator.ts +0 -603
@@ -1,75 +1,137 @@
1
1
  /**
2
- * Vercel AI SDK Encoder
2
+ * Vercel AI SDK encoder.
3
3
  *
4
- * Maps UIMessageChunk events and complete UIMessage objects to Ably channel
5
- * operations (publish, appendMessage, updateMessage).
4
+ * Two publish methods enforce direction at the call site:
6
5
  *
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.
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.
10
13
  *
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.
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.
30
19
  */
31
20
 
32
21
  import * as Ably from 'ably';
33
22
  import type * as AI from 'ai';
34
23
  import { isDataUIPart } from 'ai';
35
24
 
36
- import { HEADER_STATUS } from '../../constants.js';
25
+ import { EVENT_AI_INPUT, EVENT_AI_OUTPUT, HEADER_ROLE, HEADER_STATUS } from '../../constants.js';
37
26
  import type { EncoderCore, EncoderCoreOptions } from '../../core/codec/encoder.js';
38
27
  import { createEncoderCore } from '../../core/codec/encoder.js';
39
- import type { ChannelWriter, MessagePayload, StreamEncoder, WriteOptions } from '../../core/codec/types.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';
40
38
  import { ErrorCode, errorInfoIs } from '../../errors.js';
41
39
  import { headerWriter } from '../../utils.js';
40
+ import type {
41
+ VercelInput,
42
+ VercelOutput,
43
+ VercelToolApprovalResponsePayload,
44
+ VercelToolResultErrorPayload,
45
+ VercelToolResultPayload,
46
+ } from './events.js';
42
47
 
43
48
  // ---------------------------------------------------------------------------
44
49
  // Default implementation
45
50
  // ---------------------------------------------------------------------------
46
51
 
47
- class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIMessage> {
52
+ class DefaultUIMessageEncoder implements Encoder<VercelInput, VercelOutput> {
48
53
  private readonly _core: EncoderCore;
49
- private _aborted = false;
54
+ private readonly _messageId: string | undefined;
55
+ private _cancelled = false;
50
56
 
51
57
  constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
52
58
  this._core = createEncoderCore(writer, options);
59
+ this._messageId = options.messageId;
53
60
  }
54
61
 
55
- async appendEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
56
- switch (chunk.type) {
57
- // -- Stream start: open a message stream with persistent headers -------
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
+ }
58
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 -----------------------------------------------------
59
114
  case 'text-start': {
60
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
61
- await this._core.startStream(chunk.id, { name: 'text', data: '', headers: h }, perWrite);
62
- break;
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;
63
122
  }
64
-
65
123
  case 'reasoning-start': {
66
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
67
- await this._core.startStream(chunk.id, { name: 'reasoning', data: '', headers: h }, perWrite);
68
- break;
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;
69
131
  }
70
-
71
132
  case 'tool-input-start': {
72
133
  const h = headerWriter()
134
+ .str('type', 'tool-input')
73
135
  .str('toolCallId', chunk.toolCallId)
74
136
  .str('toolName', chunk.toolName)
75
137
  .bool('dynamic', chunk.dynamic)
@@ -77,57 +139,59 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
77
139
  .bool('providerExecuted', chunk.providerExecuted)
78
140
  .json('providerMetadata', chunk.providerMetadata)
79
141
  .build();
80
- await this._core.startStream(chunk.toolCallId, { name: 'tool-input', data: '', headers: h }, perWrite);
81
- break;
142
+ await this._core.startStream(chunk.toolCallId, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
143
+ return;
82
144
  }
83
145
 
84
- // -- Stream append: data only, core carries persistent headers --------
85
-
146
+ // -- Stream append ----------------------------------------------------
86
147
  case 'text-delta': {
87
148
  this._core.appendStream(chunk.id, chunk.delta);
88
- break;
149
+ return;
89
150
  }
90
-
91
151
  case 'reasoning-delta': {
92
152
  this._core.appendStream(chunk.id, chunk.delta);
93
- break;
153
+ return;
94
154
  }
95
-
96
155
  case 'tool-input-delta': {
97
156
  this._core.appendStream(chunk.toolCallId, chunk.inputTextDelta);
98
- break;
157
+ return;
99
158
  }
100
159
 
101
- // -- Stream close: pass all chunk headers, core merges with persistent
102
-
160
+ // -- Stream close -----------------------------------------------------
103
161
  case 'text-end': {
104
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
105
- await this._core.closeStream(chunk.id, { name: 'text', data: '', headers: h });
106
- break;
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;
107
169
  }
108
-
109
170
  case 'reasoning-end': {
110
- const h = headerWriter().str('id', chunk.id).json('providerMetadata', chunk.providerMetadata).build();
111
- await this._core.closeStream(chunk.id, { name: 'reasoning', data: '', headers: h });
112
- break;
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;
113
178
  }
114
-
115
179
  case 'tool-input-available': {
116
- // If a stream tracker exists, this tool call was streamed — close it.
117
- // Otherwise it's a non-streaming tool call — publish discrete.
118
180
  try {
119
181
  const h = headerWriter()
182
+ .str('type', 'tool-input')
120
183
  .str('toolCallId', chunk.toolCallId)
121
184
  .str('toolName', chunk.toolName)
122
185
  .json('providerMetadata', chunk.providerMetadata)
123
186
  .build();
124
- await this._core.closeStream(chunk.toolCallId, { name: 'tool-input', data: '', headers: h });
187
+ await this._core.closeStream(chunk.toolCallId, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h });
125
188
  } catch (error: unknown) {
126
- // Only fall through to discrete for "no active stream" rethrow real failures
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.
127
190
  if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
128
191
  throw error;
129
192
  }
130
193
  const h = headerWriter()
194
+ .str('type', 'tool-input')
131
195
  .str('toolCallId', chunk.toolCallId)
132
196
  .str('toolName', chunk.toolName)
133
197
  .bool('dynamic', chunk.dynamic)
@@ -135,60 +199,69 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
135
199
  .bool('providerExecuted', chunk.providerExecuted)
136
200
  .json('providerMetadata', chunk.providerMetadata)
137
201
  .build();
138
- await this._core.publishDiscrete({ name: 'tool-input', data: chunk.input, headers: h });
202
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.input, codecHeaders: h });
139
203
  }
140
- break;
204
+ return;
141
205
  }
142
206
 
143
- // -- Discrete: lifecycle events ---------------------------------------
144
-
207
+ // -- Lifecycle (discrete) ---------------------------------------------
145
208
  case 'start': {
146
209
  const h = headerWriter()
147
- .str('messageId', chunk.messageId)
210
+ .str('type', 'start')
211
+ .str('messageId', chunk.messageId ?? this._messageId)
148
212
  .json('messageMetadata', chunk.messageMetadata)
149
213
  .build();
150
- await this._core.publishDiscrete({ name: 'start', data: '', headers: h }, perWrite);
151
- break;
214
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
215
+ return;
152
216
  }
153
-
154
217
  case 'start-step': {
155
- await this._core.publishDiscrete({ name: 'start-step', data: '' }, perWrite);
156
- break;
218
+ const h = headerWriter().str('type', 'start-step').build();
219
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
220
+ return;
157
221
  }
158
-
159
222
  case 'finish-step': {
160
- await this._core.publishDiscrete({ name: 'finish-step', data: '' }, perWrite);
161
- break;
223
+ const h = headerWriter().str('type', 'finish-step').build();
224
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
225
+ return;
162
226
  }
163
-
164
227
  case 'finish': {
165
228
  const h = headerWriter()
229
+ .str('type', 'finish')
166
230
  .str('finishReason', chunk.finishReason)
167
231
  .json('messageMetadata', chunk.messageMetadata)
168
232
  .build();
169
- await this._core.publishDiscrete({ name: 'finish', data: '', headers: h }, perWrite);
170
- break;
233
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
234
+ return;
171
235
  }
172
-
173
236
  case 'error': {
174
- await this._core.publishDiscrete({ name: 'error', data: chunk.errorText }, perWrite);
175
- break;
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;
176
240
  }
177
-
178
241
  case 'abort': {
179
- this._aborted = true;
180
- await this._core.abortAllStreams(perWrite);
242
+ this._cancelled = true;
243
+ await this._core.cancelAllStreams(perWrite);
181
244
  await this._core.publishDiscrete(
182
- { name: 'abort', data: chunk.reason ?? '', headers: { [HEADER_STATUS]: 'aborted' } },
245
+ {
246
+ name: EVENT_AI_OUTPUT,
247
+ data: chunk.reason ?? '',
248
+ codecHeaders: headerWriter().str('type', 'abort').build(),
249
+ transportHeaders: { [HEADER_STATUS]: 'cancelled' },
250
+ },
183
251
  perWrite,
184
252
  );
185
- break;
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;
186
259
  }
187
260
 
188
- // -- Discrete: tool lifecycle events ----------------------------------
189
-
261
+ // -- Tool lifecycle (discrete) ----------------------------------------
190
262
  case 'tool-input-error': {
191
263
  const h = headerWriter()
264
+ .str('type', 'tool-input-error')
192
265
  .str('toolCallId', chunk.toolCallId)
193
266
  .str('toolName', chunk.toolName)
194
267
  .bool('dynamic', chunk.dynamic)
@@ -196,149 +269,215 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
196
269
  .bool('providerExecuted', chunk.providerExecuted)
197
270
  .json('providerMetadata', chunk.providerMetadata)
198
271
  .build();
199
- await this._core.publishDiscrete({
200
- name: 'tool-input-error',
201
- data: { errorText: chunk.errorText, input: chunk.input },
202
- headers: h,
203
- });
204
- break;
205
- }
206
-
207
- case 'tool-output-available': {
208
- const h = headerWriter()
209
- .str('toolCallId', chunk.toolCallId)
210
- .bool('dynamic', chunk.dynamic)
211
- .bool('providerExecuted', chunk.providerExecuted)
212
- .bool('preliminary', chunk.preliminary)
213
- .build();
214
- await this._core.publishDiscrete({
215
- name: 'tool-output-available',
216
- data: { output: chunk.output },
217
- headers: h,
218
- });
219
- break;
220
- }
221
-
222
- case 'tool-output-error': {
223
- const h = headerWriter()
224
- .str('toolCallId', chunk.toolCallId)
225
- .bool('dynamic', chunk.dynamic)
226
- .bool('providerExecuted', chunk.providerExecuted)
227
- .build();
228
- await this._core.publishDiscrete({
229
- name: 'tool-output-error',
230
- data: { errorText: chunk.errorText },
231
- headers: h,
232
- });
233
- break;
234
- }
235
-
236
- case 'tool-approval-request': {
237
- const h = headerWriter().str('toolCallId', chunk.toolCallId).str('approvalId', chunk.approvalId).build();
238
- await this._core.publishDiscrete({ name: 'tool-approval-request', data: '', headers: h }, perWrite);
239
- break;
272
+ await this._core.publishDiscrete(
273
+ { name: EVENT_AI_OUTPUT, data: { errorText: chunk.errorText, input: chunk.input }, codecHeaders: h },
274
+ perWrite,
275
+ );
276
+ return;
240
277
  }
241
-
278
+ case 'tool-output-available':
279
+ case 'tool-output-error':
280
+ case 'tool-approval-request':
242
281
  case 'tool-output-denied': {
243
- const h = headerWriter().str('toolCallId', chunk.toolCallId).build();
244
- await this._core.publishDiscrete({ name: 'tool-output-denied', data: '', headers: h }, perWrite);
245
- break;
282
+ await this._core.publishDiscrete(buildToolOutputPayload(chunk), perWrite);
283
+ return;
246
284
  }
247
285
 
248
- // -- Discrete: content parts ------------------------------------------
249
-
286
+ // -- Content parts (discrete) -----------------------------------------
250
287
  case 'file': {
251
288
  const h = headerWriter()
289
+ .str('type', 'file')
252
290
  .str('mediaType', chunk.mediaType)
253
291
  .json('providerMetadata', chunk.providerMetadata)
254
292
  .build();
255
- await this._core.publishDiscrete({ name: 'file', data: chunk.url, headers: h }, perWrite);
256
- break;
293
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.url, codecHeaders: h }, perWrite);
294
+ return;
257
295
  }
258
-
259
296
  case 'source-url': {
260
297
  const h = headerWriter()
298
+ .str('type', 'source-url')
261
299
  .str('sourceId', chunk.sourceId)
262
300
  .str('title', chunk.title)
263
301
  .json('providerMetadata', chunk.providerMetadata)
264
302
  .build();
265
- await this._core.publishDiscrete({ name: 'source-url', data: chunk.url, headers: h }, perWrite);
266
- break;
303
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.url, codecHeaders: h }, perWrite);
304
+ return;
267
305
  }
268
-
269
306
  case 'source-document': {
270
307
  const h = headerWriter()
308
+ .str('type', 'source-document')
271
309
  .str('sourceId', chunk.sourceId)
272
310
  .str('mediaType', chunk.mediaType)
273
311
  .str('title', chunk.title)
274
312
  .str('filename', chunk.filename)
275
313
  .json('providerMetadata', chunk.providerMetadata)
276
314
  .build();
277
- await this._core.publishDiscrete({ name: 'source-document', data: '', headers: h }, perWrite);
278
- break;
315
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
316
+ return;
279
317
  }
280
318
 
281
- case 'message-metadata': {
282
- const h = headerWriter().json('messageMetadata', chunk.messageMetadata).build();
283
- await this._core.publishDiscrete({ name: 'message-metadata', data: '', headers: h }, perWrite);
284
- break;
285
- }
286
-
287
- // -- Discrete: data-* custom chunks -----------------------------------
288
-
319
+ // -- data-* (discrete) ------------------------------------------------
289
320
  default: {
290
321
  if (chunk.type.startsWith('data-')) {
291
- const h = headerWriter().str('id', chunk.id).bool('transient', chunk.transient).build();
292
- const ephemeral = chunk.transient === true;
293
- await this._core.publishDiscrete({ name: chunk.type, data: chunk.data, headers: h, ephemeral }, perWrite);
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;
294
336
  }
295
- break;
337
+ throw new Ably.ErrorInfo(
338
+ `unable to publish output; unsupported chunk type '${chunk.type}'`,
339
+ ErrorCode.InvalidArgument,
340
+ 400,
341
+ );
296
342
  }
297
343
  }
298
344
  }
299
345
 
300
- async writeEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<Ably.PublishResult> {
301
- if (!chunk.type.startsWith('data-')) {
302
- throw new Ably.ErrorInfo(
303
- `unable to write event; only data-* chunk types are supported, got '${chunk.type}'`,
304
- ErrorCode.InvalidArgument,
305
- 400,
306
- );
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' };
307
364
  }
308
- const h = headerWriter()
309
- .str('id', 'id' in chunk ? chunk.id : undefined)
310
- .bool('transient', 'transient' in chunk ? chunk.transient : undefined)
311
- .build();
312
- const ephemeral = 'transient' in chunk && chunk.transient === true;
313
- return this._core.publishDiscrete(
314
- { name: chunk.type, data: 'data' in chunk ? chunk.data : undefined, headers: h, ephemeral },
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 },
315
393
  perWrite,
316
394
  );
317
395
  }
318
396
 
319
- async writeMessages(messages: AI.UIMessage[], perWrite?: WriteOptions): Promise<Ably.PublishResult> {
320
- const payloads = messages.flatMap((msg) => encodeMessagePayloads(msg));
321
- return this._core.publishDiscreteBatch(payloads, perWrite);
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
+ );
322
412
  }
323
413
 
324
- async abort(reason?: string): Promise<void> {
325
- if (this._aborted) return;
326
- this._aborted = true;
327
- await this._core.abortAllStreams();
328
- await this._core.publishDiscrete({
329
- name: 'abort',
330
- data: reason ?? '',
331
- headers: { [HEADER_STATUS]: 'aborted' },
332
- });
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);
333
431
  }
432
+ }
334
433
 
335
- async close(): Promise<void> {
336
- await this._core.close();
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
+ }
337
476
  }
338
- }
477
+ };
339
478
 
340
479
  // ---------------------------------------------------------------------------
341
- // Message payload encoding (stateless helper)
480
+ // User-message per-part payload encoding
342
481
  // ---------------------------------------------------------------------------
343
482
 
344
483
  const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
@@ -348,23 +487,31 @@ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
348
487
  for (const part of message.parts) {
349
488
  switch (part.type) {
350
489
  case 'text': {
351
- payloads.push({ name: 'text', data: part.text, headers: headerWriter().str('messageId', messageId).build() });
490
+ payloads.push({
491
+ name: EVENT_AI_INPUT,
492
+ data: part.text,
493
+ codecHeaders: headerWriter().str('type', 'text').str('messageId', messageId).build(),
494
+ });
352
495
  break;
353
496
  }
354
497
  case 'file': {
355
498
  payloads.push({
356
- name: 'file',
499
+ name: EVENT_AI_INPUT,
357
500
  data: part.url,
358
- headers: headerWriter().str('messageId', messageId).str('mediaType', part.mediaType).build(),
501
+ codecHeaders: headerWriter()
502
+ .str('type', 'file')
503
+ .str('messageId', messageId)
504
+ .str('mediaType', part.mediaType)
505
+ .build(),
359
506
  });
360
507
  break;
361
508
  }
362
509
  default: {
363
510
  if (isDataUIPart(part)) {
364
511
  payloads.push({
365
- name: part.type,
512
+ name: EVENT_AI_INPUT,
366
513
  data: part.data,
367
- headers: headerWriter().str('messageId', messageId).str('id', part.id).build(),
514
+ codecHeaders: headerWriter().str('type', part.type).str('messageId', messageId).str('id', part.id).build(),
368
515
  });
369
516
  }
370
517
  break;
@@ -373,7 +520,12 @@ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
373
520
  }
374
521
 
375
522
  if (payloads.length === 0) {
376
- payloads.push({ name: 'text', data: '', headers: headerWriter().str('messageId', messageId).build() });
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
+ });
377
529
  }
378
530
 
379
531
  return payloads;
@@ -384,13 +536,13 @@ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
384
536
  // ---------------------------------------------------------------------------
385
537
 
386
538
  /**
387
- * Create a Vercel AI SDK encoder that maps UIMessageChunk events to Ably
388
- * channel operations via the encoder core.
539
+ * Create a Vercel AI SDK encoder that maps VercelInput / VercelOutput to
540
+ * Ably channel operations via the encoder core.
389
541
  * @param writer - The channel writer to publish messages through.
390
542
  * @param options - Encoder configuration (clientId, extras, hooks, logger).
391
- * @returns A {@link StreamEncoder} for UIMessageChunk/UIMessage.
543
+ * @returns An {@link Encoder} typed in both directions for the Vercel codec.
392
544
  */
393
545
  export const createEncoder = (
394
546
  writer: ChannelWriter,
395
547
  options: EncoderCoreOptions = {},
396
- ): StreamEncoder<AI.UIMessageChunk, AI.UIMessage> => new DefaultUIMessageEncoder(writer, options);
548
+ ): Encoder<VercelInput, VercelOutput> => new DefaultUIMessageEncoder(writer, options);