@tanstack/ai 0.14.0 → 0.16.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.
Files changed (30) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +6 -3
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.js +61 -9
  4. package/dist/esm/activities/chat/index.js.map +1 -1
  5. package/dist/esm/activities/chat/messages.js +26 -3
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/stream/message-updaters.d.ts +4 -2
  8. package/dist/esm/activities/chat/stream/message-updaters.js +10 -4
  9. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  10. package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
  11. package/dist/esm/activities/chat/stream/processor.js +119 -19
  12. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  13. package/dist/esm/activities/chat/stream/types.d.ts +9 -1
  14. package/dist/esm/activities/chat/tools/tool-calls.js +3 -4
  15. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  16. package/dist/esm/middlewares/otel.d.ts +75 -0
  17. package/dist/esm/middlewares/otel.js +624 -0
  18. package/dist/esm/middlewares/otel.js.map +1 -0
  19. package/dist/esm/types.d.ts +23 -7
  20. package/package.json +15 -2
  21. package/src/activities/chat/adapter.ts +8 -2
  22. package/src/activities/chat/index.ts +74 -11
  23. package/src/activities/chat/messages.ts +29 -1
  24. package/src/activities/chat/stream/message-updaters.ts +18 -3
  25. package/src/activities/chat/stream/processor.ts +152 -17
  26. package/src/activities/chat/stream/types.ts +9 -1
  27. package/src/activities/chat/tools/tool-calls.ts +4 -4
  28. package/src/middlewares/index.ts +5 -0
  29. package/src/middlewares/otel.ts +861 -0
  30. package/src/types.ts +20 -7
@@ -90,7 +90,11 @@ export interface StreamProcessorEvents {
90
90
  state: ToolCallState,
91
91
  args: string,
92
92
  ) => void
93
- onThinkingUpdate?: (messageId: string, content: string) => void
93
+ onThinkingUpdate?: (
94
+ messageId: string,
95
+ stepId: string,
96
+ content: string,
97
+ ) => void
94
98
  }
95
99
 
