@ably/ai-transport 0.1.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 +93 -111
- package/dist/ably-ai-transport.js +2401 -1387
- 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 +44 -0
- 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 +24 -24
- package/dist/core/codec/define-codec.d.ts +100 -0
- package/dist/core/codec/encoder.d.ts +10 -12
- 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 -2
- 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/lifecycle-tracker.d.ts +10 -9
- 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 +470 -119
- package/dist/core/codec/well-known-inputs.d.ts +52 -0
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/agent-view.d.ts +296 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +55 -0
- package/dist/core/transport/headers.d.ts +121 -14
- 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-history-pages.d.ts +71 -0
- package/dist/core/transport/load-history.d.ts +44 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -9
- package/dist/core/transport/run-manager.d.ts +76 -0
- package/dist/core/transport/session-support.d.ts +55 -0
- package/dist/core/transport/tree.d.ts +523 -109
- package/dist/core/transport/types/agent.d.ts +375 -0
- package/dist/core/transport/types/client.d.ts +201 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +357 -0
- package/dist/core/transport/types/view.d.ts +249 -0
- package/dist/core/transport/types.d.ts +13 -553
- package/dist/core/transport/view.d.ts +390 -84
- package/dist/core/transport/wire-log.d.ts +102 -0
- package/dist/errors.d.ts +27 -10
- package/dist/index.d.ts +8 -9
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1365 -1010
- 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 +37 -0
- package/dist/react/contexts/client-session-provider.d.ts +56 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +13 -12
- package/dist/react/internal/skipped-session.d.ts +8 -0
- 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 +81 -50
- package/dist/utils.d.ts +48 -71
- package/dist/vercel/ably-ai-transport-vercel.js +3257 -2499
- 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 +50 -0
- 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 +7 -20
- 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 +62 -0
- package/dist/vercel/codec/tool-transitions.d.ts +2 -8
- package/dist/vercel/codec/wire-data.d.ts +34 -0
- package/dist/vercel/index.d.ts +5 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2859 -9705
- 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 -45
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +9 -7
- 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 +84 -0
- package/dist/vercel/tool-part.d.ts +21 -0
- package/dist/vercel/transport/chat-transport.d.ts +41 -24
- package/dist/vercel/transport/index.d.ts +24 -20
- package/dist/vercel/transport/run-output-stream.d.ts +54 -0
- package/dist/version.d.ts +2 -0
- package/package.json +31 -24
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +92 -0
- package/src/core/channel-options.ts +89 -0
- package/src/core/codec/codec-event.ts +27 -0
- package/src/core/codec/decoder.ts +202 -105
- package/src/core/codec/define-codec.ts +432 -0
- package/src/core/codec/encoder.ts +114 -107
- package/src/core/codec/field-bag.ts +142 -0
- package/src/core/codec/fields.ts +193 -0
- package/src/core/codec/index.ts +56 -6
- 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/lifecycle-tracker.ts +10 -9
- 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 +505 -126
- package/src/core/codec/well-known-inputs.ts +96 -0
- package/src/core/transport/agent-session.ts +1085 -0
- package/src/core/transport/agent-view.ts +738 -0
- package/src/core/transport/client-session.ts +780 -0
- package/src/core/transport/decode-fold.ts +101 -0
- package/src/core/transport/headers.ts +234 -22
- package/src/core/transport/index.ts +27 -27
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-history-pages.ts +220 -0
- package/src/core/transport/load-history.ts +271 -0
- package/src/core/transport/pipe-stream.ts +63 -39
- package/src/core/transport/run-manager.ts +243 -0
- package/src/core/transport/session-support.ts +96 -0
- package/src/core/transport/tree.ts +1293 -308
- package/src/core/transport/types/agent.ts +434 -0
- package/src/core/transport/types/client.ts +247 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +393 -0
- package/src/core/transport/types/view.ts +288 -0
- package/src/core/transport/types.ts +13 -706
- package/src/core/transport/view.ts +1229 -450
- package/src/core/transport/wire-log.ts +189 -0
- package/src/errors.ts +29 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +86 -42
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +222 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +24 -13
- package/src/react/internal/skipped-session.ts +62 -0
- 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 +178 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +138 -96
- package/src/utils.ts +83 -131
- package/src/vercel/codec/decode-lifecycle.ts +70 -0
- package/src/vercel/codec/events.ts +85 -0
- 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 +28 -21
- 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 +191 -0
- package/src/vercel/codec/tool-transitions.ts +3 -14
- package/src/vercel/codec/wire-data.ts +64 -0
- package/src/vercel/index.ts +7 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +8 -7
- 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 +44 -66
- package/src/vercel/react/use-message-sync.ts +75 -39
- package/src/vercel/run-end-reason.ts +157 -0
- package/src/vercel/tool-part.ts +25 -0
- package/src/vercel/transport/chat-transport.ts +380 -98
- package/src/vercel/transport/index.ts +38 -37
- package/src/vercel/transport/run-output-stream.ts +169 -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/codec/decoder.d.ts +0 -22
- package/dist/vercel/codec/encoder.d.ts +0 -41
- 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/codec/decoder.ts +0 -618
- package/src/vercel/codec/encoder.ts +0 -410
- 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,588 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vercel AI SDK Message Accumulator
|
|
3
|
-
*
|
|
4
|
-
* Builds and maintains a UIMessage[] list from decoder outputs.
|
|
5
|
-
* Implements MessageAccumulator<UIMessageChunk, UIMessage>.
|
|
6
|
-
*
|
|
7
|
-
* The accumulator consumes DecoderOutput[] from the decoder and groups
|
|
8
|
-
* streaming events into UIMessage objects using lifecycle boundaries
|
|
9
|
-
* (start/finish). Complete messages (from writeMessages) are inserted
|
|
10
|
-
* directly.
|
|
11
|
-
*
|
|
12
|
-
* Multiple messages can be in-progress concurrently — each is identified
|
|
13
|
-
* by the `messageId` field on DecoderOutput (read from x-ably-msg-id).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type * as AI from 'ai';
|
|
17
|
-
|
|
18
|
-
import type { DecoderOutput, MessageAccumulator } from '../../core/codec/types.js';
|
|
19
|
-
import { stripUndefined } from '../../utils.js';
|
|
20
|
-
import { toolBase, transitionToolPart } from './tool-transitions.js';
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Internal types
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
/** Status of a streamed message (text, reasoning, or tool-input). */
|
|
27
|
-
type StreamStatus = 'streaming' | 'finished' | 'aborted';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Tracks an in-progress tool part's position and accumulated streaming input.
|
|
31
|
-
* Text and reasoning parts don't need this — we write directly to the part.
|
|
32
|
-
* Tool parts need the extra `inputText` buffer because deltas arrive as raw
|
|
33
|
-
* JSON fragments that must be accumulated before parsing.
|
|
34
|
-
*/
|
|
35
|
-
interface ToolPartTracker {
|
|
36
|
-
/** Index in the message's parts array. */
|
|
37
|
-
partIndex: number;
|
|
38
|
-
/** Accumulated streaming input text (for JSON parsing on completion). */
|
|
39
|
-
inputText: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Bundled per-message state for an in-progress message. */
|
|
43
|
-
interface ActiveMessageState {
|
|
44
|
-
message: AI.UIMessage;
|
|
45
|
-
textStreams: DeltaStreamTracker;
|
|
46
|
-
reasoningStreams: DeltaStreamTracker;
|
|
47
|
-
toolTrackers: Record<string, ToolPartTracker>;
|
|
48
|
-
streamStatus: Map<string, StreamStatus>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// DeltaStreamTracker — manages text or reasoning stream accumulation
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Tracks in-progress text or reasoning streams within a single message.
|
|
57
|
-
* Owns the mapping from stream ID to part index, enforcing the pairing
|
|
58
|
-
* of part type and index map by construction.
|
|
59
|
-
*/
|
|
60
|
-
class DeltaStreamTracker {
|
|
61
|
-
private readonly _partType: 'text' | 'reasoning';
|
|
62
|
-
private _activeIndex = new Map<string, number>();
|
|
63
|
-
|
|
64
|
-
constructor(partType: 'text' | 'reasoning') {
|
|
65
|
-
this._partType = partType;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
start(id: string, msg: AI.UIMessage, streamStatus: Map<string, StreamStatus>): void {
|
|
69
|
-
this._activeIndex.set(id, msg.parts.length);
|
|
70
|
-
msg.parts.push({ type: this._partType, text: '' });
|
|
71
|
-
streamStatus.set(id, 'streaming');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
delta(id: string, msg: AI.UIMessage, text: string): void {
|
|
75
|
-
const idx = this._activeIndex.get(id);
|
|
76
|
-
if (idx === undefined) return;
|
|
77
|
-
const part = msg.parts[idx];
|
|
78
|
-
if (part?.type === this._partType) {
|
|
79
|
-
part.text += text;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
end(id: string, streamStatus: Map<string, StreamStatus>): void {
|
|
84
|
-
streamStatus.set(id, 'finished');
|
|
85
|
-
this._activeIndex.delete(id);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
reset(): void {
|
|
89
|
-
this._activeIndex = new Map();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// Default implementation
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChunk, AI.UIMessage> {
|
|
98
|
-
private readonly _messageList: AI.UIMessage[] = [];
|
|
99
|
-
private readonly _activeMessages = new Map<string, ActiveMessageState>();
|
|
100
|
-
|
|
101
|
-
get messages(): AI.UIMessage[] {
|
|
102
|
-
return this._messageList;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get completedMessages(): AI.UIMessage[] {
|
|
106
|
-
const activeSet = new Set<AI.UIMessage>();
|
|
107
|
-
for (const state of this._activeMessages.values()) {
|
|
108
|
-
activeSet.add(state.message);
|
|
109
|
-
}
|
|
110
|
-
return this._messageList.filter((msg) => !activeSet.has(msg));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
get hasActiveStream(): boolean {
|
|
114
|
-
for (const state of this._activeMessages.values()) {
|
|
115
|
-
for (const status of state.streamStatus.values()) {
|
|
116
|
-
if (status === 'streaming') return true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
processOutputs(outputs: DecoderOutput<AI.UIMessageChunk, AI.UIMessage>[]): void {
|
|
123
|
-
for (const output of outputs) {
|
|
124
|
-
if (output.kind === 'message') {
|
|
125
|
-
this._messageList.push(output.message);
|
|
126
|
-
} else if (output.messageId !== undefined) {
|
|
127
|
-
this._processEvent(output.event, output.messageId);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
updateMessage(message: AI.UIMessage): void {
|
|
133
|
-
const idx = this._messageList.findIndex((m) => m.id === message.id);
|
|
134
|
-
if (idx !== -1) {
|
|
135
|
-
this._messageList[idx] = message;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
initMessage(messageId: string, message: AI.UIMessage): void {
|
|
140
|
-
const existing = this._activeMessages.get(messageId);
|
|
141
|
-
|
|
142
|
-
if (existing) {
|
|
143
|
-
// Already active — sync with the externally updated message.
|
|
144
|
-
// Replace the message and rebuild tool trackers so the accumulator
|
|
145
|
-
// reflects updates (e.g. cross-turn amendments applied to the tree)
|
|
146
|
-
// that happened outside the streaming flow.
|
|
147
|
-
const cloned = structuredClone(message);
|
|
148
|
-
const listIdx = this._messageList.indexOf(existing.message);
|
|
149
|
-
existing.message = cloned;
|
|
150
|
-
if (listIdx !== -1) {
|
|
151
|
-
this._messageList[listIdx] = cloned;
|
|
152
|
-
}
|
|
153
|
-
existing.toolTrackers = {};
|
|
154
|
-
for (let i = 0; i < cloned.parts.length; i++) {
|
|
155
|
-
const part = cloned.parts[i];
|
|
156
|
-
if (part?.type === 'dynamic-tool') {
|
|
157
|
-
existing.toolTrackers[part.toolCallId] = { partIndex: i, inputText: '' };
|
|
158
|
-
existing.streamStatus.set(part.toolCallId, 'finished');
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Not active — create tracking state from the existing message.
|
|
165
|
-
const cloned = structuredClone(message);
|
|
166
|
-
const toolTrackers: Record<string, ToolPartTracker> = {};
|
|
167
|
-
const streamStatus = new Map<string, StreamStatus>();
|
|
168
|
-
|
|
169
|
-
for (let i = 0; i < cloned.parts.length; i++) {
|
|
170
|
-
const part = cloned.parts[i];
|
|
171
|
-
if (part?.type === 'dynamic-tool') {
|
|
172
|
-
toolTrackers[part.toolCallId] = { partIndex: i, inputText: '' };
|
|
173
|
-
streamStatus.set(part.toolCallId, 'finished');
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const state: ActiveMessageState = {
|
|
178
|
-
message: cloned,
|
|
179
|
-
textStreams: new DeltaStreamTracker('text'),
|
|
180
|
-
reasoningStreams: new DeltaStreamTracker('reasoning'),
|
|
181
|
-
toolTrackers,
|
|
182
|
-
streamStatus,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
this._activeMessages.set(messageId, state);
|
|
186
|
-
|
|
187
|
-
// If this message is already in the list (completed previously),
|
|
188
|
-
// replace in-place. Otherwise push as a new entry.
|
|
189
|
-
const existingIdx = this._messageList.findIndex((m) => m.id === message.id);
|
|
190
|
-
if (existingIdx === -1) {
|
|
191
|
-
this._messageList.push(state.message);
|
|
192
|
-
} else {
|
|
193
|
-
this._messageList[existingIdx] = state.message;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
completeMessage(messageId: string): void {
|
|
198
|
-
this._activeMessages.delete(messageId);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// -------------------------------------------------------------------------
|
|
202
|
-
// Shared helpers
|
|
203
|
-
// -------------------------------------------------------------------------
|
|
204
|
-
|
|
205
|
-
private _ensureActiveMessage(messageId: string): ActiveMessageState {
|
|
206
|
-
const existing = this._activeMessages.get(messageId);
|
|
207
|
-
if (existing) return existing;
|
|
208
|
-
|
|
209
|
-
const state: ActiveMessageState = {
|
|
210
|
-
message: { id: messageId, role: 'assistant', parts: [] },
|
|
211
|
-
textStreams: new DeltaStreamTracker('text'),
|
|
212
|
-
reasoningStreams: new DeltaStreamTracker('reasoning'),
|
|
213
|
-
toolTrackers: {},
|
|
214
|
-
streamStatus: new Map(),
|
|
215
|
-
};
|
|
216
|
-
this._activeMessages.set(messageId, state);
|
|
217
|
-
this._messageList.push(state.message);
|
|
218
|
-
return state;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Look up a tracked tool part by toolCallId within a message state.
|
|
223
|
-
* @param toolCallId - The tool call identifier to look up.
|
|
224
|
-
* @param state - The active message state to search in.
|
|
225
|
-
* @returns The tracker and current part, or undefined if not found.
|
|
226
|
-
*/
|
|
227
|
-
private _getToolPart(
|
|
228
|
-
toolCallId: string,
|
|
229
|
-
state: ActiveMessageState,
|
|
230
|
-
): { tracker: ToolPartTracker; part: AI.DynamicToolUIPart } | undefined {
|
|
231
|
-
const tracker = state.toolTrackers[toolCallId];
|
|
232
|
-
if (!tracker) return undefined;
|
|
233
|
-
|
|
234
|
-
const existing = state.message.parts[tracker.partIndex];
|
|
235
|
-
if (existing?.type !== 'dynamic-tool') return undefined;
|
|
236
|
-
|
|
237
|
-
return { tracker, part: existing };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// -------------------------------------------------------------------------
|
|
241
|
-
// Event dispatch
|
|
242
|
-
// -------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
private _processEvent(chunk: AI.UIMessageChunk, messageId: string): void {
|
|
245
|
-
switch (chunk.type) {
|
|
246
|
-
case 'start':
|
|
247
|
-
case 'start-step':
|
|
248
|
-
case 'finish-step':
|
|
249
|
-
case 'finish':
|
|
250
|
-
case 'abort':
|
|
251
|
-
case 'error':
|
|
252
|
-
case 'message-metadata': {
|
|
253
|
-
this._processLifecycle(chunk, messageId);
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
case 'text-start':
|
|
258
|
-
case 'text-delta':
|
|
259
|
-
case 'text-end':
|
|
260
|
-
case 'reasoning-start':
|
|
261
|
-
case 'reasoning-delta':
|
|
262
|
-
case 'reasoning-end': {
|
|
263
|
-
this._processTextOrReasoning(chunk, messageId);
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
case 'tool-input-start':
|
|
268
|
-
case 'tool-input-delta':
|
|
269
|
-
case 'tool-input-available':
|
|
270
|
-
case 'tool-input-error': {
|
|
271
|
-
this._processToolInput(chunk, messageId);
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
case 'tool-output-available':
|
|
276
|
-
case 'tool-output-error':
|
|
277
|
-
case 'tool-output-denied':
|
|
278
|
-
case 'tool-approval-request': {
|
|
279
|
-
this._processToolOutput(chunk, messageId);
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
case 'file':
|
|
284
|
-
case 'source-url':
|
|
285
|
-
case 'source-document': {
|
|
286
|
-
this._processContentPart(chunk, messageId);
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
default: {
|
|
291
|
-
if (chunk.type.startsWith('data-')) {
|
|
292
|
-
if (chunk.transient) break;
|
|
293
|
-
|
|
294
|
-
const state = this._ensureActiveMessage(messageId);
|
|
295
|
-
|
|
296
|
-
// CAST: chunk.type is `data-${string}` which satisfies DataUIPart,
|
|
297
|
-
// but TypeScript cannot verify the template literal matches a
|
|
298
|
-
// specific UIMessagePart variant at the type level.
|
|
299
|
-
const dataPart = stripUndefined({
|
|
300
|
-
type: chunk.type,
|
|
301
|
-
id: chunk.id,
|
|
302
|
-
data: chunk.data,
|
|
303
|
-
}) as AI.UIMessage['parts'][number];
|
|
304
|
-
|
|
305
|
-
if (chunk.id !== undefined) {
|
|
306
|
-
const idx = state.message.parts.findIndex((p) => p.type === chunk.type && 'id' in p && p.id === chunk.id);
|
|
307
|
-
if (idx !== -1) {
|
|
308
|
-
state.message.parts[idx] = dataPart;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
state.message.parts.push(dataPart);
|
|
314
|
-
}
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// -------------------------------------------------------------------------
|
|
321
|
-
// Lifecycle events
|
|
322
|
-
// -------------------------------------------------------------------------
|
|
323
|
-
|
|
324
|
-
private _processLifecycle(
|
|
325
|
-
chunk: Extract<
|
|
326
|
-
AI.UIMessageChunk,
|
|
327
|
-
{ type: 'start' | 'start-step' | 'finish-step' | 'finish' | 'abort' | 'error' | 'message-metadata' }
|
|
328
|
-
>,
|
|
329
|
-
messageId: string,
|
|
330
|
-
): void {
|
|
331
|
-
switch (chunk.type) {
|
|
332
|
-
case 'start': {
|
|
333
|
-
const state = this._ensureActiveMessage(messageId);
|
|
334
|
-
if (chunk.messageId) state.message.id = chunk.messageId;
|
|
335
|
-
if (chunk.messageMetadata !== undefined) {
|
|
336
|
-
state.message.metadata = chunk.messageMetadata;
|
|
337
|
-
}
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
case 'start-step': {
|
|
342
|
-
const state = this._ensureActiveMessage(messageId);
|
|
343
|
-
state.message.parts.push({ type: 'step-start' });
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
case 'finish-step': {
|
|
348
|
-
const state = this._activeMessages.get(messageId);
|
|
349
|
-
if (state) {
|
|
350
|
-
state.textStreams.reset();
|
|
351
|
-
state.reasoningStreams.reset();
|
|
352
|
-
}
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
case 'finish': {
|
|
357
|
-
const state = this._activeMessages.get(messageId);
|
|
358
|
-
if (state && chunk.messageMetadata !== undefined) {
|
|
359
|
-
state.message.metadata = chunk.messageMetadata;
|
|
360
|
-
}
|
|
361
|
-
this._activeMessages.delete(messageId);
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
case 'abort': {
|
|
366
|
-
const state = this._activeMessages.get(messageId);
|
|
367
|
-
if (state) {
|
|
368
|
-
for (const [id, status] of state.streamStatus) {
|
|
369
|
-
if (status === 'streaming') {
|
|
370
|
-
state.streamStatus.set(id, 'aborted');
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
this._activeMessages.delete(messageId);
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
case 'error': {
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
case 'message-metadata': {
|
|
383
|
-
const state = this._activeMessages.get(messageId);
|
|
384
|
-
if (state && chunk.messageMetadata !== undefined) {
|
|
385
|
-
state.message.metadata = chunk.messageMetadata;
|
|
386
|
-
}
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// -------------------------------------------------------------------------
|
|
393
|
-
// Text and reasoning streaming
|
|
394
|
-
// -------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
private _processTextOrReasoning(
|
|
397
|
-
chunk: Extract<
|
|
398
|
-
AI.UIMessageChunk,
|
|
399
|
-
{ type: 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' }
|
|
400
|
-
>,
|
|
401
|
-
messageId: string,
|
|
402
|
-
): void {
|
|
403
|
-
const state = this._ensureActiveMessage(messageId);
|
|
404
|
-
|
|
405
|
-
switch (chunk.type) {
|
|
406
|
-
case 'text-start': {
|
|
407
|
-
state.textStreams.start(chunk.id, state.message, state.streamStatus);
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
case 'text-delta': {
|
|
411
|
-
state.textStreams.delta(chunk.id, state.message, chunk.delta);
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
case 'text-end': {
|
|
415
|
-
state.textStreams.end(chunk.id, state.streamStatus);
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
case 'reasoning-start': {
|
|
419
|
-
state.reasoningStreams.start(chunk.id, state.message, state.streamStatus);
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
case 'reasoning-delta': {
|
|
423
|
-
state.reasoningStreams.delta(chunk.id, state.message, chunk.delta);
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
case 'reasoning-end': {
|
|
427
|
-
state.reasoningStreams.end(chunk.id, state.streamStatus);
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// -------------------------------------------------------------------------
|
|
434
|
-
// Tool input streaming
|
|
435
|
-
// -------------------------------------------------------------------------
|
|
436
|
-
|
|
437
|
-
private _processToolInput(
|
|
438
|
-
chunk: Extract<
|
|
439
|
-
AI.UIMessageChunk,
|
|
440
|
-
{ type: 'tool-input-start' | 'tool-input-delta' | 'tool-input-available' | 'tool-input-error' }
|
|
441
|
-
>,
|
|
442
|
-
messageId: string,
|
|
443
|
-
): void {
|
|
444
|
-
switch (chunk.type) {
|
|
445
|
-
case 'tool-input-start': {
|
|
446
|
-
const state = this._ensureActiveMessage(messageId);
|
|
447
|
-
const partIndex = state.message.parts.length;
|
|
448
|
-
state.message.parts.push({ ...toolBase(chunk), state: 'input-streaming', input: undefined });
|
|
449
|
-
state.toolTrackers[chunk.toolCallId] = { partIndex, inputText: '' };
|
|
450
|
-
state.streamStatus.set(chunk.toolCallId, 'streaming');
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
case 'tool-input-delta': {
|
|
455
|
-
const state = this._ensureActiveMessage(messageId);
|
|
456
|
-
const tracker = state.toolTrackers[chunk.toolCallId];
|
|
457
|
-
if (!tracker) break;
|
|
458
|
-
tracker.inputText += chunk.inputTextDelta;
|
|
459
|
-
|
|
460
|
-
let parsedInput: unknown;
|
|
461
|
-
try {
|
|
462
|
-
// CAST: JSON.parse returns any; unknown is the safe trust-boundary type.
|
|
463
|
-
parsedInput = JSON.parse(tracker.inputText) as unknown;
|
|
464
|
-
} catch {
|
|
465
|
-
parsedInput = undefined;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const found = this._getToolPart(chunk.toolCallId, state);
|
|
469
|
-
if (!found) break;
|
|
470
|
-
state.message.parts[found.tracker.partIndex] = {
|
|
471
|
-
...toolBase(found.part),
|
|
472
|
-
state: 'input-streaming',
|
|
473
|
-
input: parsedInput,
|
|
474
|
-
};
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
case 'tool-input-available': {
|
|
479
|
-
const state = this._ensureActiveMessage(messageId);
|
|
480
|
-
const found = this._getToolPart(chunk.toolCallId, state);
|
|
481
|
-
if (!found) break;
|
|
482
|
-
state.message.parts[found.tracker.partIndex] = {
|
|
483
|
-
...toolBase(found.part),
|
|
484
|
-
state: 'input-available',
|
|
485
|
-
input: chunk.input,
|
|
486
|
-
};
|
|
487
|
-
state.streamStatus.set(chunk.toolCallId, 'finished');
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
case 'tool-input-error': {
|
|
492
|
-
const state = this._ensureActiveMessage(messageId);
|
|
493
|
-
const found = this._getToolPart(chunk.toolCallId, state);
|
|
494
|
-
if (found) {
|
|
495
|
-
state.message.parts[found.tracker.partIndex] = {
|
|
496
|
-
...toolBase(found.part),
|
|
497
|
-
state: 'output-error',
|
|
498
|
-
input: chunk.input,
|
|
499
|
-
errorText: chunk.errorText,
|
|
500
|
-
};
|
|
501
|
-
} else {
|
|
502
|
-
const partIndex = state.message.parts.length;
|
|
503
|
-
state.message.parts.push({
|
|
504
|
-
...toolBase(chunk),
|
|
505
|
-
state: 'output-error',
|
|
506
|
-
input: chunk.input,
|
|
507
|
-
errorText: chunk.errorText,
|
|
508
|
-
});
|
|
509
|
-
state.toolTrackers[chunk.toolCallId] = { partIndex, inputText: '' };
|
|
510
|
-
}
|
|
511
|
-
state.streamStatus.set(chunk.toolCallId, 'finished');
|
|
512
|
-
break;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// -------------------------------------------------------------------------
|
|
518
|
-
// Tool output transitions
|
|
519
|
-
// -------------------------------------------------------------------------
|
|
520
|
-
|
|
521
|
-
private _processToolOutput(
|
|
522
|
-
chunk: Extract<
|
|
523
|
-
AI.UIMessageChunk,
|
|
524
|
-
{ type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
|
|
525
|
-
>,
|
|
526
|
-
messageId: string,
|
|
527
|
-
): void {
|
|
528
|
-
const state = this._ensureActiveMessage(messageId);
|
|
529
|
-
const found = this._getToolPart(chunk.toolCallId, state);
|
|
530
|
-
if (!found) return;
|
|
531
|
-
|
|
532
|
-
state.message.parts[found.tracker.partIndex] = transitionToolPart(found.part, chunk);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// -------------------------------------------------------------------------
|
|
536
|
-
// Content parts
|
|
537
|
-
// -------------------------------------------------------------------------
|
|
538
|
-
|
|
539
|
-
private _processContentPart(
|
|
540
|
-
chunk: Extract<AI.UIMessageChunk, { type: 'file' | 'source-url' | 'source-document' }>,
|
|
541
|
-
messageId: string,
|
|
542
|
-
): void {
|
|
543
|
-
const state = this._ensureActiveMessage(messageId);
|
|
544
|
-
|
|
545
|
-
switch (chunk.type) {
|
|
546
|
-
case 'file': {
|
|
547
|
-
state.message.parts.push({ type: 'file', mediaType: chunk.mediaType, url: chunk.url });
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
case 'source-url': {
|
|
552
|
-
state.message.parts.push(
|
|
553
|
-
stripUndefined({
|
|
554
|
-
type: 'source-url' as const,
|
|
555
|
-
sourceId: chunk.sourceId,
|
|
556
|
-
url: chunk.url,
|
|
557
|
-
title: chunk.title,
|
|
558
|
-
}),
|
|
559
|
-
);
|
|
560
|
-
break;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
case 'source-document': {
|
|
564
|
-
state.message.parts.push(
|
|
565
|
-
stripUndefined({
|
|
566
|
-
type: 'source-document' as const,
|
|
567
|
-
sourceId: chunk.sourceId,
|
|
568
|
-
mediaType: chunk.mediaType,
|
|
569
|
-
title: chunk.title,
|
|
570
|
-
filename: chunk.filename,
|
|
571
|
-
}),
|
|
572
|
-
);
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// ---------------------------------------------------------------------------
|
|
580
|
-
// Factory
|
|
581
|
-
// ---------------------------------------------------------------------------
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Create a Vercel AI SDK accumulator that builds UIMessage[] from decoder outputs.
|
|
585
|
-
* @returns A {@link MessageAccumulator} for UIMessageChunk/UIMessage.
|
|
586
|
-
*/
|
|
587
|
-
export const createAccumulator = (): MessageAccumulator<AI.UIMessageChunk, AI.UIMessage> =>
|
|
588
|
-
new DefaultUIMessageAccumulator();
|