@tanstack/ai-client 0.31.1 → 0.32.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.
@@ -144,6 +144,16 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
144
144
  /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
145
145
  private readonly rejoinRunId;
146
146
  private readonly cachesMessages;
147
+ /**
148
+ * Newest-window size from `history.pageSize`. Only set when
149
+ * `persistence === true`.
150
+ */
151
+ private readonly historyPageSize;
152
+ private hasOlderMessages;
153
+ private olderMessagesCursor;
154
+ private readonly knownServerMessageIds;
155
+ private loadOlderMessagesInFlight;
156
+ private historyGeneration;
147
157
  private devtoolsMounted;
148
158
  private readonly callbacksRef;
149
159
  constructor(options: ChatClientOptions<TTools, TContext, TInterrupts>);
@@ -470,6 +480,23 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
470
480
  * Get current messages
471
481
  */
472
482
  getMessages(): Array<UIMessage<TTools>>;
483
+ /**
484
+ * True when the last hydrate or older-page response said more messages exist.
485
+ */
486
+ getHasOlderMessages(): boolean;
487
+ /**
488
+ * Fetch the next older window and put it in front of the painted messages.
489
+ *
490
+ * No-op when there is no older page, no cursor, or a page load is already
491
+ * running. A network failure rejects and leaves the painted messages as they
492
+ * are; {@link getHasOlderMessages} stays true.
493
+ */
494
+ loadOlderMessages(): Promise<void>;
495
+ private resetHistoryPaging;
496
+ private applyHydrationPage;
497
+ private rememberServerMessageIds;
498
+ private rememberProcessorMessageIds;
499
+ private messagesForSend;
473
500
  /**
474
501
  * True when an interrupt (or another direct send) claimed the client during
475
502
  * a drain. Read via a method so cross-await mutations are not constant-folded
@@ -235,6 +235,16 @@ var ChatClient = class {
235
235
  /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
236
236
  rejoinRunId;
237
237
  cachesMessages;
238
+ /**
239
+ * Newest-window size from `history.pageSize`. Only set when
240
+ * `persistence === true`.
241
+ */
242
+ historyPageSize;
243
+ hasOlderMessages = false;
244
+ olderMessagesCursor;
245
+ knownServerMessageIds = /* @__PURE__ */ new Set();
246
+ loadOlderMessagesInFlight = false;
247
+ historyGeneration = 0;
238
248
  devtoolsMounted = false;
239
249
  callbacksRef;