96
100
  /**
@@ -139,6 +143,7 @@ export class StreamProcessor {
139
143
  private activeMessageIds: Set<string> = new Set()
140
144
  private toolCallToMessage: Map<string, string> = new Map()
141
145
  private pendingManualMessageId: string | null = null
146
+ private pendingThinkingStepId: string | null = null
142
147
 
143
148
  // Run tracking (for concurrent run safety)
144
149
  private activeRuns = new Set<string>()
@@ -541,8 +546,14 @@ export class StreamProcessor {
541
546
  )
542
547
  break
543
548
 
549
+ case 'STEP_STARTED':
550
+ this.handleStepStartedEvent(
551
+ chunk as Extract<StreamChunk, { type: 'STEP_STARTED' }>,
552
+ )
553
+ break
554
+
544
555
  default:
545
- // STEP_STARTED, STATE_SNAPSHOT, STATE_DELTA - no special handling needed
556
+ // STATE_SNAPSHOT, STATE_DELTA - no special handling needed
546
557
  break
547
558
  }
548
559
  }
@@ -564,8 +575,11 @@ export class StreamProcessor {
564
575
  totalTextContent: '',
565
576
  currentSegmentText: '',
566
577
  lastEmittedText: '',
567
- thinkingContent: '',
568
578
  hasSeenReasoningEvents: false,
579
+ thinkingSteps: new Map(),
580
+ thinkingStepSignatures: new Map(),
581
+ thinkingStepOrder: [],
582
+ currentThinkingStepId: null,
569
583
  toolCalls: new Map(),
570
584
  toolCallOrder: [],
571
585
  hasToolCallsSinceTextStart: false,
@@ -582,6 +596,23 @@ export class StreamProcessor {
582
596
  return this.messageStates.get(messageId)
583
597
  }
584
598
 
599
+ /**
600
+ * Promote a pending stepId from a STEP_STARTED that fired before the
601
+ * assistant message existed onto the given message state, so the next
602
+ * thinking event (STEP_FINISHED or REASONING_MESSAGE_CONTENT) attributes
603
+ * to the correct step.
604
+ */
605
+ private consumePendingThinkingStep(state: MessageStreamState): void {
606
+ if (!this.pendingThinkingStepId) return
607
+ const stepId = this.pendingThinkingStepId
608
+ state.currentThinkingStepId = stepId
609
+ if (!state.thinkingSteps.has(stepId)) {
610
+ state.thinkingSteps.set(stepId, '')
611
+ state.thinkingStepOrder.push(stepId)
612
+ }
613
+ this.pendingThinkingStepId = null
614
+ }
615
+
585
616
  /**
586
617
  * Get the most recent active assistant message ID.
587
618
  * Used as fallback for events that don't include a messageId.
@@ -897,7 +928,18 @@ export class StreamProcessor {
897
928
  // New tool call starting
898
929
  const initialState: ToolCallState = 'awaiting-input'
899
930
 
900
- const toolName = chunk.toolCallName
931
+ // `toolName` is a deprecated alias for `toolCallName` (see ToolCallStartEvent
932
+ // in types.ts). Accept either so chunks from older code paths or any
933
+ // adapter that only sets the deprecated field still produce a named part.
934
+ // The type marks both as required strings, but in practice some emitters
935
+ // only set one — fall back via the runtime value rather than the type.
936
+ const toolName =
937
+ (chunk as { toolCallName?: string }).toolCallName ?? chunk.toolName
938
+
939
+ // Capture provider metadata that arrived on TOOL_CALL_START so it
940
+ // round-trips back through the assistant message on the next turn
941
+ // (e.g. Gemini's thoughtSignature).
942
+ const chunkMetadata = chunk.metadata
901
943
 
902
944
  const newToolCall: InternalToolCallState = {
903
945
  id: chunk.toolCallId,
@@ -906,6 +948,7 @@ export class StreamProcessor {
906
948
  state: initialState,
907
949
  parsedArguments: undefined,
908
950
  index: chunk.index ?? state.toolCalls.size,
951
+ ...(chunkMetadata !== undefined && { metadata: chunkMetadata }),
909
952
  }
910
953
 
911
954
  state.toolCalls.set(toolCallId, newToolCall)
@@ -920,6 +963,7 @@ export class StreamProcessor {
920
963
  name: toolName,
921
964
  arguments: '',
922
965
  state: initialState,
966
+ ...(chunkMetadata !== undefined && { metadata: chunkMetadata }),
923
967
  })
924
968
  this.emitMessagesChange()
925
969
 
@@ -1155,11 +1199,45 @@ export class StreamProcessor {
1155
1199
  this.events.onError?.(new Error(errorMessage))
1156
1200
  }
1157
1201
 
1202
+ /**
1203
+ * Handle STEP_STARTED event (for thinking/reasoning content).
1204
+ *
1205
+ * Records the stepId so that subsequent STEP_FINISHED deltas accumulate
1206
+ * into their own ThinkingPart. Does not create a message — the message
1207
+ * is lazily created when the first STEP_FINISHED content arrives.
1208
+ */
1209
+ private handleStepStartedEvent(
1210
+ chunk: Extract<StreamChunk, { type: 'STEP_STARTED' }>,
1211
+ ): void {
1212
+ const stepId = chunk.stepId ?? generateMessageId()
1213
+ const activeId = this.getActiveAssistantMessageId()
1214
+ if (activeId) {
1215
+ const state = this.getMessageState(activeId)
1216
+ if (state) {
1217
+ state.currentThinkingStepId = stepId
1218
+ if (!state.thinkingSteps.has(stepId)) {
1219
+ state.thinkingSteps.set(stepId, '')
1220
+ state.thinkingStepOrder.push(stepId)
1221
+ }
1222
+ // Clear any pending stepId from a prior STEP_STARTED that fired
1223
+ // before the assistant message existed. Now that we're tracking
1224
+ // the step directly on message state, the pending value is stale
1225
+ // and must not leak into the next STEP_FINISHED (which would
1226
+ // misattribute its delta to the stale step).
1227
+ this.pendingThinkingStepId = null
1228
+ return
1229
+ }
1230
+ }
1231
+
1232
+ // No active message yet — defer until ensureAssistantMessage in STEP_FINISHED
1233
+ this.pendingThinkingStepId = stepId
1234
+ }
1235
+
1158
1236
  /**
1159
1237
  * Handle STEP_FINISHED event (for thinking/reasoning content).
1160
1238
  *
1161
- * Accumulates delta into thinkingContent and updates a single ThinkingPart
1162
- * in the UIMessage (replaced in-place, not appended).
1239
+ * Accumulates delta into the current thinking step's content and updates
1240
+ * the corresponding ThinkingPart in the UIMessage.
1163
1241
  *
1164
1242
  * @see docs/chat-architecture.md#thinkingreasoning-content — Thinking flow
1165
1243
  */
