@tanstack/ai-client 0.24.0 → 0.25.2

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,13 +1,14 @@
1
- import { AnyClientTool, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
1
+ import { AnyClientTool, InterruptDefinition, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
2
2
  import { ConnectionAdapter } from './connection-adapters.js';
3
- import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterrupt, ChatInterruptState, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
4
- type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>> = {
3
+ import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterruptState, ResolvableChatInterrupt, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
4
+ type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = {
5
5
  connection?: ConnectionAdapter;
6
6
  fetcher?: ChatFetcher;
7
7
  /** @deprecated Use `forwardedProps` instead. */
8
8
  body?: Record<string, any>;
9
9
  forwardedProps?: Record<string, any>;
10
10
  tools?: TTools;
11
+ interrupts?: TInterrupts;
11
12
  queue?: QueueOption;
12
13
  onResponse?: (response?: Response) => void | Promise<void>;
13
14
  onChunk?: (chunk: StreamChunk) => void;
@@ -17,13 +18,15 @@ type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClien
17
18
  onConnectionStatusChange?: (status: ConnectionStatus) => void;
18
19
  onSessionGeneratingChange?: (isGenerating: boolean) => void;
19
20
  onQueueChange?: (queue: Array<QueuedMessage>) => void;
20
- onResumeStateChange?: (resumeState: ChatResumeState | null, pendingInterrupts: BoundInterrupts<TTools>) => void;
21
+ onResumeStateChange?: (resumeState: ChatResumeState | null, pendingInterrupts: BoundInterrupts<TTools, TInterrupts>) => void;
21
22
  /**
22
23
  * Fires whenever the id of the run in flight changes: the new id when a run
23
24
  * starts (including a rejoin), `null` when it settles.
24
25
  */
25
26
  onRunIdChange?: (runId: string | null) => void;
26
- onInterruptStateChange?: (state: ChatInterruptState<TTools>) => void;
27
+ onInterruptStateChange?: (state: ChatInterruptState<TTools, TInterrupts>, context: {
28
+ source: 'hydrate' | 'live';
29
+ }) => void;
27
30
  onCustomEvent?: (eventType: string, data: unknown, context: {
28
31
  toolCallId?: string;
29
32
  }) => void;
@@ -43,7 +46,7 @@ export interface NormalizedQueueConfig {
43
46
  strategy?: QueueStrategy;
44
47
  }
45
48
  export declare function normalizeQueueOption(option: QueueOption | undefined): NormalizedQueueConfig;
46
- export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = unknown> {
49
+ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = unknown, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = any> {
47
50
  private readonly processor;
48
51
  private connection;
49
52
  private uniqueId;
@@ -130,7 +133,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
130
133
  private readonly cachesMessages;
131
134
  private devtoolsMounted;
132
135
  private readonly callbacksRef;
133
- constructor(options: ChatClientOptions<TTools, TContext>);
136
+ constructor(options: ChatClientOptions<TTools, TContext, TInterrupts>);
134
137
  /**
135
138
  * START TAILING: re-attach to an in-flight run so its chunks arrive here.
136
139
  *
@@ -221,12 +224,12 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
221
224
  */
222
225
  getCurrentRunId(): string | null;
223
226
  private setCurrentRunId;
224
- getInterruptState(): ChatInterruptState<TTools>;
225
- getInterrupts(): BoundInterrupts<TTools>;
227
+ getInterruptState(): ChatInterruptState<TTools, TInterrupts>;
228
+ getInterrupts(): BoundInterrupts<TTools, TInterrupts>;
226
229
  /** @deprecated Use getInterrupts(). */
227
- getPendingInterrupts(): BoundInterrupts<TTools>;
230
+ getPendingInterrupts(): BoundInterrupts<TTools, TInterrupts>;
228
231
  resolveInterrupts(approved: boolean): void;
229
- resolveInterrupts(resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void;
232
+ resolveInterrupts(resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
230
233
  cancelInterrupts(): void;
231
234
  retryInterrupts(): void;
232
235
  /** Unsafe low-level resume escape hatch. Prefer bound interrupt methods. */
@@ -5,6 +5,13 @@ import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
5
5
  import { InterruptManager } from "./interrupt-manager.js";
6
6
  import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, normalizeToUIMessage, parseWithStandardSchema } from "@tanstack/ai/client";
7
7
  //#region src/chat-client.ts
8
+ function assertUniqueInterruptDefinitions(interrupts) {
9
+ const ids = /* @__PURE__ */ new Set();
10
+ for (const interrupt of interrupts ?? []) {
11
+ if (ids.has(interrupt.id)) throw new Error(`Duplicate interrupt definition id: ${interrupt.id}`);
12
+ ids.add(interrupt.id);
13
+ }
14
+ }
8
15
  function resolveTransport(transport) {
9
16
  const { connection, fetcher } = transport;
10
17
  if (connection && fetcher) throw new Error("ChatClient: pass either `connection` or `fetcher`, not both.");
@@ -197,6 +204,7 @@ var ChatClient = class {
197
204
  devtoolsMounted = false;
198
205
  callbacksRef;
199
206
  constructor(options) {
207
+ assertUniqueInterruptDefinitions(options.interrupts);
200
208
  this.threadId = options.threadId || "";
201
209
  this.uniqueId = this.threadId;
202
210
  let cachesMessages = true;
@@ -236,8 +244,9 @@ var ChatClient = class {
236
244
  } };
237
245
  this.interruptManager = new InterruptManager({
238
246
  ...options.tools !== void 0 ? { tools: options.tools } : {},
247
+ ...options.interrupts !== void 0 ? { interrupts: options.interrupts } : {},
239
248
  submit: (submission) => this.submitInterruptBatch(submission),
240
- onChange: () => this.notifyResumeStateChange()
249
+ onChange: (source) => this.notifyResumeStateChange(source)
241
250
  });
242
251
  if (options.initialResumeSnapshot) this.applyResumeSnapshot(options.initialResumeSnapshot);
243
252
  const persistedState = this.persistor?.readInitial();
@@ -399,13 +408,13 @@ var ChatClient = class {
399
408
  applyResumeSnapshot(snapshot) {
400
409
  const resumeState = readResumeState(snapshot);
401
410
  if (resumeState === void 0) {
402
- this.interruptManager.reset();
411
+ this.interruptManager.reset({ source: "hydrate" });
403
412
  return;
404
413
  }
405
414
  this.lastResume = resumeState;
406
415
  const pendingInterrupts = Array.isArray(snapshot.pendingInterrupts) ? snapshot.pendingInterrupts : [];
407
416
  if (pendingInterrupts.length === 0) {
408
- this.interruptManager.reset();
417
+ this.interruptManager.reset({ source: "hydrate" });
409
418
  return;
410
419
  }
411
420
  const generation = this.interruptGeneration(pendingInterrupts);
@@ -414,7 +423,7 @@ var ChatClient = class {
414
423
  interruptedRunId: resumeState.runId,
415
424
  generation,
416
425
  interrupts: pendingInterrupts
417
- });
426
+ }, "hydrate");
418
427
  }
419
428
  /**
420
429
  * Apply a resume snapshot read from durable storage. Restores interrupt state,
@@ -556,7 +565,7 @@ var ChatClient = class {
556
565
  interruptedRunId,
557
566
  generation: this.interruptGeneration(chunk.outcome.interrupts),
558
567
  interrupts: chunk.outcome.interrupts
559
- });
568
+ }, "live");
560
569
  return;
561
570
  }
562
571
  const isRunlessSessionError = chunk.type === "RUN_ERROR" && !runId;
@@ -571,7 +580,7 @@ var ChatClient = class {
571
580
  this.interruptManager.reset();
572
581
  return;
573
582
  }
574
- this.notifyResumeStateChange();
583
+ this.notifyResumeStateChange("live");
575
584
  }
576
585
  /**
577
586
  * The interrupt-resume state for the active/interrupted run (its run/thread
@@ -708,11 +717,12 @@ var ChatClient = class {
708
717
  this.callbacksRef.current.onSessionGeneratingChange(isGenerating);
709
718
  this.devtoolsBridge.emitSnapshot();
710
719
  }
711
- notifyResumeStateChange() {
720
+ notifyResumeStateChange(source) {
712
721
  const resumeState = this.getResumeState();
722
+ const interruptState = this.interruptManager.getState();
713
723
  this.persistResumeSnapshot(resumeState);
714
- this.callbacksRef.current.onResumeStateChange(resumeState, this.interruptManager.getInterrupts());
715
- this.callbacksRef.current.onInterruptStateChange(this.interruptManager.getState());
724
+ this.callbacksRef.current.onResumeStateChange(resumeState, interruptState.interrupts);
725
+ this.callbacksRef.current.onInterruptStateChange(interruptState, { source });
716
726
  }
717
727
  /**
718
728
  * Build the durable resume snapshot from the current resume state + pending
@@ -901,6 +911,7 @@ var ChatClient = class {
901
911
  }
902
912
  await this.processIncomingChunk(chunk, { defer: false });
903
913
  }
914
+ if (this.pendingToolExecutions.size > 0) await Promise.all(this.pendingToolExecutions.values());
904
915
  } catch (error) {
905
916
  const isAbort = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
906
917
  if (!attached && !isAbort) refused = true;
@@ -915,6 +926,7 @@ var ChatClient = class {
915
926
  this.abortController = null;
916
927
  this.setIsLoading(false);
917
928
  if (this.status === "streaming") this.setStatus("ready");
929
+ await this.drainPostStreamActions();
918
930
  }
919
931
  }
920
932
  })();
@@ -947,7 +959,7 @@ var ChatClient = class {
947
959
  this.processor.processChunk(chunk);
948
960
  this.updateRunLifecycle(chunk);
949
961
  this.observeInterruptState(chunk);
950
- if (options?.defer !== false) await new Promise((resolve) => setTimeout(resolve, 0));
962
+ if (options?.defer !== false && (typeof document === "undefined" || !document.hidden)) await new Promise((resolve) => setTimeout(resolve, 0));
951
963
  this.resolveJoinedRun(chunk);
952
964
  }
953
965
  isActiveInterruptSubmissionFailure(chunk) {
@@ -1484,6 +1496,9 @@ var ChatClient = class {
1484
1496
  * a text-only response has nothing to auto-send.
1485
1497
  */
1486
1498
  shouldAutoSend() {
1499
+ if (this.lastResume) return false;
1500
+ if (this.activeInterruptSubmission && this.hasPendingInterrupts()) return false;
1501
+ if (this.interruptManager.getInterrupts().length > 0) return false;
1487
1502
  const lastAssistant = this.processor.getMessages().findLast((m) => m.role === "assistant");
1488
1503
  if (!lastAssistant) return false;
1489
1504
  if (!lastAssistant.parts.some((p) => p.type === "tool-call")) return false;