@tanstack/ai-client 0.33.2 → 0.35.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.
@@ -1,7 +1,7 @@
1
1
  import { AnyClientTool, InterruptDefinition, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
2
2
  import { ByokClient } from './byok.js';
3
3
  import { ConnectionAdapter } from './connection-adapters.js';
4
- import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterruptState, ResolvableChatInterrupt, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
4
+ import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterruptState, ResolvableChatInterrupt, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, SubagentHandle, UIMessage, WhenBusy } from './types.js';
5
5
  type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = {
6
6
  connection?: ConnectionAdapter;
7
7
  fetcher?: ChatFetcher;
@@ -51,6 +51,8 @@ export interface NormalizedQueueConfig {
51
51
  export declare function normalizeQueueOption(option: QueueOption | undefined): NormalizedQueueConfig;
52
52
  export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = unknown, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = any> {
53
53
  private readonly processor;
54
+ private readonly subagentHandles;
55
+ private readonly stoppedSubagentIds;
54
56
  private connection;
55
57
  private uniqueId;
56
58
  private threadId;
@@ -141,6 +143,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
141
143
  private readonly activeRunIds;
142
144
  /** Latched by `dispose()`; stops any late async callback starting new work. */
143
145
  private disposed;
146
+ /** The error a failed mount hydration set, cleared by the next successful one. */
147
+ private hydrationError;
144
148
  /** Whether a view is currently watching. See `attach` / `detach`. */
145
149
  private tailing;
146
150
  /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
@@ -217,6 +221,19 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
217
221
  * send that starts first owns the client (hydration then backs off).
218
222
  */
219
223
  private hydrateFromServer;
224
+ /**
225
+ * Surface a mount-hydration failure (`persistence: true`) on the observable
226
+ * fields, mirroring `GenerationClient.failHydration`, so "this thread failed
227
+ * to load" is distinguishable from "this thread has no messages" and the app
228
+ * can show an error / offer a retry. A genuine miss — the server having no
229
+ * record for a fresh thread — resolves normally and never reaches here; only a
230
+ * thrown transport / authorize-gate error does.
231
+ *
232
+ * Skipped when the view unmounted (`!tailing`) or a `sendMessage` took
233
+ * ownership while the hydrate GET was in flight, so a live run's state always
234
+ * wins over a stale mount-time failure — same guard as the success path above.
235
+ */
236
+ private failHydration;
220
237
  mountDevtools(): void;
221
238
  private ensureThreadId;
222
239
  /**
@@ -484,6 +501,12 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
484
501
  * Get current messages
485
502
  */
486
503
  getMessages(): Array<UIMessage<TTools>>;
504
+ getSubagents(): SubagentHandle[];
505
+ private devtoolsSnapshotQueued;
506
+ /** One devtools snapshot per microtask, however many chunks arrive. */
507
+ private queueDevtoolsSnapshot;
508
+ private syncSubagentHandles;
509
+ private stopSubagent;
487
510
  /**
488
511
  * True when the last hydrate or older-page response said more messages exist.
489
512
  */
@@ -6,7 +6,7 @@ import { fetcherToConnectionAdapter, getChunkRunId as getChunkRunId$1, normalize
6
6
  import { ChatPersistor } from "./client-persistor.js";
7
7
  import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
8
8
  import { InterruptManager } from "./interrupt-manager.js";
9
- import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
9
+ import { EventType, StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
10
10
  import { ByokBlockedError, ByokMissingError, ByokUnresolvedProviderError } from "@tanstack/ai/byok";
11
11
  //#region src/chat-client.ts
12
12
  function assertUniqueInterruptDefinitions(interrupts) {
@@ -138,12 +138,42 @@ var REJOIN_CONNECT_DEADLINE_MS = 2e3;
138
138
  var REJOIN_REBUILD_TRIGGERS = /* @__PURE__ */ new Set([
139
139
  "TEXT_MESSAGE_START",
140
140
  "TEXT_MESSAGE_CONTENT",
141
+ "REASONING_MESSAGE_CONTENT",
141
142
  "TOOL_CALL_START",
142
- "MESSAGES_SNAPSHOT"
143
+ "MESSAGES_SNAPSHOT",
144
+ "SUBAGENT_STARTED"
143
145
  ]);
146
+ function rebuildsAssistantMessage(chunk) {
147
+ if (chunk.type === "REASONING_ENCRYPTED_VALUE") return chunk.subtype === "message" && typeof chunk.encryptedValue === "string" && chunk.encryptedValue.length > 0;
148
+ if (chunk.type === "STEP_FINISHED") return "signature" in chunk && Boolean(chunk.signature);
149
+ return REJOIN_REBUILD_TRIGGERS.has(chunk.type);
150
+ }
151
+ /** Every subagent card in the messages, nested cards included. */
152
+ function collectSubagentParts(messages) {
153
+ const cards = [];
154
+ for (const message of messages) for (const part of message.parts) {
155
+ if (part.type !== "subagent") continue;
156
+ cards.push(part);
157
+ cards.push(...collectSubagentParts(part.subagent.messages));
158
+ }
159
+ return cards;
160
+ }
161
+ function readSubagentRunId(chunk) {
162
+ if ("subagentRunId" in chunk && typeof chunk.subagentRunId === "string") return chunk.subagentRunId;
163
+ }
144
164
  var MAX_RUN_LINEAGE_DEPTH = 64;
165
+ function copyForDevtools(value) {
166
+ if (Array.isArray(value)) return value.map(copyForDevtools);
167
+ if (value instanceof Date) return new Date(value.getTime());
168
+ if (value === null || typeof value !== "object") return value;
169
+ const out = {};
170
+ for (const [key, item] of Object.entries(value)) if (typeof item !== "function") out[key] = copyForDevtools(item);
171
+ return out;
172
+ }
145
173
  var ChatClient = class {
146
174
  processor;
175
+ subagentHandles = /* @__PURE__ */ new Map();
176
+ stoppedSubagentIds = /* @__PURE__ */ new Set();
147
177
  connection;
148
178
  uniqueId;
149
179
  threadId;
@@ -234,6 +264,8 @@ var ChatClient = class {
234
264
  activeRunIds = /* @__PURE__ */ new Set();
235
265
  /** Latched by `dispose()`; stops any late async callback starting new work. */
236
266
  disposed = false;
267
+ /** The error a failed mount hydration set, cleared by the next successful one. */
268
+ hydrationError;
237
269
  /** Whether a view is currently watching. See `attach` / `detach`. */
238
270
  tailing = false;
239
271
  /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
@@ -321,8 +353,10 @@ var ChatClient = class {
321
353
  ...initialMessages ? { initialMessages } : {},
322
354
  events: {
323
355
  onMessagesChange: (messages) => {
356
+ this.syncSubagentHandles();
324
357
  this.persistor?.notifyMessagesChanged(messages);
325
358
  this.callbacksRef.current.onMessagesChange(messages);
359
+ this.queueDevtoolsSnapshot();
326
360
  },
327
361
  onStreamStart: () => {
328
362
  this.setStatus("streaming");
@@ -414,6 +448,7 @@ var ChatClient = class {
414
448
  }
415
449
  }
416
450
  });
451
+ this.syncSubagentHandles();
417
452
  this.persistor?.hydrateAsync(persistedState);
418
453
  this.rejoinRunId = rejoinRunId;
419
454
  this.cachesMessages = cachesMessages;
@@ -529,12 +564,18 @@ var ChatClient = class {
529
564
  const generation = this.historyGeneration;
530
565
  try {
531
566
  result = await hydrate(this.threadId, hydrateOptions);
532
- } catch {
567
+ } catch (cause) {
568
+ if (generation === this.historyGeneration) this.failHydration(cause);
533
569
  return;
534
570
  }
535
571
  if (generation !== this.historyGeneration) return;
536
572
  if (this.disposed || !this.tailing) return;
537
573
  if (this.isLoading || this.abortController) return;
574
+ if (this.hydrationError && this.error === this.hydrationError) {
575
+ this.setError(void 0);
576
+ this.setStatus("ready");
577
+ }
578
+ this.hydrationError = void 0;
538
579
  this.applyHydrationPage(result.page);
539
580
  if (result.messages.length > 0) {
540
581
  const windowMessages = normalizeMessagesDates(result.messages);
@@ -551,6 +592,29 @@ var ChatClient = class {
551
592
  else if (result.activeRun?.runId) this.maybeRejoinInFlight(result.activeRun.runId);
552
593
  })();
553
594
  }
595
+ /**
596
+ * Surface a mount-hydration failure (`persistence: true`) on the observable
597
+ * fields, mirroring `GenerationClient.failHydration`, so "this thread failed
598
+ * to load" is distinguishable from "this thread has no messages" and the app
599
+ * can show an error / offer a retry. A genuine miss — the server having no
600
+ * record for a fresh thread — resolves normally and never reaches here; only a
601
+ * thrown transport / authorize-gate error does.
602
+ *
603
+ * Skipped when the view unmounted (`!tailing`) or a `sendMessage` took
604
+ * ownership while the hydrate GET was in flight, so a live run's state always
605
+ * wins over a stale mount-time failure — same guard as the success path above.
606
+ */
607
+ failHydration(cause) {
608
+ if (this.disposed || !this.tailing) return;
609
+ if (this.isLoading || this.abortController) return;
610
+ const error = cause instanceof Error ? cause : new Error(String(cause));
611
+ if (error instanceof ByokMissingError) this.byok?.request(error.provider, "missing");
612
+ if (error instanceof ByokBlockedError && error.reason === "locked") this.byok?.request(error.provider, "locked");
613
+ this.hydrationError = error;
614
+ this.setStatus("error");
615
+ this.setError(error);
616
+ this.callbacksRef.current.onError(error);
617
+ }
554
618
  mountDevtools() {
555
619
  this.ensureThreadId();
556
620
  if (this.devtoolsMounted) return;
@@ -882,7 +946,7 @@ var ChatClient = class {
882
946
  }
883
947
  getDevtoolsSnapshot() {
884
948
  return {
885
- messages: this.processor.getMessages(),
949
+ messages: copyForDevtools(this.processor.getMessages()),
886
950
  status: this.status,
887
951
  isLoading: this.isLoading,
888
952
  isSubscribed: this.isSubscribed,
@@ -1006,7 +1070,7 @@ var ChatClient = class {
1006
1070
  attached = true;
1007
1071
  clearTimeout(connectTimer);
1008
1072
  }
1009
- if (!rebuilt && REJOIN_REBUILD_TRIGGERS.has(chunk.type)) {
1073
+ if (!rebuilt && rebuildsAssistantMessage(chunk)) {
1010
1074
  rebuilt = true;
1011
1075
  this.dropTrailingInFlightAssistant();
1012
1076
  }
@@ -1061,7 +1125,15 @@ var ChatClient = class {
1061
1125
  }
1062
1126
  this.callbacksRef.current.onChunk(chunk);
1063
1127
  this.devtoolsBridge.observeChunk(chunk);
1128
+ const attributedId = readSubagentRunId(chunk);
1129
+ if (attributedId && chunk.type === EventType.SUBAGENT_STARTED) this.stoppedSubagentIds.delete(attributedId);
1130
+ if (attributedId && this.stoppedSubagentIds.has(attributedId)) {
1131
+ this.updateRunLifecycle(chunk);
1132
+ this.resolveJoinedRun(chunk);
1133
+ return;
1134
+ }
1064
1135
  this.processor.processChunk(chunk);
1136
+ this.syncSubagentHandles();
1065
1137
  this.updateRunLifecycle(chunk);
1066
1138
  this.observeInterruptState(chunk);
1067
1139
  if (options?.defer !== false && (typeof document === "undefined" || !document.hidden)) await new Promise((resolve) => setTimeout(resolve, 0));
@@ -1502,6 +1574,7 @@ var ChatClient = class {
1502
1574
  this.resetHistoryPaging();
1503
1575
  this.discardPendingSends();
1504
1576
  this.persistor?.remove();
1577
+ this.stoppedSubagentIds.clear();
1505
1578
  this.lastResume = null;
1506
1579
  this.interruptManager.reset();
1507
1580
  this.pendingResumeThreadId = null;
@@ -1639,6 +1712,59 @@ var ChatClient = class {
1639
1712
  getMessages() {
1640
1713
  return this.processor.getMessages();
1641
1714
  }
1715
+ getSubagents() {
1716
+ return [...this.subagentHandles.values()];
1717
+ }
1718
+ devtoolsSnapshotQueued = false;
1719
+ /** One devtools snapshot per microtask, however many chunks arrive. */
1720
+ queueDevtoolsSnapshot() {
1721
+ if (this.devtoolsSnapshotQueued) return;
1722
+ this.devtoolsSnapshotQueued = true;
1723
+ queueMicrotask(() => {
1724
+ this.devtoolsSnapshotQueued = false;
1725
+ this.devtoolsBridge.emitSnapshot();
1726
+ });
1727
+ }
1728
+ syncSubagentHandles() {
1729
+ const cards = collectSubagentParts(this.processor.getMessages());
1730
+ const present = new Set(cards.map((part) => part.subagent.id));
1731
+ for (const id of this.subagentHandles.keys()) if (!present.has(id)) this.subagentHandles.delete(id);
1732
+ for (const part of cards) {
1733
+ const id = part.subagent.id;
1734
+ const existing = this.subagentHandles.get(id);
1735
+ if (existing) {
1736
+ if (part.subagent === existing) continue;
1737
+ const { stop } = existing;
1738
+ for (const key of Object.keys(existing)) Reflect.deleteProperty(existing, key);
1739
+ Object.assign(existing, part.subagent, { stop });
1740
+ part.subagent = existing;
1741
+ continue;
1742
+ }
1743
+ const handle = {
1744
+ ...part.subagent,
1745
+ stop: () => {
1746
+ this.stopSubagent(id);
1747
+ }
1748
+ };
1749
+ this.subagentHandles.set(id, handle);
1750
+ part.subagent = handle;
1751
+ }
1752
+ }
1753
+ stopSubagent(id) {
1754
+ const handle = this.subagentHandles.get(id);
1755
+ if (!handle || handle.status !== "running") return;
1756
+ this.stoppedSubagentIds.add(id);
1757
+ this.processor.processChunk({
1758
+ type: EventType.SUBAGENT_ERROR,
1759
+ subagentRunId: id,
1760
+ message: "Stopped",
1761
+ timestamp: Date.now()
1762
+ });
1763
+ this.syncSubagentHandles();
1764
+ handle.status = "error";
1765
+ handle.error = { message: "Stopped" };
1766
+ this.abortController?.abort();
1767
+ }
1642
1768
  /**
1643
1769
  * True when the last hydrate or older-page response said more messages exist.
1644
1770
  */