@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
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
* to the core transport's send/cancel methods.
|
|
8
8
|
*
|
|
9
9
|
* useChat manages message state before calling sendMessages:
|
|
10
|
-
* - submit-message: appends the new user message, passes the full array
|
|
10
|
+
* - submit-message (new): appends the new user message, passes the full array
|
|
11
|
+
* - submit-message (edit): truncates after the edited message, replaces it,
|
|
12
|
+
* passes the truncated array with messageId set
|
|
11
13
|
* - regenerate-message: truncates after the target, passes the truncated array
|
|
12
14
|
*
|
|
13
15
|
* The adapter uses `trigger` to determine the history/messages split:
|
|
14
16
|
* - submit-message: last message is new (publish to channel), rest is history
|
|
15
17
|
* - regenerate-message: no new messages, entire array is history
|
|
18
|
+
*
|
|
19
|
+
* When messageId is set (edit or regeneration), the adapter computes fork
|
|
20
|
+
* metadata (forkOf/parent) from the conversation tree so the server can
|
|
21
|
+
* place the response on the correct branch.
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
24
|
import * as Ably from 'ably';
|
|
@@ -31,12 +37,14 @@ import { ErrorCode } from '../../errors.js';
|
|
|
31
37
|
*/
|
|
32
38
|
export interface SendMessagesRequestContext {
|
|
33
39
|
/** Chat session ID (from useChat's id). */
|
|
34
|
-
|
|
40
|
+
chatId?: string;
|
|
35
41
|
/** What triggered the request: user sent a message, or requested regeneration. */
|
|
36
42
|
trigger: 'submit-message' | 'regenerate-message';
|
|
37
43
|
/**
|
|
38
|
-
* The message ID for regeneration requests.
|
|
39
|
-
* message to regenerate.
|
|
44
|
+
* The message ID for edit or regeneration requests. For regeneration,
|
|
45
|
+
* identifies the assistant message to regenerate. For edits (submit-message
|
|
46
|
+
* with messageId), identifies the user message being replaced. Undefined
|
|
47
|
+
* when submitting a new message.
|
|
40
48
|
*/
|
|
41
49
|
messageId?: string;
|
|
42
50
|
/** Previous messages in the conversation (context for the LLM). */
|
|
@@ -46,7 +54,7 @@ export interface SendMessagesRequestContext {
|
|
|
46
54
|
/** The msg-id of the message being forked (regenerated or edited). */
|
|
47
55
|
forkOf?: string;
|
|
48
56
|
/** The msg-id of the predecessor in the conversation thread. */
|
|
49
|
-
parent?: string
|
|
57
|
+
parent?: string;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/** Options for customizing the ChatTransport behavior. */
|
|
@@ -91,7 +99,8 @@ interface ChatRequestOptions {
|
|
|
91
99
|
*
|
|
92
100
|
* Structurally compatible with the AI SDK's internal `ChatTransport<UIMessage>`
|
|
93
101
|
* interface. Extended with `close()` for releasing the underlying Ably transport
|
|
94
|
-
* resources
|
|
102
|
+
* resources and `streaming` / `onStreamingChange` for coordinating with
|
|
103
|
+
* useMessageSync.
|
|
95
104
|
*/
|
|
96
105
|
export interface ChatTransport {
|
|
97
106
|
/** Send messages and return a streaming response of UIMessageChunk events. */
|
|
@@ -123,8 +132,78 @@ export interface ChatTransport {
|
|
|
123
132
|
|
|
124
133
|
/** Close the underlying transport, releasing all resources. */
|
|
125
134
|
close(options?: CloseOptions): Promise<void>;
|
|
135
|
+
|
|
136
|
+
/** Whether an own-turn stream is currently being consumed by useChat. */
|
|
137
|
+
readonly streaming: boolean;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Subscribe to streaming state changes. The callback fires when the
|
|
141
|
+
* ChatTransport transitions between streaming and idle. Used by
|
|
142
|
+
* useMessageSync to gate setMessages calls during active streams.
|
|
143
|
+
* @param callback - Called with `true` when a stream starts, `false` when it ends.
|
|
144
|
+
* @returns Unsubscribe function.
|
|
145
|
+
*/
|
|
146
|
+
onStreamingChange(callback: (streaming: boolean) => void): () => void;
|
|
126
147
|
}
|
|
127
148
|
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Stream wrapper — passthrough that signals completion via a promise
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Wrap a ReadableStream in a passthrough TransformStream that resolves a
|
|
155
|
+
* promise when the stream completes or errors. The returned stream passes
|
|
156
|
+
* all chunks through unchanged.
|
|
157
|
+
* @param source - The original stream to wrap.
|
|
158
|
+
* @returns The wrapped stream and a `done` promise that resolves when the stream closes.
|
|
159
|
+
*/
|
|
160
|
+
const wrapStreamWithDone = <T>(source: ReadableStream<T>): { stream: ReadableStream<T>; done: Promise<void> } => {
|
|
161
|
+
let resolveDone: () => void;
|
|
162
|
+
const done = new Promise<void>((resolve) => {
|
|
163
|
+
resolveDone = resolve;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const passthrough = new TransformStream<T, T>({
|
|
167
|
+
flush: () => {
|
|
168
|
+
resolveDone();
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Pipe in the background. If the source errors or is cancelled, resolve
|
|
173
|
+
// done so the serialization queue advances.
|
|
174
|
+
// Fire-and-forget: the pipe runs independently; errors surface through
|
|
175
|
+
// the readable side that useChat consumes.
|
|
176
|
+
source.pipeTo(passthrough.writable).catch(() => {
|
|
177
|
+
resolveDone();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return { stream: passthrough.readable, done };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Unresolved tool call detection
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whether an assistant message has a `dynamic-tool` part that can't resolve
|
|
189
|
+
* without further user action. Matches:
|
|
190
|
+
* - `input-streaming` / `input-available` — tool call emitted, not yet run.
|
|
191
|
+
* - `approval-requested` — waiting for the user.
|
|
192
|
+
*
|
|
193
|
+
* Excludes `approval-responded` (streamText will run the tool this turn)
|
|
194
|
+
* and all terminal `output-*` states.
|
|
195
|
+
* @param msg - The UIMessage to inspect.
|
|
196
|
+
* @returns True when a fork-on-send is warranted to avoid shipping a
|
|
197
|
+
* dangling tool call to the LLM.
|
|
198
|
+
*/
|
|
199
|
+
const hasUnresolvedToolCall = (msg: AI.UIMessage): boolean =>
|
|
200
|
+
msg.role === 'assistant' &&
|
|
201
|
+
msg.parts.some(
|
|
202
|
+
(p) =>
|
|
203
|
+
p.type === 'dynamic-tool' &&
|
|
204
|
+
(p.state === 'input-streaming' || p.state === 'input-available' || p.state === 'approval-requested'),
|
|
205
|
+
);
|
|
206
|
+
|
|
128
207
|
// ---------------------------------------------------------------------------
|
|
129
208
|
// Factory
|
|
130
209
|
// ---------------------------------------------------------------------------
|
|
@@ -132,11 +211,14 @@ export interface ChatTransport {
|
|
|
132
211
|
/**
|
|
133
212
|
* Create a Vercel ChatTransport from a core ClientTransport.
|
|
134
213
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
214
|
+
* Exposes a `streaming` flag and `onStreamingChange` callback so that
|
|
215
|
+
* `useMessageSync` can gate `setMessages` calls during active own-turn
|
|
216
|
+
* streams, preventing the push/replace ID mismatch in useChat's `write()`.
|
|
217
|
+
*
|
|
218
|
+
* Note: concurrent `sendMessage` calls from the same user are a useChat
|
|
219
|
+
* limitation that cannot be fixed from the transport layer. The
|
|
220
|
+
* developer must respect useChat's `status` and only call `sendMessage`
|
|
221
|
+
* when status is `'ready'`.
|
|
140
222
|
* @param transport - The core client transport to wrap.
|
|
141
223
|
* @param chatOptions - Optional hooks for customizing request construction.
|
|
142
224
|
* @returns A {@link ChatTransport} compatible with Vercel's useChat hook.
|
|
@@ -144,17 +226,73 @@ export interface ChatTransport {
|
|
|
144
226
|
export const createChatTransport = (
|
|
145
227
|
transport: ClientTransport<AI.UIMessageChunk, AI.UIMessage>,
|
|
146
228
|
chatOptions?: ChatTransportOptions,
|
|
147
|
-
): ChatTransport =>
|
|
148
|
-
|
|
229
|
+
): ChatTransport => {
|
|
230
|
+
// -- Streaming state -------------------------------------------------------
|
|
231
|
+
let _streaming = false;
|
|
232
|
+
const streamingCallbacks = new Set<(streaming: boolean) => void>();
|
|
233
|
+
|
|
234
|
+
const setStreaming = (value: boolean): void => {
|
|
235
|
+
_streaming = value;
|
|
236
|
+
for (const cb of streamingCallbacks) {
|
|
237
|
+
try {
|
|
238
|
+
cb(value);
|
|
239
|
+
} catch {
|
|
240
|
+
// Isolate subscriber errors so one bad handler doesn't prevent
|
|
241
|
+
// other subscribers from being notified or block the streaming
|
|
242
|
+
// state transition.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// -- sendMessages implementation -------------------------------------------
|
|
248
|
+
|
|
249
|
+
const sendMessages: ChatTransport['sendMessages'] = async (opts) => {
|
|
149
250
|
const { messages, abortSignal, trigger, messageId } = opts;
|
|
251
|
+
const allNodes = transport.view.flattenNodes();
|
|
252
|
+
|
|
253
|
+
// useChat calls sendMessages in three distinct modes. We disambiguate
|
|
254
|
+
// by (trigger, last-message role) so each mode dispatches correctly:
|
|
255
|
+
//
|
|
256
|
+
// - 'regenerate-message' → fork an assistant
|
|
257
|
+
// - 'submit-message' + last message is assistant → continuation
|
|
258
|
+
// (auto-submit after
|
|
259
|
+
// addToolResult, or
|
|
260
|
+
// multi-step tool use)
|
|
261
|
+
// - 'submit-message' + last message is user → new user message
|
|
262
|
+
// (or edit if
|
|
263
|
+
// messageId is set)
|
|
264
|
+
//
|
|
265
|
+
// Continuation mode must NOT publish the assistant as a new message or
|
|
266
|
+
// treat messageId as a fork target — useChat v6's sendAutomaticallyWhen
|
|
267
|
+
// path always sets messageId to the last message id regardless.
|
|
268
|
+
//
|
|
269
|
+
// Client-side tool outputs are expected to be staged on the transport
|
|
270
|
+
// via transport.stageEvents() before this runs; the core transport
|
|
271
|
+
// flushes staged events into the POST body automatically.
|
|
272
|
+
const lastMessage = messages.at(-1);
|
|
273
|
+
const lastMessageNode = lastMessage ? allNodes.find((n) => n.message.id === lastMessage.id) : undefined;
|
|
274
|
+
const isContinuation = trigger === 'submit-message' && lastMessage?.role === 'assistant' && !!lastMessageNode;
|
|
150
275
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
276
|
+
// Fork-on-unresolved-tool: user sent a new message while the preceding
|
|
277
|
+
// assistant has an unresolved tool call (approval-requested, input-*).
|
|
278
|
+
// Fork the new message off the preceding assistant so the unresolved
|
|
279
|
+
// tool call stays dormant on a sibling branch. Inference this turn runs
|
|
280
|
+
// on the clean fork — the LLM never sees the dangling tool_use.
|
|
281
|
+
//
|
|
282
|
+
// Only applies to fresh user-message submits (not continuations, not
|
|
283
|
+
// regenerates, not edits-with-messageId).
|
|
284
|
+
const precedingMessage =
|
|
285
|
+
trigger === 'submit-message' && !messageId && lastMessage?.role === 'user' ? messages.at(-2) : undefined;
|
|
286
|
+
const forkSource =
|
|
287
|
+
precedingMessage && hasUnresolvedToolCall(precedingMessage)
|
|
288
|
+
? allNodes.find((n) => n.message.id === precedingMessage.id)
|
|
289
|
+
: undefined;
|
|
290
|
+
|
|
291
|
+
// Determine the history/messages split based on mode.
|
|
154
292
|
let newMessages: AI.UIMessage[];
|
|
155
293
|
let history: AI.UIMessage[];
|
|
156
294
|
|
|
157
|
-
if (trigger === 'regenerate-message') {
|
|
295
|
+
if (trigger === 'regenerate-message' || isContinuation) {
|
|
158
296
|
newMessages = [];
|
|
159
297
|
history = messages;
|
|
160
298
|
} else {
|
|
@@ -168,27 +306,38 @@ export const createChatTransport = (
|
|
|
168
306
|
// CAST: length check above guarantees at least one element; .at(-1) cannot be undefined.
|
|
169
307
|
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style -- prefer `as` over `!` per TYPES.md
|
|
170
308
|
newMessages = [messages.at(-1) as AI.UIMessage];
|
|
171
|
-
|
|
309
|
+
// When forking off an unresolved tool call, drop the unresolved
|
|
310
|
+
// assistant from history too — it belongs on the sibling branch, not
|
|
311
|
+
// the ancestor chain of the new message.
|
|
312
|
+
history = forkSource ? messages.slice(0, -2) : messages.slice(0, -1);
|
|
172
313
|
}
|
|
173
314
|
|
|
174
|
-
// Compute fork metadata
|
|
175
|
-
//
|
|
176
|
-
// parent = the parent of that message in the tree.
|
|
315
|
+
// Compute fork metadata. Only set in regenerate or edit modes — in
|
|
316
|
+
// continuation mode we do NOT fork, we continue the branch.
|
|
177
317
|
let forkOf: string | undefined;
|
|
178
|
-
let parent: string |
|
|
318
|
+
let parent: string | undefined;
|
|
179
319
|
|
|
180
|
-
if (
|
|
320
|
+
if (messageId && !isContinuation) {
|
|
321
|
+
// Regeneration: messageId = assistant to regenerate.
|
|
322
|
+
// Edit (submit-message with user message and messageId): messageId = user being replaced.
|
|
323
|
+
// In both cases forkOf = the x-ably-msg-id, parent = that message's parent.
|
|
181
324
|
forkOf = messageId;
|
|
182
|
-
|
|
183
|
-
// messageId comes from useChat (UIMessage.id), so use getNodeByKey
|
|
184
|
-
// which resolves via the codec key secondary index.
|
|
185
|
-
const node = transport.getTree().getNodeByKey(messageId);
|
|
325
|
+
const node = allNodes.find((n) => n.message.id === messageId);
|
|
186
326
|
if (node) {
|
|
187
|
-
// Use the tree node's msgId (x-ably-msg-id) as forkOf — this is
|
|
188
|
-
// what the server stamps on the wire, not the UIMessage.id.
|
|
189
327
|
forkOf = node.msgId;
|
|
190
328
|
parent = node.parentId;
|
|
191
329
|
}
|
|
330
|
+
} else if (isContinuation) {
|
|
331
|
+
// Continuation: the server's next assistant message is a child of the
|
|
332
|
+
// last assistant (no fork). Pass parent so the server places the new
|
|
333
|
+
// message correctly in the tree. isContinuation narrows lastMessageNode
|
|
334
|
+
// to defined.
|
|
335
|
+
parent = lastMessageNode.msgId;
|
|
336
|
+
} else if (forkSource) {
|
|
337
|
+
// Fork off the preceding assistant — the new user message becomes a
|
|
338
|
+
// sibling of the unresolved tool call assistant, rooted at its parent.
|
|
339
|
+
forkOf = forkSource.msgId;
|
|
340
|
+
parent = forkSource.parentId;
|
|
192
341
|
}
|
|
193
342
|
|
|
194
343
|
let sendBody: Record<string, unknown>;
|
|
@@ -196,7 +345,7 @@ export const createChatTransport = (
|
|
|
196
345
|
|
|
197
346
|
if (chatOptions?.prepareSendMessagesRequest) {
|
|
198
347
|
const prepared = chatOptions.prepareSendMessagesRequest({
|
|
199
|
-
|
|
348
|
+
chatId: opts.chatId,
|
|
200
349
|
trigger,
|
|
201
350
|
messageId,
|
|
202
351
|
history,
|
|
@@ -207,13 +356,11 @@ export const createChatTransport = (
|
|
|
207
356
|
sendBody = prepared.body ?? {};
|
|
208
357
|
sendHeaders = prepared.headers;
|
|
209
358
|
} else {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
headers: transport.getMessageHeaders(m),
|
|
213
|
-
}));
|
|
359
|
+
const historyIds = new Set(history.map((m) => m.id));
|
|
360
|
+
const historyNodes = allNodes.filter((n) => historyIds.has(n.message.id));
|
|
214
361
|
sendBody = {
|
|
215
|
-
history:
|
|
216
|
-
|
|
362
|
+
history: historyNodes,
|
|
363
|
+
chatId: opts.chatId,
|
|
217
364
|
trigger,
|
|
218
365
|
...(messageId !== undefined && { messageId }),
|
|
219
366
|
...(forkOf !== undefined && { forkOf }),
|
|
@@ -226,53 +373,52 @@ export const createChatTransport = (
|
|
|
226
373
|
if (forkOf !== undefined) sendOpts.forkOf = forkOf;
|
|
227
374
|
if (parent !== undefined) sendOpts.parent = parent;
|
|
228
375
|
|
|
229
|
-
|
|
376
|
+
// A single dispatch path: view.send with the (possibly empty)
|
|
377
|
+
// newMessages array. Any events staged via transport.stageEvents()
|
|
378
|
+
// flow automatically through _internalSend into the POST body.
|
|
379
|
+
const turn = await transport.view.send(newMessages, sendOpts);
|
|
230
380
|
|
|
231
|
-
// Wire abort signal to cancel all turns on the channel.
|
|
232
|
-
// In multi-user scenarios, any client can stop any stream — cancelling
|
|
233
|
-
// by specific turnId would only work for the sender.
|
|
234
381
|
if (abortSignal) {
|
|
235
382
|
abortSignal.addEventListener('abort', () => void transport.cancel({ all: true }), {
|
|
236
383
|
once: true,
|
|
237
384
|
});
|
|
238
385
|
}
|
|
239
386
|
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
387
|
+
// Wrap the stream to detect completion. The streaming flag gates
|
|
388
|
+
// useMessageSync so that setMessages doesn't interfere with
|
|
389
|
+
// useChat's internal write() during active streams.
|
|
390
|
+
const { stream, done } = wrapStreamWithDone(turn.stream);
|
|
391
|
+
setStreaming(true);
|
|
392
|
+
|
|
393
|
+
// Fire-and-forget: clear the streaming flag when the stream ends.
|
|
394
|
+
void done.then(() => {
|
|
395
|
+
setStreaming(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return stream;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
sendMessages,
|
|
403
|
+
|
|
404
|
+
// Observer mode handles in-progress streams automatically.
|
|
405
|
+
// The transport subscribes before attach — on the next server append,
|
|
406
|
+
// observer accumulation emits lifecycle events that useMessageSync
|
|
407
|
+
// upserts into React state.
|
|
408
|
+
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/promise-function-async -- null is required by the AI SDK ChatTransport contract; no await needed
|
|
409
|
+
reconnectToStream: () => Promise.resolve(null),
|
|
410
|
+
|
|
411
|
+
close: async (options?: CloseOptions) => transport.close(options),
|
|
412
|
+
|
|
413
|
+
get streaming(): boolean {
|
|
414
|
+
return _streaming;
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
onStreamingChange: (callback: (streaming: boolean) => void): (() => void) => {
|
|
418
|
+
streamingCallbacks.add(callback);
|
|
419
|
+
return () => {
|
|
420
|
+
streamingCallbacks.delete(callback);
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
};
|
|
@@ -27,12 +27,17 @@ import type {
|
|
|
27
27
|
} from '../../core/transport/types.js';
|
|
28
28
|
import { UIMessageCodec } from '../codec/index.js';
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/** Core client transport options with Vercel AI SDK types pre-applied. */
|
|
31
|
+
type CoreClientOpts = ClientTransportOptions<AI.UIMessageChunk, AI.UIMessage>;
|
|
32
|
+
|
|
33
|
+
/** Options for creating a Vercel client transport. Same as core options but without the codec field, and with `api` optional (defaults to `"/api/chat"`). */
|
|
34
|
+
export type VercelClientTransportOptions = Omit<CoreClientOpts, 'codec' | 'api'> & Partial<Pick<CoreClientOpts, 'api'>>;
|
|
32
35
|
|
|
33
36
|
/** Options for creating a Vercel server transport. Same as core options but without the codec field. */
|
|
34
37
|
export type VercelServerTransportOptions = Omit<ServerTransportOptions<AI.UIMessageChunk, AI.UIMessage>, 'codec'>;
|
|
35
38
|
|
|
39
|
+
export const DEFAULT_VERCEL_API = '/api/chat';
|
|
40
|
+
|
|
36
41
|
/**
|
|
37
42
|
* Create a client-side transport pre-configured with the Vercel AI SDK codec.
|
|
38
43
|
*
|
|
@@ -42,7 +47,13 @@ export type VercelServerTransportOptions = Omit<ServerTransportOptions<AI.UIMess
|
|
|
42
47
|
*/
|
|
43
48
|
export const createClientTransport = (
|
|
44
49
|
options: VercelClientTransportOptions,
|
|
45
|
-
): ClientTransport<AI.UIMessageChunk, AI.UIMessage> =>
|
|
50
|
+
): ClientTransport<AI.UIMessageChunk, AI.UIMessage> =>
|
|
51
|
+
createCoreClientTransport({
|
|
52
|
+
...options,
|
|
53
|
+
codec: UIMessageCodec,
|
|
54
|
+
// Mirrors the Vercel AI SDK's DefaultChatTransport default.
|
|
55
|
+
api: options.api ?? DEFAULT_VERCEL_API,
|
|
56
|
+
});
|
|
46
57
|
|
|
47
58
|
/**
|
|
48
59
|
* Create a server-side transport pre-configured with the Vercel AI SDK codec.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Logger } from '../../logger.js';
|
|
2
|
-
import { ConversationTree } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Create a ConversationTree that materializes branching history from a flat oplog.
|
|
5
|
-
* @param getKey - Codec function that returns a stable key for a domain message.
|
|
6
|
-
* @param logger - Logger for diagnostic output.
|
|
7
|
-
* @returns A new {@link ConversationTree} instance.
|
|
8
|
-
*/
|
|
9
|
-
export declare const createConversationTree: <TMessage>(getKey: (message: TMessage) => string, logger: Logger) => ConversationTree<TMessage>;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ClientTransport } from '../core/transport/types.js';
|
|
2
|
-
/** Handle for navigating the branching conversation tree. */
|
|
3
|
-
export interface ConversationTreeHandle<TMessage> {
|
|
4
|
-
/** Linear message list for the currently selected branch. */
|
|
5
|
-
messages: TMessage[];
|
|
6
|
-
/** Get all sibling messages at a fork point. */
|
|
7
|
-
getSiblings: (msgId: string) => TMessage[];
|
|
8
|
-
/** Whether a message has siblings (should show navigation arrows). */
|
|
9
|
-
hasSiblings: (msgId: string) => boolean;
|
|
10
|
-
/** Index of the currently selected sibling. */
|
|
11
|
-
getSelectedIndex: (msgId: string) => number;
|
|
12
|
-
/** Navigate to a sibling. Triggers re-render with updated messages. */
|
|
13
|
-
selectSibling: (msgId: string, index: number) => void;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Subscribe to transport message updates and provide branch navigation primitives.
|
|
17
|
-
* @param transport - The client transport whose conversation tree to navigate.
|
|
18
|
-
* @returns A {@link ConversationTreeHandle} with the current messages and navigation methods.
|
|
19
|
-
*/
|
|
20
|
-
export declare const useConversationTree: <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage>) => ConversationTreeHandle<TMessage>;
|
package/dist/react/use-edit.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Return a stable `edit` callback bound to the given transport.
|
|
4
|
-
* @param transport - The client transport to edit through.
|
|
5
|
-
* @returns A function that edits a user message and returns an {@link ActiveTurn} handle.
|
|
6
|
-
*/
|
|
7
|
-
export declare const useEdit: <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage>) => ((messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>);
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { ClientTransport, LoadHistoryOptions } from '../core/transport/types.js';
|
|
2
|
-
/** Handle for paginated history loading. */
|
|
3
|
-
export interface HistoryHandle {
|
|
4
|
-
/** Are there older pages available? False until `load()` has been called. */
|
|
5
|
-
hasNext: boolean;
|
|
6
|
-
/** Is a page being fetched? */
|
|
7
|
-
loading: boolean;
|
|
8
|
-
/** Load the first page (or re-load with different options). Inserts into the conversation tree. */
|
|
9
|
-
load: (options?: LoadHistoryOptions) => Promise<void>;
|
|
10
|
-
/** Fetch the next (older) page. No-op if loading or no more pages. Inserts into the conversation tree. */
|
|
11
|
-
next: () => Promise<void>;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Paginated history handle for a client transport.
|
|
15
|
-
* @param transport - The client transport to load history from, or null/undefined if not yet available.
|
|
16
|
-
* @param options - When provided, auto-loads the first page on mount. Omit or pass null for manual loading.
|
|
17
|
-
* @returns A {@link HistoryHandle} for loading and paginating through history.
|
|
18
|
-
*/
|
|
19
|
-
export declare const useHistory: <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage> | null | undefined, options?: LoadHistoryOptions | null) => HistoryHandle;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ClientTransport } from '../core/transport/types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Subscribe to transport message updates and return the current message list.
|
|
4
|
-
* @param transport - The client transport to observe.
|
|
5
|
-
* @returns The current list of decoded messages.
|
|
6
|
-
*/
|
|
7
|
-
export declare const useMessages: <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage>) => TMessage[];
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Return a stable `regenerate` callback bound to the given transport.
|
|
4
|
-
* @param transport - The client transport to regenerate through.
|
|
5
|
-
* @returns A function that regenerates an assistant message and returns an {@link ActiveTurn} handle.
|
|
6
|
-
*/
|
|
7
|
-
export declare const useRegenerate: <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage>) => ((messageId: string, options?: SendOptions) => Promise<ActiveTurn<TEvent>>);
|
package/dist/react/use-send.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Return a stable `send` callback bound to the given transport.
|
|
4
|
-
* @param transport - The client transport to send through.
|
|
5
|
-
* @returns A function that sends messages and returns an {@link ActiveTurn} handle.
|
|
6
|
-
*/
|
|
7
|
-
export declare const useSend: <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage>) => ((messages: TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>);
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useConversationTree — reactive branch navigation for a ClientTransport.
|
|
3
|
-
*
|
|
4
|
-
* Subscribes to the transport's "message" notification and provides
|
|
5
|
-
* branch navigation primitives (getSiblings, selectSibling, hasSiblings)
|
|
6
|
-
* backed by the transport's ConversationTree.
|
|
7
|
-
*
|
|
8
|
-
* Branch selection state is local to the hook instance — each component
|
|
9
|
-
* (or tab) can navigate branches independently.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
13
|
-
|
|
14
|
-
import type { ClientTransport } from '../core/transport/types.js';
|
|
15
|
-
|
|
16
|
-
/** Handle for navigating the branching conversation tree. */
|
|
17
|
-
export interface ConversationTreeHandle<TMessage> {
|
|
18
|
-
/** Linear message list for the currently selected branch. */
|
|
19
|
-
messages: TMessage[];
|
|
20
|
-
/** Get all sibling messages at a fork point. */
|
|
21
|
-
getSiblings: (msgId: string) => TMessage[];
|
|
22
|
-
/** Whether a message has siblings (should show navigation arrows). */
|
|
23
|
-
hasSiblings: (msgId: string) => boolean;
|
|
24
|
-
/** Index of the currently selected sibling. */
|
|
25
|
-
getSelectedIndex: (msgId: string) => number;
|
|
26
|
-
/** Navigate to a sibling. Triggers re-render with updated messages. */
|
|
27
|
-
selectSibling: (msgId: string, index: number) => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Subscribe to transport message updates and provide branch navigation primitives.
|
|
32
|
-
* @param transport - The client transport whose conversation tree to navigate.
|
|
33
|
-
* @returns A {@link ConversationTreeHandle} with the current messages and navigation methods.
|
|
34
|
-
*/
|
|
35
|
-
export const useConversationTree = <TEvent, TMessage>(
|
|
36
|
-
transport: ClientTransport<TEvent, TMessage>,
|
|
37
|
-
): ConversationTreeHandle<TMessage> => {
|
|
38
|
-
const [messages, setMessages] = useState<TMessage[]>(() => transport.getMessages());
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
setMessages(transport.getMessages());
|
|
42
|
-
|
|
43
|
-
const unsub = transport.on('message', () => {
|
|
44
|
-
setMessages(transport.getMessages());
|
|
45
|
-
});
|
|
46
|
-
return unsub;
|
|
47
|
-
}, [transport]);
|
|
48
|
-
|
|
49
|
-
const getSiblings = useCallback((msgId: string) => transport.getTree().getSiblings(msgId), [transport]);
|
|
50
|
-
|
|
51
|
-
const hasSiblings = useCallback((msgId: string) => transport.getTree().hasSiblings(msgId), [transport]);
|
|
52
|
-
|
|
53
|
-
const getSelectedIndex = useCallback((msgId: string) => transport.getTree().getSelectedIndex(msgId), [transport]);
|
|
54
|
-
|
|
55
|
-
const selectSibling = useCallback(
|
|
56
|
-
(msgId: string, index: number) => {
|
|
57
|
-
transport.getTree().select(msgId, index);
|
|
58
|
-
// flatten() returns a new array after select(), triggering re-render.
|
|
59
|
-
setMessages(transport.getMessages());
|
|
60
|
-
},
|
|
61
|
-
[transport],
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
messages,
|
|
66
|
-
getSiblings,
|
|
67
|
-
hasSiblings,
|
|
68
|
-
getSelectedIndex,
|
|
69
|
-
selectSibling,
|
|
70
|
-
};
|
|
71
|
-
};
|
package/src/react/use-edit.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useEdit — stable callback for editing a user message.
|
|
3
|
-
*
|
|
4
|
-
* Delegates to `transport.edit()`, which automatically computes
|
|
5
|
-
* `forkOf`, `parent`, and history from the conversation tree.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useCallback } from 'react';
|
|
9
|
-
|
|
10
|
-
import type { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Return a stable `edit` callback bound to the given transport.
|
|
14
|
-
* @param transport - The client transport to edit through.
|
|
15
|
-
* @returns A function that edits a user message and returns an {@link ActiveTurn} handle.
|
|
16
|
-
*/
|
|
17
|
-
export const useEdit = <TEvent, TMessage>(
|
|
18
|
-
transport: ClientTransport<TEvent, TMessage>,
|
|
19
|
-
): ((messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>) =>
|
|
20
|
-
useCallback(
|
|
21
|
-
async (messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>> =>
|
|
22
|
-
transport.edit(messageId, newMessages, options),
|
|
23
|
-
[transport],
|
|
24
|
-
);
|