@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.
- package/README.md +91 -100
- package/dist/ably-ai-transport.js +1553 -1238
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +116 -42
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +407 -115
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +96 -18
- package/dist/core/transport/index.d.ts +5 -6
- package/dist/core/transport/internal/bounded-map.d.ts +20 -0
- package/dist/core/transport/invocation.d.ts +74 -0
- package/dist/core/transport/load-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -9
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +373 -109
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -553
- package/dist/core/transport/view.d.ts +272 -84
- package/dist/errors.d.ts +21 -10
- package/dist/index.d.ts +6 -8
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +976 -990
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/client-session-context.d.ts +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +12 -12
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +17 -14
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +14 -13
- package/dist/react/use-tree.d.ts +30 -15
- package/dist/react/use-view.d.ts +82 -51
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +2 -2
- package/dist/vercel/index.d.ts +4 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
- package/dist/vercel/react/index.d.ts +1 -2
- package/dist/vercel/react/use-chat-transport.d.ts +30 -26
- package/dist/vercel/react/use-message-sync.d.ts +17 -30
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +43 -24
- package/dist/vercel/transport/index.d.ts +25 -21
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +30 -23
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +113 -65
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +436 -120
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +181 -22
- package/src/core/transport/index.ts +25 -26
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +54 -39
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +926 -308
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -706
- package/src/core/transport/view.ts +864 -433
- package/src/errors.ts +22 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +52 -41
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +23 -13
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +32 -22
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +139 -97
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -258
- package/src/vercel/codec/encoder.ts +343 -205
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +60 -13
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +2 -2
- package/src/vercel/index.ts +6 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
- package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
- package/src/vercel/react/index.ts +3 -5
- package/src/vercel/react/use-chat-transport.ts +47 -49
- package/src/vercel/react/use-message-sync.ts +80 -39
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +392 -98
- package/src/vercel/transport/index.ts +39 -38
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/decode-history.d.ts +0 -43
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -29
- package/dist/core/transport/turn-manager.d.ts +0 -37
- package/dist/react/contexts/transport-context.d.ts +0 -31
- package/dist/react/contexts/transport-provider.d.ts +0 -49
- package/dist/react/create-transport-hooks.d.ts +0 -124
- package/dist/react/use-active-turns.d.ts +0 -12
- package/dist/react/use-client-transport.d.ts +0 -80
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
- package/dist/vercel/tool-approvals.d.ts +0 -124
- package/dist/vercel/tool-events.d.ts +0 -26
- package/src/core/transport/client-transport.ts +0 -977
- package/src/core/transport/decode-history.ts +0 -485
- package/src/core/transport/server-transport.ts +0 -612
- package/src/core/transport/stream-router.ts +0 -136
- package/src/core/transport/turn-manager.ts +0 -165
- package/src/react/contexts/transport-context.ts +0 -37
- package/src/react/contexts/transport-provider.tsx +0 -164
- package/src/react/create-transport-hooks.ts +0 -144
- package/src/react/use-active-turns.ts +0 -72
- package/src/react/use-client-transport.ts +0 -197
- package/src/vercel/codec/accumulator.ts +0 -588
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
- package/src/vercel/tool-approvals.ts +0 -380
- package/src/vercel/tool-events.ts +0 -53
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vercel AI SDK Decoder
|
|
2
|
+
* Vercel AI SDK Decoder.
|
|
3
3
|
*
|
|
4
|
-
* Maps Ably inbound messages to
|
|
4
|
+
* Maps Ably inbound messages to {@link DecodedMessage} — a `{ inputs,
|
|
5
|
+
* outputs }` tagged result. The decoder routes by the wire `name`
|
|
6
|
+
* (`ai-input` vs `ai-output`) so the SDK never has to inspect direction:
|
|
7
|
+
* input-side messages produce `VercelInput` variants; output-side
|
|
8
|
+
* messages produce `VercelOutput` (`UIMessageChunk`) variants.
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
10
|
+
* The `LifecycleTracker` is an internal helper used to pre-roll missing
|
|
11
|
+
* `start` / `start-step` chunks on mid-stream join (history compaction,
|
|
12
|
+
* rewind miss, partial page) so the reducer always sees a clean event
|
|
13
|
+
* sequence for streamed output.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
15
|
+
* Receive-side dispatch reads the wire `name` first and then routes by
|
|
16
|
+
* the codec `type` header carrying the codec event type. Codec headers live
|
|
17
|
+
* under `extras.ai.codec` and transport headers under `extras.ai.transport`;
|
|
18
|
+
* both are read unprefixed from their respective tier.
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
import type * as Ably from 'ably';
|
|
15
22
|
import type * as AI from 'ai';
|
|
16
23
|
|
|
17
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
EVENT_AI_INPUT,
|
|
26
|
+
EVENT_AI_OUTPUT,
|
|
27
|
+
HEADER_CODEC_MESSAGE_ID,
|
|
28
|
+
HEADER_DISCRETE,
|
|
29
|
+
HEADER_ROLE,
|
|
30
|
+
HEADER_RUN_ID,
|
|
31
|
+
} from '../../constants.js';
|
|
18
32
|
import type { DecoderCore, DecoderCoreHooks, DecoderCoreOptions } from '../../core/codec/decoder.js';
|
|
19
|
-
import { createDecoderCore
|
|
33
|
+
import { createDecoderCore } from '../../core/codec/decoder.js';
|
|
20
34
|
import type { LifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
|
|
21
35
|
import { createLifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
|
|
22
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
DecodedMessage,
|
|
38
|
+
Decoder,
|
|
39
|
+
MessagePayload,
|
|
40
|
+
StreamTrackerState,
|
|
41
|
+
UserMessage,
|
|
42
|
+
} from '../../core/codec/types.js';
|
|
23
43
|
import { type DomainHeaderReader, headerReader as rawHeaderReader, stripUndefined } from '../../utils.js';
|
|
44
|
+
import type { VercelInput, VercelOutput } from './events.js';
|
|
45
|
+
|
|
46
|
+
// Decoder-internal union — the codec emits inputs and outputs through the
|
|
47
|
+
// same flat list from the underlying core and partitions on the way out.
|
|
48
|
+
type AnyEvent = VercelInput | VercelOutput;
|
|
24
49
|
|
|
25
50
|
// ---------------------------------------------------------------------------
|
|
26
51
|
// Vercel-specific header reader (casts providerMetadata to AI.ProviderMetadata)
|
|
@@ -49,45 +74,31 @@ const headerReader = (headers: Record<string, string>): VercelHeaderReader => {
|
|
|
49
74
|
// Wire format types (trust boundaries for JSON-parsed data)
|
|
50
75
|
// ---------------------------------------------------------------------------
|
|
51
76
|
|
|
52
|
-
/** Wire format for tool-input-error data payload. */
|
|
77
|
+
/** Wire format for the agent-side `tool-input-error` chunk data payload. */
|
|
53
78
|
interface ToolInputErrorWireData {
|
|
54
79
|
errorText?: string;
|
|
55
80
|
input?: unknown;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
|
-
/** Wire format for tool-output-available data payload. */
|
|
83
|
+
/** Wire format for the `tool-output-available` (agent) / `tool-result` (client) data payload. */
|
|
59
84
|
interface ToolOutputAvailableWireData {
|
|
60
85
|
output?: unknown;
|
|
61
86
|
}
|
|
62
87
|
|
|
63
|
-
/** Wire format for tool-output-error data payload. */
|
|
64
|
-
interface
|
|
88
|
+
/** Wire format for the agent-side `tool-output-error` chunk data payload. */
|
|
89
|
+
interface AgentToolOutputErrorWireData {
|
|
65
90
|
errorText?: string;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
type Out = DecoderOutput<AI.UIMessageChunk, AI.UIMessage>;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Bind eventOutput to the Vercel domain types.
|
|
76
|
-
* @param chunk - The UIMessageChunk to wrap.
|
|
77
|
-
* @returns A single-element decoder output array.
|
|
78
|
-
*/
|
|
79
|
-
const event = (chunk: AI.UIMessageChunk): Out[] => eventOutput<AI.UIMessageChunk, AI.UIMessage>(chunk);
|
|
93
|
+
/** Wire format for the client-side `tool-result-error` input data payload. */
|
|
94
|
+
interface ClientToolResultErrorWireData {
|
|
95
|
+
message?: string;
|
|
96
|
+
}
|
|
80
97
|
|
|
81
98
|
// ---------------------------------------------------------------------------
|
|
82
99
|
// JSON boundary helpers
|
|
83
100
|
// ---------------------------------------------------------------------------
|
|
84
101
|
|
|
85
|
-
/**
|
|
86
|
-
* Validate a finish reason string against the FinishReason union.
|
|
87
|
-
* @param value - The finish reason string from the wire, or undefined.
|
|
88
|
-
* @param fallback - Default finish reason if the value is not recognized.
|
|
89
|
-
* @returns The validated FinishReason.
|
|
90
|
-
*/
|
|
91
102
|
const parseFinishReason = (value: string | undefined, fallback: AI.FinishReason): AI.FinishReason => {
|
|
92
103
|
if (
|
|
93
104
|
value === 'stop' ||
|
|
@@ -102,19 +113,8 @@ const parseFinishReason = (value: string | undefined, fallback: AI.FinishReason)
|
|
|
102
113
|
return fallback;
|
|
103
114
|
};
|
|
104
115
|
|
|
105
|
-
/**
|
|
106
|
-
* Type predicate for data-* message names.
|
|
107
|
-
* @param name - The message name to check.
|
|
108
|
-
* @returns True if the name starts with "data-".
|
|
109
|
-
*/
|
|
110
116
|
const isDataEventName = (name: string): name is `data-${string}` => name.startsWith('data-');
|
|
111
117
|
|
|
112
|
-
/**
|
|
113
|
-
* Parse a string as JSON, returning the raw string if parsing fails
|
|
114
|
-
* or undefined if empty.
|
|
115
|
-
* @param value - The string to attempt JSON parsing on.
|
|
116
|
-
* @returns The parsed value, the raw string on parse failure, or undefined if empty.
|
|
117
|
-
*/
|
|
118
118
|
const parseJsonOrString = (value: string): unknown => {
|
|
119
119
|
if (!value) return undefined;
|
|
120
120
|
try {
|
|
@@ -126,12 +126,22 @@ const parseJsonOrString = (value: string): unknown => {
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
// ---------------------------------------------------------------------------
|
|
129
|
-
// Streamed message event builders
|
|
129
|
+
// Streamed message event builders (output-side)
|
|
130
130
|
// ---------------------------------------------------------------------------
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Read the codec event type from a tracker's codec headers. The encoder
|
|
134
|
+
* stamps the codec `type` header on every `ai-output` publish; the value
|
|
135
|
+
* carries the AI-SDK chunk family (`text` / `reasoning` / `tool-input`)
|
|
136
|
+
* that the stream represents.
|
|
137
|
+
* @param tracker - The stream tracker carrying the persistent headers.
|
|
138
|
+
* @returns The codec event type, or the empty string when absent.
|
|
139
|
+
*/
|
|
140
|
+
const codecTypeOf = (tracker: StreamTrackerState): string => headerReader(tracker.codecHeaders).strOr('type', '');
|
|
141
|
+
|
|
132
142
|
const buildStartChunk = (tracker: StreamTrackerState): AI.UIMessageChunk => {
|
|
133
|
-
const r = headerReader(tracker.
|
|
134
|
-
switch (tracker
|
|
143
|
+
const r = headerReader(tracker.codecHeaders);
|
|
144
|
+
switch (codecTypeOf(tracker)) {
|
|
135
145
|
case 'text': {
|
|
136
146
|
return stripUndefined({
|
|
137
147
|
type: 'text-start' as const,
|
|
@@ -164,7 +174,7 @@ const buildStartChunk = (tracker: StreamTrackerState): AI.UIMessageChunk => {
|
|
|
164
174
|
};
|
|
165
175
|
|
|
166
176
|
const buildDeltaChunk = (tracker: StreamTrackerState, delta: string): AI.UIMessageChunk => {
|
|
167
|
-
switch (tracker
|
|
177
|
+
switch (codecTypeOf(tracker)) {
|
|
168
178
|
case 'text': {
|
|
169
179
|
return { type: 'text-delta', id: tracker.streamId, delta };
|
|
170
180
|
}
|
|
@@ -182,7 +192,7 @@ const buildDeltaChunk = (tracker: StreamTrackerState, delta: string): AI.UIMessa
|
|
|
182
192
|
|
|
183
193
|
const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AI.UIMessageChunk => {
|
|
184
194
|
const r = headerReader(closingHeaders);
|
|
185
|
-
switch (tracker
|
|
195
|
+
switch (codecTypeOf(tracker)) {
|
|
186
196
|
case 'text': {
|
|
187
197
|
return stripUndefined({
|
|
188
198
|
type: 'text-end' as const,
|
|
@@ -201,7 +211,7 @@ const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<strin
|
|
|
201
211
|
return stripUndefined({
|
|
202
212
|
type: 'tool-input-available' as const,
|
|
203
213
|
toolCallId: tracker.streamId,
|
|
204
|
-
toolName: r.strOr('toolName', headerReader(tracker.
|
|
214
|
+
toolName: r.strOr('toolName', headerReader(tracker.codecHeaders).strOr('toolName', '')),
|
|
205
215
|
input: parseJsonOrString(tracker.accumulated),
|
|
206
216
|
providerMetadata: r.providerMetadata(),
|
|
207
217
|
});
|
|
@@ -213,7 +223,7 @@ const buildEndChunk = (tracker: StreamTrackerState, closingHeaders: Record<strin
|
|
|
213
223
|
};
|
|
214
224
|
|
|
215
225
|
// ---------------------------------------------------------------------------
|
|
216
|
-
// Lifecycle tracker configuration (synthetic event phases)
|
|
226
|
+
// Lifecycle tracker configuration (synthetic event phases on mid-stream join)
|
|
217
227
|
// ---------------------------------------------------------------------------
|
|
218
228
|
|
|
219
229
|
const createVercelLifecycleTracker = (): LifecycleTracker<AI.UIMessageChunk> =>
|
|
@@ -228,107 +238,108 @@ const createVercelLifecycleTracker = (): LifecycleTracker<AI.UIMessageChunk> =>
|
|
|
228
238
|
},
|
|
229
239
|
]);
|
|
230
240
|
|
|
231
|
-
/**
|
|
232
|
-
* Run the lifecycle tracker and wrap results as DecoderOutput events.
|
|
233
|
-
* @param lifecycle - The lifecycle tracker instance.
|
|
234
|
-
* @param turnId - The turn scope ID.
|
|
235
|
-
* @param context - Context passed through to phase build functions.
|
|
236
|
-
* @returns Decoder outputs for any synthesized lifecycle events.
|
|
237
|
-
*/
|
|
238
|
-
const ensurePhases = (
|
|
239
|
-
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
240
|
-
turnId: string,
|
|
241
|
-
context: Record<string, string | undefined>,
|
|
242
|
-
): Out[] => lifecycle.ensurePhases(turnId, context).map((e) => ({ kind: 'event', event: e }));
|
|
243
|
-
|
|
244
241
|
// ---------------------------------------------------------------------------
|
|
245
|
-
// Discrete
|
|
242
|
+
// Discrete output decoders (ai-output → UIMessageChunk)
|
|
246
243
|
// ---------------------------------------------------------------------------
|
|
247
244
|
|
|
248
|
-
const decodeStart = (
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
const decodeStart = (
|
|
246
|
+
r: VercelHeaderReader,
|
|
247
|
+
runId: string,
|
|
248
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
249
|
+
): AI.UIMessageChunk[] => {
|
|
250
|
+
lifecycle.markEmitted(runId, 'start');
|
|
251
|
+
return [
|
|
251
252
|
stripUndefined({
|
|
252
253
|
type: 'start' as const,
|
|
253
254
|
messageId: r.str('messageId'),
|
|
254
255
|
messageMetadata: r.json('messageMetadata'),
|
|
255
256
|
}),
|
|
256
|
-
|
|
257
|
+
];
|
|
257
258
|
};
|
|
258
259
|
|
|
259
|
-
const decodeStartStep = (
|
|
260
|
-
lifecycle.markEmitted(
|
|
261
|
-
return
|
|
260
|
+
const decodeStartStep = (runId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AI.UIMessageChunk[] => {
|
|
261
|
+
lifecycle.markEmitted(runId, 'start-step');
|
|
262
|
+
return [{ type: 'start-step' }];
|
|
262
263
|
};
|
|
263
264
|
|
|
264
|
-
const decodeFinishStep = (
|
|
265
|
-
lifecycle.resetPhase(
|
|
266
|
-
return
|
|
265
|
+
const decodeFinishStep = (runId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AI.UIMessageChunk[] => {
|
|
266
|
+
lifecycle.resetPhase(runId, 'start-step');
|
|
267
|
+
return [{ type: 'finish-step' }];
|
|
267
268
|
};
|
|
268
269
|
|
|
269
|
-
const decodeFinish = (
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
const decodeFinish = (
|
|
271
|
+
r: VercelHeaderReader,
|
|
272
|
+
runId: string,
|
|
273
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
274
|
+
): AI.UIMessageChunk[] => {
|
|
275
|
+
lifecycle.clearScope(runId);
|
|
276
|
+
return [
|
|
272
277
|
stripUndefined({
|
|
273
278
|
type: 'finish' as const,
|
|
274
279
|
finishReason: parseFinishReason(r.str('finishReason'), 'stop'),
|
|
275
280
|
messageMetadata: r.json('messageMetadata'),
|
|
276
281
|
}),
|
|
277
|
-
|
|
282
|
+
];
|
|
278
283
|
};
|
|
279
284
|
|
|
280
|
-
const decodeError = (
|
|
281
|
-
|
|
285
|
+
const decodeError = (
|
|
286
|
+
data: unknown,
|
|
287
|
+
runId: string,
|
|
288
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
289
|
+
): AI.UIMessageChunk[] => {
|
|
290
|
+
lifecycle.clearScope(runId);
|
|
282
291
|
const errorText = typeof data === 'string' ? data : '';
|
|
283
|
-
return
|
|
292
|
+
return [{ type: 'error', errorText }];
|
|
284
293
|
};
|
|
285
294
|
|
|
286
|
-
const decodeAbort = (
|
|
287
|
-
|
|
295
|
+
const decodeAbort = (
|
|
296
|
+
data: unknown,
|
|
297
|
+
runId: string,
|
|
298
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
299
|
+
): AI.UIMessageChunk[] => {
|
|
300
|
+
lifecycle.clearScope(runId);
|
|
288
301
|
const reason = typeof data === 'string' && data ? data : undefined;
|
|
289
|
-
return
|
|
302
|
+
return [stripUndefined({ type: 'abort' as const, reason })];
|
|
290
303
|
};
|
|
291
304
|
|
|
292
|
-
const decodeMessageMetadata = (r: VercelHeaderReader):
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const decodeSourceUrl = (r: VercelHeaderReader, data: unknown):
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const decodeToolInputError = (r: VercelHeaderReader, data: unknown): Out[] => {
|
|
305
|
+
const decodeMessageMetadata = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
|
|
306
|
+
{ type: 'message-metadata', messageMetadata: r.json('messageMetadata') },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const decodeFile = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => [
|
|
310
|
+
stripUndefined({
|
|
311
|
+
type: 'file' as const,
|
|
312
|
+
url: typeof data === 'string' ? data : '',
|
|
313
|
+
mediaType: r.strOr('mediaType', ''),
|
|
314
|
+
providerMetadata: r.providerMetadata(),
|
|
315
|
+
}),
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const decodeSourceUrl = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => [
|
|
319
|
+
stripUndefined({
|
|
320
|
+
type: 'source-url' as const,
|
|
321
|
+
sourceId: r.strOr('sourceId', ''),
|
|
322
|
+
url: typeof data === 'string' ? data : '',
|
|
323
|
+
title: r.str('title'),
|
|
324
|
+
providerMetadata: r.providerMetadata(),
|
|
325
|
+
}),
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const decodeSourceDocument = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
|
|
329
|
+
stripUndefined({
|
|
330
|
+
type: 'source-document' as const,
|
|
331
|
+
sourceId: r.strOr('sourceId', ''),
|
|
332
|
+
mediaType: r.strOr('mediaType', ''),
|
|
333
|
+
title: r.strOr('title', ''),
|
|
334
|
+
filename: r.str('filename'),
|
|
335
|
+
providerMetadata: r.providerMetadata(),
|
|
336
|
+
}),
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
const decodeToolInputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
|
|
329
340
|
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
330
341
|
const parsed = data as ToolInputErrorWireData | undefined;
|
|
331
|
-
return
|
|
342
|
+
return [
|
|
332
343
|
stripUndefined({
|
|
333
344
|
type: 'tool-input-error' as const,
|
|
334
345
|
toolCallId: r.strOr('toolCallId', ''),
|
|
@@ -340,13 +351,13 @@ const decodeToolInputError = (r: VercelHeaderReader, data: unknown): Out[] => {
|
|
|
340
351
|
providerExecuted: r.bool('providerExecuted'),
|
|
341
352
|
providerMetadata: r.providerMetadata(),
|
|
342
353
|
}),
|
|
343
|
-
|
|
354
|
+
];
|
|
344
355
|
};
|
|
345
356
|
|
|
346
|
-
const
|
|
357
|
+
const decodeAgentToolOutputAvailable = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
|
|
347
358
|
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
348
359
|
const parsed = data as ToolOutputAvailableWireData | undefined;
|
|
349
|
-
return
|
|
360
|
+
return [
|
|
350
361
|
stripUndefined({
|
|
351
362
|
type: 'tool-output-available' as const,
|
|
352
363
|
toolCallId: r.strOr('toolCallId', ''),
|
|
@@ -355,13 +366,13 @@ const decodeToolOutputAvailable = (r: VercelHeaderReader, data: unknown): Out[]
|
|
|
355
366
|
providerExecuted: r.bool('providerExecuted'),
|
|
356
367
|
preliminary: r.bool('preliminary'),
|
|
357
368
|
}),
|
|
358
|
-
|
|
369
|
+
];
|
|
359
370
|
};
|
|
360
371
|
|
|
361
|
-
const
|
|
372
|
+
const decodeAgentToolOutputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
|
|
362
373
|
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
363
|
-
const parsed = data as
|
|
364
|
-
return
|
|
374
|
+
const parsed = data as AgentToolOutputErrorWireData | undefined;
|
|
375
|
+
return [
|
|
365
376
|
stripUndefined({
|
|
366
377
|
type: 'tool-output-error' as const,
|
|
367
378
|
toolCallId: r.strOr('toolCallId', ''),
|
|
@@ -369,92 +380,80 @@ const decodeToolOutputError = (r: VercelHeaderReader, data: unknown): Out[] => {
|
|
|
369
380
|
dynamic: r.bool('dynamic'),
|
|
370
381
|
providerExecuted: r.bool('providerExecuted'),
|
|
371
382
|
}),
|
|
372
|
-
|
|
383
|
+
];
|
|
373
384
|
};
|
|
374
385
|
|
|
375
|
-
const decodeToolApprovalRequest = (r: VercelHeaderReader):
|
|
376
|
-
|
|
386
|
+
const decodeToolApprovalRequest = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
|
|
387
|
+
{
|
|
377
388
|
type: 'tool-approval-request',
|
|
378
389
|
toolCallId: r.strOr('toolCallId', ''),
|
|
379
390
|
approvalId: r.strOr('approvalId', ''),
|
|
380
|
-
}
|
|
391
|
+
},
|
|
392
|
+
];
|
|
381
393
|
|
|
382
|
-
const decodeToolOutputDenied = (r: VercelHeaderReader):
|
|
383
|
-
|
|
394
|
+
const decodeToolOutputDenied = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
|
|
395
|
+
{ type: 'tool-output-denied', toolCallId: r.strOr('toolCallId', '') },
|
|
396
|
+
];
|
|
384
397
|
|
|
385
|
-
const decodeDataEvent = (name: `data-${string}`, r: VercelHeaderReader, data: unknown):
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
398
|
+
const decodeDataEvent = (name: `data-${string}`, r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => [
|
|
399
|
+
stripUndefined({
|
|
400
|
+
type: name,
|
|
401
|
+
data,
|
|
402
|
+
id: r.str('id'),
|
|
403
|
+
transient: r.bool('transient'),
|
|
404
|
+
}),
|
|
405
|
+
];
|
|
394
406
|
|
|
395
407
|
// ---------------------------------------------------------------------------
|
|
396
|
-
// Non-streaming tool-input helper
|
|
408
|
+
// Non-streaming tool-input helper (agent-side)
|
|
397
409
|
// ---------------------------------------------------------------------------
|
|
398
410
|
|
|
399
411
|
const decodeNonStreamingToolInput = (
|
|
400
412
|
r: VercelHeaderReader,
|
|
401
413
|
data: unknown,
|
|
402
|
-
|
|
414
|
+
runId: string,
|
|
403
415
|
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
404
|
-
):
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
type: 'tool-input-available' as const,
|
|
424
|
-
toolCallId: r.strOr('toolCallId', ''),
|
|
425
|
-
toolName: r.strOr('toolName', ''),
|
|
426
|
-
input: data,
|
|
427
|
-
providerMetadata: r.providerMetadata(),
|
|
428
|
-
}),
|
|
429
|
-
},
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
return outputs;
|
|
433
|
-
};
|
|
416
|
+
): AI.UIMessageChunk[] => [
|
|
417
|
+
...lifecycle.ensurePhases(runId, { messageId: r.str('messageId') }),
|
|
418
|
+
stripUndefined({
|
|
419
|
+
type: 'tool-input-start' as const,
|
|
420
|
+
toolCallId: r.strOr('toolCallId', ''),
|
|
421
|
+
toolName: r.strOr('toolName', ''),
|
|
422
|
+
dynamic: r.bool('dynamic'),
|
|
423
|
+
title: r.str('title'),
|
|
424
|
+
providerExecuted: r.bool('providerExecuted'),
|
|
425
|
+
providerMetadata: r.providerMetadata(),
|
|
426
|
+
}),
|
|
427
|
+
stripUndefined({
|
|
428
|
+
type: 'tool-input-available' as const,
|
|
429
|
+
toolCallId: r.strOr('toolCallId', ''),
|
|
430
|
+
toolName: r.strOr('toolName', ''),
|
|
431
|
+
input: data,
|
|
432
|
+
providerMetadata: r.providerMetadata(),
|
|
433
|
+
}),
|
|
434
|
+
];
|
|
434
435
|
|
|
435
436
|
// ---------------------------------------------------------------------------
|
|
436
|
-
//
|
|
437
|
+
// Input-side decoders (ai-input → VercelInput)
|
|
437
438
|
// ---------------------------------------------------------------------------
|
|
438
439
|
|
|
439
440
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* @param input - The discrete message payload to decode.
|
|
447
|
-
* @returns A single-element array with the reconstructed UIMessage, or empty if unrecognized.
|
|
441
|
+
* Decode a single discrete message part (from the user-message multi-part
|
|
442
|
+
* wire format) into a {@link UserMessage} carrying a one-part
|
|
443
|
+
* UIMessage. The reducer's `_foldUserMessage` merges parts that share
|
|
444
|
+
* the same codec-message-id.
|
|
445
|
+
* @param input - The discrete message payload (name, data, headers).
|
|
446
|
+
* @returns A single `user-message` input, or an empty array when the part type is unrecognised.
|
|
448
447
|
*/
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
const role = (h[HEADER_ROLE] ?? 'user') as AI.UIMessage['role'];
|
|
448
|
+
const decodeDiscreteMessagePart = (input: MessagePayload): VercelInput[] => {
|
|
449
|
+
const r = headerReader(input.codecHeaders ?? {});
|
|
450
|
+
const role = (input.transportHeaders?.[HEADER_ROLE] ?? 'user') as AI.UIMessage['role'];
|
|
453
451
|
const messageId = r.str('messageId') ?? '';
|
|
452
|
+
const codecType = r.strOr('type', '');
|
|
454
453
|
|
|
455
454
|
let part: AI.UIMessage['parts'][number] | undefined;
|
|
456
455
|
|
|
457
|
-
switch (
|
|
456
|
+
switch (codecType) {
|
|
458
457
|
case 'text': {
|
|
459
458
|
part = { type: 'text', text: typeof input.data === 'string' ? input.data : '' };
|
|
460
459
|
break;
|
|
@@ -468,9 +467,8 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
|
|
|
468
467
|
break;
|
|
469
468
|
}
|
|
470
469
|
default: {
|
|
471
|
-
if (isDataEventName(
|
|
472
|
-
|
|
473
|
-
part = stripUndefined({ type: input.name, id: r.str('id'), data: input.data }) as AI.UIMessage['parts'][number];
|
|
470
|
+
if (isDataEventName(codecType)) {
|
|
471
|
+
part = stripUndefined({ type: codecType, id: r.str('id'), data: input.data });
|
|
474
472
|
}
|
|
475
473
|
break;
|
|
476
474
|
}
|
|
@@ -479,76 +477,102 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
|
|
|
479
477
|
if (!part) return [];
|
|
480
478
|
|
|
481
479
|
const message: AI.UIMessage = { id: messageId, role, parts: [part] };
|
|
482
|
-
|
|
480
|
+
const userMessage: UserMessage<AI.UIMessage> = { kind: 'user-message', message };
|
|
481
|
+
return [userMessage];
|
|
483
482
|
};
|
|
484
483
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
* rather than a streaming lifecycle event. Distinguished by the `x-ably-discrete` header
|
|
488
|
-
* which {@link publishDiscreteBatch} sets on batch-published message payloads. Lifecycle
|
|
489
|
-
* events published via {@link publishDiscrete} (including streaming `data-*` chunks)
|
|
490
|
-
* do not carry this header.
|
|
491
|
-
* @param name - The Ably message name to check.
|
|
492
|
-
* @param headers - The Ably message headers to inspect for discrete marker presence.
|
|
493
|
-
* @returns True if this is a discrete message part, false if it's a lifecycle event.
|
|
494
|
-
*/
|
|
495
|
-
const isDiscreteMessagePart = (name: string, headers: Record<string, string>): boolean =>
|
|
496
|
-
(name === 'text' || name === 'file' || isDataEventName(name)) && HEADER_DISCRETE in headers;
|
|
497
|
-
|
|
498
|
-
const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
|
|
499
|
-
const h = input.headers ?? {};
|
|
500
|
-
const r = headerReader(h);
|
|
501
|
-
const turnId = h[HEADER_TURN_ID] ?? '';
|
|
502
|
-
|
|
503
|
-
// Discrete message parts from writeMessages (user messages, history entries).
|
|
504
|
-
// Distinguished from lifecycle events by the presence of x-ably-discrete.
|
|
505
|
-
if (isDiscreteMessagePart(input.name, h)) {
|
|
506
|
-
return decodeDiscreteMessage(input);
|
|
507
|
-
}
|
|
484
|
+
const isDiscreteMessagePart = (codecType: string, headers: Record<string, string>): boolean =>
|
|
485
|
+
(codecType === 'text' || codecType === 'file' || isDataEventName(codecType)) && HEADER_DISCRETE in headers;
|
|
508
486
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
487
|
+
const decodeClientToolResult = (codecMessageId: string, r: VercelHeaderReader, data: unknown): VercelInput[] => {
|
|
488
|
+
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
489
|
+
const parsed = data as ToolOutputAvailableWireData | undefined;
|
|
490
|
+
return [
|
|
491
|
+
{
|
|
492
|
+
kind: 'tool-result',
|
|
493
|
+
codecMessageId,
|
|
494
|
+
payload: { toolCallId: r.strOr('toolCallId', ''), output: parsed?.output },
|
|
495
|
+
},
|
|
496
|
+
];
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const decodeClientToolResultError = (codecMessageId: string, r: VercelHeaderReader, data: unknown): VercelInput[] => {
|
|
500
|
+
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
501
|
+
const parsed = data as ClientToolResultErrorWireData | undefined;
|
|
502
|
+
return [
|
|
503
|
+
{
|
|
504
|
+
kind: 'tool-result-error',
|
|
505
|
+
codecMessageId,
|
|
506
|
+
payload: { toolCallId: r.strOr('toolCallId', ''), message: parsed?.message ?? '' },
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const decodeClientToolApprovalResponse = (codecMessageId: string, r: VercelHeaderReader): VercelInput[] => [
|
|
512
|
+
{
|
|
513
|
+
kind: 'tool-approval-response',
|
|
514
|
+
codecMessageId,
|
|
515
|
+
payload: stripUndefined({
|
|
516
|
+
toolCallId: r.strOr('toolCallId', ''),
|
|
517
|
+
approved: r.bool('approved') ?? false,
|
|
518
|
+
reason: r.str('reason'),
|
|
519
|
+
}),
|
|
520
|
+
},
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// Discrete payload dispatch
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
512
526
|
|
|
513
|
-
|
|
527
|
+
const decodeAiOutputPayload = (
|
|
528
|
+
codecType: string,
|
|
529
|
+
r: VercelHeaderReader,
|
|
530
|
+
data: unknown,
|
|
531
|
+
runId: string,
|
|
532
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
533
|
+
): AnyEvent[] => {
|
|
534
|
+
switch (codecType) {
|
|
514
535
|
case 'start': {
|
|
515
|
-
return decodeStart(r,
|
|
536
|
+
return decodeStart(r, runId, lifecycle);
|
|
516
537
|
}
|
|
517
538
|
case 'start-step': {
|
|
518
|
-
return decodeStartStep(
|
|
539
|
+
return decodeStartStep(runId, lifecycle);
|
|
519
540
|
}
|
|
520
541
|
case 'finish-step': {
|
|
521
|
-
return decodeFinishStep(
|
|
542
|
+
return decodeFinishStep(runId, lifecycle);
|
|
522
543
|
}
|
|
523
544
|
case 'finish': {
|
|
524
|
-
return decodeFinish(r,
|
|
545
|
+
return decodeFinish(r, runId, lifecycle);
|
|
525
546
|
}
|
|
526
547
|
case 'error': {
|
|
527
|
-
return decodeError(
|
|
548
|
+
return decodeError(data, runId, lifecycle);
|
|
528
549
|
}
|
|
529
550
|
case 'abort': {
|
|
530
|
-
return decodeAbort(
|
|
551
|
+
return decodeAbort(data, runId, lifecycle);
|
|
531
552
|
}
|
|
532
553
|
case 'message-metadata': {
|
|
533
554
|
return decodeMessageMetadata(r);
|
|
534
555
|
}
|
|
535
556
|
case 'file': {
|
|
536
|
-
return decodeFile(r,
|
|
557
|
+
return decodeFile(r, data);
|
|
537
558
|
}
|
|
538
559
|
case 'source-url': {
|
|
539
|
-
return decodeSourceUrl(r,
|
|
560
|
+
return decodeSourceUrl(r, data);
|
|
540
561
|
}
|
|
541
562
|
case 'source-document': {
|
|
542
563
|
return decodeSourceDocument(r);
|
|
543
564
|
}
|
|
565
|
+
case 'tool-input': {
|
|
566
|
+
return decodeNonStreamingToolInput(r, data, runId, lifecycle);
|
|
567
|
+
}
|
|
544
568
|
case 'tool-input-error': {
|
|
545
|
-
return decodeToolInputError(r,
|
|
569
|
+
return decodeToolInputError(r, data);
|
|
546
570
|
}
|
|
547
571
|
case 'tool-output-available': {
|
|
548
|
-
return
|
|
572
|
+
return decodeAgentToolOutputAvailable(r, data);
|
|
549
573
|
}
|
|
550
574
|
case 'tool-output-error': {
|
|
551
|
-
return
|
|
575
|
+
return decodeAgentToolOutputError(r, data);
|
|
552
576
|
}
|
|
553
577
|
case 'tool-approval-request': {
|
|
554
578
|
return decodeToolApprovalRequest(r);
|
|
@@ -557,50 +581,105 @@ const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracke
|
|
|
557
581
|
return decodeToolOutputDenied(r);
|
|
558
582
|
}
|
|
559
583
|
default: {
|
|
560
|
-
return isDataEventName(
|
|
584
|
+
return isDataEventName(codecType) ? decodeDataEvent(codecType, r, data) : [];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const decodeAiInputPayload = (codecType: string, input: MessagePayload, r: VercelHeaderReader): AnyEvent[] => {
|
|
590
|
+
// Multi-part user-message parts (text / file / data-*) carry discrete
|
|
591
|
+
// because they ride publishDiscreteBatch; the receive-side fans them back
|
|
592
|
+
// out into a UserMessage.
|
|
593
|
+
if (isDiscreteMessagePart(codecType, input.transportHeaders ?? {})) {
|
|
594
|
+
return decodeDiscreteMessagePart(input);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const codecMessageId = input.transportHeaders?.[HEADER_CODEC_MESSAGE_ID] ?? '';
|
|
598
|
+
|
|
599
|
+
switch (codecType) {
|
|
600
|
+
case 'tool-result': {
|
|
601
|
+
return decodeClientToolResult(codecMessageId, r, input.data);
|
|
602
|
+
}
|
|
603
|
+
case 'tool-result-error': {
|
|
604
|
+
return decodeClientToolResultError(codecMessageId, r, input.data);
|
|
605
|
+
}
|
|
606
|
+
case 'tool-approval-response': {
|
|
607
|
+
return decodeClientToolApprovalResponse(codecMessageId, r);
|
|
608
|
+
}
|
|
609
|
+
case 'regenerate': {
|
|
610
|
+
// Wire-only signal — carries `parent` / `msg-regenerate` on transport
|
|
611
|
+
// headers, no domain payload. The agent's input-event lookup reads
|
|
612
|
+
// transport headers directly from the inbound Ably message; no
|
|
613
|
+
// projection fold is needed here.
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
default: {
|
|
617
|
+
return [];
|
|
561
618
|
}
|
|
562
619
|
}
|
|
563
620
|
};
|
|
564
621
|
|
|
622
|
+
const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): AnyEvent[] => {
|
|
623
|
+
const r = headerReader(input.codecHeaders ?? {});
|
|
624
|
+
const runId = input.transportHeaders?.[HEADER_RUN_ID] ?? '';
|
|
625
|
+
const codecType = r.strOr('type', '');
|
|
626
|
+
|
|
627
|
+
if (input.name === EVENT_AI_INPUT) {
|
|
628
|
+
return decodeAiInputPayload(codecType, input, r);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (input.name === EVENT_AI_OUTPUT) {
|
|
632
|
+
return decodeAiOutputPayload(codecType, r, input.data, runId, lifecycle);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return [];
|
|
636
|
+
};
|
|
637
|
+
|
|
565
638
|
// ---------------------------------------------------------------------------
|
|
566
639
|
// Decoder core hooks
|
|
567
640
|
// ---------------------------------------------------------------------------
|
|
568
641
|
|
|
569
|
-
const createHooks = (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const messageId = headerReader(tracker.headers).str('messageId');
|
|
575
|
-
const outputs = ensurePhases(lifecycle, turnId, { messageId });
|
|
576
|
-
outputs.push({ kind: 'event', event: buildStartChunk(tracker) });
|
|
577
|
-
return outputs;
|
|
642
|
+
const createHooks = (lifecycle: LifecycleTracker<AI.UIMessageChunk>): DecoderCoreHooks<AnyEvent> => ({
|
|
643
|
+
buildStartEvents: (tracker: StreamTrackerState): AnyEvent[] => {
|
|
644
|
+
const runId = tracker.transportHeaders[HEADER_RUN_ID] ?? '';
|
|
645
|
+
const messageId = headerReader(tracker.codecHeaders).str('messageId');
|
|
646
|
+
return [...lifecycle.ensurePhases(runId, { messageId }), buildStartChunk(tracker)];
|
|
578
647
|
},
|
|
579
648
|
|
|
580
|
-
buildDeltaEvents: (tracker: StreamTrackerState, delta: string):
|
|
649
|
+
buildDeltaEvents: (tracker: StreamTrackerState, delta: string): AnyEvent[] => [buildDeltaChunk(tracker, delta)],
|
|
581
650
|
|
|
582
|
-
buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>):
|
|
583
|
-
|
|
651
|
+
buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AnyEvent[] => [
|
|
652
|
+
buildEndChunk(tracker, closingHeaders),
|
|
653
|
+
],
|
|
584
654
|
|
|
585
|
-
decodeDiscrete: (payload: MessagePayload):
|
|
655
|
+
decodeDiscrete: (payload: MessagePayload): AnyEvent[] => decodeDiscretePayload(payload, lifecycle),
|
|
586
656
|
});
|
|
587
657
|
|
|
588
658
|
// ---------------------------------------------------------------------------
|
|
589
659
|
// Default implementation
|
|
590
660
|
// ---------------------------------------------------------------------------
|
|
591
661
|
|
|
592
|
-
|
|
593
|
-
|
|
662
|
+
const isInput = (event: AnyEvent): event is VercelInput => 'kind' in event;
|
|
663
|
+
|
|
664
|
+
class DefaultUIMessageDecoder implements Decoder<VercelInput, VercelOutput> {
|
|
665
|
+
private readonly _core: DecoderCore<AnyEvent>;
|
|
594
666
|
|
|
595
667
|
constructor(options: DecoderCoreOptions = {}) {
|
|
596
|
-
this._core = createDecoderCore<
|
|
597
|
-
createHooks(createVercelLifecycleTracker()),
|
|
598
|
-
options,
|
|
599
|
-
);
|
|
668
|
+
this._core = createDecoderCore<AnyEvent>(createHooks(createVercelLifecycleTracker()), options);
|
|
600
669
|
}
|
|
601
670
|
|
|
602
|
-
decode(message: Ably.InboundMessage):
|
|
603
|
-
|
|
671
|
+
decode(message: Ably.InboundMessage): DecodedMessage<VercelInput, VercelOutput> {
|
|
672
|
+
const events = this._core.decode(message);
|
|
673
|
+
const inputs: VercelInput[] = [];
|
|
674
|
+
const outputs: VercelOutput[] = [];
|
|
675
|
+
for (const event of events) {
|
|
676
|
+
if (isInput(event)) {
|
|
677
|
+
inputs.push(event);
|
|
678
|
+
} else {
|
|
679
|
+
outputs.push(event);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return { inputs, outputs };
|
|
604
683
|
}
|
|
605
684
|
}
|
|
606
685
|
|
|
@@ -609,10 +688,9 @@ class DefaultUIMessageDecoder implements StreamDecoder<AI.UIMessageChunk, AI.UIM
|
|
|
609
688
|
// ---------------------------------------------------------------------------
|
|
610
689
|
|
|
611
690
|
/**
|
|
612
|
-
* Create a Vercel AI SDK decoder that maps Ably messages to
|
|
613
|
-
* events and UIMessage objects via the decoder core.
|
|
691
|
+
* Create a Vercel AI SDK decoder that maps Ably messages to {@link DecodedMessage}.
|
|
614
692
|
* @param options - Decoder configuration (callbacks, logger).
|
|
615
|
-
* @returns A {@link
|
|
693
|
+
* @returns A {@link Decoder} typed in both directions for the Vercel codec.
|
|
616
694
|
*/
|
|
617
|
-
export const createDecoder = (options: DecoderCoreOptions = {}):
|
|
695
|
+
export const createDecoder = (options: DecoderCoreOptions = {}): Decoder<VercelInput, VercelOutput> =>
|
|
618
696
|
new DefaultUIMessageDecoder(options);
|