@ably/ai-transport 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -19
- package/dist/ably-ai-transport.js +1790 -1091
- 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 +2 -2
- package/dist/core/agent.d.ts +20 -5
- package/dist/core/channel-options.d.ts +57 -0
- package/dist/core/codec/codec-event.d.ts +9 -0
- package/dist/core/codec/decoder.d.ts +4 -1
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +2 -7
- package/dist/core/codec/field-bag.d.ts +85 -0
- package/dist/core/codec/fields.d.ts +141 -0
- package/dist/core/codec/index.d.ts +8 -1
- package/dist/core/codec/input-descriptor-decoder.d.ts +19 -0
- package/dist/core/codec/input-descriptor-encoder.d.ts +22 -0
- package/dist/core/codec/input-descriptors.d.ts +281 -0
- package/dist/core/codec/output-descriptor-decoder.d.ts +29 -0
- package/dist/core/codec/output-descriptor-encoder.d.ts +31 -0
- package/dist/core/codec/output-descriptors.d.ts +237 -0
- package/dist/core/codec/types.d.ts +95 -36
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/decode-fold.d.ts +40 -32
- package/dist/core/transport/headers.d.ts +30 -1
- package/dist/core/transport/index.d.ts +1 -1
- package/dist/core/transport/invocation.d.ts +1 -1
- package/dist/core/transport/load-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +21 -16
- package/dist/core/transport/run-manager.d.ts +9 -11
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +165 -15
- package/dist/core/transport/types/agent.d.ts +120 -98
- package/dist/core/transport/types/client.d.ts +45 -12
- package/dist/core/transport/types/tree.d.ts +52 -10
- package/dist/core/transport/types/view.d.ts +55 -28
- package/dist/core/transport/view.d.ts +176 -58
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +10 -4
- package/dist/index.d.ts +6 -5
- package/dist/react/ably-ai-transport-react.js +784 -415
- 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 +2 -1
- package/dist/react/contexts/client-session-provider.d.ts +3 -0
- package/dist/react/index.d.ts +2 -1
- package/dist/react/internal/skipped-session.d.ts +8 -0
- package/dist/react/use-view.d.ts +3 -3
- package/dist/utils.d.ts +22 -54
- package/dist/vercel/ably-ai-transport-vercel.js +2297 -2026
- 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/decode-lifecycle.d.ts +9 -0
- package/dist/vercel/codec/events.d.ts +1 -2
- package/dist/vercel/codec/fields.d.ts +44 -0
- package/dist/vercel/codec/fold-content.d.ts +16 -0
- package/dist/vercel/codec/fold-data.d.ts +16 -0
- package/dist/vercel/codec/fold-input.d.ts +67 -0
- package/dist/vercel/codec/fold-lifecycle.d.ts +16 -0
- package/dist/vercel/codec/fold-text.d.ts +16 -0
- package/dist/vercel/codec/fold-tool-input.d.ts +17 -0
- package/dist/vercel/codec/fold-tool-output.d.ts +16 -0
- package/dist/vercel/codec/index.d.ts +5 -30
- package/dist/vercel/codec/inputs.d.ts +11 -0
- package/dist/vercel/codec/outputs.d.ts +11 -0
- package/dist/vercel/codec/reducer-state.d.ts +121 -0
- package/dist/vercel/codec/reducer.d.ts +20 -102
- package/dist/vercel/codec/tool-transitions.d.ts +0 -6
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2013 -9500
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +1 -70
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +2 -1
- package/dist/vercel/run-end-reason.d.ts +66 -11
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +0 -2
- package/dist/vercel/transport/index.d.ts +1 -1
- package/dist/vercel/transport/run-output-stream.d.ts +6 -8
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/constants.ts +2 -2
- package/src/core/agent.ts +43 -19
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +145 -21
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +13 -54
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +43 -0
- package/src/core/codec/input-descriptor-decoder.ts +97 -0
- package/src/core/codec/input-descriptor-encoder.ts +150 -0
- package/src/core/codec/input-descriptors.ts +373 -0
- package/src/core/codec/output-descriptor-decoder.ts +139 -0
- package/src/core/codec/output-descriptor-encoder.ts +101 -0
- package/src/core/codec/output-descriptors.ts +307 -0
- package/src/core/codec/types.ts +99 -36
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +330 -589
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +74 -69
- package/src/core/transport/decode-fold.ts +57 -47
- package/src/core/transport/headers.ts +57 -4
- package/src/core/transport/index.ts +2 -1
- package/src/core/transport/invocation.ts +1 -1
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +63 -61
- package/src/core/transport/pipe-stream.ts +10 -1
- package/src/core/transport/run-manager.ts +25 -31
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +414 -47
- package/src/core/transport/types/agent.ts +129 -102
- package/src/core/transport/types/client.ts +49 -13
- package/src/core/transport/types/tree.ts +61 -12
- package/src/core/transport/types/view.ts +57 -28
- package/src/core/transport/view.ts +520 -172
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +10 -3
- package/src/index.ts +44 -11
- package/src/react/contexts/client-session-context.ts +1 -1
- package/src/react/contexts/client-session-provider.tsx +38 -2
- package/src/react/index.ts +2 -1
- package/src/react/internal/skipped-session.ts +62 -0
- package/src/react/use-client-session.ts +7 -30
- package/src/react/use-view.ts +3 -3
- package/src/utils.ts +31 -97
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +1 -3
- package/src/vercel/codec/fields.ts +58 -0
- package/src/vercel/codec/fold-content.ts +54 -0
- package/src/vercel/codec/fold-data.ts +46 -0
- package/src/vercel/codec/fold-input.ts +255 -0
- package/src/vercel/codec/fold-lifecycle.ts +85 -0
- package/src/vercel/codec/fold-text.ts +55 -0
- package/src/vercel/codec/fold-tool-input.ts +86 -0
- package/src/vercel/codec/fold-tool-output.ts +79 -0
- package/src/vercel/codec/index.ts +23 -63
- package/src/vercel/codec/inputs.ts +116 -0
- package/src/vercel/codec/outputs.ts +207 -0
- package/src/vercel/codec/reducer-state.ts +169 -0
- package/src/vercel/codec/reducer.ts +52 -838
- package/src/vercel/codec/tool-transitions.ts +1 -12
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +1 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +1 -1
- package/src/vercel/react/use-chat-transport.ts +8 -28
- package/src/vercel/react/use-message-sync.ts +5 -10
- package/src/vercel/run-end-reason.ts +95 -16
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +10 -22
- package/src/vercel/transport/index.ts +1 -1
- package/src/vercel/transport/run-output-stream.ts +7 -8
- package/src/version.ts +1 -1
- package/dist/core/transport/branch-chain.d.ts +0 -43
- package/dist/core/transport/load-conversation.d.ts +0 -128
- package/dist/vercel/codec/decoder.d.ts +0 -9
- package/dist/vercel/codec/encoder.d.ts +0 -11
- package/src/core/transport/branch-chain.ts +0 -58
- package/src/core/transport/load-conversation.ts +0 -355
- package/src/vercel/codec/decoder.ts +0 -696
- package/src/vercel/codec/encoder.ts +0 -548
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel output (`ai-output`) event descriptors — the single source of truth for
|
|
3
|
+
* encoding/decoding `UIMessageChunk` outputs. `defineCodec` injects the
|
|
4
|
+
* direction-scoped `{ event, stream }` builder; the generic drivers consume the
|
|
5
|
+
* returned array. Adding an ordinary output event is one entry here.
|
|
6
|
+
*
|
|
7
|
+
* Author-facing acceptance gate: this file contains **zero `as` casts**. The
|
|
8
|
+
* injected `event`/`stream` builders narrow each chunk member, so the
|
|
9
|
+
* `data`/`encode`/`decode` callbacks are fully typed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as Ably from 'ably';
|
|
13
|
+
|
|
14
|
+
import type { OutputBuilder, OutputDescriptor } from '../../core/codec/index.js';
|
|
15
|
+
import { boolField, jsonField, strField } from '../../core/codec/index.js';
|
|
16
|
+
import { ErrorCode, errorInfoIs } from '../../errors.js';
|
|
17
|
+
import { parseJsonOrString, stripUndefined } from '../../utils.js';
|
|
18
|
+
import type { VercelOutput } from './events.js';
|
|
19
|
+
import {
|
|
20
|
+
fDynamic,
|
|
21
|
+
fFinishReason,
|
|
22
|
+
fId,
|
|
23
|
+
fMediaType,
|
|
24
|
+
fMessageId,
|
|
25
|
+
fMeta,
|
|
26
|
+
fProviderExecuted,
|
|
27
|
+
fSourceId,
|
|
28
|
+
fTitle,
|
|
29
|
+
fToolCallId,
|
|
30
|
+
fToolName,
|
|
31
|
+
} from './fields.js';
|
|
32
|
+
import {
|
|
33
|
+
asString,
|
|
34
|
+
isAgentToolOutputErrorWireData,
|
|
35
|
+
isToolInputErrorWireData,
|
|
36
|
+
isToolOutputAvailableWireData,
|
|
37
|
+
} from './wire-data.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Vercel codec's `ai-output` descriptors, built from the injected
|
|
41
|
+
* direction-scoped builder.
|
|
42
|
+
* @param builder - The `{ event, stream }` builder curried on `VercelOutput`.
|
|
43
|
+
* @param builder.event - Define a single discrete output event.
|
|
44
|
+
* @param builder.stream - Define a streamed output family (start / delta / end).
|
|
45
|
+
* @returns The output descriptor table the generic output drivers consume.
|
|
46
|
+
*/
|
|
47
|
+
export const outputs = ({ event, stream }: OutputBuilder<VercelOutput>): readonly OutputDescriptor<VercelOutput>[] => [
|
|
48
|
+
// --- streamed families -----------------------------------------------------
|
|
49
|
+
|
|
50
|
+
stream('text', {
|
|
51
|
+
start: 'text-start',
|
|
52
|
+
delta: 'text-delta',
|
|
53
|
+
end: 'text-end',
|
|
54
|
+
idField: 'id',
|
|
55
|
+
deltaField: 'delta',
|
|
56
|
+
fields: [fId, fMeta],
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
stream('reasoning', {
|
|
60
|
+
start: 'reasoning-start',
|
|
61
|
+
delta: 'reasoning-delta',
|
|
62
|
+
end: 'reasoning-end',
|
|
63
|
+
idField: 'id',
|
|
64
|
+
deltaField: 'delta',
|
|
65
|
+
fields: [fId, fMeta],
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
// tool-input streams; the close step is a close-or-discrete fallback, the end
|
|
69
|
+
// chunk reconstructs `input` from the accumulated text, and the family also
|
|
70
|
+
// decodes from a non-streamed discrete publish.
|
|
71
|
+
stream('tool-input', {
|
|
72
|
+
start: 'tool-input-start',
|
|
73
|
+
delta: 'tool-input-delta',
|
|
74
|
+
end: 'tool-input-available',
|
|
75
|
+
idField: 'toolCallId',
|
|
76
|
+
deltaField: 'inputTextDelta',
|
|
77
|
+
fields: [fToolCallId, fToolName, fDynamic, fTitle, fProviderExecuted, fMeta],
|
|
78
|
+
onEnd: async (c, core, { h, name }) => {
|
|
79
|
+
try {
|
|
80
|
+
await core.closeStream(c.toolCallId, {
|
|
81
|
+
name,
|
|
82
|
+
data: '',
|
|
83
|
+
codecHeaders: h(c, ['toolCallId', 'toolName', 'providerMetadata']),
|
|
84
|
+
});
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
// closeStream raises InvalidArgument when there is no active stream for
|
|
87
|
+
// this id; fall through to a discrete publish, rethrow anything else.
|
|
88
|
+
if (!(error instanceof Ably.ErrorInfo && errorInfoIs(error, ErrorCode.InvalidArgument))) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
await core.publishDiscrete({ name, data: c.input, codecHeaders: h(c) });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
decodeEnd: ({ streamId, accumulated, codecHeaders, closingCodecHeaders }) => [
|
|
95
|
+
stripUndefined({
|
|
96
|
+
type: 'tool-input-available' as const,
|
|
97
|
+
toolCallId: streamId,
|
|
98
|
+
toolName: fToolName.read(closingCodecHeaders) || fToolName.read(codecHeaders),
|
|
99
|
+
input: parseJsonOrString(accumulated),
|
|
100
|
+
providerMetadata: fMeta.read(closingCodecHeaders),
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
decodeDiscrete: ({ codecHeaders, data }) => [
|
|
104
|
+
stripUndefined({
|
|
105
|
+
type: 'tool-input-start' as const,
|
|
106
|
+
toolCallId: fToolCallId.read(codecHeaders),
|
|
107
|
+
toolName: fToolName.read(codecHeaders),
|
|
108
|
+
dynamic: fDynamic.read(codecHeaders),
|
|
109
|
+
title: fTitle.read(codecHeaders),
|
|
110
|
+
providerExecuted: fProviderExecuted.read(codecHeaders),
|
|
111
|
+
providerMetadata: fMeta.read(codecHeaders),
|
|
112
|
+
}),
|
|
113
|
+
stripUndefined({
|
|
114
|
+
type: 'tool-input-available' as const,
|
|
115
|
+
toolCallId: fToolCallId.read(codecHeaders),
|
|
116
|
+
toolName: fToolName.read(codecHeaders),
|
|
117
|
+
input: data,
|
|
118
|
+
providerMetadata: fMeta.read(codecHeaders),
|
|
119
|
+
}),
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
// --- discrete lifecycle events ---------------------------------------------
|
|
124
|
+
|
|
125
|
+
// `start` injects the encoder's configured messageId as a fallback, so it
|
|
126
|
+
// builds its headers through a hatch rather than a pure descriptor.
|
|
127
|
+
event('start', {
|
|
128
|
+
fields: [fMessageId, jsonField('messageMetadata')],
|
|
129
|
+
encode: async (c, core, { h, name, messageId, opts }) => {
|
|
130
|
+
await core.publishDiscrete(
|
|
131
|
+
{ name, data: '', codecHeaders: h({ ...c, messageId: c.messageId ?? messageId }) },
|
|
132
|
+
opts,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
event('start-step'),
|
|
137
|
+
event('finish-step'),
|
|
138
|
+
event('finish', {
|
|
139
|
+
fields: [fFinishReason, jsonField('messageMetadata')],
|
|
140
|
+
}),
|
|
141
|
+
event('message-metadata', { fields: [jsonField('messageMetadata')] }),
|
|
142
|
+
event('error', {
|
|
143
|
+
data: { encode: (c) => c.errorText, decode: (data) => ({ errorText: asString(data) }) },
|
|
144
|
+
}),
|
|
145
|
+
|
|
146
|
+
// abort: an ordinary discrete output carrying its reason as wire data. The
|
|
147
|
+
// agent's own stream emits it on abort; run cancellation closes in-flight
|
|
148
|
+
// streams via the encoder's cancelStreams() and terminates via the transport
|
|
149
|
+
// ai-run-end event — this chunk is content, not the run terminator.
|
|
150
|
+
event('abort', {
|
|
151
|
+
data: {
|
|
152
|
+
encode: (c) => c.reason ?? '',
|
|
153
|
+
decode: (data) => (typeof data === 'string' && data ? { reason: data } : {}),
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
|
|
157
|
+
// --- content parts ---------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
event('file', {
|
|
160
|
+
fields: [fMediaType, fMeta],
|
|
161
|
+
data: { encode: (c) => c.url, decode: (data) => ({ url: asString(data) }) },
|
|
162
|
+
}),
|
|
163
|
+
event('source-url', {
|
|
164
|
+
fields: [fSourceId, fTitle, fMeta],
|
|
165
|
+
data: { encode: (c) => c.url, decode: (data) => ({ url: asString(data) }) },
|
|
166
|
+
}),
|
|
167
|
+
event('source-document', {
|
|
168
|
+
fields: [fSourceId, fMediaType, strField('title', ''), strField('filename'), fMeta],
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
// --- tool lifecycle (discrete) ---------------------------------------------
|
|
172
|
+
|
|
173
|
+
event('tool-input-error', {
|
|
174
|
+
fields: [fToolCallId, fToolName, fDynamic, fTitle, fProviderExecuted, fMeta],
|
|
175
|
+
data: {
|
|
176
|
+
encode: (c) => ({ errorText: c.errorText, input: c.input }),
|
|
177
|
+
decode: (data) =>
|
|
178
|
+
isToolInputErrorWireData(data) ? { errorText: data.errorText ?? '', input: data.input } : { errorText: '' },
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
event('tool-output-available', {
|
|
182
|
+
fields: [fToolCallId, fDynamic, fProviderExecuted, boolField('preliminary')],
|
|
183
|
+
data: {
|
|
184
|
+
encode: (c) => ({ output: c.output }),
|
|
185
|
+
decode: (data) => (isToolOutputAvailableWireData(data) ? { output: data.output } : {}),
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
event('tool-output-error', {
|
|
189
|
+
fields: [fToolCallId, fDynamic, fProviderExecuted],
|
|
190
|
+
data: {
|
|
191
|
+
encode: (c) => ({ errorText: c.errorText }),
|
|
192
|
+
decode: (data) => ({ errorText: isAgentToolOutputErrorWireData(data) ? (data.errorText ?? '') : '' }),
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
event('tool-approval-request', {
|
|
196
|
+
fields: [fToolCallId, strField('approvalId', '')],
|
|
197
|
+
}),
|
|
198
|
+
event('tool-output-denied', { fields: [fToolCallId] }),
|
|
199
|
+
|
|
200
|
+
// --- data-* wildcard -------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
event('data-*', {
|
|
203
|
+
fields: [fId, boolField('transient')],
|
|
204
|
+
ephemeral: (c) => c.transient === true,
|
|
205
|
+
data: { encode: (c) => c.data, decode: (data) => ({ data }) },
|
|
206
|
+
}),
|
|
207
|
+
];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reducer state: the projection shape, its internal tracker types,
|
|
3
|
+
* `init`, and the message/tracker lookup helpers the per-concern fold modules
|
|
4
|
+
* build on. This module is the base of the reducer's import DAG — the fold
|
|
5
|
+
* modules depend on it; it depends on none of them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type * as AI from 'ai';
|
|
9
|
+
|
|
10
|
+
import type { CodecMessage } from '../../core/codec/index.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Internal tracker state
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tracks an in-progress tool part within a UIMessage. Text and reasoning
|
|
18
|
+
* parts don't need this — we write to them directly via partIndex. Tool
|
|
19
|
+
* parts need an extra `inputText` buffer because deltas arrive as raw
|
|
20
|
+
* JSON fragments that must be accumulated before parsing.
|
|
21
|
+
*/
|
|
22
|
+
export interface ToolPartTracker {
|
|
23
|
+
/** Index in the message's parts array. */
|
|
24
|
+
partIndex: number;
|
|
25
|
+
/** Accumulated streaming input text (for JSON parsing on completion). */
|
|
26
|
+
inputText: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Per-codecMessageId tracking state for in-progress streams within a UIMessage. */
|
|
30
|
+
export interface MessageTrackers {
|
|
31
|
+
/** Text stream id → partIndex. */
|
|
32
|
+
text: Map<string, number>;
|
|
33
|
+
/** Reasoning stream id → partIndex. */
|
|
34
|
+
reasoning: Map<string, number>;
|
|
35
|
+
/** Tool call id → tracker. */
|
|
36
|
+
tools: Map<string, ToolPartTracker>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Projection
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The per-Run state produced by the Vercel codec's reducer.
|
|
45
|
+
*
|
|
46
|
+
* The SDK reads only `messages` (via `Codec.getMessages`). The remaining
|
|
47
|
+
* fields are internal to the reducer; they happen to live on the
|
|
48
|
+
* projection because the projection is the only thing the reducer can
|
|
49
|
+
* carry from fold to fold (it has no instance state).
|
|
50
|
+
*/
|
|
51
|
+
export interface VercelProjection {
|
|
52
|
+
/**
|
|
53
|
+
* UIMessages produced or modified in this Run, in publication order,
|
|
54
|
+
* each paired with its codec-message-id. The reducer correlates strictly
|
|
55
|
+
* on `codecMessageId`; `message.id` is preserved verbatim from the source
|
|
56
|
+
* (the AI SDK stream's `start.messageId` for assistants, the caller's id
|
|
57
|
+
* for user messages) and is never used as an identity key.
|
|
58
|
+
*/
|
|
59
|
+
messages: CodecMessage<AI.UIMessage>[];
|
|
60
|
+
/** Per-codecMessageId tracker state for streamed parts. Internal — do not access. */
|
|
61
|
+
trackers: Map<string, MessageTrackers>;
|
|
62
|
+
/**
|
|
63
|
+
* Tool-resolution events that arrived before any assistant in this
|
|
64
|
+
* projection had a matching `toolCallId`. Re-evaluated on every
|
|
65
|
+
* subsequent fold so that an out-of-order tool output is folded as
|
|
66
|
+
* soon as the corresponding assistant lands.
|
|
67
|
+
*/
|
|
68
|
+
pendingToolResolutions: PendingToolResolution[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A buffered tool resolution waiting for its assistant message to arrive.
|
|
73
|
+
* The reducer scans pending entries after every successful fold so an
|
|
74
|
+
* out-of-order tool output is promoted as soon as the matching assistant
|
|
75
|
+
* is added to the projection.
|
|
76
|
+
*/
|
|
77
|
+
export interface PendingToolResolution {
|
|
78
|
+
/** The codec-message-id of the assistant the resolution targets. */
|
|
79
|
+
targetCodecMessageId: string;
|
|
80
|
+
/** Tool call this resolution targets. */
|
|
81
|
+
toolCallId: string;
|
|
82
|
+
/** Variant of the tool-resolution used to transition the assistant's tool part. */
|
|
83
|
+
resolution:
|
|
84
|
+
| { kind: 'tool-result'; output: unknown }
|
|
85
|
+
| { kind: 'tool-result-error'; message: string }
|
|
86
|
+
| { kind: 'tool-approval-response'; approved: boolean; reason?: string };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A located `dynamic-tool` part with its owning message and tracker. */
|
|
90
|
+
export interface OwnerLookup {
|
|
91
|
+
/** The message owning the tool part. */
|
|
92
|
+
message: AI.UIMessage;
|
|
93
|
+
/** The tracker pointing at the part's index. */
|
|
94
|
+
tracker: ToolPartTracker;
|
|
95
|
+
/** The resolved `dynamic-tool` part itself. */
|
|
96
|
+
part: AI.DynamicToolUIPart;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// init
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build an empty initial projection.
|
|
105
|
+
* @returns A fresh VercelProjection with no messages and no tracker state.
|
|
106
|
+
*/
|
|
107
|
+
export const init = (): VercelProjection => ({
|
|
108
|
+
messages: [],
|
|
109
|
+
trackers: new Map(),
|
|
110
|
+
pendingToolResolutions: [],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Message + tracker helpers
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the assistant message for a codec-message-id, creating an empty
|
|
119
|
+
* placeholder when none exists yet.
|
|
120
|
+
* @param state - Projection to read or extend.
|
|
121
|
+
* @param codecMessageId - The codec-message-id to resolve.
|
|
122
|
+
* @returns The existing or newly-seeded UIMessage for that id.
|
|
123
|
+
*/
|
|
124
|
+
export const ensureMessage = (state: VercelProjection, codecMessageId: string): AI.UIMessage => {
|
|
125
|
+
let entry = state.messages.find((e) => e.codecMessageId === codecMessageId);
|
|
126
|
+
if (!entry) {
|
|
127
|
+
// No source id seen yet — seed the domain `message.id` with the
|
|
128
|
+
// codec-message-id as a fallback. The `start` chunk overwrites it with
|
|
129
|
+
// the stream's `messageId` when the stream provides one.
|
|
130
|
+
entry = { codecMessageId, message: { id: codecMessageId, role: 'assistant', parts: [] } };
|
|
131
|
+
state.messages.push(entry);
|
|
132
|
+
}
|
|
133
|
+
return entry.message;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the stream trackers for a codec-message-id, creating empty maps
|
|
138
|
+
* when none exist yet.
|
|
139
|
+
* @param state - Projection to read or extend.
|
|
140
|
+
* @param messageId - The codec-message-id whose trackers to resolve.
|
|
141
|
+
* @returns The existing or newly-created tracker maps for that id.
|
|
142
|
+
*/
|
|
143
|
+
export const ensureTrackers = (state: VercelProjection, messageId: string): MessageTrackers => {
|
|
144
|
+
let trackers = state.trackers.get(messageId);
|
|
145
|
+
if (!trackers) {
|
|
146
|
+
trackers = { text: new Map(), reasoning: new Map(), tools: new Map() };
|
|
147
|
+
state.trackers.set(messageId, trackers);
|
|
148
|
+
}
|
|
149
|
+
return trackers;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the `dynamic-tool` part tracked for a toolCallId within a message.
|
|
154
|
+
* @param message - The message whose parts to read.
|
|
155
|
+
* @param trackers - The message's tracker maps.
|
|
156
|
+
* @param toolCallId - The tool call to resolve.
|
|
157
|
+
* @returns The tracker and part, or `undefined` if untracked or the part is not a dynamic-tool.
|
|
158
|
+
*/
|
|
159
|
+
export const getToolPart = (
|
|
160
|
+
message: AI.UIMessage,
|
|
161
|
+
trackers: MessageTrackers,
|
|
162
|
+
toolCallId: string,
|
|
163
|
+
): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined => {
|
|
164
|
+
const tracker = trackers.tools.get(toolCallId);
|
|
165
|
+
if (!tracker) return undefined;
|
|
166
|
+
const part = message.parts[tracker.partIndex];
|
|
167
|
+
if (part?.type !== 'dynamic-tool') return undefined;
|
|
168
|
+
return { tracker, part };
|
|
169
|
+
};
|