@ably/ai-transport 0.0.1 → 0.1.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 +54 -47
- package/dist/ably-ai-transport.js +1006 -539
- 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 +4 -0
- package/dist/core/codec/types.d.ts +19 -2
- package/dist/core/transport/decode-history.d.ts +8 -6
- package/dist/core/transport/headers.d.ts +4 -2
- package/dist/core/transport/index.d.ts +4 -1
- package/dist/core/transport/pipe-stream.d.ts +3 -2
- package/dist/core/transport/stream-router.d.ts +11 -1
- package/dist/core/transport/tree.d.ts +171 -0
- package/dist/core/transport/turn-manager.d.ts +4 -1
- package/dist/core/transport/types.d.ts +270 -119
- package/dist/core/transport/view.d.ts +166 -0
- package/dist/errors.d.ts +19 -2
- package/dist/index.d.ts +3 -1
- package/dist/react/ably-ai-transport-react.js +1019 -486
- 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/transport-context.d.ts +31 -0
- package/dist/react/contexts/transport-provider.d.ts +49 -0
- package/dist/react/create-transport-hooks.d.ts +124 -0
- package/dist/react/index.d.ts +14 -8
- package/dist/react/use-ably-messages.d.ts +14 -8
- package/dist/react/use-active-turns.d.ts +7 -3
- package/dist/react/use-client-transport.d.ts +78 -5
- package/dist/react/use-create-view.d.ts +22 -0
- package/dist/react/use-tree.d.ts +20 -0
- package/dist/react/use-view.d.ts +79 -0
- package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
- 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/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +3 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
- package/dist/vercel/react/index.d.ts +5 -0
- package/dist/vercel/react/use-chat-transport.d.ts +61 -20
- package/dist/vercel/react/use-message-sync.d.ts +41 -9
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
- package/dist/vercel/tool-approvals.d.ts +124 -0
- package/dist/vercel/tool-events.d.ts +26 -0
- package/dist/vercel/transport/chat-transport.d.ts +33 -11
- package/dist/vercel/transport/index.d.ts +5 -2
- package/package.json +23 -17
- package/src/constants.ts +6 -0
- package/src/core/codec/encoder.ts +10 -1
- package/src/core/codec/types.ts +19 -3
- package/src/core/transport/client-transport.ts +382 -364
- package/src/core/transport/decode-history.ts +229 -81
- package/src/core/transport/headers.ts +6 -2
- package/src/core/transport/index.ts +13 -5
- package/src/core/transport/pipe-stream.ts +8 -5
- package/src/core/transport/server-transport.ts +212 -58
- package/src/core/transport/stream-router.ts +21 -3
- package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
- package/src/core/transport/turn-manager.ts +28 -10
- package/src/core/transport/types.ts +318 -139
- package/src/core/transport/view.ts +840 -0
- package/src/errors.ts +21 -1
- package/src/index.ts +10 -5
- package/src/react/contexts/transport-context.ts +37 -0
- package/src/react/contexts/transport-provider.tsx +164 -0
- package/src/react/create-transport-hooks.ts +144 -0
- package/src/react/index.ts +15 -8
- package/src/react/use-ably-messages.ts +34 -16
- package/src/react/use-active-turns.ts +28 -17
- package/src/react/use-client-transport.ts +184 -24
- package/src/react/use-create-view.ts +68 -0
- package/src/react/use-tree.ts +53 -0
- package/src/react/use-view.ts +233 -0
- package/src/react/vite.config.ts +4 -1
- package/src/vercel/codec/accumulator.ts +64 -79
- package/src/vercel/codec/decoder.ts +11 -8
- package/src/vercel/codec/encoder.ts +68 -54
- package/src/vercel/codec/index.ts +0 -2
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +17 -0
- package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
- package/src/vercel/react/index.ts +14 -0
- package/src/vercel/react/use-chat-transport.ts +164 -42
- package/src/vercel/react/use-message-sync.ts +77 -19
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/tool-approvals.ts +380 -0
- package/src/vercel/tool-events.ts +53 -0
- package/src/vercel/transport/chat-transport.ts +225 -79
- package/src/vercel/transport/index.ts +14 -3
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- package/src/react/use-send.ts +0 -25
|
@@ -17,6 +17,7 @@ import type * as AI from 'ai';
|
|
|
17
17
|
|
|
18
18
|
import type { DecoderOutput, MessageAccumulator } from '../../core/codec/types.js';
|
|
19
19
|
import { stripUndefined } from '../../utils.js';
|
|
20
|
+
import { toolBase, transitionToolPart } from './tool-transitions.js';
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Internal types
|
|
@@ -38,15 +39,6 @@ interface ToolPartTracker {
|
|
|
38
39
|
inputText: string;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
/** Fields shared by all DynamicToolUIPart state variants. */
|
|
42
|
-
interface ToolBaseFields {
|
|
43
|
-
type: 'dynamic-tool';
|
|
44
|
-
toolName: string;
|
|
45
|
-
toolCallId: string;
|
|
46
|
-
title?: string;
|
|
47
|
-
providerExecuted?: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
42
|
/** Bundled per-message state for an in-progress message. */
|
|
51
43
|
interface ActiveMessageState {
|
|
52
44
|
message: AI.UIMessage;
|
|
@@ -56,34 +48,6 @@ interface ActiveMessageState {
|
|
|
56
48
|
streamStatus: Map<string, StreamStatus>;
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Tool base helper
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extract the state-independent base fields for a DynamicToolUIPart.
|
|
65
|
-
* Works with both chunks (tool-input-start, etc.) and existing parts.
|
|
66
|
-
* @param source - Any object containing the required tool identity fields.
|
|
67
|
-
* @param source.toolCallId - The tool call identifier.
|
|
68
|
-
* @param source.toolName - The tool name.
|
|
69
|
-
* @param source.title - Optional display title.
|
|
70
|
-
* @param source.providerExecuted - Whether the provider executed the tool.
|
|
71
|
-
* @returns Base fields shared across all DynamicToolUIPart state variants.
|
|
72
|
-
*/
|
|
73
|
-
const toolBase = (source: {
|
|
74
|
-
toolCallId: string;
|
|
75
|
-
toolName: string;
|
|
76
|
-
title?: string;
|
|
77
|
-
providerExecuted?: boolean;
|
|
78
|
-
}): ToolBaseFields =>
|
|
79
|
-
stripUndefined({
|
|
80
|
-
type: 'dynamic-tool' as const,
|
|
81
|
-
toolCallId: source.toolCallId,
|
|
82
|
-
toolName: source.toolName,
|
|
83
|
-
title: source.title,
|
|
84
|
-
providerExecuted: source.providerExecuted,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
51
|
// ---------------------------------------------------------------------------
|
|
88
52
|
// DeltaStreamTracker — manages text or reasoning stream accumulation
|
|
89
53
|
// ---------------------------------------------------------------------------
|
|
@@ -172,6 +136,68 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
|
|
|
172
136
|
}
|
|
173
137
|
}
|
|
174
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
|
+
|
|
175
201
|
// -------------------------------------------------------------------------
|
|
176
202
|
// Shared helpers
|
|
177
203
|
// -------------------------------------------------------------------------
|
|
@@ -503,48 +529,7 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
|
|
|
503
529
|
const found = this._getToolPart(chunk.toolCallId, state);
|
|
504
530
|
if (!found) return;
|
|
505
531
|
|
|
506
|
-
|
|
507
|
-
case 'tool-output-available': {
|
|
508
|
-
state.message.parts[found.tracker.partIndex] = stripUndefined({
|
|
509
|
-
...toolBase(found.part),
|
|
510
|
-
state: 'output-available' as const,
|
|
511
|
-
input: found.part.input,
|
|
512
|
-
output: chunk.output,
|
|
513
|
-
preliminary: chunk.preliminary,
|
|
514
|
-
});
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
case 'tool-output-error': {
|
|
519
|
-
state.message.parts[found.tracker.partIndex] = {
|
|
520
|
-
...toolBase(found.part),
|
|
521
|
-
state: 'output-error',
|
|
522
|
-
input: found.part.input,
|
|
523
|
-
errorText: chunk.errorText,
|
|
524
|
-
};
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
case 'tool-output-denied': {
|
|
529
|
-
state.message.parts[found.tracker.partIndex] = {
|
|
530
|
-
...toolBase(found.part),
|
|
531
|
-
state: 'output-denied',
|
|
532
|
-
input: found.part.input,
|
|
533
|
-
approval: { id: '', approved: false },
|
|
534
|
-
};
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
case 'tool-approval-request': {
|
|
539
|
-
state.message.parts[found.tracker.partIndex] = {
|
|
540
|
-
...toolBase(found.part),
|
|
541
|
-
state: 'approval-requested',
|
|
542
|
-
input: found.part.input,
|
|
543
|
-
approval: { id: chunk.approvalId },
|
|
544
|
-
};
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
532
|
+
state.message.parts[found.tracker.partIndex] = transitionToolPart(found.part, chunk);
|
|
548
533
|
}
|
|
549
534
|
|
|
550
535
|
// -------------------------------------------------------------------------
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type * as Ably from 'ably';
|
|
15
15
|
import type * as AI from 'ai';
|
|
16
16
|
|
|
17
|
-
import { HEADER_ROLE, HEADER_TURN_ID } from '../../constants.js';
|
|
17
|
+
import { HEADER_DISCRETE, HEADER_ROLE, HEADER_TURN_ID } from '../../constants.js';
|
|
18
18
|
import type { DecoderCore, DecoderCoreHooks, DecoderCoreOptions } from '../../core/codec/decoder.js';
|
|
19
19
|
import { createDecoderCore, eventOutput } from '../../core/codec/decoder.js';
|
|
20
20
|
import type { LifecycleTracker } from '../../core/codec/lifecycle-tracker.js';
|
|
@@ -277,7 +277,8 @@ const decodeFinish = (r: VercelHeaderReader, turnId: string, lifecycle: Lifecycl
|
|
|
277
277
|
);
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
-
const decodeError = (data: unknown): Out[] => {
|
|
280
|
+
const decodeError = (data: unknown, turnId: string, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
|
|
281
|
+
lifecycle.clearScope(turnId);
|
|
281
282
|
const errorText = typeof data === 'string' ? data : '';
|
|
282
283
|
return event({ type: 'error', errorText });
|
|
283
284
|
};
|
|
@@ -483,14 +484,16 @@ const decodeDiscreteMessage = (input: MessagePayload): Out[] => {
|
|
|
483
484
|
|
|
484
485
|
/**
|
|
485
486
|
* Whether a message name represents a discrete message part (written by writeMessages)
|
|
486
|
-
* rather than a streaming lifecycle event.
|
|
487
|
-
*
|
|
487
|
+
* rather than a streaming lifecycle event. Distinguished by the `x-ably-discrete` header
|
|
488
|
+
* which {@link publishDiscreteBatch} sets on batch-published message payloads. Lifecycle
|
|
489
|
+
* events published via {@link publishDiscrete} (including streaming `data-*` chunks)
|
|
490
|
+
* do not carry this header.
|
|
488
491
|
* @param name - The Ably message name to check.
|
|
489
|
-
* @param headers - The Ably message headers to inspect for
|
|
492
|
+
* @param headers - The Ably message headers to inspect for discrete marker presence.
|
|
490
493
|
* @returns True if this is a discrete message part, false if it's a lifecycle event.
|
|
491
494
|
*/
|
|
492
495
|
const isDiscreteMessagePart = (name: string, headers: Record<string, string>): boolean =>
|
|
493
|
-
(name === 'text' || name === 'file' || isDataEventName(name)) &&
|
|
496
|
+
(name === 'text' || name === 'file' || isDataEventName(name)) && HEADER_DISCRETE in headers;
|
|
494
497
|
|
|
495
498
|
const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracker<AI.UIMessageChunk>): Out[] => {
|
|
496
499
|
const h = input.headers ?? {};
|
|
@@ -498,7 +501,7 @@ const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracke
|
|
|
498
501
|
const turnId = h[HEADER_TURN_ID] ?? '';
|
|
499
502
|
|
|
500
503
|
// Discrete message parts from writeMessages (user messages, history entries).
|
|
501
|
-
// Distinguished from lifecycle events by the presence of x-ably-
|
|
504
|
+
// Distinguished from lifecycle events by the presence of x-ably-discrete.
|
|
502
505
|
if (isDiscreteMessagePart(input.name, h)) {
|
|
503
506
|
return decodeDiscreteMessage(input);
|
|
504
507
|
}
|
|
@@ -521,7 +524,7 @@ const decodeDiscretePayload = (input: MessagePayload, lifecycle: LifecycleTracke
|
|
|
521
524
|
return decodeFinish(r, turnId, lifecycle);
|
|
522
525
|
}
|
|
523
526
|
case 'error': {
|
|
524
|
-
return decodeError(input.data);
|
|
527
|
+
return decodeError(input.data, turnId, lifecycle);
|
|
525
528
|
}
|
|
526
529
|
case 'abort': {
|
|
527
530
|
return decodeAbort(input.data, turnId, lifecycle);
|
|
@@ -40,16 +40,78 @@ import type { ChannelWriter, MessagePayload, StreamEncoder, WriteOptions } from
|
|
|
40
40
|
import { ErrorCode, errorInfoIs } from '../../errors.js';
|
|
41
41
|
import { headerWriter } from '../../utils.js';
|
|
42
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Discrete event payload builder
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a MessagePayload for discrete (non-streaming) event types.
|
|
49
|
+
* Used by both `writeEvent` and `appendEvent` for tool output events,
|
|
50
|
+
* content parts, and data-* custom chunks.
|
|
51
|
+
* @param chunk - The UI message chunk to encode.
|
|
52
|
+
* @returns The message payload for publishing to the channel.
|
|
53
|
+
*/
|
|
54
|
+
const buildDiscretePayload = (chunk: AI.UIMessageChunk): MessagePayload => {
|
|
55
|
+
switch (chunk.type) {
|
|
56
|
+
case 'tool-output-available': {
|
|
57
|
+
const h = headerWriter()
|
|
58
|
+
.str('toolCallId', chunk.toolCallId)
|
|
59
|
+
.bool('dynamic', chunk.dynamic)
|
|
60
|
+
.bool('providerExecuted', chunk.providerExecuted)
|
|
61
|
+
.bool('preliminary', chunk.preliminary)
|
|
62
|
+
.build();
|
|
63
|
+
return { name: 'tool-output-available', data: { output: chunk.output }, headers: h };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'tool-output-error': {
|
|
67
|
+
const h = headerWriter()
|
|
68
|
+
.str('toolCallId', chunk.toolCallId)
|
|
69
|
+
.bool('dynamic', chunk.dynamic)
|
|
70
|
+
.bool('providerExecuted', chunk.providerExecuted)
|
|
71
|
+
.build();
|
|
72
|
+
return { name: 'tool-output-error', data: { errorText: chunk.errorText }, headers: h };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'tool-approval-request': {
|
|
76
|
+
const h = headerWriter().str('toolCallId', chunk.toolCallId).str('approvalId', chunk.approvalId).build();
|
|
77
|
+
return { name: 'tool-approval-request', data: '', headers: h };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'tool-output-denied': {
|
|
81
|
+
const h = headerWriter().str('toolCallId', chunk.toolCallId).build();
|
|
82
|
+
return { name: 'tool-output-denied', data: '', headers: h };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
default: {
|
|
86
|
+
if (chunk.type.startsWith('data-')) {
|
|
87
|
+
// CAST: data-* chunks always have id, transient, and data fields per AI SDK types.
|
|
88
|
+
// TypeScript can't narrow the template literal union in a default case.
|
|
89
|
+
const dataChunk = chunk as Extract<AI.UIMessageChunk, { type: `data-${string}` }>;
|
|
90
|
+
const h = headerWriter().str('id', dataChunk.id).bool('transient', dataChunk.transient).build();
|
|
91
|
+
const ephemeral = dataChunk.transient === true;
|
|
92
|
+
return { name: chunk.type, data: dataChunk.data, headers: h, ephemeral };
|
|
93
|
+
}
|
|
94
|
+
throw new Ably.ErrorInfo(
|
|
95
|
+
`unable to write event; unsupported chunk type '${chunk.type}'`,
|
|
96
|
+
ErrorCode.InvalidArgument,
|
|
97
|
+
400,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
43
103
|
// ---------------------------------------------------------------------------
|
|
44
104
|
// Default implementation
|
|
45
105
|
// ---------------------------------------------------------------------------
|
|
46
106
|
|
|
47
107
|
class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIMessage> {
|
|
48
108
|
private readonly _core: EncoderCore;
|
|
109
|
+
private readonly _messageId: string | undefined;
|
|
49
110
|
private _aborted = false;
|
|
50
111
|
|
|
51
112
|
constructor(writer: ChannelWriter, options: EncoderCoreOptions = {}) {
|
|
52
113
|
this._core = createEncoderCore(writer, options);
|
|
114
|
+
this._messageId = options.messageId;
|
|
53
115
|
}
|
|
54
116
|
|
|
55
117
|
async appendEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<void> {
|
|
@@ -144,7 +206,7 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
|
|
|
144
206
|
|
|
145
207
|
case 'start': {
|
|
146
208
|
const h = headerWriter()
|
|
147
|
-
.str('messageId', chunk.messageId)
|
|
209
|
+
.str('messageId', chunk.messageId ?? this._messageId)
|
|
148
210
|
.json('messageMetadata', chunk.messageMetadata)
|
|
149
211
|
.build();
|
|
150
212
|
await this._core.publishDiscrete({ name: 'start', data: '', headers: h }, perWrite);
|
|
@@ -204,44 +266,11 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
|
|
|
204
266
|
break;
|
|
205
267
|
}
|
|
206
268
|
|
|
207
|
-
case 'tool-output-available':
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.bool('dynamic', chunk.dynamic)
|
|
211
|
-
.bool('providerExecuted', chunk.providerExecuted)
|
|
212
|
-
.bool('preliminary', chunk.preliminary)
|
|
213
|
-
.build();
|
|
214
|
-
await this._core.publishDiscrete({
|
|
215
|
-
name: 'tool-output-available',
|
|
216
|
-
data: { output: chunk.output },
|
|
217
|
-
headers: h,
|
|
218
|
-
});
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
case 'tool-output-error': {
|
|
223
|
-
const h = headerWriter()
|
|
224
|
-
.str('toolCallId', chunk.toolCallId)
|
|
225
|
-
.bool('dynamic', chunk.dynamic)
|
|
226
|
-
.bool('providerExecuted', chunk.providerExecuted)
|
|
227
|
-
.build();
|
|
228
|
-
await this._core.publishDiscrete({
|
|
229
|
-
name: 'tool-output-error',
|
|
230
|
-
data: { errorText: chunk.errorText },
|
|
231
|
-
headers: h,
|
|
232
|
-
});
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
case 'tool-approval-request': {
|
|
237
|
-
const h = headerWriter().str('toolCallId', chunk.toolCallId).str('approvalId', chunk.approvalId).build();
|
|
238
|
-
await this._core.publishDiscrete({ name: 'tool-approval-request', data: '', headers: h }, perWrite);
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
|
|
269
|
+
case 'tool-output-available':
|
|
270
|
+
case 'tool-output-error':
|
|
271
|
+
case 'tool-approval-request':
|
|
242
272
|
case 'tool-output-denied': {
|
|
243
|
-
|
|
244
|
-
await this._core.publishDiscrete({ name: 'tool-output-denied', data: '', headers: h }, perWrite);
|
|
273
|
+
await this._core.publishDiscrete(buildDiscretePayload(chunk), perWrite);
|
|
245
274
|
break;
|
|
246
275
|
}
|
|
247
276
|
|
|
@@ -298,22 +327,7 @@ class DefaultUIMessageEncoder implements StreamEncoder<AI.UIMessageChunk, AI.UIM
|
|
|
298
327
|
}
|
|
299
328
|
|
|
300
329
|
async writeEvent(chunk: AI.UIMessageChunk, perWrite?: WriteOptions): Promise<Ably.PublishResult> {
|
|
301
|
-
|
|
302
|
-
throw new Ably.ErrorInfo(
|
|
303
|
-
`unable to write event; only data-* chunk types are supported, got '${chunk.type}'`,
|
|
304
|
-
ErrorCode.InvalidArgument,
|
|
305
|
-
400,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
const h = headerWriter()
|
|
309
|
-
.str('id', 'id' in chunk ? chunk.id : undefined)
|
|
310
|
-
.bool('transient', 'transient' in chunk ? chunk.transient : undefined)
|
|
311
|
-
.build();
|
|
312
|
-
const ephemeral = 'transient' in chunk && chunk.transient === true;
|
|
313
|
-
return this._core.publishDiscrete(
|
|
314
|
-
{ name: chunk.type, data: 'data' in chunk ? chunk.data : undefined, headers: h, ephemeral },
|
|
315
|
-
perWrite,
|
|
316
|
-
);
|
|
330
|
+
return this._core.publishDiscrete(buildDiscretePayload(chunk), perWrite);
|
|
317
331
|
}
|
|
318
332
|
|
|
319
333
|
async writeMessages(messages: AI.UIMessage[], perWrite?: WriteOptions): Promise<Ably.PublishResult> {
|
|
@@ -30,8 +30,6 @@ export const UIMessageCodec: Codec<AI.UIMessageChunk, AI.UIMessage> = {
|
|
|
30
30
|
createDecoder,
|
|
31
31
|
createAccumulator,
|
|
32
32
|
|
|
33
|
-
getMessageKey: (message: AI.UIMessage): string => message.id,
|
|
34
|
-
|
|
35
33
|
isTerminal: (event: AI.UIMessageChunk): boolean =>
|
|
36
34
|
event.type === 'finish' || event.type === 'error' || event.type === 'abort',
|
|
37
35
|
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool part transition logic for the Vercel AI SDK codec.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the accumulator so the tool output state transition logic
|
|
5
|
+
* lives in one place, reusable by the accumulator and any other callers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type * as AI from 'ai';
|
|
9
|
+
|
|
10
|
+
import { stripUndefined } from '../../utils.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Tool output chunk type guard
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** The set of UIMessageChunk types that represent tool output transitions. */
|
|
17
|
+
export type ToolOutputChunk = Extract<
|
|
18
|
+
AI.UIMessageChunk,
|
|
19
|
+
{ type: 'tool-output-available' | 'tool-output-error' | 'tool-output-denied' | 'tool-approval-request' }
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a UIMessageChunk is a tool output transition event.
|
|
24
|
+
* @param chunk - The chunk to test.
|
|
25
|
+
* @returns True if the chunk is a tool output transition type.
|
|
26
|
+
*/
|
|
27
|
+
export const isToolOutputChunk = (chunk: AI.UIMessageChunk): chunk is ToolOutputChunk =>
|
|
28
|
+
chunk.type === 'tool-output-available' ||
|
|
29
|
+
chunk.type === 'tool-output-error' ||
|
|
30
|
+
chunk.type === 'tool-output-denied' ||
|
|
31
|
+
chunk.type === 'tool-approval-request';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Tool base helper
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Fields shared by all DynamicToolUIPart state variants. */
|
|
38
|
+
interface ToolBaseFields {
|
|
39
|
+
type: 'dynamic-tool';
|
|
40
|
+
toolName: string;
|
|
41
|
+
toolCallId: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
providerExecuted?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract the state-independent base fields for a DynamicToolUIPart.
|
|
48
|
+
* Works with both chunks (tool-input-start, etc.) and existing parts.
|
|
49
|
+
* @param source - Any object containing the required tool identity fields.
|
|
50
|
+
* @param source.toolCallId - The tool call identifier.
|
|
51
|
+
* @param source.toolName - The tool name.
|
|
52
|
+
* @param source.title - Optional display title.
|
|
53
|
+
* @param source.providerExecuted - Whether the provider executed the tool.
|
|
54
|
+
* @returns Base fields shared across all DynamicToolUIPart state variants.
|
|
55
|
+
*/
|
|
56
|
+
export const toolBase = (source: {
|
|
57
|
+
toolCallId: string;
|
|
58
|
+
toolName: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
providerExecuted?: boolean;
|
|
61
|
+
}): ToolBaseFields =>
|
|
62
|
+
stripUndefined({
|
|
63
|
+
type: 'dynamic-tool' as const,
|
|
64
|
+
toolCallId: source.toolCallId,
|
|
65
|
+
toolName: source.toolName,
|
|
66
|
+
title: source.title,
|
|
67
|
+
providerExecuted: source.providerExecuted,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Tool part transition
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Transition a DynamicToolUIPart to a new state based on a tool output chunk.
|
|
76
|
+
* Pure function — does not mutate the input part.
|
|
77
|
+
* @param part - The existing tool part to transition.
|
|
78
|
+
* @param chunk - The tool output chunk describing the transition.
|
|
79
|
+
* @returns A new DynamicToolUIPart in the target state.
|
|
80
|
+
*/
|
|
81
|
+
export const transitionToolPart = (part: AI.DynamicToolUIPart, chunk: ToolOutputChunk): AI.DynamicToolUIPart => {
|
|
82
|
+
const base = toolBase(part);
|
|
83
|
+
|
|
84
|
+
switch (chunk.type) {
|
|
85
|
+
case 'tool-output-available': {
|
|
86
|
+
return stripUndefined({
|
|
87
|
+
...base,
|
|
88
|
+
state: 'output-available' as const,
|
|
89
|
+
input: part.input,
|
|
90
|
+
output: chunk.output,
|
|
91
|
+
preliminary: chunk.preliminary,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'tool-output-error': {
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
state: 'output-error',
|
|
99
|
+
input: part.input,
|
|
100
|
+
errorText: chunk.errorText,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'tool-output-denied': {
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
state: 'output-denied',
|
|
108
|
+
input: part.input,
|
|
109
|
+
approval: { id: '', approved: false },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case 'tool-approval-request': {
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
state: 'approval-requested',
|
|
117
|
+
input: part.input,
|
|
118
|
+
approval: { id: chunk.approvalId },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
package/src/vercel/index.ts
CHANGED
|
@@ -10,3 +10,20 @@ export type {
|
|
|
10
10
|
VercelServerTransportOptions,
|
|
11
11
|
} from './transport/index.js';
|
|
12
12
|
export { createChatTransport, createClientTransport, createServerTransport } from './transport/index.js';
|
|
13
|
+
|
|
14
|
+
// Server-side tool result merge helper
|
|
15
|
+
export { applyToolEventsToHistory } from './tool-events.js';
|
|
16
|
+
|
|
17
|
+
// Server-side tool approval helpers
|
|
18
|
+
export type {
|
|
19
|
+
PrepareApprovalTurnOptions,
|
|
20
|
+
PrepareApprovalTurnResult,
|
|
21
|
+
StreamResponseWithApprovalRedirectOptions,
|
|
22
|
+
ToolApprovalDecision,
|
|
23
|
+
} from './tool-approvals.js';
|
|
24
|
+
export {
|
|
25
|
+
applyToolApprovalsToHistory,
|
|
26
|
+
extractApprovalDecisionsFromHistory,
|
|
27
|
+
prepareApprovalTurn,
|
|
28
|
+
streamResponseWithApprovalRedirect,
|
|
29
|
+
} from './tool-approvals.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type * as Ably from 'ably';
|
|
2
|
+
import type * as AI from 'ai';
|
|
3
|
+
import { createContext } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ClientTransport } from '../../../core/transport/types.js';
|
|
6
|
+
import type { ChatTransport } from '../../transport/chat-transport.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single entry in the chat transport registry, holding both the
|
|
10
|
+
* underlying {@link ClientTransport} and the {@link ChatTransport} wrapping it.
|
|
11
|
+
*/
|
|
12
|
+
export interface ChatTransportSlot {
|
|
13
|
+
/** The underlying client transport used to create the chat transport. */
|
|
14
|
+
readonly transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>;
|
|
15
|
+
/** Construction error from the underlying {@link ClientTransport}, or `undefined` on success. */
|
|
16
|
+
readonly transportError: Ably.ErrorInfo | undefined;
|
|
17
|
+
/** The chat transport adapter for use with Vercel's useChat hook. */
|
|
18
|
+
readonly chatTransport: ChatTransport;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The shape of the single {@link ChatTransportContext} value.
|
|
23
|
+
* Combines the nearest slot with the full registry in one context object.
|
|
24
|
+
*/
|
|
25
|
+
export interface ChatTransportContextValue {
|
|
26
|
+
/** The slot from the nearest {@link ChatTransportProvider} in the tree. */
|
|
27
|
+
readonly nearest: ChatTransportSlot | undefined;
|
|
28
|
+
/** All registered slots, keyed by channelName. */
|
|
29
|
+
readonly providers: Readonly<Record<string, ChatTransportSlot>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Context that carries both the nearest {@link ChatTransportSlot} and the full registry of
|
|
34
|
+
* registered slots keyed by channelName. Populated by {@link ChatTransportProvider};
|
|
35
|
+
* read by {@link useChatTransport}.
|
|
36
|
+
*/
|
|
37
|
+
export const ChatTransportContext = createContext<ChatTransportContextValue>({
|
|
38
|
+
nearest: undefined,
|
|
39
|
+
providers: {},
|
|
40
|
+
});
|