@ably/ai-transport 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -100
- package/dist/ably-ai-transport.js +1553 -1238
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +116 -42
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +407 -115
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +96 -18
- package/dist/core/transport/index.d.ts +5 -6
- package/dist/core/transport/internal/bounded-map.d.ts +20 -0
- package/dist/core/transport/invocation.d.ts +74 -0
- package/dist/core/transport/load-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -9
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +373 -109
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -553
- package/dist/core/transport/view.d.ts +272 -84
- package/dist/errors.d.ts +21 -10
- package/dist/index.d.ts +6 -8
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +976 -990
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/client-session-context.d.ts +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +12 -12
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +17 -14
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +14 -13
- package/dist/react/use-tree.d.ts +30 -15
- package/dist/react/use-view.d.ts +82 -51
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +2 -2
- package/dist/vercel/index.d.ts +4 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
- package/dist/vercel/react/index.d.ts +1 -2
- package/dist/vercel/react/use-chat-transport.d.ts +30 -26
- package/dist/vercel/react/use-message-sync.d.ts +17 -30
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +43 -24
- package/dist/vercel/transport/index.d.ts +25 -21
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +30 -23
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +113 -65
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +436 -120
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +181 -22
- package/src/core/transport/index.ts +25 -26
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +54 -39
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +926 -308
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -706
- package/src/core/transport/view.ts +864 -433
- package/src/errors.ts +22 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +52 -41
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +23 -13
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +32 -22
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +139 -97
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -258
- package/src/vercel/codec/encoder.ts +343 -205
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +60 -13
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +2 -2
- package/src/vercel/index.ts +6 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
- package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
- package/src/vercel/react/index.ts +3 -5
- package/src/vercel/react/use-chat-transport.ts +47 -49
- package/src/vercel/react/use-message-sync.ts +80 -39
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +392 -98
- package/src/vercel/transport/index.ts +39 -38
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/decode-history.d.ts +0 -43
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -29
- package/dist/core/transport/turn-manager.d.ts +0 -37
- package/dist/react/contexts/transport-context.d.ts +0 -31
- package/dist/react/contexts/transport-provider.d.ts +0 -49
- package/dist/react/create-transport-hooks.d.ts +0 -124
- package/dist/react/use-active-turns.d.ts +0 -12
- package/dist/react/use-client-transport.d.ts +0 -80
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
- package/dist/vercel/tool-approvals.d.ts +0 -124
- package/dist/vercel/tool-events.d.ts +0 -26
- package/src/core/transport/client-transport.ts +0 -977
- package/src/core/transport/decode-history.ts +0 -485
- package/src/core/transport/server-transport.ts +0 -612
- package/src/core/transport/stream-router.ts +0 -136
- package/src/core/transport/turn-manager.ts +0 -165
- package/src/react/contexts/transport-context.ts +0 -37
- package/src/react/contexts/transport-provider.tsx +0 -164
- package/src/react/create-transport-hooks.ts +0 -144
- package/src/react/use-active-turns.ts +0 -72
- package/src/react/use-client-transport.ts +0 -197
- package/src/vercel/codec/accumulator.ts +0 -588
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
- package/src/vercel/tool-approvals.ts +0 -380
- package/src/vercel/tool-events.ts +0 -53
|
@@ -1,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();
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useStagedAddToolApprovalResponse — wrap useChat's `addToolApprovalResponse`
|
|
3
|
-
* so the approval response is also applied to the transport tree
|
|
4
|
-
* synchronously at click time.
|
|
5
|
-
*
|
|
6
|
-
* Patching the tree at click time eliminates the useChat↔tree divergence
|
|
7
|
-
* the ChatTransport would otherwise have to reconcile via a history
|
|
8
|
-
* overlay, and closes the observer-turn race that could wipe the
|
|
9
|
-
* approval state between `addToolApprovalResponse` and
|
|
10
|
-
* `sendAutomaticallyWhen`'s evaluation.
|
|
11
|
-
*
|
|
12
|
-
* Use this in place of useChat's raw `addToolApprovalResponse` wherever
|
|
13
|
-
* you wire Approve / Deny buttons.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type * as AI from 'ai';
|
|
17
|
-
import type { ChatAddToolApproveResponseFunction } from 'ai';
|
|
18
|
-
import { useCallback } from 'react';
|
|
19
|
-
|
|
20
|
-
import type { ClientTransport } from '../../core/transport/types.js';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Returns a function with the same signature as useChat's
|
|
24
|
-
* `addToolApprovalResponse`, but additionally applies the approval
|
|
25
|
-
* response to the transport tree via `stageMessage` before delegating.
|
|
26
|
-
*
|
|
27
|
-
* If the tool call identified by `opts.id` isn't found in the tree,
|
|
28
|
-
* the tree update is skipped and the raw function is still called —
|
|
29
|
-
* matches useChat's tolerant behavior for stale approval ids.
|
|
30
|
-
* @param transport - The client transport whose tree to patch.
|
|
31
|
-
* @param addToolApprovalResponse - The raw function from `useChat()`.
|
|
32
|
-
* @returns A drop-in replacement that patches the tree then delegates.
|
|
33
|
-
*/
|
|
34
|
-
export const useStagedAddToolApprovalResponse = (
|
|
35
|
-
transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
|
|
36
|
-
addToolApprovalResponse: ChatAddToolApproveResponseFunction,
|
|
37
|
-
): ChatAddToolApproveResponseFunction =>
|
|
38
|
-
useCallback<ChatAddToolApproveResponseFunction>(
|
|
39
|
-
(opts) => {
|
|
40
|
-
stageApprovalResponseOnTree(transport, opts);
|
|
41
|
-
return addToolApprovalResponse(opts);
|
|
42
|
-
},
|
|
43
|
-
[transport, addToolApprovalResponse],
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Locate the assistant message whose `dynamic-tool` part carries the
|
|
48
|
-
* given `approval.id`, build a patched copy with the part transitioned
|
|
49
|
-
* to `approval-responded`, and stage the patched message on the tree.
|
|
50
|
-
* @param transport - The transport whose tree to patch.
|
|
51
|
-
* @param opts - The approval response being applied.
|
|
52
|
-
* @param opts.id - The approval id matching a dynamic-tool part in the tree.
|
|
53
|
-
* @param opts.approved - Whether the user approved or denied.
|
|
54
|
-
* @param opts.reason - Optional reason accompanying the response.
|
|
55
|
-
*/
|
|
56
|
-
const stageApprovalResponseOnTree = (
|
|
57
|
-
transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
|
|
58
|
-
opts: { id: string; approved: boolean; reason?: string },
|
|
59
|
-
): void => {
|
|
60
|
-
const nodes = transport.view.flattenNodes();
|
|
61
|
-
for (const node of nodes) {
|
|
62
|
-
const partIndex = node.message.parts.findIndex((p) => p.type === 'dynamic-tool' && p.approval?.id === opts.id);
|
|
63
|
-
if (partIndex === -1) continue;
|
|
64
|
-
|
|
65
|
-
// CAST: findIndex predicate above narrows this to a dynamic-tool part
|
|
66
|
-
// with a non-undefined approval.
|
|
67
|
-
const part = node.message.parts[partIndex] as AI.DynamicToolUIPart;
|
|
68
|
-
|
|
69
|
-
// Build the approval-responded variant directly rather than spreading
|
|
70
|
-
// `part`, which TypeScript narrows to whichever source-state variant
|
|
71
|
-
// the union discriminator inferred and then rejects when we change
|
|
72
|
-
// `state` to a variant with different approval/output constraints.
|
|
73
|
-
const patchedPart: AI.DynamicToolUIPart = {
|
|
74
|
-
type: 'dynamic-tool',
|
|
75
|
-
toolName: part.toolName,
|
|
76
|
-
toolCallId: part.toolCallId,
|
|
77
|
-
state: 'approval-responded',
|
|
78
|
-
input: part.input,
|
|
79
|
-
approval: { id: opts.id, approved: opts.approved, reason: opts.reason },
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const patchedParts = [...node.message.parts];
|
|
83
|
-
patchedParts[partIndex] = patchedPart;
|
|
84
|
-
transport.stageMessage(node.msgId, { ...node.message, parts: patchedParts });
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
};
|