@tanstack/ai-client 0.35.1 → 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 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,
@@ -310,7 +310,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
310
310
  */
311
311
  private startSubscription;
312
312
  /**
313
- * 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.
314
315
  */
315
316
  private consumeSubscription;
316
317
  /**
@@ -332,7 +333,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
332
333
  * give up after {@link REJOIN_CONNECT_DEADLINE_MS} if no chunk arrives and
333
334
  * clear the dead pointer so it does not retry on the next load.
334
335
  *
335
- * Replay chunks are processed WITHOUT the per-chunk yield the live path uses,
336
+ * Replay chunks are processed WITHOUT the time-slice yield the live path uses,
336
337
  * so the buffered prefix snaps in and only the genuinely-live tail streams at
337
338
  * network speed — a reload looks like the run continued, not like it re-typed.
338
339
  */
@@ -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, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
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 ?? []) {
@@ -582,7 +588,7 @@ var ChatClient = class {
582
588
  this.processor.setMessages(windowMessages);
583
589
  this.rememberServerMessageIds(windowMessages);
584
590
  }
585
- 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({
586
592
  resumeState: {
587
593
  threadId: this.threadId,
588
594
  runId: result.interrupts.runId
@@ -1011,13 +1017,21 @@ var ChatClient = class {
1011
1017
  });
1012
1018
  }
1013
1019
  /**
1014
- * 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.
1015
1022
  */
1016
1023
  async consumeSubscription(signal) {
1017
1024
  const stream = this.connection.subscribe(signal);
1025
+ let chunkProcessingTime = 0;
1018
1026
  for await (const chunk of stream) {
1019
1027
  if (signal.aborted) break;
1020
- await this.processIncomingChunk(chunk);
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();
1021
1035
  }
1022
1036
  }
1023
1037
  /**
@@ -1039,7 +1053,7 @@ var ChatClient = class {
1039
1053
  * give up after {@link REJOIN_CONNECT_DEADLINE_MS} if no chunk arrives and
1040
1054
  * clear the dead pointer so it does not retry on the next load.
1041
1055
  *
1042
- * Replay chunks are processed WITHOUT the per-chunk yield the live path uses,
1056
+ * Replay chunks are processed WITHOUT the time-slice yield the live path uses,
1043
1057
  * so the buffered prefix snaps in and only the genuinely-live tail streams at
1044
1058
  * network speed — a reload looks like the run continued, not like it re-typed.
1045
1059
  */
@@ -1074,7 +1088,7 @@ var ChatClient = class {
1074
1088
  rebuilt = true;
1075
1089
  this.dropTrailingInFlightAssistant();
1076
1090
  }
1077
- await this.processIncomingChunk(chunk, { defer: false });
1091
+ this.processIncomingChunk(chunk);
1078
1092
  }
1079
1093
  if (this.pendingToolExecutions.size > 0) await Promise.all(this.pendingToolExecutions.values());
1080
1094
  } catch (error) {
@@ -1107,7 +1121,7 @@ var ChatClient = class {
1107
1121
  const last = messages[messages.length - 1];
1108
1122
  if (last && last.role === "assistant") this.processor.setMessages(messages.slice(0, -1));
1109
1123
  }
1110
- async processIncomingChunk(chunk, options) {
1124
+ processIncomingChunk(chunk) {
1111
1125
  chunk = restoreInboundChunk(chunk);
1112
1126
  if (chunk.type === "RUN_ERROR" && this.isActiveInterruptSubmissionFailure(chunk)) {
1113
1127
  const interruptErrors = tanstackMetadata(chunk)?.interruptErrors;
@@ -1136,7 +1150,6 @@ var ChatClient = class {
1136
1150
  this.syncSubagentHandles();
1137
1151
  this.updateRunLifecycle(chunk);
1138
1152
  this.observeInterruptState(chunk);
1139
- if (options?.defer !== false && (typeof document === "undefined" || !document.hidden)) await new Promise((resolve) => setTimeout(resolve, 0));
1140
1153
  this.resolveJoinedRun(chunk);
1141
1154
  }
1142
1155
  isActiveInterruptSubmissionFailure(chunk) {
@@ -1591,10 +1604,12 @@ var ChatClient = class {
1591
1604
  await this.addToolResultForClientTool(result, clientTool, this.streamContinuationGeneration);
1592
1605
  }
1593
1606
  async addToolResultForClientTool(result, clientTool, continuationGeneration, context) {
1594
- if (clientTool && result.state !== "output-error") try {
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);
1595
1610
  result = {
1596
1611
  ...result,
1597
- output: this.validateClientToolOutput(clientTool, result.output)
1612
+ output
1598
1613
  };
1599
1614
  } catch (error) {
1600
1615
  result = {
@@ -1608,16 +1623,17 @@ var ChatClient = class {
1608
1623
  if (continuationGeneration !== this.continuationGeneration) return;
1609
1624
  this.processor.addToolResult(result.toolCallId, result.output, result.state === "output-error" ? result.errorText || "Tool execution failed" : void 0);
1610
1625
  this.devtoolsBridge.emitSnapshot();
1611
- if (this.interruptManager.resolveClientToolOutput(result.toolCallId, result.state === "output-error" ? { error: result.errorText || "Tool execution failed" } : result.output)) return;
1626
+ if (result.state === "output-error" ? this.interruptManager.resolveClientToolError(result.toolCallId, result.errorText || "Tool execution failed") : this.interruptManager.resolveClientToolOutput(result.toolCallId, result.output)) return;
1612
1627
  if (this.isLoading) {
1613
1628
  this.queuePostStreamAction(() => continuationGeneration === this.continuationGeneration ? this.checkForContinuation() : Promise.resolve());
1614
1629
  return;
1615
1630
  }
1616
1631
  await this.checkForContinuation();
1617
1632
  }
1618
- validateClientToolOutput(clientTool, output) {
1619
- if (clientTool.outputSchema && isStandardSchema(clientTool.outputSchema)) return parseWithStandardSchema(clientTool.outputSchema, output);
1620
- return output;
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;
1621
1637
  }
1622
1638
  /**
1623
1639
  * Respond to a tool approval request