@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.
- package/README.md +114 -116
- package/dist/ably-ai-transport.js +1743 -961
- 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 +117 -39
- 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 +410 -101
- 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 +97 -17
- package/dist/core/transport/index.d.ts +5 -3
- 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 -8
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +435 -0
- 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 -402
- package/dist/core/transport/view.d.ts +354 -0
- package/dist/errors.d.ts +37 -9
- package/dist/index.d.ts +6 -6
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1164 -645
- 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 +16 -10
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +20 -11
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +23 -0
- package/dist/react/use-tree.d.ts +35 -0
- package/dist/react/use-view.d.ts +110 -0
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
- 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 +50 -0
- package/dist/vercel/index.d.ts +4 -2
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +33 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
- package/dist/vercel/react/index.d.ts +4 -0
- package/dist/vercel/react/use-chat-transport.d.ts +66 -21
- package/dist/vercel/react/use-message-sync.d.ts +31 -12
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +71 -30
- package/dist/vercel/transport/index.d.ts +25 -18
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +47 -34
- package/src/constants.ts +126 -47
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +115 -58
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +438 -106
- 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 +182 -19
- package/src/core/transport/index.ts +29 -22
- 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 +58 -40
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +1167 -0
- 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 -527
- package/src/core/transport/view.ts +1271 -0
- package/src/errors.ts +42 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +55 -39
- 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 +27 -10
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +47 -19
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +72 -0
- package/src/react/use-tree.ts +84 -0
- package/src/react/use-view.ts +275 -0
- package/src/react/vite.config.ts +4 -1
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -255
- package/src/vercel/codec/encoder.ts +348 -196
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +59 -14
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +7 -3
- package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
- package/src/vercel/react/index.ts +13 -1
- package/src/vercel/react/use-chat-transport.ts +162 -42
- package/src/vercel/react/use-message-sync.ts +121 -22
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +553 -113
- package/src/vercel/transport/index.ts +40 -28
- 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/conversation-tree.d.ts +0 -9
- package/dist/core/transport/decode-history.d.ts +0 -41
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -19
- package/dist/core/transport/turn-manager.d.ts +0 -34
- package/dist/react/use-active-turns.d.ts +0 -8
- package/dist/react/use-client-transport.d.ts +0 -7
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/src/core/transport/client-transport.ts +0 -959
- package/src/core/transport/conversation-tree.ts +0 -434
- package/src/core/transport/decode-history.ts +0 -337
- package/src/core/transport/server-transport.ts +0 -458
- package/src/core/transport/stream-router.ts +0 -118
- package/src/core/transport/turn-manager.ts +0 -147
- package/src/react/use-active-turns.ts +0 -61
- package/src/react/use-client-transport.ts +0 -37
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- package/src/react/use-send.ts +0 -25
- package/src/vercel/codec/accumulator.ts +0 -603
|
@@ -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,106 +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 = (
|
|
285
|
+
const decodeError = (
|
|
286
|
+
data: unknown,
|
|
287
|
+
runId: string,
|
|
288
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
289
|
+
): AI.UIMessageChunk[] => {
|
|
290
|
+
lifecycle.clearScope(runId);
|
|
281
291
|
const errorText = typeof data === 'string' ? data : '';
|
|
282
|
-
return
|
|
292
|
+
return [{ type: 'error', errorText }];
|
|
283
293
|
};
|
|
284
294
|
|
|
285
|
-
const decodeAbort = (
|
|
286
|
-
|
|
295
|
+
const decodeAbort = (
|
|
296
|
+
data: unknown,
|
|
297
|
+
runId: string,
|
|
298
|
+
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
299
|
+
): AI.UIMessageChunk[] => {
|
|
300
|
+
lifecycle.clearScope(runId);
|
|
287
301
|
const reason = typeof data === 'string' && data ? data : undefined;
|
|
288
|
-
return
|
|
302
|
+
return [stripUndefined({ type: 'abort' as const, reason })];
|
|
289
303
|
};
|
|
290
304
|
|
|
291
|
-
const decodeMessageMetadata = (r: VercelHeaderReader):
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const decodeSourceUrl = (r: VercelHeaderReader, data: unknown):
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
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[] => {
|
|
328
340
|
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
329
341
|
const parsed = data as ToolInputErrorWireData | undefined;
|
|
330
|
-
return
|
|
342
|
+
return [
|
|
331
343
|
stripUndefined({
|
|
332
344
|
type: 'tool-input-error' as const,
|
|
333
345
|
toolCallId: r.strOr('toolCallId', ''),
|
|
@@ -339,13 +351,13 @@ const decodeToolInputError = (r: VercelHeaderReader, data: unknown): Out[] => {
|
|
|
339
351
|
providerExecuted: r.bool('providerExecuted'),
|
|
340
352
|
providerMetadata: r.providerMetadata(),
|
|
341
353
|
}),
|
|
342
|
-
|
|
354
|
+
];
|
|
343
355
|
};
|
|
344
356
|
|
|
345
|
-
const
|
|
357
|
+
const decodeAgentToolOutputAvailable = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
|
|
346
358
|
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
347
359
|
const parsed = data as ToolOutputAvailableWireData | undefined;
|
|
348
|
-
return
|
|
360
|
+
return [
|
|
349
361
|
stripUndefined({
|
|
350
362
|
type: 'tool-output-available' as const,
|
|
351
363
|
toolCallId: r.strOr('toolCallId', ''),
|
|
@@ -354,13 +366,13 @@ const decodeToolOutputAvailable = (r: VercelHeaderReader, data: unknown): Out[]
|
|
|
354
366
|
providerExecuted: r.bool('providerExecuted'),
|
|
355
367
|
preliminary: r.bool('preliminary'),
|
|
356
368
|
}),
|
|
357
|
-
|
|
369
|
+
];
|
|
358
370
|
};
|
|
359
371
|
|
|
360
|
-
const
|
|
372
|
+
const decodeAgentToolOutputError = (r: VercelHeaderReader, data: unknown): AI.UIMessageChunk[] => {
|
|
361
373
|
// CAST: Trust boundary — encoder produced the expected object shape.
|
|
362
|
-
const parsed = data as
|
|
363
|
-
return
|
|
374
|
+
const parsed = data as AgentToolOutputErrorWireData | undefined;
|
|
375
|
+
return [
|
|
364
376
|
stripUndefined({
|
|
365
377
|
type: 'tool-output-error' as const,
|
|
366
378
|
toolCallId: r.strOr('toolCallId', ''),
|
|
@@ -368,92 +380,80 @@ const decodeToolOutputError = (r: VercelHeaderReader, data: unknown): Out[] => {
|
|
|
368
380
|
dynamic: r.bool('dynamic'),
|
|
369
381
|
providerExecuted: r.bool('providerExecuted'),
|
|
370
382
|
}),
|
|
371
|
-
|
|
383
|
+
];
|
|
372
384
|
};
|
|
373
385
|
|
|
374
|
-
const decodeToolApprovalRequest = (r: VercelHeaderReader):
|
|
375
|
-
|
|
386
|
+
const decodeToolApprovalRequest = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
|
|
387
|
+
{
|
|
376
388
|
type: 'tool-approval-request',
|
|
377
389
|
toolCallId: r.strOr('toolCallId', ''),
|
|
378
390
|
approvalId: r.strOr('approvalId', ''),
|
|
379
|
-
}
|
|
391
|
+
},
|
|
392
|
+
];
|
|
380
393
|
|
|
381
|
-
const decodeToolOutputDenied = (r: VercelHeaderReader):
|
|
382
|
-
|
|
394
|
+
const decodeToolOutputDenied = (r: VercelHeaderReader): AI.UIMessageChunk[] => [
|
|
395
|
+
{ type: 'tool-output-denied', toolCallId: r.strOr('toolCallId', '') },
|
|
396
|
+
];
|
|
383
397
|
|
|
384
|
-
const decodeDataEvent = (name: `data-${string}`, r: VercelHeaderReader, data: unknown):
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
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
|
+
];
|
|
393
406
|
|
|
394
407
|
// ---------------------------------------------------------------------------
|
|
395
|
-
// Non-streaming tool-input helper
|
|
408
|
+
// Non-streaming tool-input helper (agent-side)
|
|
396
409
|
// ---------------------------------------------------------------------------
|
|
397
410
|
|
|
398
411
|
const decodeNonStreamingToolInput = (
|
|
399
412
|
r: VercelHeaderReader,
|
|
400
413
|
data: unknown,
|
|
401
|
-
|
|
414
|
+
runId: string,
|
|
402
415
|
lifecycle: LifecycleTracker<AI.UIMessageChunk>,
|
|
403
|
-
):
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
type: 'tool-input-available' as const,
|
|
423
|
-
toolCallId: r.strOr('toolCallId', ''),
|
|
424
|
-
toolName: r.strOr('toolName', ''),
|
|
425
|
-
input: data,
|
|
426
|
-
providerMetadata: r.providerMetadata(),
|
|
427
|
-
}),
|
|
428
|
-
},
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
return outputs;
|
|
432
|
-
};
|
|
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
|
+
];
|
|
433
435
|
|
|
434
436
|
// ---------------------------------------------------------------------------
|
|
435
|
-
//
|
|
437
|
+
// Input-side decoders (ai-input → VercelInput)
|
|
436
438
|
// ---------------------------------------------------------------------------
|
|
437
439
|
|
|
438
440
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* @param input - The discrete message payload to decode.
|
|
446
|
-
* @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.
|
|
447
447
|
*/
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
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'];
|
|
452
451
|
const messageId = r.str('messageId') ?? '';
|
|
452
|
+
const codecType = r.strOr('type', '');
|
|
453
453
|
|
|
454
454
|
let part: AI.UIMessage['parts'][number] | undefined;
|
|
455
455
|
|
|
456
|
-
switch (
|
|
456
|
+
switch (codecType) {
|
|
457
457
|
case 'text': {
|
|
458
458
|
part = { type: 'text', text: typeof input.data === 'string' ? input.data : '' };
|
|
459
459
|
break;
|
|
@@ -467,9 +467,8 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
|
|
|
467
467
|
break;
|
|
468
468
|
}
|
|
469
469
|
default: {
|
|
470
|
-
if (isDataEventName(
|
|
471
|
-
|
|
472
|
-
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 });
|
|
473
472
|
}
|
|
474
473
|
break;
|
|
475
474
|
}
|
|
@@ -478,74 +477,102 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
|
|
|
478
477
|
if (!part) return [];
|
|
479
478
|
|
|
480
479
|
const message: AI.UIMessage = { id: messageId, role, parts: [part] };
|
|
481
|
-
|
|
480
|
+
const userMessage: UserMessage<AI.UIMessage> = { kind: 'user-message', message };
|
|
481
|
+
return [userMessage];
|
|
482
482
|
};
|
|
483
483
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
* rather than a streaming lifecycle event. Discrete message parts carry x-ably-role
|
|
487
|
-
* and encode a single UIMessage part each.
|
|
488
|
-
* @param name - The Ably message name to check.
|
|
489
|
-
* @param headers - The Ably message headers to inspect for role presence.
|
|
490
|
-
* @returns True if this is a discrete message part, false if it's a lifecycle event.
|
|
491
|
-
*/
|
|
492
|
-
const isDiscreteMessagePart = (name: string, headers: Record<string, string>): boolean =>
|
|
493
|
-
(name === 'text' || name === 'file' || isDataEventName(name)) && HEADER_ROLE in headers;
|
|
494
|
-
|
|
495
|
-
const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
|
|
496
|
-
const h = input.headers ?? {};
|
|
497
|
-
const r = headerReader(h);
|
|
498
|
-
const turnId = h[HEADER_TURN_ID] ?? '';
|
|
499
|
-
|
|
500
|
-
// Discrete message parts from writeMessages (user messages, history entries).
|
|
501
|
-
// Distinguished from lifecycle events by the presence of x-ably-role.
|
|
502
|
-
if (isDiscreteMessagePart(input.name, h)) {
|
|
503
|
-
return decodeDiscreteMessage(input);
|
|
504
|
-
}
|
|
484
|
+
const isDiscreteMessagePart = (codecType: string, headers: Record<string, string>): boolean =>
|
|
485
|
+
(codecType === 'text' || codecType === 'file' || isDataEventName(codecType)) && HEADER_DISCRETE in headers;
|
|
505
486
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
509
526
|
|
|
510
|
-
|
|
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) {
|
|
511
535
|
case 'start': {
|
|
512
|
-
return decodeStart(r,
|
|
536
|
+
return decodeStart(r, runId, lifecycle);
|
|
513
537
|
}
|
|
514
538
|
case 'start-step': {
|
|
515
|
-
return decodeStartStep(
|
|
539
|
+
return decodeStartStep(runId, lifecycle);
|
|
516
540
|
}
|
|
517
541
|
case 'finish-step': {
|
|
518
|
-
return decodeFinishStep(
|
|
542
|
+
return decodeFinishStep(runId, lifecycle);
|
|
519
543
|
}
|
|
520
544
|
case 'finish': {
|
|
521
|
-
return decodeFinish(r,
|
|
545
|
+
return decodeFinish(r, runId, lifecycle);
|
|
522
546
|
}
|
|
523
547
|
case 'error': {
|
|
524
|
-
return decodeError(
|
|
548
|
+
return decodeError(data, runId, lifecycle);
|
|
525
549
|
}
|
|
526
550
|
case 'abort': {
|
|
527
|
-
return decodeAbort(
|
|
551
|
+
return decodeAbort(data, runId, lifecycle);
|
|
528
552
|
}
|
|
529
553
|
case 'message-metadata': {
|
|
530
554
|
return decodeMessageMetadata(r);
|
|
531
555
|
}
|
|
532
556
|
case 'file': {
|
|
533
|
-
return decodeFile(r,
|
|
557
|
+
return decodeFile(r, data);
|
|
534
558
|
}
|
|
535
559
|
case 'source-url': {
|
|
536
|
-
return decodeSourceUrl(r,
|
|
560
|
+
return decodeSourceUrl(r, data);
|
|
537
561
|
}
|
|
538
562
|
case 'source-document': {
|
|
539
563
|
return decodeSourceDocument(r);
|
|
540
564
|
}
|
|
565
|
+
case 'tool-input': {
|
|
566
|
+
return decodeNonStreamingToolInput(r, data, runId, lifecycle);
|
|
567
|
+
}
|
|
541
568
|
case 'tool-input-error': {
|
|
542
|
-
return decodeToolInputError(r,
|
|
569
|
+
return decodeToolInputError(r, data);
|
|
543
570
|
}
|
|
544
571
|
case 'tool-output-available': {
|
|
545
|
-
return
|
|
572
|
+
return decodeAgentToolOutputAvailable(r, data);
|
|
546
573
|
}
|
|
547
574
|
case 'tool-output-error': {
|
|
548
|
-
return
|
|
575
|
+
return decodeAgentToolOutputError(r, data);
|
|
549
576
|
}
|
|
550
577
|
case 'tool-approval-request': {
|
|
551
578
|
return decodeToolApprovalRequest(r);
|
|
@@ -554,50 +581,105 @@ const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracke
|
|
|
554
581
|
return decodeToolOutputDenied(r);
|
|
555
582
|
}
|
|
556
583
|
default: {
|
|
557
|
-
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 [];
|
|
558
618
|
}
|
|
559
619
|
}
|
|
560
620
|
};
|
|
561
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
|
+
|
|
562
638
|
// ---------------------------------------------------------------------------
|
|
563
639
|
// Decoder core hooks
|
|
564
640
|
// ---------------------------------------------------------------------------
|
|
565
641
|
|
|
566
|
-
const createHooks = (
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const messageId = headerReader(tracker.headers).str('messageId');
|
|
572
|
-
const outputs = ensurePhases(lifecycle, turnId, { messageId });
|
|
573
|
-
outputs.push({ kind: 'event', event: buildStartChunk(tracker) });
|
|
574
|
-
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)];
|
|
575
647
|
},
|
|
576
648
|
|
|
577
|
-
buildDeltaEvents: (tracker: StreamTrackerState, delta: string):
|
|
649
|
+
buildDeltaEvents: (tracker: StreamTrackerState, delta: string): AnyEvent[] => [buildDeltaChunk(tracker, delta)],
|
|
578
650
|
|
|
579
|
-
buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>):
|
|
580
|
-
|
|
651
|
+
buildEndEvents: (tracker: StreamTrackerState, closingHeaders: Record<string, string>): AnyEvent[] => [
|
|
652
|
+
buildEndChunk(tracker, closingHeaders),
|
|
653
|
+
],
|
|
581
654
|
|
|
582
|
-
decodeDiscrete: (payload: MessagePayload):
|
|
655
|
+
decodeDiscrete: (payload: MessagePayload): AnyEvent[] => decodeDiscretePayload(payload, lifecycle),
|
|
583
656
|
});
|
|
584
657
|
|
|
585
658
|
// ---------------------------------------------------------------------------
|
|
586
659
|
// Default implementation
|
|
587
660
|
// ---------------------------------------------------------------------------
|
|
588
661
|
|
|
589
|
-
|
|
590
|
-
|
|
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>;
|
|
591
666
|
|
|
592
667
|
constructor(options: DecoderCoreOptions = {}) {
|
|
593
|
-
this._core = createDecoderCore<
|
|
594
|
-
createHooks(createVercelLifecycleTracker()),
|
|
595
|
-
options,
|
|
596
|
-
);
|
|
668
|
+
this._core = createDecoderCore<AnyEvent>(createHooks(createVercelLifecycleTracker()), options);
|
|
597
669
|
}
|
|
598
670
|
|
|
599
|
-
decode(message: Ably.InboundMessage):
|
|
600
|
-
|
|
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 };
|
|
601
683
|
}
|
|
602
684
|
}
|
|
603
685
|
|
|
@@ -606,10 +688,9 @@ class DefaultUIMessageDecoder implements StreamDecoder<AI.UIMessageChunk, AI.UIM
|
|
|
606
688
|
// ---------------------------------------------------------------------------
|
|
607
689
|
|
|
608
690
|
/**
|
|
609
|
-
* Create a Vercel AI SDK decoder that maps Ably messages to
|
|
610
|
-
* events and UIMessage objects via the decoder core.
|
|
691
|
+
* Create a Vercel AI SDK decoder that maps Ably messages to {@link DecodedMessage}.
|
|
611
692
|
* @param options - Decoder configuration (callbacks, logger).
|
|
612
|
-
* @returns A {@link
|
|
693
|
+
* @returns A {@link Decoder} typed in both directions for the Vercel codec.
|
|
613
694
|
*/
|
|
614
|
-
export const createDecoder = (options: DecoderCoreOptions = {}):
|
|
695
|
+
export const createDecoder = (options: DecoderCoreOptions = {}): Decoder<VercelInput, VercelOutput> =>
|
|
615
696
|
new DefaultUIMessageDecoder(options);
|