@tanstack/ai 0.45.0 → 0.46.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
@@ -36,6 +36,14 @@
36
36
  </a>
37
37
  </div>
38
38
 
39
+ <br />
40
+
41
+ <div align="center">
42
+ <a href="https://tanstack.com/blog/tanstack-open-source-awards-2026">
43
+ <img src="https://raw.githubusercontent.com/TanStack/ai/16826d81cade868956df240d6239a671e689193c/media/js-open-source-award-2026-ai-project-of-the-year.svg" alt="Winner of the 2026 JavaScript Open Source Award for AI Project of the Year" width="250" />
44
+ </a>
45
+ </div>
46
+
39
47
  # TanStack AI
40
48
 
41
49
  Type-safe, provider-agnostic TypeScript SDK for building streaming chat,
@@ -191,7 +199,7 @@ Learn more in the
191
199
  build low-latency realtime voice experiences.
192
200
  - [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let
193
201
  models write and execute TypeScript inside a secure isolate.
194
- - [Code Mode with Skills](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-skills) -
202
+ - [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) -
195
203
  give Code Mode reusable runtime capabilities.
196
204
 
197
205
  ## Providers
@@ -208,6 +216,7 @@ Official adapters include:
208
216
  | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
209
217
  | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
210
218
  | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
219
+ | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
211
220
  | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
212
221
 
213
222
  The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
@@ -15,9 +15,10 @@ import { INTERRUPT_BINDING_METADATA_KEY, InterruptResumeValidationError, readUno
15
15
  import { normalizeToolResult } from "../../utilities/tool-result.js";
16
16
  import { isProviderExecutedToolCall } from "../../utilities/provider-executed.js";
17
17
  import { LazyToolManager } from "./tools/lazy-tool-manager.js";
18
+ import { assertUniqueToolNames } from "./tools/unique-tool-names.js";
18
19
  import { MiddlewareAbortError, ToolCallManager, executeToolCalls } from "./tools/tool-calls.js";
19
20
  import { maxIterations } from "./agent-loop-strategies.js";
20
- import { convertMessagesToModelMessages, generateMessageId } from "./messages.js";
21
+ import { convertMessagesToModelMessages, generateMessageId, modelMessageToUIMessage } from "./messages.js";
21
22
  import { MiddlewareRunner } from "./middleware/compose.js";
22
23
  import { provideSandboxRuntime } from "./middleware/sandbox-runtime.js";
23
24
  import { provideRunDisconnect } from "./middleware/run-disconnect.js";
@@ -148,9 +149,11 @@ var TextEngine = class {
148
149
  accumulatedThinking = [];
149
150
  currentThinkingContent = "";
150
151
  currentThinkingSignature = "";
152
+ hasSeenReasoningEvents = false;
151
153
  eventOptions;
152
154
  eventToolNames;
153
155
  finishedEvent = null;
156
+ streamedToolErrorResults = /* @__PURE__ */ new Map();
154
157
  deferredToolCallRunFinishedChunks = [];
155
158
  earlyTermination = false;
156
159
  toolPhase = "continue";
@@ -201,6 +204,7 @@ var TextEngine = class {
201
204
  this.initialApprovals = approvals;
202
205
  this.initialClientToolResults = clientToolResults;
203
206
  this.messages = convertMessagesToModelMessages(config.params.messages);
207
+ assertUniqueToolNames(config.params.tools || []);
204
208
  this.lazyToolManager = new LazyToolManager(config.params.tools || [], this.messages, config.params.lazyToolsConfig);
205
209
  this.tools = this.lazyToolManager.getActiveTools();
206
210
  this.toolCallManager = new ToolCallManager(this.tools);
@@ -354,6 +358,7 @@ var TextEngine = class {
354
358
  duration: Date.now() - this.streamStartTime
355
359
  });
356
360
  } else {
361
+ this.addTerminalReasoningMessage();
357
362
  this.terminalHookCalled = true;
358
363
  await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
359
364
  finishReason: this.lastFinishReason,
@@ -457,7 +462,9 @@ var TextEngine = class {
457
462
  this.accumulatedThinking = [];
458
463
  this.currentThinkingContent = "";
459
464
  this.currentThinkingSignature = "";
465
+ this.hasSeenReasoningEvents = false;
460
466
  this.finishedEvent = null;
467
+ this.streamedToolErrorResults.clear();
461
468
  this.middlewareCtx.currentMessageId = this.currentMessageId;
462
469
  this.middlewareCtx.accumulatedContent = "";
463
470
  await this.middlewareRunner.runOnIteration(this.middlewareCtx, {
@@ -591,7 +598,10 @@ var TextEngine = class {
591
598
  case "STEP_STARTED":
592
599
  this.handleStepStartedEvent();
593
600
  break;
594
- case "STEP_FINISHED": this.handleStepFinishedEvent(chunk);
601
+ case "STEP_FINISHED":
602
+ this.handleStepFinishedEvent(chunk);
603
+ break;
604
+ case "REASONING_MESSAGE_CONTENT": this.handleReasoningMessageContentEvent(chunk);
595
605
  }
596
606
  }
597
607
  handleTextMessageContentEvent(chunk) {
@@ -616,6 +626,16 @@ var TextEngine = class {
616
626
  }
617
627
  handleToolCallEndEvent(chunk) {
618
628
  this.toolCallManager.completeToolCall(chunk);
629
+ if (chunk.state !== "output-error" || chunk.result === void 0) return;
630
+ const toolCall = this.toolCallManager.getToolCalls().find((candidate) => candidate.id === chunk.toolCallId);
631
+ if (!toolCall) return;
632
+ this.streamedToolErrorResults.set(chunk.toolCallId, {
633
+ toolCallId: chunk.toolCallId,
634
+ toolName: toolCall.function.name,
635
+ result: chunk.result,
636
+ ...chunk.input !== void 0 && { input: chunk.input },
637
+ state: "output-error"
638
+ });
619
639
  }
620
640
  handleRunFinishedEvent(chunk) {
621
641
  this.finishedEvent = chunk;
@@ -645,9 +665,19 @@ var TextEngine = class {
645
665
  this.finalizeCurrentThinkingStep();
646
666
  }
647
667
  handleStepFinishedEvent(chunk) {
648
- if (chunk.delta) this.currentThinkingContent += chunk.delta;
668
+ if (!this.hasSeenReasoningEvents) {
669
+ if (chunk.delta) this.currentThinkingContent += chunk.delta;
670
+ else if (chunk.content) {
671
+ if (chunk.content.startsWith(this.currentThinkingContent)) this.currentThinkingContent = chunk.content;
672
+ else if (!this.currentThinkingContent.startsWith(chunk.content)) this.currentThinkingContent += chunk.content;
673
+ }
674
+ }
649
675
  if (chunk.signature) this.currentThinkingSignature = chunk.signature;
650
676
  }
677
+ handleReasoningMessageContentEvent(chunk) {
678
+ this.hasSeenReasoningEvents = true;
679
+ this.currentThinkingContent += chunk.delta;
680
+ }
651
681
  /**
652
682
  * Tools available for execution this turn. The discovery tool is dropped
653
683
  * from the advertised set (`this.tools`) once every lazy tool is discovered,
@@ -751,6 +781,7 @@ var TextEngine = class {
751
781
  this.addAssistantToolCallMessage(toolCalls);
752
782
  const undiscoveredLazyResults = [];
753
783
  const executableToolCalls = toolCalls.filter((tc) => {
784
+ if (this.streamedToolErrorResults.has(tc.id)) return false;
754
785
  if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
755
786
  undiscoveredLazyResults.push({
756
787
  toolCallId: tc.id,
@@ -762,7 +793,7 @@ var TextEngine = class {
762
793
  }
763
794
  return true;
764
795
  });
765
- const deferredErrorResults = [...undiscoveredLazyResults];
796
+ const deferredErrorResults = [...this.streamedToolErrorResults.values(), ...undiscoveredLazyResults];
766
797
  if (executableToolCalls.length === 0) {
767
798
  yield* this.flushDeferredToolCallRunFinishedChunks();
768
799
  if (deferredErrorResults.length > 0) for (const chunk of this.buildToolResultChunks(deferredErrorResults, finishEvent)) yield* this.pipeThroughMiddleware(chunk);
@@ -858,6 +889,20 @@ var TextEngine = class {
858
889
  }];
859
890
  this.middlewareCtx.messages = this.messages;
860
891
  }
892
+ addTerminalReasoningMessage() {
893
+ this.finalizeCurrentThinkingStep();
894
+ if (this.accumulatedThinking.length === 0) return;
895
+ const messages = this.middlewareCtx.messages;
896
+ if (messages.some((message) => message.role === "assistant" && message.id === this.currentMessageId)) return;
897
+ this.messages = [...messages, {
898
+ role: "assistant",
899
+ content: this.accumulatedContent || null,
900
+ id: this.currentMessageId ?? void 0,
901
+ createdAt: this.currentMessageCreatedAt ?? void 0,
902
+ thinking: this.accumulatedThinking
903
+ }];
904
+ this.middlewareCtx.messages = this.messages;
905
+ }
861
906
  /**
862
907
  * Extract client state (approvals and client tool results) from original messages.
863
908
  * This is called in the constructor BEFORE converting to ModelMessage format,
@@ -967,10 +1012,13 @@ var TextEngine = class {
967
1012
  buildMessagesSnapshotChunk() {
968
1013
  const messages = this.messages.map((message, index) => {
969
1014
  const content = typeof message.content === "string" ? message.content : message.content === null ? void 0 : JSON.stringify(message.content);
1015
+ const id = message.id || `snapshot_${this.runIdOverride ?? this.requestId}_${index}`;
1016
+ const parts = message.role === "assistant" && message.thinking?.length ? modelMessageToUIMessage(message, id).parts : void 0;
970
1017
  return {
971
- id: message.id || `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
1018
+ id,
972
1019
  role: message.role,
973
1020
  ...content !== void 0 ? { content } : {},
1021
+ ...parts ? { parts } : {},
974
1022
  ..."toolCalls" in message && message.toolCalls ? { toolCalls: message.toolCalls } : {},
975
1023
  ..."toolCallId" in message && message.toolCallId ? { toolCallId: message.toolCallId } : {}
976
1024
  };
@@ -1742,6 +1790,7 @@ var TextEngine = class {
1742
1790
  this.applyResumeToolState(config.resumeToolState);
1743
1791
  this.messages = config.messages;
1744
1792
  this.systemPrompts = config.systemPrompts;
1793
+ assertUniqueToolNames(config.tools);
1745
1794
  this.tools = config.tools;
1746
1795
  this.params = {
1747
1796
  ...this.params,
@@ -1866,6 +1915,7 @@ var TextEngine = class {
1866
1915
  */
1867
1916
  function chat(options) {
1868
1917
  validateCapabilities(options.middleware ?? [], options.adapter);
1918
+ if (options.tools) assertUniqueToolNames(options.tools);
1869
1919
  const { outputSchema, stream } = options;
1870
1920
  if (outputSchema && stream === true) return runStreamingStructuredOutput({
1871
1921
  ...options,