@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.
- package/dist/esm/chat-client.d.ts +24 -1
- package/dist/esm/chat-client.js +131 -5
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/types.d.ts +84 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/ui/selectors.js +2 -1
- package/dist/esm/ui/selectors.js.map +1 -1
- package/dist/esm/ui/types.d.ts +53 -10
- package/dist/esm/ui.d.ts +1 -1
- package/dist/esm/web-mcp-tools.d.ts +60 -0
- package/dist/esm/web-mcp-tools.js +92 -1
- package/dist/esm/web-mcp-tools.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +198 -4
- package/src/index.ts +15 -1
- package/src/types.ts +122 -6
- package/src/ui/selectors.ts +1 -0
- package/src/ui/types.ts +119 -11
- package/src/ui.ts +7 -0
- package/src/web-mcp-tools.ts +181 -0
|
@@ -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
|
*/
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -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 &&
|
|
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
|
*/
|