@tanstack/ai-client 0.22.0 → 0.23.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 +15 -1
- package/dist/esm/audio-recorder.js +190 -213
- package/dist/esm/audio-recorder.js.map +1 -1
- package/dist/esm/chat-client.d.ts +172 -3
- package/dist/esm/chat-client.js +1656 -1386
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/cleared-stream-tracker.d.ts +23 -0
- package/dist/esm/cleared-stream-tracker.js +97 -0
- package/dist/esm/cleared-stream-tracker.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +25 -12
- package/dist/esm/client-persistor.js +260 -235
- package/dist/esm/client-persistor.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +231 -10
- package/dist/esm/connection-adapters.js +989 -574
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +79 -139
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +31 -1
- package/dist/esm/devtools.js +977 -1127
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/events.js +224 -226
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +145 -2
- package/dist/esm/generation-client.js +659 -321
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-reconstruct.d.ts +21 -0
- package/dist/esm/generation-reconstruct.js +85 -0
- package/dist/esm/generation-reconstruct.js.map +1 -0
- package/dist/esm/generation-types.d.ts +289 -3
- package/dist/esm/generation-types.js +356 -13
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +7 -39
- package/dist/esm/interrupt-manager.d.ts +77 -0
- package/dist/esm/interrupt-manager.js +787 -0
- package/dist/esm/interrupt-manager.js.map +1 -0
- package/dist/esm/mcp-app-bridge.js +56 -64
- package/dist/esm/mcp-app-bridge.js.map +1 -1
- package/dist/esm/realtime-client.js +366 -440
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/response-stream.js +19 -26
- package/dist/esm/response-stream.js.map +1 -1
- package/dist/esm/sse-parser.js +44 -47
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/sse-utils.js +8 -9
- package/dist/esm/sse-utils.js.map +1 -1
- package/dist/esm/storage-adapters.d.ts +62 -0
- package/dist/esm/storage-adapters.js +174 -0
- package/dist/esm/storage-adapters.js.map +1 -0
- package/dist/esm/types.d.ts +212 -10
- package/dist/esm/types.js +38 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +113 -2
- package/dist/esm/video-generation-client.js +665 -379
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -7
- package/src/chat-client.ts +1079 -61
- package/src/cleared-stream-tracker.ts +151 -0
- package/src/client-persistor.ts +102 -33
- package/src/connection-adapters.ts +1185 -142
- package/src/devtools-noop.ts +4 -3
- package/src/devtools.ts +121 -3
- package/src/generation-client.ts +563 -13
- package/src/generation-reconstruct.ts +121 -0
- package/src/generation-types.ts +727 -3
- package/src/index.ts +56 -1
- package/src/interrupt-manager.ts +1440 -0
- package/src/storage-adapters.ts +242 -0
- package/src/types.ts +301 -9
- package/src/video-generation-client.ts +479 -13
- package/dist/esm/index.js.map +0 -1
|
@@ -1,325 +1,663 @@
|
|
|
1
|
-
import { GENERATION_EVENTS } from "./generation-types.js";
|
|
2
1
|
import { createNoOpGenerationDevtoolsBridge } from "./devtools-noop.js";
|
|
2
|
+
import { GENERATION_EVENTS, GENERATION_STREAM_TRUNCATED_MESSAGE, GENERATION_UNRESTORABLE_RESULT_MESSAGE, clientStateFromResumeStatus, createGenerationHydrationError, createGenerationResultSnapshot, parseGenerationResumeSnapshot, updateGenerationResumeSnapshot } from "./generation-types.js";
|
|
3
3
|
import { parseSSEResponse } from "./sse-parser.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
4
|
+
//#region src/generation-client.ts
|
|
5
|
+
/**
|
|
6
|
+
* A lightweight, generic client for one-shot generation tasks
|
|
7
|
+
* (image, speech, transcription, summarize).
|
|
8
|
+
*
|
|
9
|
+
* Supports two transport modes:
|
|
10
|
+
* - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).
|
|
11
|
+
* Server wraps results in StreamChunk events with CUSTOM event names.
|
|
12
|
+
* - **Fetcher** — Direct async function call. No streaming protocol needed.
|
|
13
|
+
*
|
|
14
|
+
* @template TInput - The input type for the generation request
|
|
15
|
+
* @template TResult - The result type returned by the generation
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // With streaming connection adapter
|
|
20
|
+
* const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({
|
|
21
|
+
* connection: fetchServerSentEvents('/api/generate/image'),
|
|
22
|
+
* onResultChange: setResult,
|
|
23
|
+
* onLoadingChange: setIsLoading,
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* // With fetcher (direct)
|
|
27
|
+
* const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({
|
|
28
|
+
* fetcher: async (input) => {
|
|
29
|
+
* const res = await fetch('/api/generate/image', {
|
|
30
|
+
* method: 'POST',
|
|
31
|
+
* body: JSON.stringify(input),
|
|
32
|
+
* })
|
|
33
|
+
* return res.json()
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* await client.generate({ prompt: 'A sunset over mountains' })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
var GenerationClient = class {
|
|
41
|
+
connection;
|
|
42
|
+
fetcher;
|
|
43
|
+
hydrateGenerationHandler;
|
|
44
|
+
joinRunHandler;
|
|
45
|
+
uniqueId;
|
|
46
|
+
devtoolsMetadata;
|
|
47
|
+
devtoolsBridge;
|
|
48
|
+
threadId;
|
|
49
|
+
persistenceScope;
|
|
50
|
+
serverDriven = false;
|
|
51
|
+
body;
|
|
52
|
+
result = null;
|
|
53
|
+
input = null;
|
|
54
|
+
progress = null;
|
|
55
|
+
isLoading = false;
|
|
56
|
+
error = void 0;
|
|
57
|
+
status = "idle";
|
|
58
|
+
resumeSnapshot;
|
|
59
|
+
lastEmittedResumeState;
|
|
60
|
+
abortController = null;
|
|
61
|
+
rejoinedRunId;
|
|
62
|
+
callbacksRef;
|
|
63
|
+
devtoolsMounted = false;
|
|
64
|
+
disposed = false;
|
|
65
|
+
serverHydrationStarted = false;
|
|
66
|
+
constructor(options) {
|
|
67
|
+
this.uniqueId = options.threadId ?? options.id ?? this.generateUniqueId("generation");
|
|
68
|
+
this.threadId = options.threadId ?? this.uniqueId;
|
|
69
|
+
this.persistenceScope = options.threadId;
|
|
70
|
+
this.connection = options.connection;
|
|
71
|
+
this.fetcher = options.fetcher;
|
|
72
|
+
this.hydrateGenerationHandler = options.hydrateGeneration;
|
|
73
|
+
this.joinRunHandler = options.joinRun;
|
|
74
|
+
this.body = options.body ?? {};
|
|
75
|
+
this.serverDriven = options.persistence === true;
|
|
76
|
+
if (options.persistence && !this.persistenceScope) console.warn("[TanStack AI] `persistence` needs a stable `threadId` to key on. Without one nothing will be restored after a reload. Pass a `threadId` derived from your own domain (e.g. `product-123-hero`).");
|
|
77
|
+
this.callbacksRef = {
|
|
78
|
+
onResult: options.onResult,
|
|
79
|
+
onError: options.onError,
|
|
80
|
+
onProgress: options.onProgress,
|
|
81
|
+
onChunk: options.onChunk,
|
|
82
|
+
onResultChange: options.onResultChange,
|
|
83
|
+
onLoadingChange: options.onLoadingChange,
|
|
84
|
+
onErrorChange: options.onErrorChange,
|
|
85
|
+
onStatusChange: options.onStatusChange,
|
|
86
|
+
onResumeSnapshotChange: options.onResumeSnapshotChange,
|
|
87
|
+
onResumeStateChange: options.onResumeStateChange,
|
|
88
|
+
reconstructResult: options.reconstructResult
|
|
89
|
+
};
|
|
90
|
+
this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools);
|
|
91
|
+
this.devtoolsBridge = (options.devtoolsBridgeFactory ?? createNoOpGenerationDevtoolsBridge)(this.buildDevtoolsBridgeOptions());
|
|
92
|
+
}
|
|
93
|
+
buildDevtoolsBridgeOptions() {
|
|
94
|
+
return {
|
|
95
|
+
hookId: this.uniqueId,
|
|
96
|
+
clientId: this.uniqueId,
|
|
97
|
+
threadId: this.threadId,
|
|
98
|
+
metadata: this.devtoolsMetadata,
|
|
99
|
+
getCoreState: () => ({
|
|
100
|
+
input: this.input,
|
|
101
|
+
result: this.result,
|
|
102
|
+
progress: this.progress,
|
|
103
|
+
status: this.status,
|
|
104
|
+
isLoading: this.isLoading,
|
|
105
|
+
...this.error ? { error: this.error.message } : {}
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
mountDevtools() {
|
|
110
|
+
this.disposed = false;
|
|
111
|
+
this.maybeHydrateFromServer();
|
|
112
|
+
this.maybeResumeInFlight();
|
|
113
|
+
if (this.devtoolsMounted) return;
|
|
114
|
+
this.devtoolsMounted = true;
|
|
115
|
+
this.devtoolsBridge.emitRegistered();
|
|
116
|
+
this.devtoolsBridge.emitSnapshot();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Trigger a generation request.
|
|
120
|
+
* Only one generation can be in-flight at a time; calling generate()
|
|
121
|
+
* while already generating will be a no-op.
|
|
122
|
+
*/
|
|
123
|
+
async generate(input) {
|
|
124
|
+
if (this.disposed) return;
|
|
125
|
+
if (this.isLoading) return;
|
|
126
|
+
this.mountDevtools();
|
|
127
|
+
this.input = input;
|
|
128
|
+
this.progress = null;
|
|
129
|
+
const runId = this.devtoolsBridge.beginRun(input);
|
|
130
|
+
this.setIsLoading(true);
|
|
131
|
+
this.setStatus("generating");
|
|
132
|
+
this.setError(void 0);
|
|
133
|
+
const abortController = new AbortController();
|
|
134
|
+
this.abortController = abortController;
|
|
135
|
+
const { signal } = abortController;
|
|
136
|
+
try {
|
|
137
|
+
if (this.fetcher) {
|
|
138
|
+
const result = await this.fetcher(input, { signal });
|
|
139
|
+
if (signal.aborted) return;
|
|
140
|
+
if (result instanceof Response) await this.processStream(parseSSEResponse(result, signal), runId, signal);
|
|
141
|
+
else {
|
|
142
|
+
this.devtoolsBridge.ensureRunStarted(runId);
|
|
143
|
+
this.setResult(result);
|
|
144
|
+
this.setStatus("success");
|
|
145
|
+
this.completePlainFetcherResumeSnapshot(result);
|
|
146
|
+
}
|
|
147
|
+
} else if (this.connection) {
|
|
148
|
+
const mergedData = {
|
|
149
|
+
...this.body,
|
|
150
|
+
...input
|
|
151
|
+
};
|
|
152
|
+
const stream = this.connection.connect([], mergedData, signal, this.createRunContext(runId));
|
|
153
|
+
await this.processStream(stream, runId, signal);
|
|
154
|
+
} else throw new Error("GenerationClient requires either a connection or fetcher option");
|
|
155
|
+
if (!signal.aborted && this.status === "success") {
|
|
156
|
+
this.progress = completeProgressValue(this.progress);
|
|
157
|
+
this.devtoolsBridge.finishRun(this.devtoolsBridge.getActiveRunId() ?? runId, "run:completed", "completed");
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (signal.aborted) return;
|
|
161
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
162
|
+
this.setError(error);
|
|
163
|
+
this.setStatus("error");
|
|
164
|
+
this.recordResumeSnapshotError(error);
|
|
165
|
+
this.devtoolsBridge.finishRun(this.devtoolsBridge.getActiveRunId() ?? runId, "run:errored", "errored", error.message);
|
|
166
|
+
this.callbacksRef.onError?.(error);
|
|
167
|
+
} finally {
|
|
168
|
+
if (this.abortController === abortController) {
|
|
169
|
+
this.abortController = null;
|
|
170
|
+
this.setIsLoading(false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Process a stream of AG-UI events from the streaming connection adapter.
|
|
176
|
+
*
|
|
177
|
+
* Throws {@link GENERATION_STREAM_TRUNCATED_MESSAGE} when the iteration ends
|
|
178
|
+
* without a terminal chunk. A `for await` over a stream that simply stops —
|
|
179
|
+
* proxy idle timeout, server restart, a durable log missing its terminal
|
|
180
|
+
* append — returns normally and would otherwise leave the caller's `status`
|
|
181
|
+
* on `generating` forever, with the persisted snapshot still `running` so
|
|
182
|
+
* every reload rejoins the same dead run. Throwing routes it through the
|
|
183
|
+
* caller's error path instead, which settles the status and rewrites the
|
|
184
|
+
* snapshot so nothing chases it again.
|
|
185
|
+
*/
|
|
186
|
+
async processStream(source, fallbackRunId, signal) {
|
|
187
|
+
let streamRunId;
|
|
188
|
+
let sawTerminalChunk = false;
|
|
189
|
+
for await (const chunk of source) {
|
|
190
|
+
if (signal.aborted) break;
|
|
191
|
+
this.callbacksRef.onChunk?.(chunk);
|
|
192
|
+
this.observeResumeSnapshot(chunk);
|
|
193
|
+
const chunkRunId = "runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0;
|
|
194
|
+
switch (chunk.type) {
|
|
195
|
+
case "RUN_STARTED":
|
|
196
|
+
streamRunId = chunk.runId;
|
|
197
|
+
this.devtoolsBridge.ensureRunStarted(chunk.runId);
|
|
198
|
+
break;
|
|
199
|
+
case "CUSTOM":
|
|
200
|
+
this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId);
|
|
201
|
+
if (chunk.name === GENERATION_EVENTS.RESULT) this.setResult(chunk.value);
|
|
202
|
+
else if (chunk.name === GENERATION_EVENTS.PROGRESS) {
|
|
203
|
+
const { progress, message } = chunk.value;
|
|
204
|
+
this.setProgress(progress, message);
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
case "RUN_FINISHED":
|
|
208
|
+
streamRunId = chunk.runId;
|
|
209
|
+
sawTerminalChunk = true;
|
|
210
|
+
this.devtoolsBridge.ensureRunStarted(chunk.runId);
|
|
211
|
+
this.setStatus("success");
|
|
212
|
+
break;
|
|
213
|
+
case "RUN_ERROR": {
|
|
214
|
+
this.devtoolsBridge.ensureRunStarted(chunkRunId ?? streamRunId ?? fallbackRunId);
|
|
215
|
+
const msg = chunk.message || chunk.error?.message || "An error occurred";
|
|
216
|
+
throw new Error(msg);
|
|
217
|
+
}
|
|
218
|
+
default: break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!sawTerminalChunk && !signal.aborted) throw new Error(GENERATION_STREAM_TRUNCATED_MESSAGE);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Abort any in-flight generation request.
|
|
225
|
+
*/
|
|
226
|
+
stop() {
|
|
227
|
+
const runId = this.devtoolsBridge.getActiveRunId();
|
|
228
|
+
if (this.abortController) {
|
|
229
|
+
this.abortController.abort();
|
|
230
|
+
this.abortController = null;
|
|
231
|
+
}
|
|
232
|
+
this.setIsLoading(false);
|
|
233
|
+
if (this.status === "generating") {
|
|
234
|
+
this.setStatus("idle");
|
|
235
|
+
if (runId) this.devtoolsBridge.finishRun(runId, "run:cancelled", "cancelled");
|
|
236
|
+
}
|
|
237
|
+
if (this.resumeSnapshot && this.resumeSnapshot.status === "running") {
|
|
238
|
+
this.resumeSnapshot = {
|
|
239
|
+
...this.resumeSnapshot,
|
|
240
|
+
resumeState: null,
|
|
241
|
+
status: "idle"
|
|
242
|
+
};
|
|
243
|
+
this.notifyResumeSnapshotChanged();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Clear the result, error, and return to idle state. Also drops the client's
|
|
248
|
+
* in-memory resume snapshot, so a remount restores nothing. The server-side
|
|
249
|
+
* record is untouched — this client no longer writes one — so a full page
|
|
250
|
+
* reload under `persistence: true` re-hydrates the last generation again.
|
|
251
|
+
*/
|
|
252
|
+
reset() {
|
|
253
|
+
this.stop();
|
|
254
|
+
this.setResult(null);
|
|
255
|
+
this.input = null;
|
|
256
|
+
this.progress = null;
|
|
257
|
+
this.devtoolsBridge.resetRuns();
|
|
258
|
+
this.setError(void 0);
|
|
259
|
+
this.setStatus("idle");
|
|
260
|
+
this.clearResumeSnapshot();
|
|
261
|
+
this.devtoolsBridge.emitState();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Update options without recreating the client.
|
|
265
|
+
*/
|
|
266
|
+
updateOptions(options) {
|
|
267
|
+
if (options.body !== void 0) this.body = options.body ?? {};
|
|
268
|
+
if (options.onResult !== void 0) this.callbacksRef.onResult = options.onResult;
|
|
269
|
+
if (options.onError !== void 0) this.callbacksRef.onError = options.onError;
|
|
270
|
+
if (options.onProgress !== void 0) this.callbacksRef.onProgress = options.onProgress;
|
|
271
|
+
if (options.onChunk !== void 0) this.callbacksRef.onChunk = options.onChunk;
|
|
272
|
+
}
|
|
273
|
+
dispose() {
|
|
274
|
+
this.disposed = true;
|
|
275
|
+
if (this.abortController) {
|
|
276
|
+
this.abortController.abort();
|
|
277
|
+
this.abortController = null;
|
|
278
|
+
}
|
|
279
|
+
this.setIsLoading(false);
|
|
280
|
+
this.devtoolsBridge.dispose();
|
|
281
|
+
this.devtoolsMounted = false;
|
|
282
|
+
this.serverHydrationStarted = false;
|
|
283
|
+
this.rejoinedRunId = void 0;
|
|
284
|
+
}
|
|
285
|
+
getResult() {
|
|
286
|
+
return this.result;
|
|
287
|
+
}
|
|
288
|
+
getIsLoading() {
|
|
289
|
+
return this.isLoading;
|
|
290
|
+
}
|
|
291
|
+
getError() {
|
|
292
|
+
return this.error;
|
|
293
|
+
}
|
|
294
|
+
getStatus() {
|
|
295
|
+
return this.status;
|
|
296
|
+
}
|
|
297
|
+
getResumeSnapshot() {
|
|
298
|
+
return this.resumeSnapshot ? {
|
|
299
|
+
...this.resumeSnapshot,
|
|
300
|
+
...this.resumeSnapshot.pendingArtifacts ? { pendingArtifacts: [...this.resumeSnapshot.pendingArtifacts] } : {},
|
|
301
|
+
...this.resumeSnapshot.result ? { result: {
|
|
302
|
+
...this.resumeSnapshot.result,
|
|
303
|
+
...this.resumeSnapshot.result.artifacts ? { artifacts: [...this.resumeSnapshot.result.artifacts] } : {}
|
|
304
|
+
} } : {},
|
|
305
|
+
...this.resumeSnapshot.error ? { error: { ...this.resumeSnapshot.error } } : {},
|
|
306
|
+
...this.resumeSnapshot.lastEvent ? { lastEvent: { ...this.resumeSnapshot.lastEvent } } : {}
|
|
307
|
+
} : void 0;
|
|
308
|
+
}
|
|
309
|
+
setResult(rawResult) {
|
|
310
|
+
if (rawResult === null) {
|
|
311
|
+
this.result = null;
|
|
312
|
+
this.callbacksRef.onResultChange?.(null);
|
|
313
|
+
this.devtoolsBridge.recordResultChange();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (this.callbacksRef.onResult) {
|
|
317
|
+
const transformed = this.callbacksRef.onResult(rawResult);
|
|
318
|
+
if (transformed === null) {
|
|
319
|
+
this.devtoolsBridge.emitState();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (transformed !== void 0) {
|
|
323
|
+
this.result = transformed;
|
|
324
|
+
this.callbacksRef.onResultChange?.(this.result);
|
|
325
|
+
this.devtoolsBridge.recordResultChange();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
this.result = rawResult;
|
|
330
|
+
this.callbacksRef.onResultChange?.(this.result);
|
|
331
|
+
this.devtoolsBridge.recordResultChange();
|
|
332
|
+
}
|
|
333
|
+
setIsLoading(isLoading) {
|
|
334
|
+
this.isLoading = isLoading;
|
|
335
|
+
this.callbacksRef.onLoadingChange?.(isLoading);
|
|
336
|
+
this.devtoolsBridge.recordLoadingChange();
|
|
337
|
+
}
|
|
338
|
+
setError(error) {
|
|
339
|
+
this.error = error;
|
|
340
|
+
this.callbacksRef.onErrorChange?.(error);
|
|
341
|
+
this.devtoolsBridge.recordErrorChange(error);
|
|
342
|
+
}
|
|
343
|
+
setStatus(status) {
|
|
344
|
+
this.status = status;
|
|
345
|
+
this.callbacksRef.onStatusChange?.(status);
|
|
346
|
+
this.devtoolsBridge.recordStatusChange(status);
|
|
347
|
+
}
|
|
348
|
+
setProgress(value, message) {
|
|
349
|
+
this.progress = {
|
|
350
|
+
value,
|
|
351
|
+
...message ? { message } : {}
|
|
352
|
+
};
|
|
353
|
+
if (message === void 0) this.callbacksRef.onProgress?.(value);
|
|
354
|
+
else this.callbacksRef.onProgress?.(value, message);
|
|
355
|
+
this.devtoolsBridge.recordProgressChange();
|
|
356
|
+
}
|
|
357
|
+
createDevtoolsMetadata(metadata) {
|
|
358
|
+
return {
|
|
359
|
+
hookName: metadata?.hookName ?? "useGeneration",
|
|
360
|
+
...metadata?.framework ? { framework: metadata.framework } : {},
|
|
361
|
+
...metadata?.outputKind ? { outputKind: metadata.outputKind } : {},
|
|
362
|
+
...metadata?.name ? { name: metadata.name } : {}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
generateUniqueId(prefix) {
|
|
366
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
367
|
+
}
|
|
368
|
+
createRunContext(runId) {
|
|
369
|
+
return {
|
|
370
|
+
threadId: this.threadId,
|
|
371
|
+
runId
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
observeResumeSnapshot(chunk) {
|
|
375
|
+
this.resumeSnapshot = updateGenerationResumeSnapshot(this.resumeSnapshot, chunk);
|
|
376
|
+
this.notifyResumeSnapshotChanged();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Notify the (internal) snapshot listener AND emit the public resume state.
|
|
380
|
+
* The snapshot stays internal (persistence + devtools); the hook consumes
|
|
381
|
+
* `resumeState`, mirroring the chat client.
|
|
382
|
+
*/
|
|
383
|
+
notifyResumeSnapshotChanged() {
|
|
384
|
+
this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot);
|
|
385
|
+
this.emitResumeState();
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Derive the public `resumeState` from the internal snapshot: the in-flight
|
|
389
|
+
* run identity, with any in-flight artifact refs folded under it. `null` once
|
|
390
|
+
* no run is in flight.
|
|
391
|
+
*
|
|
392
|
+
* The snapshot is rebuilt for every chunk, so emitting unconditionally would
|
|
393
|
+
* hand each framework hook a fresh object per chunk and re-render the
|
|
394
|
+
* component on every stream event. `resumeState` only changes at run
|
|
395
|
+
* boundaries and when artifacts land, so skip the notification unless it
|
|
396
|
+
* materially changed — same gate the persistence writes use.
|
|
397
|
+
*/
|
|
398
|
+
emitResumeState() {
|
|
399
|
+
const snapshot = this.resumeSnapshot;
|
|
400
|
+
const state = snapshot?.resumeState;
|
|
401
|
+
const resumeState = state ? {
|
|
402
|
+
...state,
|
|
403
|
+
...snapshot?.pendingArtifacts && snapshot.pendingArtifacts.length > 0 ? { pendingArtifacts: [...snapshot.pendingArtifacts] } : {}
|
|
404
|
+
} : null;
|
|
405
|
+
const signature = JSON.stringify(resumeState);
|
|
406
|
+
if (signature === this.lastEmittedResumeState) return;
|
|
407
|
+
this.lastEmittedResumeState = signature;
|
|
408
|
+
this.callbacksRef.onResumeStateChange?.(resumeState);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Repaint the normal fields from a restored snapshot (client store or server
|
|
412
|
+
* hydrate), so a reload presents the run in `result` / `status` / `error`
|
|
413
|
+
* exactly as a just-finished run would, never a bolt-on snapshot object.
|
|
414
|
+
* `isLoading` stays false: the client never auto-tails a restored run. The
|
|
415
|
+
* snapshot is not re-persisted here (it came from storage / the server).
|
|
416
|
+
*
|
|
417
|
+
* When the activity's mapper DECLINES a `complete` snapshot the repaint
|
|
418
|
+
* settles as an error instead: `success` with a `null` result is a state no
|
|
419
|
+
* consumer can render, and it hides the real cause (an output artifact
|
|
420
|
+
* persisted without a serve URL). A decline on any other status is expected —
|
|
421
|
+
* a `running` snapshot has no result yet, the rejoin will deliver it.
|
|
422
|
+
*/
|
|
423
|
+
repaintFromSnapshot(snapshot) {
|
|
424
|
+
this.resumeSnapshot = snapshot;
|
|
425
|
+
this.notifyResumeSnapshotChanged();
|
|
426
|
+
this.setStatus(clientStateFromResumeStatus(snapshot.status));
|
|
427
|
+
this.setError(snapshot.error ? Object.assign(new Error(snapshot.error.message), snapshot.error.code ? { code: snapshot.error.code } : {}) : void 0);
|
|
428
|
+
const restored = this.reconstructRestoredResult(snapshot);
|
|
429
|
+
if (restored !== null) this.setResult(restored);
|
|
430
|
+
else if (this.callbacksRef.reconstructResult && snapshot.status === "complete") this.reportUnrestorableResult();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Report a `complete` snapshot the activity's mapper could not rebuild.
|
|
434
|
+
* Runs after the status/error repaint above, so it wins over the snapshot's
|
|
435
|
+
* own `complete` status.
|
|
436
|
+
*/
|
|
437
|
+
reportUnrestorableResult() {
|
|
438
|
+
const error = new Error(GENERATION_UNRESTORABLE_RESULT_MESSAGE);
|
|
439
|
+
this.setStatus("error");
|
|
440
|
+
this.setError(error);
|
|
441
|
+
this.callbacksRef.onError?.(error);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Repaint a restored snapshot (client store or server hydrate) and, when it
|
|
445
|
+
* reports a run still in flight, tail that run to completion via `joinRun`
|
|
446
|
+
* (from the connection, or the `joinRun` option when the transport can't
|
|
447
|
+
* carry one).
|
|
448
|
+
*
|
|
449
|
+
* A `running` snapshot that no `joinRun` handler can tail is repainted as an
|
|
450
|
+
* interrupted error instead of a `generating` status that would never
|
|
451
|
+
* settle: an interrupted generation cannot be resumed, only re-run.
|
|
452
|
+
*/
|
|
453
|
+
repaintRestoredSnapshot(snapshot, activeRunId) {
|
|
454
|
+
if (snapshot.status !== "running") {
|
|
455
|
+
this.repaintFromSnapshot(snapshot);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const joinRun = this.connection?.joinRun ?? this.joinRunHandler;
|
|
459
|
+
const runId = activeRunId ?? snapshot.resumeState?.runId;
|
|
460
|
+
if (runId && joinRun) {
|
|
461
|
+
this.repaintFromSnapshot(snapshot);
|
|
462
|
+
this.rejoinInFlight(runId);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.repaintFromSnapshot({
|
|
466
|
+
...snapshot,
|
|
467
|
+
resumeState: null,
|
|
468
|
+
status: "error",
|
|
469
|
+
error: { message: "The previous generation was interrupted before it finished and cannot be resumed — generate again to retry." }
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Build the restorable result shape from the snapshot and hand it to the
|
|
474
|
+
* per-activity `reconstructResult` mapper (injected by the specialized
|
|
475
|
+
* client/hook, which knows the concrete result type).
|
|
476
|
+
*
|
|
477
|
+
* Returns `null` both when no mapper is set (nothing to rebuild — `result`
|
|
478
|
+
* simply stays null) and when the mapper declines. The caller distinguishes
|
|
479
|
+
* the two: see {@link repaintFromSnapshot}.
|
|
480
|
+
*/
|
|
481
|
+
reconstructRestoredResult(snapshot) {
|
|
482
|
+
const build = this.callbacksRef.reconstructResult;
|
|
483
|
+
if (!build) return null;
|
|
484
|
+
const result = snapshot.result;
|
|
485
|
+
return build({
|
|
486
|
+
...result?.id !== void 0 ? { id: result.id } : {},
|
|
487
|
+
...result?.model !== void 0 ? { model: result.model } : {},
|
|
488
|
+
...result?.status !== void 0 ? { status: result.status } : {},
|
|
489
|
+
...result?.providerJobId !== void 0 ? { providerJobId: result.providerJobId } : {},
|
|
490
|
+
...result?.expiresAt !== void 0 ? { expiresAt: result.expiresAt } : {},
|
|
491
|
+
...result?.text !== void 0 ? { text: result.text } : {},
|
|
492
|
+
...result?.usage !== void 0 ? { usage: result.usage } : {},
|
|
493
|
+
...snapshot.activity !== void 0 ? { activity: snapshot.activity } : {},
|
|
494
|
+
artifacts: result?.artifacts ?? []
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The plain (non-Response) fetcher path never observes stream chunks, so
|
|
499
|
+
* the terminal snapshot is built here from the fetcher's own result. A
|
|
500
|
+
* stale `error` from a previous run is intentionally dropped — this run
|
|
501
|
+
* succeeded.
|
|
502
|
+
*/
|
|
503
|
+
completePlainFetcherResumeSnapshot(rawResult) {
|
|
504
|
+
const previous = this.resumeSnapshot;
|
|
505
|
+
const result = createGenerationResultSnapshot(rawResult);
|
|
506
|
+
this.resumeSnapshot = {
|
|
507
|
+
schemaVersion: 1,
|
|
508
|
+
resumeState: null,
|
|
509
|
+
status: "complete",
|
|
510
|
+
...previous?.activity ? { activity: previous.activity } : {},
|
|
511
|
+
...previous?.pendingArtifacts && previous.pendingArtifacts.length > 0 ? { pendingArtifacts: [...previous.pendingArtifacts] } : {},
|
|
512
|
+
...result ? { result } : previous?.result ? { result: { ...previous.result } } : {}
|
|
513
|
+
};
|
|
514
|
+
this.notifyResumeSnapshotChanged();
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Records a transport-level failure (network drop, throwing callback) in
|
|
518
|
+
* the snapshot. Without this, only a server-emitted RUN_ERROR chunk would
|
|
519
|
+
* mark the snapshot `error`, leaving a persisted record that claims the
|
|
520
|
+
* run is still in flight.
|
|
521
|
+
*/
|
|
522
|
+
recordResumeSnapshotError(error) {
|
|
523
|
+
if (this.status !== "error") this.setStatus("error");
|
|
524
|
+
this.setError(error);
|
|
525
|
+
if (this.resumeSnapshot?.status === "error") return;
|
|
526
|
+
if (!this.resumeSnapshot && !this.serverDriven) return;
|
|
527
|
+
const previous = this.resumeSnapshot;
|
|
528
|
+
this.resumeSnapshot = {
|
|
529
|
+
schemaVersion: 1,
|
|
530
|
+
resumeState: null,
|
|
531
|
+
status: "error",
|
|
532
|
+
...previous?.activity ? { activity: previous.activity } : {},
|
|
533
|
+
...previous?.pendingArtifacts && previous.pendingArtifacts.length > 0 ? { pendingArtifacts: [...previous.pendingArtifacts] } : {},
|
|
534
|
+
...previous?.result ? { result: { ...previous.result } } : {},
|
|
535
|
+
error: { message: error.message }
|
|
536
|
+
};
|
|
537
|
+
this.notifyResumeSnapshotChanged();
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Drop the client's in-memory snapshot and re-emit. Purely local — this
|
|
541
|
+
* client writes no storage, so nothing persisted is removed.
|
|
542
|
+
*/
|
|
543
|
+
clearResumeSnapshot() {
|
|
544
|
+
this.resumeSnapshot = void 0;
|
|
545
|
+
this.lastEmittedResumeState = void 0;
|
|
546
|
+
this.notifyResumeSnapshotChanged();
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Server-driven mount hydration entry point (`persistence: true`). Runs at
|
|
550
|
+
* most once, from the commit-phase mount path (`mountDevtools`) — never the
|
|
551
|
+
* constructor / render phase — so remounts and speculative renders can't
|
|
552
|
+
* re-fire the hydrate GET.
|
|
553
|
+
*/
|
|
554
|
+
maybeHydrateFromServer() {
|
|
555
|
+
if (!this.serverDriven || this.serverHydrationStarted) return;
|
|
556
|
+
this.serverHydrationStarted = true;
|
|
557
|
+
if (this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler) this.hydrateFromServer();
|
|
558
|
+
else console.warn("[TanStack AI] `persistence: true` (server-driven) needs a `hydrateGeneration` handler — either a connection that implements one (e.g. `fetchServerSentEvents` / `fetchHttpStream`, or `stream()` / `rpcStream()` with persistence handlers) or the `hydrateGeneration` option. Without one, nothing is persisted or restored.");
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Server-driven mount hydration (`persistence: true`). The client holds no
|
|
562
|
+
* local snapshot; on mount it asks the server — keyed by the stable threadId —
|
|
563
|
+
* for the last generation's resume snapshot, validates it, and repaints it. It
|
|
564
|
+
* never auto-starts a run, and never blocks: a `generate()` that starts first
|
|
565
|
+
* owns the client and hydration backs off, mirroring the chat client.
|
|
566
|
+
*
|
|
567
|
+
* A genuine **miss** (the server reports no record for the thread) is silent —
|
|
568
|
+
* a fresh thread is not an error. A genuine **failure** (transport error, a
|
|
569
|
+
* 403 from the authorize gate, a malformed body, a record the client's own
|
|
570
|
+
* validator rejects) is surfaced through `status` / `error` / `onError`, so a
|
|
571
|
+
* broken server is distinguishable from an empty one and the app can retry.
|
|
572
|
+
*/
|
|
573
|
+
hydrateFromServer() {
|
|
574
|
+
const hydrate = this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler;
|
|
575
|
+
if (!hydrate) return;
|
|
576
|
+
if (this.resumeSnapshot || this.isLoading || this.status !== "idle") return;
|
|
577
|
+
(async () => {
|
|
578
|
+
let res;
|
|
579
|
+
try {
|
|
580
|
+
res = await hydrate(this.threadId);
|
|
581
|
+
} catch (cause) {
|
|
582
|
+
this.failHydration(createGenerationHydrationError("the request to the server did not succeed", cause));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (!res.resumeSnapshot) return;
|
|
586
|
+
const snapshot = parseGenerationResumeSnapshot(res.resumeSnapshot);
|
|
587
|
+
if (!snapshot) {
|
|
588
|
+
this.failHydration(createGenerationHydrationError("the server returned a record this client cannot read (unknown schema version, or a missing/invalid `status` or `resumeState`)"));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (this.resumeSnapshot || this.isLoading || this.status !== "idle") return;
|
|
592
|
+
this.repaintRestoredSnapshot(snapshot, res.activeRun?.runId);
|
|
593
|
+
})();
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Surface a hydration failure on the observable fields. Skipped when a
|
|
597
|
+
* `generate()` took ownership while the hydrate GET was in flight — the live
|
|
598
|
+
* run's state must win over a stale mount-time failure.
|
|
599
|
+
*/
|
|
600
|
+
failHydration(error) {
|
|
601
|
+
if (this.resumeSnapshot || this.isLoading || this.status !== "idle") return;
|
|
602
|
+
this.setStatus("error");
|
|
603
|
+
this.setError(error);
|
|
604
|
+
this.callbacksRef.onError?.(error);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Re-attach to an already-loaded `running` snapshot (the remount case). Safe
|
|
608
|
+
* to call repeatedly: `rejoinInFlight` dedupes on `rejoinedRunId` and bails
|
|
609
|
+
* when a run is already in flight, so on the first mount (where the rejoin was
|
|
610
|
+
* already started from `repaintRestoredSnapshot`) this is a no-op.
|
|
611
|
+
*/
|
|
612
|
+
maybeResumeInFlight() {
|
|
613
|
+
if (this.resumeSnapshot?.status !== "running") return;
|
|
614
|
+
const runId = this.resumeSnapshot.resumeState?.runId;
|
|
615
|
+
if (runId) this.rejoinInFlight(runId);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Re-attach to a run that is still generating and stream it to completion,
|
|
619
|
+
* mirroring the chat client's mount-time rejoin. Reuses `processStream`, so
|
|
620
|
+
* `result` / `progress` / `status` repaint from the replayed chunks exactly as
|
|
621
|
+
* a live run does. Best-effort: a live `generate()` owns the client and is
|
|
622
|
+
* never stomped, and the same run is only rejoined once.
|
|
623
|
+
*/
|
|
624
|
+
rejoinInFlight(runId) {
|
|
625
|
+
const joinRun = this.connection?.joinRun ?? this.joinRunHandler;
|
|
626
|
+
if (!joinRun) return;
|
|
627
|
+
if (this.rejoinedRunId === runId) return;
|
|
628
|
+
if (this.isLoading || this.abortController) return;
|
|
629
|
+
this.rejoinedRunId = runId;
|
|
630
|
+
const controller = new AbortController();
|
|
631
|
+
this.abortController = controller;
|
|
632
|
+
this.setIsLoading(true);
|
|
633
|
+
this.setStatus("generating");
|
|
634
|
+
(async () => {
|
|
635
|
+
try {
|
|
636
|
+
await this.processStream(joinRun(runId, controller.signal), runId, controller.signal);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (!controller.signal.aborted) {
|
|
639
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
640
|
+
this.recordResumeSnapshotError(failure);
|
|
641
|
+
this.callbacksRef.onError?.(failure);
|
|
642
|
+
}
|
|
643
|
+
} finally {
|
|
644
|
+
if (this.abortController === controller) {
|
|
645
|
+
this.abortController = null;
|
|
646
|
+
this.setIsLoading(false);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
})();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
314
652
|
function completeProgressValue(progress) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
653
|
+
if (!progress) return null;
|
|
654
|
+
const message = progress.message;
|
|
655
|
+
return {
|
|
656
|
+
value: 100,
|
|
657
|
+
...message ? { message } : {}
|
|
658
|
+
};
|
|
321
659
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//# sourceMappingURL=generation-client.js.map
|
|
660
|
+
//#endregion
|
|
661
|
+
export { GenerationClient };
|
|
662
|
+
|
|
663
|
+
//# sourceMappingURL=generation-client.js.map
|