@tanstack/ai-client 0.24.0 → 0.25.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 +14 -11
- package/dist/esm/chat-client.js +24 -9
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +3 -1
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/interrupt-manager.d.ts +15 -10
- package/dist/esm/interrupt-manager.js +176 -83
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/types.d.ts +40 -13
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/chat-client.ts +108 -37
- package/src/connection-adapters.ts +14 -2
- package/src/index.ts +3 -0
- package/src/interrupt-manager.ts +386 -145
- package/src/types.ts +99 -11
|
@@ -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,
|
|
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
|
|
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:
|
|
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. */
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -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,
|
|
715
|
-
this.callbacksRef.current.onInterruptStateChange(
|
|
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
|
})();
|
|
@@ -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;
|