@ably/ai-transport 0.1.0 → 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 (163) hide show
  1. package/README.md +91 -100
  2. package/dist/ably-ai-transport.js +1553 -1238
  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 +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 +407 -115
  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 +96 -18
  18. package/dist/core/transport/index.d.ts +5 -6
  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 -9
  24. package/dist/core/transport/run-manager.d.ts +78 -0
  25. package/dist/core/transport/tree.d.ts +373 -109
  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 -553
  32. package/dist/core/transport/view.d.ts +272 -84
  33. package/dist/errors.d.ts +21 -10
  34. package/dist/index.d.ts +6 -8
  35. package/dist/logger.d.ts +12 -0
  36. package/dist/react/ably-ai-transport-react.js +976 -990
  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 +12 -12
  44. package/dist/react/internal/use-resolved-session.d.ts +36 -0
  45. package/dist/react/use-ably-messages.d.ts +17 -14
  46. package/dist/react/use-client-session.d.ts +81 -0
  47. package/dist/react/use-create-view.d.ts +14 -13
  48. package/dist/react/use-tree.d.ts +30 -15
  49. package/dist/react/use-view.d.ts +82 -51
  50. package/dist/utils.d.ts +32 -23
  51. package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
  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 +2 -2
  61. package/dist/vercel/index.d.ts +4 -5
  62. package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
  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 +33 -8
  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 +7 -6
  67. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
  68. package/dist/vercel/react/index.d.ts +1 -2
  69. package/dist/vercel/react/use-chat-transport.d.ts +30 -26
  70. package/dist/vercel/react/use-message-sync.d.ts +17 -30
  71. package/dist/vercel/run-end-reason.d.ts +29 -0
  72. package/dist/vercel/transport/chat-transport.d.ts +43 -24
  73. package/dist/vercel/transport/index.d.ts +25 -21
  74. package/dist/vercel/transport/run-output-stream.d.ts +56 -0
  75. package/dist/version.d.ts +2 -0
  76. package/package.json +30 -23
  77. package/src/constants.ts +124 -51
  78. package/src/core/agent.ts +68 -0
  79. package/src/core/codec/decoder.ts +71 -98
  80. package/src/core/codec/encoder.ts +113 -65
  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 +436 -120
  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 +181 -22
  89. package/src/core/transport/index.ts +25 -26
  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 +54 -39
  95. package/src/core/transport/run-manager.ts +249 -0
  96. package/src/core/transport/tree.ts +926 -308
  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 -706
  103. package/src/core/transport/view.ts +864 -433
  104. package/src/errors.ts +22 -9
  105. package/src/event-emitter.ts +3 -2
  106. package/src/index.ts +52 -41
  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 +23 -13
  112. package/src/react/internal/use-resolved-session.ts +63 -0
  113. package/src/react/use-ably-messages.ts +32 -22
  114. package/src/react/use-client-session.ts +201 -0
  115. package/src/react/use-create-view.ts +33 -29
  116. package/src/react/use-tree.ts +61 -30
  117. package/src/react/use-view.ts +139 -97
  118. package/src/utils.ts +63 -45
  119. package/src/vercel/codec/decoder.ts +336 -258
  120. package/src/vercel/codec/encoder.ts +343 -205
  121. package/src/vercel/codec/events.ts +87 -0
  122. package/src/vercel/codec/index.ts +60 -13
  123. package/src/vercel/codec/reducer.ts +977 -0
  124. package/src/vercel/codec/tool-transitions.ts +2 -2
  125. package/src/vercel/index.ts +6 -19
  126. package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
  127. package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
  128. package/src/vercel/react/index.ts +3 -5
  129. package/src/vercel/react/use-chat-transport.ts +47 -49
  130. package/src/vercel/react/use-message-sync.ts +80 -39
  131. package/src/vercel/run-end-reason.ts +78 -0
  132. package/src/vercel/transport/chat-transport.ts +392 -98
  133. package/src/vercel/transport/index.ts +39 -38
  134. package/src/vercel/transport/run-output-stream.ts +170 -0
  135. package/src/version.ts +2 -0
  136. package/dist/core/transport/client-transport.d.ts +0 -10
  137. package/dist/core/transport/decode-history.d.ts +0 -43
  138. package/dist/core/transport/server-transport.d.ts +0 -7
  139. package/dist/core/transport/stream-router.d.ts +0 -29
  140. package/dist/core/transport/turn-manager.d.ts +0 -37
  141. package/dist/react/contexts/transport-context.d.ts +0 -31
  142. package/dist/react/contexts/transport-provider.d.ts +0 -49
  143. package/dist/react/create-transport-hooks.d.ts +0 -124
  144. package/dist/react/use-active-turns.d.ts +0 -12
  145. package/dist/react/use-client-transport.d.ts +0 -80
  146. package/dist/vercel/codec/accumulator.d.ts +0 -21
  147. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
  148. package/dist/vercel/tool-approvals.d.ts +0 -124
  149. package/dist/vercel/tool-events.d.ts +0 -26
  150. package/src/core/transport/client-transport.ts +0 -977
  151. package/src/core/transport/decode-history.ts +0 -485
  152. package/src/core/transport/server-transport.ts +0 -612
  153. package/src/core/transport/stream-router.ts +0 -136
  154. package/src/core/transport/turn-manager.ts +0 -165
  155. package/src/react/contexts/transport-context.ts +0 -37
  156. package/src/react/contexts/transport-provider.tsx +0 -164
  157. package/src/react/create-transport-hooks.ts +0 -144
  158. package/src/react/use-active-turns.ts +0 -72
  159. package/src/react/use-client-transport.ts +0 -197
  160. package/src/vercel/codec/accumulator.ts +0 -588
  161. package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
  162. package/src/vercel/tool-approvals.ts +0 -380
  163. package/src/vercel/tool-events.ts +0 -53
