@tanstack/ai-client 0.28.0 → 0.29.1
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/dist/esm/chat-client.d.ts +9 -0
- package/dist/esm/chat-client.js +60 -15
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +17 -1
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +1 -0
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +5 -1
- package/dist/esm/devtools.js +54 -10
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/message-date-normalizer.d.ts +3 -0
- package/dist/esm/message-date-normalizer.js +35 -0
- package/dist/esm/message-date-normalizer.js.map +1 -0
- package/dist/esm/storage-adapters.js +13 -1
- package/dist/esm/storage-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +113 -15
- package/src/connection-adapters.ts +28 -2
- package/src/devtools-noop.ts +1 -0
- package/src/devtools.ts +98 -12
- package/src/message-date-normalizer.ts +29 -0
- package/src/storage-adapters.ts +13 -2
- package/src/types.ts +5 -0
|
@@ -123,8 +123,16 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
123
123
|
private continuationPending;
|
|
124
124
|
private subscriptionAbortController;
|
|
125
125
|
private processingResolve;
|
|
126
|
+
/**
|
|
127
|
+
* `connect()` adapters push the full HTTP body into the subscribe queue, then
|
|
128
|
+
* wait until that queue is idle. After `send()` returns, every chunk from this
|
|
129
|
+
* request has been processed. Subscribe/send sockets do not drain that way.
|
|
130
|
+
*/
|
|
131
|
+
private connectionDrainsOnSend;
|
|
126
132
|
private errorReportedGeneration;
|
|
127
133
|
private streamGeneration;
|
|
134
|
+
private continuationGeneration;
|
|
135
|
+
private streamContinuationGeneration;
|
|
128
136
|
private continuationSkipped;
|
|
129
137
|
private draining;
|
|
130
138
|
private sessionGenerating;
|
|
@@ -239,6 +247,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
239
247
|
retryInterrupts(): void;
|
|
240
248
|
/** Unsafe low-level resume escape hatch. Prefer bound interrupt methods. */
|
|
241
249
|
resumeInterruptsUnsafe(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
|
|
250
|
+
private resumeInterruptsUnsafeForGeneration;
|
|
242
251
|
/** @deprecated Use bound interrupt methods or resumeInterruptsUnsafe(). */
|
|
243
252
|
resumeInterrupts(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
|
|
244
253
|
private submitInterruptBatch;
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { prepareResolvedByokHeaders, resolveByokProviderId } from "./byok/resolve.js";
|
|
2
2
|
import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
|
|
3
|
+
import { normalizeMessagesDates } from "./message-date-normalizer.js";
|
|
3
4
|
import { fetcherToConnectionAdapter, getChunkRunId as getChunkRunId$1, normalizeConnectionAdapter } from "./connection-adapters.js";
|
|
4
5
|
import { ChatPersistor } from "./client-persistor.js";
|
|
5
6
|
import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
|
|
@@ -21,6 +22,16 @@ function resolveTransport(transport) {
|
|
|
21
22
|
if (fetcher) return fetcherToConnectionAdapter(fetcher);
|
|
22
23
|
throw new Error("ChatClient: either `connection` or `fetcher` is required.");
|
|
23
24
|
}
|
|
25
|
+
function connectionDrainsOnSend(connection) {
|
|
26
|
+
return "connect" in connection;
|
|
27
|
+
}
|
|
28
|
+
function isIntermediateToolTurn(chunk) {
|
|
29
|
+
if (chunk.type !== "RUN_FINISHED") return false;
|
|
30
|
+
if (chunk.outcome?.type === "interrupt") return false;
|
|
31
|
+
const extra = chunk;
|
|
32
|
+
if (extra.finishReason !== void 0) return extra.finishReason === "tool_calls";
|
|
33
|
+
return tanstackMetadata(chunk)?.finishReason === "tool_calls";
|
|
34
|
+
}
|
|
24
35
|
function normalizeQueueOption(option) {
|
|
25
36
|
const base = {
|
|
26
37
|
whenBusy: "queue",
|
|
@@ -203,8 +214,16 @@ var ChatClient = class {
|
|
|
203
214
|
continuationPending = false;
|
|
204
215
|
subscriptionAbortController = null;
|
|
205
216
|
processingResolve = null;
|
|
217
|
+
/**
|
|
218
|
+
* `connect()` adapters push the full HTTP body into the subscribe queue, then
|
|
219
|
+
* wait until that queue is idle. After `send()` returns, every chunk from this
|
|
220
|
+
* request has been processed. Subscribe/send sockets do not drain that way.
|
|
221
|
+
*/
|
|
222
|
+
connectionDrainsOnSend = false;
|
|
206
223
|
errorReportedGeneration = null;
|
|
207
224
|
streamGeneration = 0;
|
|
225
|
+
continuationGeneration = 0;
|
|
226
|
+
streamContinuationGeneration = 0;
|
|
208
227
|
continuationSkipped = false;
|
|
209
228
|
draining = false;
|
|
210
229
|
sessionGenerating = false;
|
|
@@ -236,7 +255,9 @@ var ChatClient = class {
|
|
|
236
255
|
this.byokProvider = options.byokProvider;
|
|
237
256
|
this.context = options.context;
|
|
238
257
|
this.queueConfig = normalizeQueueOption(options.queue);
|
|
239
|
-
|
|
258
|
+
const transport = resolveTransport(options);
|
|
259
|
+
this.connectionDrainsOnSend = connectionDrainsOnSend(transport);
|
|
260
|
+
this.connection = normalizeConnectionAdapter(transport);
|
|
240
261
|
this.clientToolsRef = { current: /* @__PURE__ */ new Map() };
|
|
241
262
|
if (options.tools) for (const tool of options.tools) this.clientToolsRef.current.set(tool.name, tool);
|
|
242
263
|
this.devtoolsBridge = (options.devtoolsBridgeFactory ?? createNoOpChatDevtoolsBridge)(this.buildDevtoolsBridgeOptions(options.devtools));
|
|
@@ -333,6 +354,7 @@ var ChatClient = class {
|
|
|
333
354
|
const clientTool = (this.activeClientTools ?? this.clientToolsRef.current).get(args.toolName);
|
|
334
355
|
const executeFunc = clientTool?.execute;
|
|
335
356
|
if (executeFunc) {
|
|
357
|
+
const continuationGeneration = this.continuationGeneration;
|
|
336
358
|
const runEventContext = this.devtoolsBridge.getCurrentRunEventContext();
|
|
337
359
|
const executionPromise = (async () => {
|
|
338
360
|
try {
|
|
@@ -347,7 +369,7 @@ var ChatClient = class {
|
|
|
347
369
|
tool: args.toolName,
|
|
348
370
|
output,
|
|
349
371
|
state: "output-available"
|
|
350
|
-
}, clientTool, runEventContext);
|
|
372
|
+
}, clientTool, continuationGeneration, runEventContext);
|
|
351
373
|
} catch (error) {
|
|
352
374
|
await this.addToolResultForClientTool({
|
|
353
375
|
toolCallId: args.toolCallId,
|
|
@@ -355,7 +377,7 @@ var ChatClient = class {
|
|
|
355
377
|
output: null,
|
|
356
378
|
state: "output-error",
|
|
357
379
|
errorText: error.message
|
|
358
|
-
}, clientTool, runEventContext);
|
|
380
|
+
}, clientTool, continuationGeneration, runEventContext);
|
|
359
381
|
} finally {
|
|
360
382
|
this.pendingToolExecutions.delete(args.toolCallId);
|
|
361
383
|
}
|
|
@@ -370,6 +392,7 @@ var ChatClient = class {
|
|
|
370
392
|
},
|
|
371
393
|
onCustomEvent: (eventType, data, context) => {
|
|
372
394
|
if (eventType === "memory:state") this.devtoolsBridge.recordMemoryState(data);
|
|
395
|
+
if (eventType === "skills:state") this.devtoolsBridge.recordSkillsState(data);
|
|
373
396
|
this.callbacksRef.current.onCustomEvent(eventType, data, context);
|
|
374
397
|
}
|
|
375
398
|
}
|
|
@@ -491,7 +514,7 @@ var ChatClient = class {
|
|
|
491
514
|
}
|
|
492
515
|
if (this.disposed || !this.tailing) return;
|
|
493
516
|
if (this.isLoading || this.abortController) return;
|
|
494
|
-
if (result.messages.length > 0) this.processor.setMessages(result.messages);
|
|
517
|
+
if (result.messages.length > 0) this.processor.setMessages(normalizeMessagesDates(result.messages));
|
|
495
518
|
if (result.interrupts && result.interrupts.pending.length > 0) this.applyResumeSnapshot({
|
|
496
519
|
resumeState: {
|
|
497
520
|
threadId: this.threadId,
|
|
@@ -558,7 +581,8 @@ var ChatClient = class {
|
|
|
558
581
|
this.clearedStreamTracker.onSessionRunError();
|
|
559
582
|
}
|
|
560
583
|
this.setSessionGenerating(this.activeRunIds.size > 0);
|
|
561
|
-
|
|
584
|
+
const skipProcessingResolve = chunk.type === "RUN_FINISHED" && isIntermediateToolTurn(chunk);
|
|
585
|
+
if (options?.resolveProcessing !== false && !skipProcessingResolve) this.resolveProcessing();
|
|
562
586
|
}
|
|
563
587
|
/**
|
|
564
588
|
* Track interrupt state off the stream's terminal events. A RUN_FINISHED with
|
|
@@ -649,10 +673,14 @@ var ChatClient = class {
|
|
|
649
673
|
resumeInterruptsUnsafe(resume, state) {
|
|
650
674
|
const target = state ?? this.lastResume;
|
|
651
675
|
if (!target) return Promise.resolve(false);
|
|
676
|
+
return this.resumeInterruptsUnsafeForGeneration(resume, target, this.continuationGeneration);
|
|
677
|
+
}
|
|
678
|
+
resumeInterruptsUnsafeForGeneration(resume, target, continuationGeneration) {
|
|
679
|
+
if (continuationGeneration !== this.continuationGeneration) return Promise.resolve(false);
|
|
652
680
|
if (this.isLoading) return new Promise((resolve, reject) => {
|
|
653
681
|
this.queuePostStreamAction(async () => {
|
|
654
682
|
try {
|
|
655
|
-
resolve(await this.
|
|
683
|
+
resolve(await this.resumeInterruptsUnsafeForGeneration(resume, target, continuationGeneration));
|
|
656
684
|
} catch (error) {
|
|
657
685
|
reject(error);
|
|
658
686
|
}
|
|
@@ -668,6 +696,7 @@ var ChatClient = class {
|
|
|
668
696
|
return this.resumeInterruptsUnsafe(resume, state);
|
|
669
697
|
}
|
|
670
698
|
async submitInterruptBatch(submission) {
|
|
699
|
+
const continuationGeneration = this.continuationGeneration;
|
|
671
700
|
this.activeInterruptSubmission = submission;
|
|
672
701
|
this.interruptSubmissionFailure = void 0;
|
|
673
702
|
for (const resolution of submission.resolutions) {
|
|
@@ -676,12 +705,13 @@ var ChatClient = class {
|
|
|
676
705
|
const approvalId = resolution.interruptId;
|
|
677
706
|
this.processor.addToolApprovalResponse(approvalId, approved);
|
|
678
707
|
}
|
|
679
|
-
const resumed = await this.
|
|
708
|
+
const resumed = await this.resumeInterruptsUnsafeForGeneration([...submission.resolutions], {
|
|
680
709
|
threadId: submission.threadId,
|
|
681
710
|
runId: submission.interruptedRunId
|
|
682
|
-
}).finally(() => {
|
|
683
|
-
this.activeInterruptSubmission = void 0;
|
|
711
|
+
}, continuationGeneration).finally(() => {
|
|
712
|
+
if (this.activeInterruptSubmission === submission) this.activeInterruptSubmission = void 0;
|
|
684
713
|
});
|
|
714
|
+
if (continuationGeneration !== this.continuationGeneration) return;
|
|
685
715
|
const failure = this.takeInterruptSubmissionFailure();
|
|
686
716
|
if (failure !== void 0) throw { errors: failure.errors };
|
|
687
717
|
if (!resumed) throw new Error("Interrupt continuation could not be started.");
|
|
@@ -906,6 +936,7 @@ var ChatClient = class {
|
|
|
906
936
|
threadId: this.threadId,
|
|
907
937
|
runId
|
|
908
938
|
};
|
|
939
|
+
this.streamContinuationGeneration = this.continuationGeneration;
|
|
909
940
|
this.setIsLoading(true);
|
|
910
941
|
this.setStatus("streaming");
|
|
911
942
|
(async () => {
|
|
@@ -1224,6 +1255,7 @@ var ChatClient = class {
|
|
|
1224
1255
|
async streamResponse() {
|
|
1225
1256
|
if (this.isLoading) return false;
|
|
1226
1257
|
const generation = ++this.streamGeneration;
|
|
1258
|
+
this.streamContinuationGeneration = this.continuationGeneration;
|
|
1227
1259
|
const resumeThreadId = this.pendingResumeThreadId;
|
|
1228
1260
|
const resumeParentRunId = this.pendingResumeParentRunId;
|
|
1229
1261
|
const resumeItems = this.pendingResumeItems;
|
|
@@ -1290,6 +1322,7 @@ var ChatClient = class {
|
|
|
1290
1322
|
this.devtoolsBridge.emitSnapshot();
|
|
1291
1323
|
await this.connection.send(messages, mergedBody, signal, runContext);
|
|
1292
1324
|
if (generation !== this.streamGeneration || signal.aborted) return false;
|
|
1325
|
+
if (this.connectionDrainsOnSend) this.resolveProcessing();
|
|
1293
1326
|
await processingComplete;
|
|
1294
1327
|
if (generation !== this.streamGeneration) return false;
|
|
1295
1328
|
if (this.status === "error") {
|
|
@@ -1390,9 +1423,13 @@ var ChatClient = class {
|
|
|
1390
1423
|
* Stop the current stream
|
|
1391
1424
|
*/
|
|
1392
1425
|
stop() {
|
|
1426
|
+
this.continuationGeneration++;
|
|
1393
1427
|
const hadLocalStream = this.abortController !== null;
|
|
1394
1428
|
this.cancelInFlightStream({ setReadyStatus: true });
|
|
1395
1429
|
this.discardPendingSends();
|
|
1430
|
+
this.lastResume = null;
|
|
1431
|
+
this.activeInterruptSubmission = void 0;
|
|
1432
|
+
this.interruptManager.reset();
|
|
1396
1433
|
if (hadLocalStream) this.resetSessionGenerating();
|
|
1397
1434
|
this.events.stopped();
|
|
1398
1435
|
}
|
|
@@ -1427,9 +1464,9 @@ var ChatClient = class {
|
|
|
1427
1464
|
*/
|
|
1428
1465
|
async addToolResult(result) {
|
|
1429
1466
|
const clientTool = this.clientToolsRef.current.get(result.tool);
|
|
1430
|
-
await this.addToolResultForClientTool(result, clientTool);
|
|
1467
|
+
await this.addToolResultForClientTool(result, clientTool, this.streamContinuationGeneration);
|
|
1431
1468
|
}
|
|
1432
|
-
async addToolResultForClientTool(result, clientTool, context) {
|
|
1469
|
+
async addToolResultForClientTool(result, clientTool, continuationGeneration, context) {
|
|
1433
1470
|
if (clientTool && result.state !== "output-error") try {
|
|
1434
1471
|
result = {
|
|
1435
1472
|
...result,
|
|
@@ -1444,11 +1481,12 @@ var ChatClient = class {
|
|
|
1444
1481
|
};
|
|
1445
1482
|
}
|
|
1446
1483
|
this.events.toolResultAdded(result.toolCallId, result.tool, result.output, result.state || "output-available", context);
|
|
1484
|
+
if (continuationGeneration !== this.continuationGeneration) return;
|
|
1447
1485
|
this.processor.addToolResult(result.toolCallId, result.output, result.state === "output-error" ? result.errorText || "Tool execution failed" : void 0);
|
|
1448
1486
|
this.devtoolsBridge.emitSnapshot();
|
|
1449
1487
|
if (this.interruptManager.resolveClientToolOutput(result.toolCallId, result.state === "output-error" ? { error: result.errorText || "Tool execution failed" } : result.output)) return;
|
|
1450
1488
|
if (this.isLoading) {
|
|
1451
|
-
this.queuePostStreamAction(() => this.checkForContinuation());
|
|
1489
|
+
this.queuePostStreamAction(() => continuationGeneration === this.continuationGeneration ? this.checkForContinuation() : Promise.resolve());
|
|
1452
1490
|
return;
|
|
1453
1491
|
}
|
|
1454
1492
|
await this.checkForContinuation();
|
|
@@ -1486,7 +1524,11 @@ var ChatClient = class {
|
|
|
1486
1524
|
* Queue an action to be executed after the current stream ends
|
|
1487
1525
|
*/
|
|
1488
1526
|
queuePostStreamAction(action) {
|
|
1489
|
-
this.
|
|
1527
|
+
const continuationGeneration = this.continuationGeneration;
|
|
1528
|
+
this.postStreamActions.push(async () => {
|
|
1529
|
+
if (continuationGeneration !== this.continuationGeneration) return;
|
|
1530
|
+
await action();
|
|
1531
|
+
});
|
|
1490
1532
|
}
|
|
1491
1533
|
/**
|
|
1492
1534
|
* Drain and execute all queued post-stream actions
|
|
@@ -1505,6 +1547,7 @@ var ChatClient = class {
|
|
|
1505
1547
|
* Check if we should continue the flow and do so if needed
|
|
1506
1548
|
*/
|
|
1507
1549
|
async checkForContinuation() {
|
|
1550
|
+
if (this.streamContinuationGeneration !== this.continuationGeneration) return;
|
|
1508
1551
|
if (this.hasPendingInterrupts()) return;
|
|
1509
1552
|
if (this.continuationPending || this.isLoading) {
|
|
1510
1553
|
this.continuationSkipped = true;
|
|
@@ -1688,10 +1731,12 @@ var ChatClient = class {
|
|
|
1688
1731
|
this.resetSessionGenerating();
|
|
1689
1732
|
this.setIsSubscribed(false);
|
|
1690
1733
|
this.setConnectionStatus("disconnected");
|
|
1691
|
-
|
|
1734
|
+
const transport = resolveTransport({
|
|
1692
1735
|
connection: options.connection,
|
|
1693
1736
|
fetcher: options.fetcher
|
|
1694
|
-
})
|
|
1737
|
+
});
|
|
1738
|
+
this.connectionDrainsOnSend = connectionDrainsOnSend(transport);
|
|
1739
|
+
this.connection = normalizeConnectionAdapter(transport);
|
|
1695
1740
|
if (wasSubscribed) this.subscribe();
|
|
1696
1741
|
}
|
|
1697
1742
|
if (options.body !== void 0) this.bodyOption = options.body;
|