@tanstack/ai-client 0.7.1 → 0.7.4
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 +6 -6
- package/dist/esm/chat-client.d.ts +62 -3
- package/dist/esm/chat-client.js +264 -54
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +28 -12
- package/dist/esm/connection-adapters.js +93 -0
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +5 -5
- package/dist/esm/generation-client.js +1 -1
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +5 -5
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/types.d.ts +23 -2
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +5 -5
- package/dist/esm/video-generation-client.js +1 -1
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +355 -86
- package/src/connection-adapters.ts +147 -12
- package/src/generation-client.ts +7 -7
- package/src/generation-types.ts +5 -5
- package/src/index.ts +3 -0
- package/src/types.ts +31 -2
- package/src/video-generation-client.ts +6 -6
package/README.md
CHANGED
|
@@ -87,18 +87,18 @@ Available adapters: `openaiText`, `openaiEmbed`, `openaiSummarize`, `anthropicTe
|
|
|
87
87
|
<td>
|
|
88
88
|
<a href="https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS" >
|
|
89
89
|
<picture>
|
|
90
|
-
|
|
91
|
-
<source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-
|
|
92
|
-
<img src="https://tanstack.com/assets/coderabbit-light-
|
|
90
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-D643Zkrv.svg" />
|
|
91
|
+
<source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-CIzGLYU_.svg" />
|
|
92
|
+
<img src="https://tanstack.com/assets/coderabbit-light-CIzGLYU_.svg" height="40" alt="CodeRabbit" />
|
|
93
93
|
</picture>
|
|
94
94
|
</a>
|
|
95
95
|
</td>
|
|
96
96
|
<td>
|
|
97
97
|
<a href="https://www.cloudflare.com?utm_source=tanstack">
|
|
98
98
|
<picture>
|
|
99
|
-
|
|
100
|
-
<source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-
|
|
101
|
-
<img src="https://tanstack.com/assets/cloudflare-
|
|
99
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-Co-Tyjbl.svg" />
|
|
100
|
+
<source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-6Ojsn8yh.svg" />
|
|
101
|
+
<img src="https://tanstack.com/assets/cloudflare-white-Co-Tyjbl.svg" height="60" alt="Cloudflare" />
|
|
102
102
|
</picture>
|
|
103
103
|
</a>
|
|
104
104
|
</td>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai';
|
|
2
2
|
import { ConnectionAdapter } from './connection-adapters.js';
|
|
3
|
-
import { ChatClientOptions, ChatClientState, MultimodalContent, UIMessage } from './types.js';
|
|
3
|
+
import { ChatClientOptions, ChatClientState, ConnectionStatus, MultimodalContent, UIMessage } from './types.js';
|
|
4
4
|
export declare class ChatClient {
|
|
5
5
|
private processor;
|
|
6
6
|
private connection;
|
|
@@ -8,8 +8,10 @@ export declare class ChatClient {
|
|
|
8
8
|
private body;
|
|
9
9
|
private pendingMessageBody;
|
|
10
10
|
private isLoading;
|
|
11
|
+
private isSubscribed;
|
|
11
12
|
private error;
|
|
12
13
|
private status;
|
|
14
|
+
private connectionStatus;
|
|
13
15
|
private abortController;
|
|
14
16
|
private events;
|
|
15
17
|
private clientToolsRef;
|
|
@@ -18,17 +20,44 @@ export declare class ChatClient {
|
|
|
18
20
|
private postStreamActions;
|
|
19
21
|
private pendingToolExecutions;
|
|
20
22
|
private continuationPending;
|
|
23
|
+
private subscriptionAbortController;
|
|
24
|
+
private processingResolve;
|
|
25
|
+
private errorReportedGeneration;
|
|
26
|
+
private streamGeneration;
|
|
21
27
|
private continuationSkipped;
|
|
28
|
+
private sessionGenerating;
|
|
29
|
+
private activeRunIds;
|
|
22
30
|
private callbacksRef;
|
|
23
31
|
constructor(options: ChatClientOptions);
|
|
24
32
|
private generateUniqueId;
|
|
25
33
|
private setIsLoading;
|
|
26
34
|
private setStatus;
|
|
35
|
+
private setIsSubscribed;
|
|
36
|
+
private setConnectionStatus;
|
|
37
|
+
private setSessionGenerating;
|
|
38
|
+
private resetSessionGenerating;
|
|
27
39
|
private setError;
|
|
40
|
+
private abortSubscriptionLoop;
|
|
41
|
+
private resolveProcessing;
|
|
42
|
+
private cancelInFlightStream;
|
|
43
|
+
private reportStreamError;
|
|
28
44
|
/**
|
|
29
|
-
*
|
|
45
|
+
* Start the background subscription loop.
|
|
30
46
|
*/
|
|
31
|
-
private
|
|
47
|
+
private startSubscription;
|
|
48
|
+
/**
|
|
49
|
+
* Consume chunks from the connection subscription.
|
|
50
|
+
*/
|
|
51
|
+
private consumeSubscription;
|
|
52
|
+
/**
|
|
53
|
+
* Ensure subscription loop is running, starting it if needed.
|
|
54
|
+
*/
|
|
55
|
+
private ensureSubscription;
|
|
56
|
+
/**
|
|
57
|
+
* Create a promise that resolves when onStreamEnd fires.
|
|
58
|
+
* Used by streamResponse to await processing completion.
|
|
59
|
+
*/
|
|
60
|
+
private waitForProcessing;
|
|
32
61
|
/**
|
|
33
62
|
* Send a message and stream the response.
|
|
34
63
|
* Supports both simple string content and multimodal content (images, audio, video, documents).
|
|
@@ -83,6 +112,18 @@ export declare class ChatClient {
|
|
|
83
112
|
* Returns true if the stream completed successfully, false on abort or error.
|
|
84
113
|
*/
|
|
85
114
|
private streamResponse;
|
|
115
|
+
/**
|
|
116
|
+
* Start the client subscription loop.
|
|
117
|
+
* This controls the connection lifecycle independently from request lifecycle.
|
|
118
|
+
*/
|
|
119
|
+
subscribe(options?: {
|
|
120
|
+
restart?: boolean;
|
|
121
|
+
}): void;
|
|
122
|
+
/**
|
|
123
|
+
* Unsubscribe and fully tear down live behavior.
|
|
124
|
+
* This aborts an in-flight request and the subscription loop.
|
|
125
|
+
*/
|
|
126
|
+
unsubscribe(): void;
|
|
86
127
|
/**
|
|
87
128
|
* Reload the last assistant message
|
|
88
129
|
*/
|
|
@@ -140,6 +181,21 @@ export declare class ChatClient {
|
|
|
140
181
|
* Get current status
|
|
141
182
|
*/
|
|
142
183
|
getStatus(): ChatClientState;
|
|
184
|
+
/**
|
|
185
|
+
* Get whether the subscription loop is active
|
|
186
|
+
*/
|
|
187
|
+
getIsSubscribed(): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Get current connection lifecycle status
|
|
190
|
+
*/
|
|
191
|
+
getConnectionStatus(): ConnectionStatus;
|
|
192
|
+
/**
|
|
193
|
+
* Whether the shared session is actively generating.
|
|
194
|
+
* Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).
|
|
195
|
+
* Unlike `isLoading` (request-local), this reflects shared generation
|
|
196
|
+
* activity visible to all subscribers (e.g. across tabs/devices).
|
|
197
|
+
*/
|
|
198
|
+
getSessionGenerating(): boolean;
|
|
143
199
|
/**
|
|
144
200
|
* Get current error
|
|
145
201
|
*/
|
|
@@ -159,6 +215,9 @@ export declare class ChatClient {
|
|
|
159
215
|
onChunk?: (chunk: StreamChunk) => void;
|
|
160
216
|
onFinish?: (message: UIMessage) => void;
|
|
161
217
|
onError?: (error: Error) => void;
|
|
218
|
+
onSubscriptionChange?: (isSubscribed: boolean) => void;
|
|
219
|
+
onConnectionStatusChange?: (status: ConnectionStatus) => void;
|
|
220
|
+
onSessionGeneratingChange?: (isGenerating: boolean) => void;
|
|
162
221
|
onCustomEvent?: (eventType: string, data: unknown, context: {
|
|
163
222
|
toolCallId?: string;
|
|
164
223
|
}) => void;
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import { StreamProcessor, normalizeToUIMessage, generateMessageId } from "@tanstack/ai";
|
|
2
2
|
import { DefaultChatClientEventEmitter } from "./events.js";
|
|
3
|
+
import { normalizeConnectionAdapter } from "./connection-adapters.js";
|
|
3
4
|
class ChatClient {
|
|
4
5
|
constructor(options) {
|
|
5
6
|
this.body = {};
|
|
6
7
|
this.pendingMessageBody = void 0;
|
|
7
8
|
this.isLoading = false;
|
|
9
|
+
this.isSubscribed = false;
|
|
8
10
|
this.error = void 0;
|
|
9
11
|
this.status = "ready";
|
|
12
|
+
this.connectionStatus = "disconnected";
|
|
10
13
|
this.abortController = null;
|
|
11
14
|
this.currentStreamId = null;
|
|
12
15
|
this.currentMessageId = null;
|
|
13
16
|
this.postStreamActions = [];
|
|
14
17
|
this.pendingToolExecutions = /* @__PURE__ */ new Map();
|
|
15
18
|
this.continuationPending = false;
|
|
19
|
+
this.subscriptionAbortController = null;
|
|
20
|
+
this.processingResolve = null;
|
|
21
|
+
this.errorReportedGeneration = null;
|
|
22
|
+
this.streamGeneration = 0;
|
|
16
23
|
this.continuationSkipped = false;
|
|
24
|
+
this.sessionGenerating = false;
|
|
25
|
+
this.activeRunIds = /* @__PURE__ */ new Set();
|
|
17
26
|
this.uniqueId = options.id || this.generateUniqueId("chat");
|
|
18
27
|
this.body = options.body || {};
|
|
19
|
-
this.connection = options.connection;
|
|
28
|
+
this.connection = normalizeConnectionAdapter(options.connection);
|
|
20
29
|
this.events = new DefaultChatClientEventEmitter(this.uniqueId);
|
|
21
30
|
this.clientToolsRef = { current: /* @__PURE__ */ new Map() };
|
|
22
31
|
if (options.tools) {
|
|
@@ -42,6 +51,12 @@ class ChatClient {
|
|
|
42
51
|
}),
|
|
43
52
|
onStatusChange: options.onStatusChange || (() => {
|
|
44
53
|
}),
|
|
54
|
+
onSubscriptionChange: options.onSubscriptionChange || (() => {
|
|
55
|
+
}),
|
|
56
|
+
onConnectionStatusChange: options.onConnectionStatusChange || (() => {
|
|
57
|
+
}),
|
|
58
|
+
onSessionGeneratingChange: options.onSessionGeneratingChange || (() => {
|
|
59
|
+
}),
|
|
45
60
|
onCustomEvent: options.onCustomEvent || (() => {
|
|
46
61
|
})
|
|
47
62
|
}
|
|
@@ -55,15 +70,29 @@ class ChatClient {
|
|
|
55
70
|
},
|
|
56
71
|
onStreamStart: () => {
|
|
57
72
|
this.setStatus("streaming");
|
|
73
|
+
const assistantMessageId = this.processor.getCurrentAssistantMessageId();
|
|
74
|
+
if (!assistantMessageId) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const messages = this.processor.getMessages();
|
|
78
|
+
const assistantMessage = messages.find(
|
|
79
|
+
(m) => m.id === assistantMessageId
|
|
80
|
+
);
|
|
81
|
+
if (assistantMessage) {
|
|
82
|
+
this.currentMessageId = assistantMessage.id;
|
|
83
|
+
this.events.messageAppended(
|
|
84
|
+
assistantMessage,
|
|
85
|
+
this.currentStreamId || void 0
|
|
86
|
+
);
|
|
87
|
+
}
|
|
58
88
|
},
|
|
59
89
|
onStreamEnd: (message) => {
|
|
60
90
|
this.callbacksRef.current.onFinish(message);
|
|
61
91
|
this.setStatus("ready");
|
|
92
|
+
this.resolveProcessing();
|
|
62
93
|
},
|
|
63
94
|
onError: (error) => {
|
|
64
|
-
this.
|
|
65
|
-
this.setStatus("error");
|
|
66
|
-
this.callbacksRef.current.onError(error);
|
|
95
|
+
this.reportStreamError(error);
|
|
67
96
|
},
|
|
68
97
|
onTextUpdate: (messageId, content) => {
|
|
69
98
|
if (this.currentStreamId) {
|
|
@@ -156,47 +185,135 @@ class ChatClient {
|
|
|
156
185
|
this.status = status;
|
|
157
186
|
this.callbacksRef.current.onStatusChange(status);
|
|
158
187
|
}
|
|
188
|
+
setIsSubscribed(isSubscribed) {
|
|
189
|
+
this.isSubscribed = isSubscribed;
|
|
190
|
+
this.callbacksRef.current.onSubscriptionChange(isSubscribed);
|
|
191
|
+
}
|
|
192
|
+
setConnectionStatus(status) {
|
|
193
|
+
this.connectionStatus = status;
|
|
194
|
+
this.callbacksRef.current.onConnectionStatusChange(status);
|
|
195
|
+
}
|
|
196
|
+
setSessionGenerating(isGenerating) {
|
|
197
|
+
if (this.sessionGenerating === isGenerating) return;
|
|
198
|
+
this.sessionGenerating = isGenerating;
|
|
199
|
+
this.callbacksRef.current.onSessionGeneratingChange(isGenerating);
|
|
200
|
+
}
|
|
201
|
+
resetSessionGenerating() {
|
|
202
|
+
this.activeRunIds.clear();
|
|
203
|
+
this.setSessionGenerating(false);
|
|
204
|
+
}
|
|
159
205
|
setError(error) {
|
|
160
206
|
this.error = error;
|
|
161
207
|
this.callbacksRef.current.onErrorChange(error);
|
|
162
208
|
this.events.errorChanged(error?.message || null);
|
|
163
209
|
}
|
|
210
|
+
abortSubscriptionLoop() {
|
|
211
|
+
this.subscriptionAbortController?.abort();
|
|
212
|
+
this.subscriptionAbortController = null;
|
|
213
|
+
}
|
|
214
|
+
resolveProcessing() {
|
|
215
|
+
this.processingResolve?.();
|
|
216
|
+
this.processingResolve = null;
|
|
217
|
+
}
|
|
218
|
+
cancelInFlightStream(options) {
|
|
219
|
+
this.abortController?.abort();
|
|
220
|
+
this.abortController = null;
|
|
221
|
+
if (options?.abortSubscription) {
|
|
222
|
+
this.abortSubscriptionLoop();
|
|
223
|
+
}
|
|
224
|
+
this.resolveProcessing();
|
|
225
|
+
this.setIsLoading(false);
|
|
226
|
+
if (options?.setReadyStatus) {
|
|
227
|
+
this.setStatus("ready");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
reportStreamError(error) {
|
|
231
|
+
const alreadyReported = this.errorReportedGeneration === this.streamGeneration;
|
|
232
|
+
this.setError(error);
|
|
233
|
+
if (this.isLoading || this.status === "submitted" || this.status === "streaming") {
|
|
234
|
+
this.setStatus("error");
|
|
235
|
+
}
|
|
236
|
+
if (!alreadyReported) {
|
|
237
|
+
this.errorReportedGeneration = this.streamGeneration;
|
|
238
|
+
this.callbacksRef.current.onError(error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Start the background subscription loop.
|
|
243
|
+
*/
|
|
244
|
+
startSubscription() {
|
|
245
|
+
this.subscriptionAbortController = new AbortController();
|
|
246
|
+
const signal = this.subscriptionAbortController.signal;
|
|
247
|
+
this.consumeSubscription(signal).catch((err) => {
|
|
248
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
249
|
+
this.setConnectionStatus("error");
|
|
250
|
+
this.resetSessionGenerating();
|
|
251
|
+
this.setIsSubscribed(false);
|
|
252
|
+
this.reportStreamError(err);
|
|
253
|
+
}
|
|
254
|
+
this.resolveProcessing();
|
|
255
|
+
}).finally(() => {
|
|
256
|
+
if (this.subscriptionAbortController?.signal !== signal) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.subscriptionAbortController = null;
|
|
260
|
+
if (!signal.aborted && this.isSubscribed) {
|
|
261
|
+
this.setIsSubscribed(false);
|
|
262
|
+
if (this.connectionStatus !== "error") {
|
|
263
|
+
this.setConnectionStatus("disconnected");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
164
268
|
/**
|
|
165
|
-
*
|
|
269
|
+
* Consume chunks from the connection subscription.
|
|
166
270
|
*/
|
|
167
|
-
async
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
271
|
+
async consumeSubscription(signal) {
|
|
272
|
+
const stream = this.connection.subscribe(signal);
|
|
273
|
+
for await (const chunk of stream) {
|
|
274
|
+
if (signal.aborted) break;
|
|
275
|
+
if (this.connectionStatus === "connecting") {
|
|
276
|
+
this.setConnectionStatus("connected");
|
|
277
|
+
}
|
|
171
278
|
this.callbacksRef.current.onChunk(chunk);
|
|
172
279
|
this.processor.processChunk(chunk);
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
}
|
|
280
|
+
if (chunk.type === "RUN_STARTED") {
|
|
281
|
+
this.activeRunIds.add(chunk.runId);
|
|
282
|
+
this.setSessionGenerating(true);
|
|
283
|
+
}
|
|
284
|
+
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
|
|
285
|
+
if (chunk.runId) {
|
|
286
|
+
this.activeRunIds.delete(chunk.runId);
|
|
287
|
+
} else if (chunk.type === "RUN_ERROR") {
|
|
288
|
+
this.activeRunIds.clear();
|
|
184
289
|
}
|
|
290
|
+
this.setSessionGenerating(this.activeRunIds.size > 0);
|
|
291
|
+
this.resolveProcessing();
|
|
185
292
|
}
|
|
186
293
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
187
294
|
}
|
|
188
|
-
|
|
189
|
-
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Ensure subscription loop is running, starting it if needed.
|
|
298
|
+
*/
|
|
299
|
+
ensureSubscription() {
|
|
300
|
+
if (!this.isSubscribed) {
|
|
301
|
+
this.subscribe();
|
|
302
|
+
return;
|
|
190
303
|
}
|
|
191
|
-
this.
|
|
192
|
-
|
|
193
|
-
this.currentStreamId = null;
|
|
194
|
-
this.currentMessageId = null;
|
|
195
|
-
if (messageId) {
|
|
196
|
-
const messages = this.processor.getMessages();
|
|
197
|
-
return messages.find((m) => m.id === messageId) || null;
|
|
304
|
+
if (!this.subscriptionAbortController || this.subscriptionAbortController.signal.aborted) {
|
|
305
|
+
this.subscribe({ restart: true });
|
|
198
306
|
}
|
|
199
|
-
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Create a promise that resolves when onStreamEnd fires.
|
|
310
|
+
* Used by streamResponse to await processing completion.
|
|
311
|
+
*/
|
|
312
|
+
waitForProcessing() {
|
|
313
|
+
this.resolveProcessing();
|
|
314
|
+
return new Promise((resolve) => {
|
|
315
|
+
this.processingResolve = resolve;
|
|
316
|
+
});
|
|
200
317
|
}
|
|
201
318
|
/**
|
|
202
319
|
* Send a message and stream the response.
|
|
@@ -289,9 +406,11 @@ class ChatClient {
|
|
|
289
406
|
if (this.isLoading) {
|
|
290
407
|
return false;
|
|
291
408
|
}
|
|
409
|
+
const generation = ++this.streamGeneration;
|
|
292
410
|
this.setIsLoading(true);
|
|
293
411
|
this.setStatus("submitted");
|
|
294
412
|
this.setError(void 0);
|
|
413
|
+
this.errorReportedGeneration = null;
|
|
295
414
|
this.abortController = new AbortController();
|
|
296
415
|
this.pendingToolExecutions.clear();
|
|
297
416
|
let streamCompletedSuccessfully = false;
|
|
@@ -304,41 +423,89 @@ class ChatClient {
|
|
|
304
423
|
conversationId: this.uniqueId
|
|
305
424
|
};
|
|
306
425
|
this.pendingMessageBody = void 0;
|
|
307
|
-
|
|
426
|
+
this.currentStreamId = this.generateUniqueId("stream");
|
|
427
|
+
this.currentMessageId = null;
|
|
428
|
+
this.processor.prepareAssistantMessage();
|
|
429
|
+
this.ensureSubscription();
|
|
430
|
+
const processingComplete = this.waitForProcessing();
|
|
431
|
+
await this.connection.send(
|
|
308
432
|
messages,
|
|
309
433
|
mergedBody,
|
|
310
434
|
this.abortController.signal
|
|
311
435
|
);
|
|
312
|
-
await
|
|
436
|
+
await processingComplete;
|
|
437
|
+
if (generation !== this.streamGeneration) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if (this.status === "error") {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if (this.pendingToolExecutions.size > 0) {
|
|
444
|
+
await Promise.all(this.pendingToolExecutions.values());
|
|
445
|
+
}
|
|
446
|
+
this.processor.finalizeStream();
|
|
313
447
|
streamCompletedSuccessfully = true;
|
|
314
448
|
} catch (err) {
|
|
315
449
|
if (err instanceof Error) {
|
|
316
450
|
if (err.name === "AbortError") {
|
|
317
451
|
return false;
|
|
318
452
|
}
|
|
319
|
-
this.
|
|
320
|
-
|
|
321
|
-
|
|
453
|
+
if (generation === this.streamGeneration) {
|
|
454
|
+
this.reportStreamError(err);
|
|
455
|
+
}
|
|
322
456
|
}
|
|
323
457
|
} finally {
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
458
|
+
if (generation === this.streamGeneration) {
|
|
459
|
+
this.currentStreamId = null;
|
|
460
|
+
this.currentMessageId = null;
|
|
461
|
+
this.abortController = null;
|
|
462
|
+
this.setIsLoading(false);
|
|
463
|
+
this.pendingMessageBody = void 0;
|
|
464
|
+
await this.drainPostStreamActions();
|
|
465
|
+
if (streamCompletedSuccessfully) {
|
|
466
|
+
const messages = this.processor.getMessages();
|
|
467
|
+
const lastPart = messages.at(-1)?.parts.at(-1);
|
|
468
|
+
if (lastPart?.type === "tool-result" && this.shouldAutoSend()) {
|
|
469
|
+
try {
|
|
470
|
+
await this.checkForContinuation();
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error("Failed to continue flow after tool result:", error);
|
|
473
|
+
}
|
|
336
474
|
}
|
|
337
475
|
}
|
|
338
476
|
}
|
|
339
477
|
}
|
|
340
478
|
return streamCompletedSuccessfully;
|
|
341
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Start the client subscription loop.
|
|
482
|
+
* This controls the connection lifecycle independently from request lifecycle.
|
|
483
|
+
*/
|
|
484
|
+
subscribe(options) {
|
|
485
|
+
const restart = options?.restart === true;
|
|
486
|
+
if (this.isSubscribed && !restart) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (this.isSubscribed && restart) {
|
|
490
|
+
this.abortSubscriptionLoop();
|
|
491
|
+
}
|
|
492
|
+
this.setIsSubscribed(true);
|
|
493
|
+
this.setConnectionStatus("connecting");
|
|
494
|
+
this.startSubscription();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Unsubscribe and fully tear down live behavior.
|
|
498
|
+
* This aborts an in-flight request and the subscription loop.
|
|
499
|
+
*/
|
|
500
|
+
unsubscribe() {
|
|
501
|
+
this.cancelInFlightStream({
|
|
502
|
+
setReadyStatus: true,
|
|
503
|
+
abortSubscription: true
|
|
504
|
+
});
|
|
505
|
+
this.resetSessionGenerating();
|
|
506
|
+
this.setIsSubscribed(false);
|
|
507
|
+
this.setConnectionStatus("disconnected");
|
|
508
|
+
}
|
|
342
509
|
/**
|
|
343
510
|
* Reload the last assistant message
|
|
344
511
|
*/
|
|
@@ -349,6 +516,9 @@ class ChatClient {
|
|
|
349
516
|
(m) => m.role === "user"
|
|
350
517
|
);
|
|
351
518
|
if (lastUserMessageIndex === -1) return;
|
|
519
|
+
if (this.isLoading) {
|
|
520
|
+
this.cancelInFlightStream();
|
|
521
|
+
}
|
|
352
522
|
this.events.reloaded(lastUserMessageIndex);
|
|
353
523
|
this.processor.removeMessagesAfter(lastUserMessageIndex);
|
|
354
524
|
await this.streamResponse();
|
|
@@ -357,12 +527,7 @@ class ChatClient {
|
|
|
357
527
|
* Stop the current stream
|
|
358
528
|
*/
|
|
359
529
|
stop() {
|
|
360
|
-
|
|
361
|
-
this.abortController.abort();
|
|
362
|
-
this.abortController = null;
|
|
363
|
-
}
|
|
364
|
-
this.setIsLoading(false);
|
|
365
|
-
this.setStatus("ready");
|
|
530
|
+
this.cancelInFlightStream({ setReadyStatus: true });
|
|
366
531
|
this.events.stopped();
|
|
367
532
|
}
|
|
368
533
|
/**
|
|
@@ -485,6 +650,27 @@ class ChatClient {
|
|
|
485
650
|
getStatus() {
|
|
486
651
|
return this.status;
|
|
487
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* Get whether the subscription loop is active
|
|
655
|
+
*/
|
|
656
|
+
getIsSubscribed() {
|
|
657
|
+
return this.isSubscribed;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Get current connection lifecycle status
|
|
661
|
+
*/
|
|
662
|
+
getConnectionStatus() {
|
|
663
|
+
return this.connectionStatus;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Whether the shared session is actively generating.
|
|
667
|
+
* Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).
|
|
668
|
+
* Unlike `isLoading` (request-local), this reflects shared generation
|
|
669
|
+
* activity visible to all subscribers (e.g. across tabs/devices).
|
|
670
|
+
*/
|
|
671
|
+
getSessionGenerating() {
|
|
672
|
+
return this.sessionGenerating;
|
|
673
|
+
}
|
|
488
674
|
/**
|
|
489
675
|
* Get current error
|
|
490
676
|
*/
|
|
@@ -502,7 +688,22 @@ class ChatClient {
|
|
|
502
688
|
*/
|
|
503
689
|
updateOptions(options) {
|
|
504
690
|
if (options.connection !== void 0) {
|
|
505
|
-
|
|
691
|
+
const wasSubscribed = this.isSubscribed;
|
|
692
|
+
if (this.isLoading) {
|
|
693
|
+
this.cancelInFlightStream({
|
|
694
|
+
setReadyStatus: true,
|
|
695
|
+
abortSubscription: true
|
|
696
|
+
});
|
|
697
|
+
} else if (wasSubscribed) {
|
|
698
|
+
this.abortSubscriptionLoop();
|
|
699
|
+
}
|
|
700
|
+
this.resetSessionGenerating();
|
|
701
|
+
this.setIsSubscribed(false);
|
|
702
|
+
this.setConnectionStatus("disconnected");
|
|
703
|
+
this.connection = normalizeConnectionAdapter(options.connection);
|
|
704
|
+
if (wasSubscribed) {
|
|
705
|
+
this.subscribe();
|
|
706
|
+
}
|
|
506
707
|
}
|
|
507
708
|
if (options.body !== void 0) {
|
|
508
709
|
this.body = options.body;
|
|
@@ -525,6 +726,15 @@ class ChatClient {
|
|
|
525
726
|
if (options.onError !== void 0) {
|
|
526
727
|
this.callbacksRef.current.onError = options.onError;
|
|
527
728
|
}
|
|
729
|
+
if (options.onSubscriptionChange !== void 0) {
|
|
730
|
+
this.callbacksRef.current.onSubscriptionChange = options.onSubscriptionChange;
|
|
731
|
+
}
|
|
732
|
+
if (options.onConnectionStatusChange !== void 0) {
|
|
733
|
+
this.callbacksRef.current.onConnectionStatusChange = options.onConnectionStatusChange;
|
|
734
|
+
}
|
|
735
|
+
if (options.onSessionGeneratingChange !== void 0) {
|
|
736
|
+
this.callbacksRef.current.onSessionGeneratingChange = options.onSessionGeneratingChange;
|
|
737
|
+
}
|
|
528
738
|
if (options.onCustomEvent !== void 0) {
|
|
529
739
|
this.callbacksRef.current.onCustomEvent = options.onCustomEvent;
|
|
530
740
|
}
|