@@ -1,137 +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
- // Discrete event payload builder
49
+ // Default implementation
45
50
  // ---------------------------------------------------------------------------
46
51
 
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
- }
52
+ class DefaultUIMessageEncoder implements Encoder<VercelInput, VercelOutput> {
53
+ private readonly _core: EncoderCore;
54
+ private readonly _messageId: string | undefined;
55
+ private _cancelled = false;
79
56
 
80
- case 'tool-output-denied': {
81
- const h = headerWriter().str('toolCallId', chunk.toolCallId).build();
82
- return { name: 'tool-output-denied', data: '', headers: h };
83
- }
57
+ constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
58
+ this._core = createEncoderCore(writer, options);
59
+ this._messageId = options.messageId;
60
+ }
84
61
 
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 };
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;
93
83
  }
94
- throw new Ably.ErrorInfo(
95
- `unable to write event; unsupported chunk type '${chunk.type}'`,
96
- ErrorCode.InvalidArgument,
97
- 400,
98
- );
99
84
  }
100
85
  }
101
- };
102
86
 
103
- // ---------------------------------------------------------------------------
104
- // Default implementation
105
- // ---------------------------------------------------------------------------
87
+ async publishOutput(output: VercelOutput, options?: WriteOptions): Promise<void> {
88
+ await this._publishChunk(output, options);
89
+ }
106
90
 
107
- class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIMessage> {
108
- private readonly _core: EncoderCore;
109
- private readonly _messageId: string | undefined;
110
- private _aborted = false;
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
+ }
111
102
 
112
- constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
113
- this._core = createEncoderCore(writer, options);
114
- this._messageId = options.messageId;
103
+ async close(): Promise<void> {
104
+ await this._core.close();
115
105
  }
116
106
 
117
- async appendEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
118
- switch (chunk.type) {
119
- // -- Stream start: open a message stream with persistent headers -------
107
+ // -------------------------------------------------------------------------
108
+ // VercelOutput routing — UIMessageChunk
109
+ // -------------------------------------------------------------------------
120
110
 
111
+ private async _publishChunk(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
112
+ switch (chunk.type) {
113
+ // -- Stream start -----------------------------------------------------
121
114
  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;
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;
125
122
  }
126
-
127
123
  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;
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
131
  }
132
-
133
132
  case 'tool-input-start': {
134
133
  const h = headerWriter()
134
+ .str('type', 'tool-input')
135
135
  .str('toolCallId', chunk.toolCallId)
136
136
  .str('toolName', chunk.toolName)
137
137
  .bool('dynamic', chunk.dynamic)
@@ -139,57 +139,59 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
139
139
  .bool('providerExecuted', chunk.providerExecuted)
140
140
  .json('providerMetadata', chunk.providerMetadata)
141
141
  .build();
142
- await this._core.startStream(chunk.toolCallId, { name: 'tool-input', data: '', headers: h }, perWrite);
143
- break;
142
+ await this._core.startStream(chunk.toolCallId, { name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
143
+ return;
144
144
  }
145
145
 
146
- // -- Stream append: data only, core carries persistent headers --------
147
-
146
+ // -- Stream append ----------------------------------------------------
148
147
  case 'text-delta': {
149
148
  this._core.appendStream(chunk.id, chunk.delta);
150
- break;
149
+ return;
151
150
  }
152
-
153
151
  case 'reasoning-delta': {
154
152
  this._core.appendStream(chunk.id, chunk.delta);
155
- break;
153
+ return;
156
154
  }
157
-
158
155
  case 'tool-input-delta': {
159
156
  this._core.appendStream(chunk.toolCallId, chunk.inputTextDelta);
160
- break;
157
+ return;
161
158
  }
162
159
 
163
- // -- Stream close: pass all chunk headers, core merges with persistent
164
-
160
+ // -- Stream close -----------------------------------------------------
165
161
  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;
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
169
  }
170
-
171
170
  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;
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;
175
178
  }
176
-
177
179
  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
180
  try {
181
181
  const h = headerWriter()
182
+ .str('type', 'tool-input')
182
183
  .str('toolCallId', chunk.toolCallId)
183
184
  .str('toolName', chunk.toolName)
184
185
  .json('providerMetadata', chunk.providerMetadata)
185
186
  .build();
186
- 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 });
187
188
  } catch (error: unknown) {
188
- // 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.
189
190
  if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
190
191
  throw error;
191
192
  }