240
250
  constructor(options) {
@@ -245,10 +255,12 @@ var ChatClient = class {
245
255
  if (options.persistence === true) {
246
256
  if (!options.threadId) throw new Error("[TanStack AI] persistence needs a stable `threadId` to key on. Pass a threadId from your app (for example support-42).");
247
257
  cachesMessages = false;
258
+ this.historyPageSize = options.history?.pageSize;
248
259
  } else if (options.persistence) {
249
260
  if (!options.threadId) throw new Error("[TanStack AI] persistence needs a stable `threadId` to key on. Pass a threadId from your app (for example support-42).");
250
261
  this.persistor = new ChatPersistor(options.persistence, options.threadId, (messages) => this.processor.setMessages(messages), (snapshot) => this.applyPersistedResume(snapshot));
251
- }
262
+ this.historyPageSize = void 0;
263
+ } else this.historyPageSize = void 0;
252
264
  this.bodyOption = options.body || {};
253
265
  this.forwardedPropsOption = options.forwardedProps || {};
254
266
  this.byok = options.byok;
@@ -506,16 +518,25 @@ var ChatClient = class {
506
518
  if (!hydrate) return;
507
519
  if (this.isLoading || this.abortController) return;
508
520
  if (this.disposed) return;
521
+ const pageSize = this.historyPageSize;
522
+ const hydrateOptions = pageSize === void 0 ? void 0 : { limit: pageSize };
509
523
  (async () => {
510
524
  let result;
525
+ const generation = this.historyGeneration;
511
526
  try {
512
- result = await hydrate(this.threadId);
527
+ result = await hydrate(this.threadId, hydrateOptions);
513
528
  } catch {
514
529
  return;
515
530
  }
531
+ if (generation !== this.historyGeneration) return;
516
532
  if (this.disposed || !this.tailing) return;
517
533
  if (this.isLoading || this.abortController) return;
518
- if (result.messages.length > 0) this.processor.setMessages(normalizeMessagesDates(result.messages));
534
+ this.applyHydrationPage(result.page);
535
+ if (result.messages.length > 0) {
536
+ const windowMessages = normalizeMessagesDates(result.messages);
537
+ this.processor.setMessages(windowMessages);
538
+ this.rememberServerMessageIds(windowMessages);
539
+ }
519
540
  if (result.interrupts && result.interrupts.pending.length > 0) this.applyResumeSnapshot({
520
541
  resumeState: {
521
542
  threadId: this.threadId,
@@ -1275,7 +1296,7 @@ var ChatClient = class {
1275
1296
  let activeDevtoolsRunId = null;
1276
1297
  let runTerminalEventEmitted = false;
1277
1298
  try {
1278
- const messages = this.processor.getMessages();
1299
+ const messages = this.messagesForSend(this.processor.getMessages());
1279
1300
  const clientTools = new Map(this.clientToolsRef.current);
1280
1301
  const runtimeContext = this.context;
1281
1302
  await this.callbacksRef.current.onResponse();
@@ -1368,6 +1389,7 @@ var ChatClient = class {
1368
1389
  }
1369
1390
  await this.drainPostStreamActions();
1370
1391
  if (streamCompletedSuccessfully) {
1392
+ this.rememberProcessorMessageIds();
1371
1393
  if (this.status !== "ready") this.setStatus("ready");
1372
1394
  if (!this.messageQueueDraining) await this.drainQueue();
1373
1395
  } else this.flushQueue();
@@ -1446,6 +1468,7 @@ var ChatClient = class {
1446
1468
  } else if (this.activeRunIds.size > 0) this.resetSessionGenerating({ preserveClearedStreamTracking: true });
1447
1469
  this.persistor?.beginClear();
1448
1470
  this.processor.clearMessages();
1471
+ this.resetHistoryPaging();
1449
1472
  this.discardPendingSends();
1450
1473
  this.persistor?.remove();
1451
1474
  this.lastResume = null;
@@ -1586,6 +1609,79 @@ var ChatClient = class {
1586
1609
  return this.processor.getMessages();
1587
1610
  }
1588
1611
  /**
1612
+ * True when the last hydrate or older-page response said more messages exist.
1613
+ */
1614
+ getHasOlderMessages() {
1615
+ return this.hasOlderMessages;
1616
+ }
1617
+ /**
1618
+ * Fetch the next older window and put it in front of the painted messages.
1619
+ *
1620
+ * No-op when there is no older page, no cursor, or a page load is already
1621
+ * running. A network failure rejects and leaves the painted messages as they
1622
+ * are; {@link getHasOlderMessages} stays true.
1623
+ */
1624
+ async loadOlderMessages() {
1625
+ const hydrate = this.connection.hydrate;
1626
+ if (hydrate === void 0) return;
1627
+ if (this.disposed) return;
1628
+ if (!this.hasOlderMessages) return;
1629
+ const cursor = this.olderMessagesCursor;
1630
+ if (cursor === void 0) return;
1631
+ if (this.loadOlderMessagesInFlight) return;
1632
+ const hydrateOptions = { before: cursor };
1633
+ if (this.historyPageSize !== void 0) hydrateOptions.limit = this.historyPageSize;
1634
+ this.loadOlderMessagesInFlight = true;
1635
+ const generation = this.historyGeneration;
1636
+ try {
1637
+ const result = await hydrate(this.threadId, hydrateOptions);
1638
+ if (this.disposed) return;
1639
+ if (generation !== this.historyGeneration) return;
1640
+ const olderMessages = normalizeMessagesDates(result.messages);
1641
+ if (olderMessages.length === 0) throw new Error("Older page was empty");
1642
+ this.applyHydrationPage(result.page);
1643
+ this.processor.prependMessages(olderMessages);
1644
+ this.rememberServerMessageIds(olderMessages);
1645
+ } finally {
1646
+ this.loadOlderMessagesInFlight = false;
1647
+ }
1648
+ }
1649
+ resetHistoryPaging() {
1650
+ this.hasOlderMessages = false;
1651
+ this.olderMessagesCursor = void 0;
1652
+ this.knownServerMessageIds.clear();
1653
+ this.historyGeneration++;
1654
+ }
1655
+ applyHydrationPage(page) {
1656
+ if (page?.truncated === true) {
1657
+ this.hasOlderMessages = true;
1658
+ this.olderMessagesCursor = page.cursor;
1659
+ return;
1660
+ }
1661
+ this.hasOlderMessages = false;
1662
+ this.olderMessagesCursor = void 0;
1663
+ }
1664
+ rememberServerMessageIds(messages) {
1665
+ const ids = messages.map((message) => message.id);
1666
+ for (const id of ids) this.knownServerMessageIds.add(id);
1667
+ }
1668
+ rememberProcessorMessageIds() {
1669
+ if (this.historyPageSize === void 0) return;
1670
+ this.rememberServerMessageIds(this.processor.getMessages());
1671
+ }
1672
+ messagesForSend(messages) {
1673
+ if (this.historyPageSize === void 0) return messages;
1674
+ const unknownMessages = [];
1675
+ for (const message of messages) {
1676
+ if (this.knownServerMessageIds.has(message.id)) continue;
1677
+ unknownMessages.push(message);
1678
+ }
1679
+ if (unknownMessages.length > 0) return unknownMessages;
1680
+ const lastUserIndex = messages.findLastIndex((message) => message.role === "user");
1681
+ if (lastUserIndex === -1) return [];
1682
+ return messages.slice(lastUserIndex);
1683
+ }
1684
+ /**
1589
1685
  * True when an interrupt (or another direct send) claimed the client during
1590
1686
  * a drain. Read via a method so cross-await mutations are not constant-folded
1591
1687
  * by control-flow analysis.