@tanstack/ai 0.47.2 → 0.47.3

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.
@@ -22,8 +22,9 @@ import {
22
22
  generateMessageId,
23
23
  uiMessageToModelMessages,
24
24
  } from '../messages.js'
25
- import { normalizeToolResult } from '../../../utilities/tool-result'
25
+ import { runErrorEventToError } from '../../../utilities/errors'
26
26
  import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
27
+ import { normalizeToolResult } from '../../../utilities/tool-result'
27
28
  import { defaultJSONParser } from './json-parser'
28
29
  import {
29
30
  appendStructuredOutputDelta,
@@ -819,6 +820,15 @@ export class StreamProcessor {
819
820
  // Update activeMessageIds
820
821
  this.activeMessageIds.delete(pendingId)
821
822
  this.activeMessageIds.add(messageId)
823
+
824
+ // TOOL_CALL_ARGS/END route through toolCallToMessage. Keep those
825
+ // entries on the remapped id so later args still accumulate
826
+ // (interleaved text can arrive as a full START/CONTENT/END block).
827
+ for (const [toolCallId, mappedMessageId] of this.toolCallToMessage) {
828
+ if (mappedMessageId === pendingId) {
829
+ this.toolCallToMessage.set(toolCallId, messageId)
830
+ }
831
+ }
822
832
  }
823
833
 
824
834
  // Ensure state exists
@@ -886,9 +896,6 @@ export class StreamProcessor {
886
896
  if (state.currentSegmentText !== state.lastEmittedText) {
887
897
  this.emitTextUpdateForMessage(messageId)
888
898
  }
889
-
890
- // Complete all tool calls for this message
891
- this.completeAllToolCallsForMessage(messageId)
892
899
  }
893
900
 
894
901
  /**
@@ -1139,9 +1146,6 @@ export class StreamProcessor {
1139
1146
  ): void {
1140
1147
  const { messageId, state } = this.ensureAssistantMessage(chunk.messageId)
1141
1148
 
1142
- // Content arriving means all current tool calls for this message are complete
1143
- this.completeAllToolCallsForMessage(messageId)
1144
-
1145
1149
  if (this.structuredMessageIds.has(messageId)) {
1146
1150
  // `chunk.delta` is incremental; `chunk.content` is sometimes cumulative
1147
1151
  // (mirrors what the plain-text branch handles below). Reconcile against
@@ -1681,15 +1685,7 @@ export class StreamProcessor {
1681
1685
  // the surfaced Error so consumers can recover the upstream detail that the
1682
1686
  // RUN_ERROR's `message` alone discards. Both are optional and added only
1683
1687
  // when present, keeping the Error backward compatible.
1684
- const error = new Error(errorMessage)
1685
- const code = chunk.code ?? chunk.error?.code
1686
- if (code !== undefined) {
1687
- Object.assign(error, { code })
1688
- }
1689
- if (chunk.rawEvent !== undefined) {
1690
- Object.assign(error, { rawEvent: chunk.rawEvent })
1691
- }
1692
- this.events.onError?.(error)
1688
+ this.events.onError?.(runErrorEventToError(chunk))
1693
1689
  }
1694
1690
 
1695
1691
  /**
@@ -2087,8 +2083,17 @@ export class StreamProcessor {
2087
2083
  // counts as a completed tool call in getCompletedToolCalls()/getState().
2088
2084
  toolCall.state = 'input-complete'
2089
2085
 
2090
- // Try final parse
2091
- toolCall.parsedArguments = this.jsonParser.parse(toolCall.arguments)
2086
+ // Only surface `input` from a strict parse. The streaming partial-JSON
2087
+ // parser closes unterminated strings, so truncated arguments would become
2088
+ // a plausible but wrong object (GitHub issue #1017). If parse fails,
2089
+ // `input` stays unset and consumers use the raw `arguments` string.
2090
+ let strictParseSucceeded = false
2091
+ try {
2092
+ toolCall.parsedArguments = JSON.parse(toolCall.arguments)
2093
+ strictParseSucceeded = true
2094
+ } catch {
2095
+ toolCall.parsedArguments = undefined
2096
+ }
2092
2097
 
2093
2098
  // Don't downgrade the rendered part of a call that already reached the
2094
2099
  // terminal 'error' state (e.g. an output-error TOOL_CALL_RESULT arrived
@@ -2116,9 +2121,7 @@ export class StreamProcessor {
2116
2121
  name: toolCall.name,
2117
2122
  arguments: toolCall.arguments,
2118
2123
  state: 'input-complete',
2119
- ...(toolCall.parsedArguments !== undefined && {
2120
- input: toolCall.parsedArguments,
2121
- }),
2124
+ ...(strictParseSucceeded && { input: toolCall.parsedArguments }),
2122
2125
  ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
2123
2126
  })
2124
2127
  this.emitMessagesChange()
@@ -38,6 +38,7 @@ export interface ChatStreamCapable {
38
38
  * - Groq: `max_completion_tokens`
39
39
  * - Gemini: `maxOutputTokens`
40
40
  * - OpenRouter: `maxCompletionTokens`
41
+ * - LLM Gateway: `max_tokens`
41
42
  * - Ollama: nested `options.num_predict` (no entry — see `applyMaxLength`)
42
43
  */
43
44
  const MAX_TOKENS_KEY_BY_ADAPTER: Record<string, string> = {
@@ -47,6 +48,9 @@ const MAX_TOKENS_KEY_BY_ADAPTER: Record<string, string> = {
47
48
  groq: 'max_completion_tokens',
48
49
  gemini: 'maxOutputTokens',
49
50
  openrouter: 'maxCompletionTokens',
51
+ // LLM Gateway exposes an OpenAI-compatible Chat Completions surface whose
52
+ // only output cap is `max_tokens` — it does not read `max_completion_tokens`.
53
+ llmgateway: 'max_tokens',
50
54
  }
51
55
 
52
56
  /**
@@ -9,6 +9,7 @@ import { notifyRunDisconnected } from './delivery-disconnect'
9
9
  import { resolveResumeRunId } from './stream-durability'
10
10
  import { EventType } from './types'
11
11
  import { resolveDebugOption } from './logger/resolve'
12
+ import { runErrorEventToError } from './utilities/errors'
12
13
  import type { LockStore } from './activities/chat/middleware/locks'
13
14
  import type {
14
15
  RunRecord,
@@ -46,6 +47,10 @@ export async function streamToText(
46
47
  let accumulatedContent = ''
47
48
 
48
49
  for await (const chunk of stream) {
50
+ if (chunk.type === 'RUN_ERROR') {
51
+ throw runErrorEventToError(chunk)
52
+ }
53
+
49
54
  if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
50
55
  accumulatedContent += chunk.delta
51
56
  }
@@ -1,3 +1,5 @@
1
+ import type { StreamChunk } from '../types'
2
+
1
3
  /**
2
4
  * Best-effort extraction of a human-readable message from an unknown thrown
3
5
  * value, returning `undefined` when none can be found.
@@ -27,3 +29,24 @@ export function errorTypeName(err: unknown): string {
27
29
  }
28
30
  return 'Error'
29
31
  }
32
+
33
+ /**
34
+ * Convert an AG-UI RUN_ERROR event to the Error shape exposed to consumers.
35
+ * Preserves the provider code and sanitized raw event when available, while
36
+ * accepting the deprecated nested error payload for backward compatibility.
37
+ */
38
+ export function runErrorEventToError(
39
+ chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
40
+ ): Error {
41
+ const error = new Error(
42
+ chunk.message || chunk.error?.message || 'An error occurred',
43
+ )
44
+ const code = chunk.code ?? chunk.error?.code
45
+ if (code !== undefined) {
46
+ Object.assign(error, { code })
47
+ }
48
+ if (chunk.rawEvent !== undefined) {
49
+ Object.assign(error, { rawEvent: chunk.rawEvent })
50
+ }
51
+ return error
52
+ }