192
193
  const h = headerWriter()
194
+ .str('type', 'tool-input')
193
195
  .str('toolCallId', chunk.toolCallId)
194
196
  .str('toolName', chunk.toolName)
195
197
  .bool('dynamic', chunk.dynamic)
@@ -197,60 +199,69 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
197
199
  .bool('providerExecuted', chunk.providerExecuted)
198
200
  .json('providerMetadata', chunk.providerMetadata)
199
201
  .build();
200
- 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 });
201
203
  }
202
- break;
204
+ return;
203
205
  }
204
206
 
205
- // -- Discrete: lifecycle events ---------------------------------------
206
-
207
+ // -- Lifecycle (discrete) ---------------------------------------------
207
208
  case 'start': {
208
209
  const h = headerWriter()
210
+ .str('type', 'start')
209
211
  .str('messageId', chunk.messageId ?? this._messageId)
210
212
  .json('messageMetadata', chunk.messageMetadata)
211
213
  .build();
212
- await this._core.publishDiscrete({ name: 'start', data: '', headers: h }, perWrite);
213
- break;
214
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
215
+ return;
214
216
  }
215
-
216
217
  case 'start-step': {
217
- await this._core.publishDiscrete({ name: 'start-step', data: '' }, perWrite);
218
- 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;
219
221
  }
220
-
221
222
  case 'finish-step': {
222
- await this._core.publishDiscrete({ name: 'finish-step', data: '' }, perWrite);
223
- 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;
224
226
  }
225
-
226
227
  case 'finish': {
227
228
  const h = headerWriter()
229
+ .str('type', 'finish')
228
230
  .str('finishReason', chunk.finishReason)
229
231
  .json('messageMetadata', chunk.messageMetadata)
230
232
  .build();
231
- await this._core.publishDiscrete({ name: 'finish', data: '', headers: h }, perWrite);
232
- break;
233
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
234
+ return;
233
235
  }
234
-
235
236
  case 'error': {
236
- await this._core.publishDiscrete({ name: 'error', data: chunk.errorText }, perWrite);
237
- 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;
238
240
  }
239
-
240
241
  case 'abort': {
241
- this._aborted = true;
242
- await this._core.abortAllStreams(perWrite);
242
+ this._cancelled = true;
243
+ await this._core.cancelAllStreams(perWrite);
243
244
  await this._core.publishDiscrete(
244
- { 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
+ },
245
251
  perWrite,
246
252
  );
247
- 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;
248
259
  }
249
260
 
250
- // -- Discrete: tool lifecycle events ----------------------------------
251
-
261
+ // -- Tool lifecycle (discrete) ----------------------------------------
252
262
  case 'tool-input-error': {
253
263
  const h = headerWriter()
264
+ .str('type', 'tool-input-error')
254
265
  .str('toolCallId', chunk.toolCallId)
255
266
  .str('toolName', chunk.toolName)
256
267
  .bool('dynamic', chunk.dynamic)
@@ -258,101 +269,215 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
258
269
  .bool('providerExecuted', chunk.providerExecuted)
259
270
  .json('providerMetadata', chunk.providerMetadata)
260
271
  .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;
272
+ await this._core.publishDiscrete(
273
+ { name: EVENT_AI_OUTPUT, data: { errorText: chunk.errorText, input: chunk.input }, codecHeaders: h },
274
+ perWrite,
275
+ );
276
+ return;
267
277
  }
268
-
269
278
  case 'tool-output-available':
270
279
  case 'tool-output-error':
271
280
  case 'tool-approval-request':
272
281
  case 'tool-output-denied': {
273
- await this._core.publishDiscrete(buildDiscretePayload(chunk), perWrite);
274
- break;
282
+ await this._core.publishDiscrete(buildToolOutputPayload(chunk), perWrite);
283
+ return;
275
284
  }
276
285
 