@@ -1175,10 +1253,38 @@ export class StreamProcessor {
1175
1253
  // REASONING_MESSAGE_CONTENT events for this message, skip the duplicate
1176
1254
  // thinking content from STEP_FINISHED to avoid doubled content.
1177
1255
  if (state.hasSeenReasoningEvents) {
1256
+ if (chunk.signature) {
1257
+ const stepId = state.currentThinkingStepId ?? chunk.stepId
1258
+ if (!stepId) return
1259
+ const thinking = state.thinkingSteps.get(stepId)
1260
+ if (thinking !== undefined) {
1261
+ state.thinkingStepSignatures.set(stepId, chunk.signature)
1262
+ this.messages = updateThinkingPart(
1263
+ this.messages,
1264
+ messageId,
1265
+ stepId,
1266
+ thinking,
1267
+ chunk.signature,
1268
+ )
1269
+ this.emitMessagesChange()
1270
+ }
1271
+ }
1178
1272
  return
1179
1273
  }
1180
1274
 
1181
- const previous = state.thinkingContent
1275
+ this.consumePendingThinkingStep(state)
1276
+
1277
+ const stepId =
1278
+ state.currentThinkingStepId ?? chunk.stepId ?? generateMessageId()
1279
+
1280
+ // Auto-initialize if no prior STEP_STARTED (backward compat)
1281
+ if (!state.thinkingSteps.has(stepId)) {
1282
+ state.thinkingSteps.set(stepId, '')
1283
+ state.thinkingStepOrder.push(stepId)
1284
+ state.currentThinkingStepId = stepId
1285
+ }
1286
+
1287
+ const previous = state.thinkingSteps.get(stepId)!
1182
1288
  let nextThinking = previous
1183
1289
 
1184
1290
  // Prefer delta over content
@@ -1194,25 +1300,31 @@ export class StreamProcessor {
1194
1300
  }
1195
1301
  }
1196
1302
 
1197
- state.thinkingContent = nextThinking
1303
+ state.thinkingSteps.set(stepId, nextThinking)
1304
+
1305
+ if (chunk.signature) {
1306
+ state.thinkingStepSignatures.set(stepId, chunk.signature)
1307
+ }
1198
1308
 
1199
1309
  // Update UIMessage
1200
1310
  this.messages = updateThinkingPart(
1201
1311
  this.messages,
1202
1312
  messageId,
1203
- state.thinkingContent,
1313
+ stepId,
1314
+ nextThinking,
1315
+ state.thinkingStepSignatures.get(stepId),
1204
1316
  )
1205
1317
  this.emitMessagesChange()
1206
1318
 
1207
1319
  // Emit granular event
1208
- this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
1320
+ this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
1209
1321
  }
1210
1322
 
1211
1323
  /**
1212
1324
  * Handle REASONING_MESSAGE_CONTENT event (AG-UI reasoning protocol).
1213
1325
  *
1214
- * Accumulates reasoning delta into thinkingContent and updates the ThinkingPart
1215
- * in the UIMessage.
1326
+ * Accumulates reasoning delta into thinking content and updates the
1327
+ * corresponding ThinkingPart in the UIMessage.
1216
1328
  */
