@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.
@@ -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;
@@ -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
- this.connection = normalizeConnectionAdapter(resolveTransport(options));
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
- if (options?.resolveProcessing !== false) this.resolveProcessing();
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.resumeInterruptsUnsafe(resume, target));
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.resumeInterruptsUnsafe([...submission.resolutions], {
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.postStreamActions.push(action);
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
- this.connection = normalizeConnectionAdapter(resolveTransport({
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;