@tanstack/ai-client 0.34.0 → 0.36.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.
- package/README.md +1 -0
- package/dist/esm/chat-client.d.ts +18 -2
- package/dist/esm/chat-client.js +69 -16
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/interrupt-manager.d.ts +3 -0
- package/dist/esm/interrupt-manager.js +62 -5
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/types.d.ts +3 -1
- package/dist/esm/types.js.map +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 +5 -5
- package/src/chat-client.ts +134 -38
- package/src/index.ts +8 -1
- package/src/interrupt-manager.ts +91 -6
- package/src/types.ts +3 -0
- package/src/web-mcp-tools.ts +181 -0
package/README.md
CHANGED
|
@@ -240,6 +240,7 @@ Official adapters include:
|
|
|
240
240
|
| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
|
|
241
241
|
| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
|
|
242
242
|
| [`@tanstack/ai-reactor`](https://tanstack.com/ai/latest/docs/adapters/reactor) | Reactor live world and video generation (Orbis, Happy Oyster, LingBot, Helios, FastH3) |
|
|
243
|
+
| [`@tanstack/ai-worldlabs`](https://tanstack.com/ai/latest/docs/adapters/worldlabs) | World Labs Marble persistent 3D world generation |
|
|
243
244
|
| [`@tanstack/ai-cloudflare`](https://tanstack.com/ai/latest/docs/adapters/cloudflare) | Cloudflare Workers AI chat, embeddings, image, speech, transcription, and AI Gateway |
|
|
244
245
|
|
|
245
246
|
The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
|
|
@@ -143,6 +143,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
143
143
|
private readonly activeRunIds;
|
|
144
144
|
/** Latched by `dispose()`; stops any late async callback starting new work. */
|
|
145
145
|
private disposed;
|
|
146
|
+
/** The error a failed mount hydration set, cleared by the next successful one. */
|
|
147
|
+
private hydrationError;
|
|
146
148
|
/** Whether a view is currently watching. See `attach` / `detach`. */
|
|
147
149
|
private tailing;
|
|
148
150
|
/** Constructor inputs `attach()` needs on every re-attach, not just the first. */
|
|
@@ -219,6 +221,19 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
219
221
|
* send that starts first owns the client (hydration then backs off).
|
|
220
222
|
*/
|
|
221
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;
|
|
222
237
|
mountDevtools(): void;
|
|
223
238
|
private ensureThreadId;
|
|
224
239
|
/**
|
|
@@ -295,7 +310,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
295
310
|
*/
|
|
296
311
|
private startSubscription;
|
|
297
312
|
/**
|
|
298
|
-
* Consume chunks from the connection subscription.
|
|
313
|
+
* Consume chunks from the connection subscription. Chunks are processed in
|
|
314
|
+
* order; the loop yields to the host after each processing budget.
|
|
299
315
|
*/
|
|
300
316
|
private consumeSubscription;
|
|
301
317
|
/**
|
|
@@ -317,7 +333,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
317
333
|
* give up after {@link REJOIN_CONNECT_DEADLINE_MS} if no chunk arrives and
|
|
318
334
|
* clear the dead pointer so it does not retry on the next load.
|
|
319
335
|
*
|
|
320
|
-
* Replay chunks are processed WITHOUT the
|
|
336
|
+
* Replay chunks are processed WITHOUT the time-slice yield the live path uses,
|
|
321
337
|
* so the buffered prefix snaps in and only the genuinely-live tail streams at
|
|
322
338
|
* network speed — a reload looks like the run continued, not like it re-typed.
|
|
323
339
|
*/
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -6,9 +6,15 @@ 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 { EventType, StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage,
|
|
9
|
+
import { EventType, StreamProcessor, cloneAndDeepFreezeJson, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, restoreInboundChunk, tanstackMetadata, validateWithStandardSchema } from "@tanstack/ai/client";
|
|
10
10
|
import { ByokBlockedError, ByokMissingError, ByokUnresolvedProviderError } from "@tanstack/ai/byok";
|
|
11
11
|
//#region src/chat-client.ts
|
|
12
|
+
var STREAM_PROCESSING_BUDGET_MS = 8;
|
|
13
|
+
function yieldToHost() {
|
|
14
|
+
const { scheduler } = globalThis;
|
|
15
|
+
if (scheduler?.yield) return scheduler.yield();
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
17
|
+
}
|
|
12
18
|
function assertUniqueInterruptDefinitions(interrupts) {
|
|
13
19
|
const ids = /* @__PURE__ */ new Set();
|
|
14
20
|
for (const interrupt of interrupts ?? []) {
|
|
@@ -138,10 +144,16 @@ var REJOIN_CONNECT_DEADLINE_MS = 2e3;
|
|
|
138
144
|
var REJOIN_REBUILD_TRIGGERS = /* @__PURE__ */ new Set([
|
|
139
145
|
"TEXT_MESSAGE_START",
|
|
140
146
|
"TEXT_MESSAGE_CONTENT",
|
|
147
|
+
"REASONING_MESSAGE_CONTENT",
|
|
141
148
|
"TOOL_CALL_START",
|
|
142
149
|
"MESSAGES_SNAPSHOT",
|
|
143
150
|
"SUBAGENT_STARTED"
|
|
144
151
|
]);
|
|
152
|
+
function rebuildsAssistantMessage(chunk) {
|
|
153
|
+
if (chunk.type === "REASONING_ENCRYPTED_VALUE") return chunk.subtype === "message" && typeof chunk.encryptedValue === "string" && chunk.encryptedValue.length > 0;
|
|
154
|
+
if (chunk.type === "STEP_FINISHED") return "signature" in chunk && Boolean(chunk.signature);
|
|
155
|
+
return REJOIN_REBUILD_TRIGGERS.has(chunk.type);
|
|
156
|
+
}
|
|
145
157
|
/** Every subagent card in the messages, nested cards included. */
|
|
146
158
|
function collectSubagentParts(messages) {
|
|
147
159
|
const cards = [];
|
|
@@ -258,6 +270,8 @@ var ChatClient = class {
|
|
|
258
270
|
activeRunIds = /* @__PURE__ */ new Set();
|
|
259
271
|
/** Latched by `dispose()`; stops any late async callback starting new work. */
|
|
260
272
|
disposed = false;
|
|
273
|
+
/** The error a failed mount hydration set, cleared by the next successful one. */
|
|
274
|
+
hydrationError;
|
|
261
275
|
/** Whether a view is currently watching. See `attach` / `detach`. */
|
|
262
276
|
tailing = false;
|
|
263
277
|
/** Constructor inputs `attach()` needs on every re-attach, not just the first. */
|
|
@@ -556,19 +570,25 @@ var ChatClient = class {
|
|
|
556
570
|
const generation = this.historyGeneration;
|
|
557
571
|
try {
|
|
558
572
|
result = await hydrate(this.threadId, hydrateOptions);
|
|
559
|
-
} catch {
|
|
573
|
+
} catch (cause) {
|
|
574
|
+
if (generation === this.historyGeneration) this.failHydration(cause);
|
|
560
575
|
return;
|
|
561
576
|
}
|
|
562
577
|
if (generation !== this.historyGeneration) return;
|
|
563
578
|
if (this.disposed || !this.tailing) return;
|
|
564
579
|
if (this.isLoading || this.abortController) return;
|
|
580
|
+
if (this.hydrationError && this.error === this.hydrationError) {
|
|
581
|
+
this.setError(void 0);
|
|
582
|
+
this.setStatus("ready");
|
|
583
|
+
}
|
|
584
|
+
this.hydrationError = void 0;
|
|
565
585
|
this.applyHydrationPage(result.page);
|
|
566
586
|
if (result.messages.length > 0) {
|
|
567
587
|
const windowMessages = normalizeMessagesDates(result.messages);
|
|
568
588
|
this.processor.setMessages(windowMessages);
|
|
569
589
|
this.rememberServerMessageIds(windowMessages);
|
|
570
590
|
}
|
|
571
|
-
if (result.interrupts && result.interrupts.pending.length > 0) this.applyResumeSnapshot({
|
|
591
|
+
if (result.interrupts && result.interrupts.pending.length > 0 && (!result.activeRun?.runId || result.activeRun.runId === result.interrupts.runId)) this.applyResumeSnapshot({
|
|
572
592
|
resumeState: {
|
|
573
593
|
threadId: this.threadId,
|
|
574
594
|
runId: result.interrupts.runId
|
|
@@ -578,6 +598,29 @@ var ChatClient = class {
|
|
|
578
598
|
else if (result.activeRun?.runId) this.maybeRejoinInFlight(result.activeRun.runId);
|
|
579
599
|
})();
|
|
580
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Surface a mount-hydration failure (`persistence: true`) on the observable
|
|
603
|
+
* fields, mirroring `GenerationClient.failHydration`, so "this thread failed
|
|
604
|
+
* to load" is distinguishable from "this thread has no messages" and the app
|
|
605
|
+
* can show an error / offer a retry. A genuine miss — the server having no
|
|
606
|
+
* record for a fresh thread — resolves normally and never reaches here; only a
|
|
607
|
+
* thrown transport / authorize-gate error does.
|
|
608
|
+
*
|
|
609
|
+
* Skipped when the view unmounted (`!tailing`) or a `sendMessage` took
|
|
610
|
+
* ownership while the hydrate GET was in flight, so a live run's state always
|
|
611
|
+
* wins over a stale mount-time failure — same guard as the success path above.
|
|
612
|
+
*/
|
|
613
|
+
failHydration(cause) {
|
|
614
|
+
if (this.disposed || !this.tailing) return;
|
|
615
|
+
if (this.isLoading || this.abortController) return;
|
|
616
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
617
|
+
if (error instanceof ByokMissingError) this.byok?.request(error.provider, "missing");
|
|
618
|
+
if (error instanceof ByokBlockedError && error.reason === "locked") this.byok?.request(error.provider, "locked");
|
|
619
|
+
this.hydrationError = error;
|
|
620
|
+
this.setStatus("error");
|
|
621
|
+
this.setError(error);
|
|
622
|
+
this.callbacksRef.current.onError(error);
|
|
623
|
+
}
|
|
581
624
|
mountDevtools() {
|
|
582
625
|
this.ensureThreadId();
|
|
583
626
|
if (this.devtoolsMounted) return;
|
|
@@ -974,13 +1017,21 @@ var ChatClient = class {
|
|
|
974
1017
|
});
|
|
975
1018
|
}
|
|
976
1019
|
/**
|
|
977
|
-
* Consume chunks from the connection subscription.
|
|
1020
|
+
* Consume chunks from the connection subscription. Chunks are processed in
|
|
1021
|
+
* order; the loop yields to the host after each processing budget.
|
|
978
1022
|
*/
|
|
979
1023
|
async consumeSubscription(signal) {
|
|
980
1024
|
const stream = this.connection.subscribe(signal);
|
|
1025
|
+
let chunkProcessingTime = 0;
|
|
981
1026
|
for await (const chunk of stream) {
|
|
982
1027
|
if (signal.aborted) break;
|
|
983
|
-
|
|
1028
|
+
const startedAt = performance.now();
|
|
1029
|
+
this.processIncomingChunk(chunk);
|
|
1030
|
+
chunkProcessingTime += performance.now() - startedAt;
|
|
1031
|
+
if (chunkProcessingTime < STREAM_PROCESSING_BUDGET_MS) continue;
|
|
1032
|
+
chunkProcessingTime = 0;
|
|
1033
|
+
if (typeof document !== "undefined" && document.hidden) continue;
|
|
1034
|
+
await yieldToHost();
|
|
984
1035
|
}
|
|
985
1036
|
}
|
|
986
1037
|
/**
|
|
@@ -1002,7 +1053,7 @@ var ChatClient = class {
|
|
|
1002
1053
|
* give up after {@link REJOIN_CONNECT_DEADLINE_MS} if no chunk arrives and
|
|
1003
1054
|
* clear the dead pointer so it does not retry on the next load.
|
|
1004
1055
|
*
|
|
1005
|
-
* Replay chunks are processed WITHOUT the
|
|
1056
|
+
* Replay chunks are processed WITHOUT the time-slice yield the live path uses,
|
|
1006
1057
|
* so the buffered prefix snaps in and only the genuinely-live tail streams at
|
|
1007
1058
|
* network speed — a reload looks like the run continued, not like it re-typed.
|
|
1008
1059
|
*/
|
|
@@ -1033,11 +1084,11 @@ var ChatClient = class {
|
|
|
1033
1084
|
attached = true;
|
|
1034
1085
|
clearTimeout(connectTimer);
|
|
1035
1086
|
}
|
|
1036
|
-
if (!rebuilt &&
|
|
1087
|
+
if (!rebuilt && rebuildsAssistantMessage(chunk)) {
|
|
1037
1088
|
rebuilt = true;
|
|
1038
1089
|
this.dropTrailingInFlightAssistant();
|
|
1039
1090
|
}
|
|
1040
|
-
|
|
1091
|
+
this.processIncomingChunk(chunk);
|
|
1041
1092
|
}
|
|
1042
1093
|
if (this.pendingToolExecutions.size > 0) await Promise.all(this.pendingToolExecutions.values());
|
|
1043
1094
|
} catch (error) {
|
|
@@ -1070,7 +1121,7 @@ var ChatClient = class {
|
|
|
1070
1121
|
const last = messages[messages.length - 1];
|
|
1071
1122
|
if (last && last.role === "assistant") this.processor.setMessages(messages.slice(0, -1));
|
|
1072
1123
|
}
|
|
1073
|
-
|
|
1124
|
+
processIncomingChunk(chunk) {
|
|
1074
1125
|
chunk = restoreInboundChunk(chunk);
|
|
1075
1126
|
if (chunk.type === "RUN_ERROR" && this.isActiveInterruptSubmissionFailure(chunk)) {
|
|
1076
1127
|
const interruptErrors = tanstackMetadata(chunk)?.interruptErrors;
|
|
@@ -1099,7 +1150,6 @@ var ChatClient = class {
|
|
|
1099
1150
|
this.syncSubagentHandles();
|
|
1100
1151
|
this.updateRunLifecycle(chunk);
|
|
1101
1152
|
this.observeInterruptState(chunk);
|
|
1102
|
-
if (options?.defer !== false && (typeof document === "undefined" || !document.hidden)) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1103
1153
|
this.resolveJoinedRun(chunk);
|
|
1104
1154
|
}
|
|
1105
1155
|
isActiveInterruptSubmissionFailure(chunk) {
|
|
@@ -1554,10 +1604,12 @@ var ChatClient = class {
|
|
|
1554
1604
|
await this.addToolResultForClientTool(result, clientTool, this.streamContinuationGeneration);
|
|
1555
1605
|
}
|
|
1556
1606
|
async addToolResultForClientTool(result, clientTool, continuationGeneration, context) {
|
|
1557
|
-
if (
|
|
1607
|
+
if (result.state !== "output-error") try {
|
|
1608
|
+
let output = clientTool?.outputSchema && isStandardSchema(clientTool.outputSchema) ? await this.validateClientToolOutput(clientTool, result.output) : result.output;
|
|
1609
|
+
if (this.interruptManager.getDescriptors().some((interrupt) => interrupt.toolCallId === result.toolCallId)) output = cloneAndDeepFreezeJson(output);
|
|
1558
1610
|
result = {
|
|
1559
1611
|
...result,
|
|
1560
|
-
output
|
|
1612
|
+
output
|
|
1561
1613
|
};
|
|
1562
1614
|
} catch (error) {
|
|
1563
1615
|
result = {
|
|
@@ -1571,16 +1623,17 @@ var ChatClient = class {
|
|
|
1571
1623
|
if (continuationGeneration !== this.continuationGeneration) return;
|
|
1572
1624
|
this.processor.addToolResult(result.toolCallId, result.output, result.state === "output-error" ? result.errorText || "Tool execution failed" : void 0);
|
|
1573
1625
|
this.devtoolsBridge.emitSnapshot();
|
|
1574
|
-
if (
|
|
1626
|
+
if (result.state === "output-error" ? this.interruptManager.resolveClientToolError(result.toolCallId, result.errorText || "Tool execution failed") : this.interruptManager.resolveClientToolOutput(result.toolCallId, result.output)) return;
|
|
1575
1627
|
if (this.isLoading) {
|
|
1576
1628
|
this.queuePostStreamAction(() => continuationGeneration === this.continuationGeneration ? this.checkForContinuation() : Promise.resolve());
|
|
1577
1629
|
return;
|
|
1578
1630
|
}
|
|
1579
1631
|
await this.checkForContinuation();
|
|
1580
1632
|
}
|
|
1581
|
-
validateClientToolOutput(clientTool, output) {
|
|
1582
|
-
|
|
1583
|
-
|
|
1633
|
+
async validateClientToolOutput(clientTool, output) {
|
|
1634
|
+
const validation = await validateWithStandardSchema(clientTool.outputSchema, output);
|
|
1635
|
+
if (!validation.success) throw new Error(validation.issues.map((issue) => issue.message).join(", "));
|
|
1636
|
+
return validation.data;
|
|
1584
1637
|
}
|
|
1585
1638
|
/**
|
|
1586
1639
|
* Respond to a tool approval request
|