277
- // -- Discrete: content parts ------------------------------------------
278
-
286
+ // -- Content parts (discrete) -----------------------------------------
279
287
  case 'file': {
280
288
  const h = headerWriter()
289
+ .str('type', 'file')
281
290
  .str('mediaType', chunk.mediaType)
282
291
  .json('providerMetadata', chunk.providerMetadata)
283
292
  .build();
284
- await this._core.publishDiscrete({ name: 'file', data: chunk.url, headers: h }, perWrite);
285
- break;
293
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.url, codecHeaders: h }, perWrite);
294
+ return;
286
295
  }
287
-
288
296
  case 'source-url': {
289
297
  const h = headerWriter()
298
+ .str('type', 'source-url')
290
299
  .str('sourceId', chunk.sourceId)
291
300
  .str('title', chunk.title)
292
301
  .json('providerMetadata', chunk.providerMetadata)
293
302
  .build();
294
- await this._core.publishDiscrete({ name: 'source-url', data: chunk.url, headers: h }, perWrite);
295
- break;
303
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: chunk.url, codecHeaders: h }, perWrite);
304
+ return;
296
305
  }
297
-
298
306
  case 'source-document': {
299
307
  const h = headerWriter()
308
+ .str('type', 'source-document')
300
309
  .str('sourceId', chunk.sourceId)
301
310
  .str('mediaType', chunk.mediaType)
302
311
  .str('title', chunk.title)
303
312
  .str('filename', chunk.filename)
304
313
  .json('providerMetadata', chunk.providerMetadata)
305
314
  .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;
315
+ await this._core.publishDiscrete({ name: EVENT_AI_OUTPUT, data: '', codecHeaders: h }, perWrite);
316
+ return;
314
317
  }
315
318
 
316
- // -- Discrete: data-* custom chunks -----------------------------------
317
-
319
+ // -- data-* (discrete) ------------------------------------------------
318
320
  default: {
319
321
  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);
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;
323
336
  }
324
- break;
337
+ throw new Ably.ErrorInfo(
338
+ `unable to publish output; unsupported chunk type '${chunk.type}'`,
339
+ ErrorCode.InvalidArgument,
340
+ 400,
341
+ );
325
342
  }
326
343
  }
327
344
  }
328
345
 
329
- async writeEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<Ably.PublishResult> {
330
- return this._core.publishDiscrete(buildDiscretePayload(chunk), perWrite);
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);
331
366
  }
332
367
 
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);
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);
336
379
  }
337
380
 
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
- });
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
+ );
347
395
  }
348
396
 
349
- async close(): Promise<void> {
350
- await this._core.close();
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);
351
431
  }
352
432
  }
353
433
 
354
434
  // ---------------------------------------------------------------------------
355
- // Message payload encoding (stateless helper)
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
356
481
  // ---------------------------------------------------------------------------
357
482
 
358
483
  const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
@@ -362,23 +487,31 @@ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
362
487
  for (const part of message.parts) {
363
488
  switch (part.type) {
364
489
  case 'text': {
365
- 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
+ });
366
495
  break;
367
496
  }
368
497
  case 'file': {
369
498
  payloads.push({
370
- name: 'file',
499
+ name: EVENT_AI_INPUT,
371
500
  data: part.url,
372
- 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(),
373
506
  });
374
507
  break;
375
508
  }
376
509
  default: {
377
510
  if (isDataUIPart(part)) {
378
511
  payloads.push({
379
- name: part.type,
512
+ name: EVENT_AI_INPUT,
380
513
  data: part.data,
381
- 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(),
382
515
  });
383
516
  }
384
517
  break;
@@ -387,7 +520,12 @@ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
387
520
  }
388
521
 
389
522
  if (payloads.length === 0) {
390
- 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
+ });
391
529
  }
392
530
 
393
531
  return payloads;
@@ -398,13 +536,13 @@ const encodeMessagePayloads = (message: AI.UIMessage): MessagePayload[] => {
398
536
  // ---------------------------------------------------------------------------
399
537
 
400
538
  /**
401
- * Create a Vercel AI SDK encoder that maps UIMessageChunk events to Ably
402
- * 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.
403
541
  * @param writer - The channel writer to publish messages through.
404
542
  * @param options - Encoder configuration (clientId, extras, hooks, logger).
405
- * @returns A {@link StreamEncoder} for UIMessageChunk/UIMessage.
543
+ * @returns An {@link Encoder} typed in both directions for the Vercel codec.
406
544
  */
407
545
  export const createEncoder = (
408
546
  writer: ChannelWriter,
409
547
  options: EncoderCoreOptions = {},
410
- ): StreamEncoder<AI.UIMessageChunk, AI.UIMessage> => new DefaultUIMessageEncoder(writer, options);
548
+ ): Encoder<VercelInput, VercelOutput> => new DefaultUIMessageEncoder(writer, options);