1217
1329
  private handleReasoningMessageContentEvent(
1218
1330
  chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
@@ -1223,16 +1335,29 @@ export class StreamProcessor {
1223
1335
 
1224
1336
  state.hasSeenReasoningEvents = true
1225
1337
  const delta = chunk.delta || ''
1226
- state.thinkingContent = state.thinkingContent + delta
1338
+
1339
+ this.consumePendingThinkingStep(state)
1340
+
1341
+ const stepId = state.currentThinkingStepId ?? chunk.messageId
1342
+ if (!state.thinkingSteps.has(stepId)) {
1343
+ state.thinkingSteps.set(stepId, '')
1344
+ state.thinkingStepOrder.push(stepId)
1345
+ state.currentThinkingStepId = stepId
1346
+ }
1347
+
1348
+ const nextThinking = (state.thinkingSteps.get(stepId) ?? '') + delta
1349
+ state.thinkingSteps.set(stepId, nextThinking)
1227
1350
 
1228
1351
  this.messages = updateThinkingPart(
1229
1352
  this.messages,
1230
1353
  messageId,
1231
- state.thinkingContent,
1354
+ stepId,
1355
+ nextThinking,
1356
+ state.thinkingStepSignatures.get(stepId),
1232
1357
  )
1233
1358
  this.emitMessagesChange()
1234
1359
 
1235
- this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
1360
+ this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
1236
1361
  }
1237
1362
 
1238
1363
  /**
@@ -1386,6 +1511,7 @@ export class StreamProcessor {
1386
1511
  name: toolCall.name,
1387
1512
  arguments: toolCall.arguments,
1388
1513
  state: 'input-complete',
1514
+ ...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
1389
1515
  })
1390
1516
  this.emitMessagesChange()
1391
1517
 
@@ -1501,6 +1627,10 @@ export class StreamProcessor {
1501
1627
  name: tc.name,
1502
1628
  arguments: tc.arguments,
1503
1629
  },
1630
+ // Preserve provider metadata (e.g. Gemini thoughtSignature) on
1631
+ // ProcessorResult.toolCalls so callers using process()/getResult()
1632
+ // get the same round-trip support as the streaming UI path.
1633
+ ...(tc.metadata !== undefined && { metadata: tc.metadata }),
1504
1634
  })
1505
1635
  }
1506
1636
  }
@@ -1518,7 +1648,9 @@ export class StreamProcessor {
1518
1648
 
1519
1649
  for (const state of this.messageStates.values()) {
1520
1650
  content += state.totalTextContent
1521
- thinking += state.thinkingContent
1651
+ for (const stepId of state.thinkingStepOrder) {
1652
+ thinking += state.thinkingSteps.get(stepId) ?? ''
1653
+ }
1522
1654
  }
1523
1655
 
1524
1656
  return {
@@ -1540,7 +1672,9 @@ export class StreamProcessor {
1540
1672
 
1541
1673
  for (const state of this.messageStates.values()) {
1542
1674
  content += state.totalTextContent
1543
- thinking += state.thinkingContent
1675
+ for (const stepId of state.thinkingStepOrder) {
1676
+ thinking += state.thinkingSteps.get(stepId) ?? ''
1677
+ }
1544
1678
  for (const [id, tc] of state.toolCalls) {
1545
1679
  toolCalls.set(id, tc)
1546
1680
  }
@@ -1586,6 +1720,7 @@ export class StreamProcessor {
1586
1720
  this.activeRuns.clear()
1587
1721
  this.toolCallToMessage.clear()
1588
1722
  this.pendingManualMessageId = null
1723
+ this.pendingThinkingStepId = null
1589
1724
  this.finishReason = null
1590
1725
  this.hasError = false
1591
1726
  this.isDone = false
@@ -25,6 +25,11 @@ export interface InternalToolCallState {
25
25
  state: ToolCallState
26
26
  parsedArguments?: any
27
27
  index: number
28
+ /** Provider-specific metadata that round-trips with the tool call
29
+ * (e.g. Gemini's `thoughtSignature`). Untyped at this layer because
30
+ * the stream processor is provider-agnostic; adapters narrow it
31
+ * via their `TToolCallMetadata` generic. */
32
+ metadata?: Record<string, unknown>
28
33
  }
29
34
 
30
35
  /**
@@ -56,8 +61,11 @@ export interface MessageStreamState {
56
61
  totalTextContent: string
57
62
  currentSegmentText: string
58
63
  lastEmittedText: string
59
- thinkingContent: string
60
64
  hasSeenReasoningEvents: boolean
65
+ thinkingSteps: Map<string, string>
66
+ thinkingStepSignatures: Map<string, string>
67
+ thinkingStepOrder: Array<string>
68
+ currentThinkingStepId: string | null
61
69
  toolCalls: Map<string, InternalToolCallState>
62
70
  toolCallOrder: Array<string>
63
71
  hasToolCallsSinceTextStart: boolean
@@ -93,7 +93,9 @@ export class ToolCallManager {
93
93
  */
94
94
  addToolCallStartEvent(event: ToolCallStartEvent): void {
95
95
  const index = event.index ?? this.toolCallsMap.size
96
- const name = event.toolCallName
96
+ const runtimeEvent = event as Partial<ToolCallStartEvent> &
97
+ Pick<ToolCallStartEvent, 'toolName'>
98
+ const name = runtimeEvent.toolCallName ?? runtimeEvent.toolName
97
99
  this.toolCallsMap.set(index, {
98
100
  id: event.toolCallId,
99
101
  type: 'function',
@@ -101,9 +103,7 @@ export class ToolCallManager {
101
103
  name,
102
104
  arguments: '',
103
105
  },
104
- ...(event.providerMetadata && {
105
- providerMetadata: event.providerMetadata,
106
- }),
106
+ ...(event.metadata !== undefined && { metadata: event.metadata }),
107
107
  })
108
108
  }
109
109
 
@@ -11,3 +11,8 @@ export {
11
11
  type ContentGuardRule,
12
12
  type ContentFilteredInfo,
13
13
  } from './content-guard'
14
+
15
+ // otelMiddleware is exported from the dedicated subpath
16
+ // `@tanstack/ai/middlewares/otel` so that importing the main middlewares barrel
17
+ // does not eagerly require `@opentelemetry/api` (which is an optional peer
18
+ // dependency).