@tanstack/ai 0.13.0 → 0.15.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 (81) hide show
  1. package/dist/esm/activities/chat/index.js +61 -9
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/messages.js +22 -1
  4. package/dist/esm/activities/chat/messages.js.map +1 -1
  5. package/dist/esm/activities/chat/stream/message-updaters.d.ts +3 -2
  6. package/dist/esm/activities/chat/stream/message-updaters.js +7 -3
  7. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  8. package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
  9. package/dist/esm/activities/chat/stream/processor.js +107 -15
  10. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  11. package/dist/esm/activities/chat/stream/types.d.ts +4 -1
  12. package/dist/esm/activities/chat/tools/tool-calls.js +2 -1
  13. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  14. package/dist/esm/activities/error-payload.d.ts +12 -0
  15. package/dist/esm/activities/error-payload.js +25 -0
  16. package/dist/esm/activities/error-payload.js.map +1 -0
  17. package/dist/esm/activities/generateAudio/adapter.d.ts +62 -0
  18. package/dist/esm/activities/generateAudio/adapter.js +14 -0
  19. package/dist/esm/activities/generateAudio/adapter.js.map +1 -0
  20. package/dist/esm/activities/generateAudio/index.d.ts +74 -0
  21. package/dist/esm/activities/generateAudio/index.js +80 -0
  22. package/dist/esm/activities/generateAudio/index.js.map +1 -0
  23. package/dist/esm/activities/generateImage/adapter.d.ts +1 -1
  24. package/dist/esm/activities/generateImage/adapter.js +1 -1
  25. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  26. package/dist/esm/activities/generateSpeech/adapter.d.ts +1 -1
  27. package/dist/esm/activities/generateSpeech/adapter.js +1 -1
  28. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  29. package/dist/esm/activities/generateSpeech/index.d.ts +3 -3
  30. package/dist/esm/activities/generateSpeech/index.js +11 -0
  31. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  32. package/dist/esm/activities/generateTranscription/adapter.d.ts +1 -1
  33. package/dist/esm/activities/generateTranscription/adapter.js +1 -1
  34. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  35. package/dist/esm/activities/generateTranscription/index.d.ts +3 -3
  36. package/dist/esm/activities/generateTranscription/index.js +11 -0
  37. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  38. package/dist/esm/activities/generateVideo/index.js +7 -7
  39. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  40. package/dist/esm/activities/index.d.ts +5 -2
  41. package/dist/esm/activities/index.js +11 -6
  42. package/dist/esm/activities/index.js.map +1 -1
  43. package/dist/esm/activities/stream-generation-result.js +5 -6
  44. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  45. package/dist/esm/adapter-internals.d.ts +1 -0
  46. package/dist/esm/adapter-internals.js +3 -1
  47. package/dist/esm/adapter-internals.js.map +1 -1
  48. package/dist/esm/index.d.ts +3 -2
  49. package/dist/esm/index.js +3 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/middlewares/otel.d.ts +75 -0
  52. package/dist/esm/middlewares/otel.js +624 -0
  53. package/dist/esm/middlewares/otel.js.map +1 -0
  54. package/dist/esm/stream-to-response.js +3 -8
  55. package/dist/esm/stream-to-response.js.map +1 -1
  56. package/dist/esm/types.d.ts +71 -6
  57. package/package.json +15 -2
  58. package/skills/ai-core/media-generation/SKILL.md +154 -10
  59. package/src/activities/chat/index.ts +74 -11
  60. package/src/activities/chat/messages.ts +23 -1
  61. package/src/activities/chat/stream/message-updaters.ts +10 -3
  62. package/src/activities/chat/stream/processor.ts +140 -17
  63. package/src/activities/chat/stream/types.ts +4 -1
  64. package/src/activities/chat/tools/tool-calls.ts +3 -1
  65. package/src/activities/error-payload.ts +35 -0
  66. package/src/activities/generateAudio/adapter.ts +89 -0
  67. package/src/activities/generateAudio/index.ts +224 -0
  68. package/src/activities/generateImage/adapter.ts +1 -1
  69. package/src/activities/generateSpeech/adapter.ts +1 -1
  70. package/src/activities/generateSpeech/index.ts +17 -7
  71. package/src/activities/generateTranscription/adapter.ts +1 -1
  72. package/src/activities/generateTranscription/index.ts +27 -4
  73. package/src/activities/generateVideo/index.ts +8 -8
  74. package/src/activities/index.ts +22 -0
  75. package/src/activities/stream-generation-result.ts +6 -7
  76. package/src/adapter-internals.ts +1 -0
  77. package/src/index.ts +4 -0
  78. package/src/middlewares/index.ts +5 -0
  79. package/src/middlewares/otel.ts +861 -0
  80. package/src/stream-to-response.ts +5 -10
  81. package/src/types.ts +79 -5
@@ -281,6 +281,10 @@ class TextEngine<
281
281
  private totalChunkCount = 0
282
282
  private currentMessageId: string | null = null
283
283
  private accumulatedContent = ''
284
+ private accumulatedThinking: Array<{ content: string; signature?: string }> =
285
+ []
286
+ private currentThinkingContent = ''
287
+ private currentThinkingSignature = ''
284
288
  private eventOptions?: Record<string, unknown>
285
289
  private eventToolNames?: Array<string>
286
290
  private finishedEvent: RunFinishedEvent | null = null
@@ -556,6 +560,9 @@ class TextEngine<
556
560
  private async beginIteration(): Promise<void> {
557
561
  this.currentMessageId = this.createId('msg')
558
562
  this.accumulatedContent = ''
563
+ this.accumulatedThinking = []
564
+ this.currentThinkingContent = ''
565
+ this.currentThinkingSignature = ''
559
566
  this.finishedEvent = null
560
567
 
561
568
  // Update mutable context fields
@@ -666,6 +673,9 @@ class TextEngine<
666
673
  case 'RUN_ERROR':
667
674
  this.handleRunErrorEvent(chunk)
668
675
  break
676
+ case 'STEP_STARTED':
677
+ this.handleStepStartedEvent()
678
+ break
669
679
  case 'STEP_FINISHED':
670
680
  this.handleStepFinishedEvent(chunk)
671
681
  break
@@ -683,7 +693,7 @@ class TextEngine<
683
693
  break
684
694
 
685
695
  default:
686
- // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
696
+ // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END,
687
697
  // STATE_SNAPSHOT, STATE_DELTA, CUSTOM
688
698
  // - no special handling needed in chat activity
689
699
  break
@@ -726,10 +736,32 @@ class TextEngine<
726
736
  this.earlyTermination = true
727
737
  }
728
738
 
739
+ private finalizeCurrentThinkingStep(): void {
740
+ if (this.currentThinkingContent) {
741
+ this.accumulatedThinking.push({
742
+ content: this.currentThinkingContent,
743
+ ...(this.currentThinkingSignature && {
744
+ signature: this.currentThinkingSignature,
745
+ }),
746
+ })
747
+ this.currentThinkingContent = ''
748
+ this.currentThinkingSignature = ''
749
+ }
750
+ }
751
+
752
+ private handleStepStartedEvent(): void {
753
+ this.finalizeCurrentThinkingStep()
754
+ }
755
+
729
756
  private handleStepFinishedEvent(
730
- _chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
757
+ chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
731
758
  ): void {
732
- // State tracking for STEP_FINISHED is handled by middleware
759
+ if (chunk.delta) {
760
+ this.currentThinkingContent += chunk.delta
761
+ }
762
+ if (chunk.signature) {
763
+ this.currentThinkingSignature = chunk.signature
764
+ }
733
765
  }
734
766
 
735
767
  private async *checkForPendingToolCalls(): AsyncGenerator<
@@ -1057,12 +1089,17 @@ class TextEngine<
1057
1089
  }
1058
1090
 
1059
1091
  private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
1092
+ this.finalizeCurrentThinkingStep()
1093
+
1060
1094
  this.messages = [
1061
1095
  ...this.messages,
1062
1096
  {
1063
1097
  role: 'assistant',
1064
1098
  content: this.accumulatedContent || null,
1065
1099
  toolCalls,
1100
+ ...(this.accumulatedThinking.length > 0 && {
1101
+ thinking: this.accumulatedThinking,
1102
+ }),
1066
1103
  },
1067
1104
  ]
1068
1105
  }
@@ -1213,6 +1250,7 @@ class TextEngine<
1213
1250
  timestamp: Date.now(),
1214
1251
  model: finishEvent.model,
1215
1252
  toolCallId: result.toolCallId,
1253
+ toolCallName: result.toolName,
1216
1254
  toolName: result.toolName,
1217
1255
  } as StreamChunk)
1218
1256
 
@@ -1248,14 +1286,39 @@ class TextEngine<
1248
1286
  role: 'tool',
1249
1287
  } as StreamChunk)
1250
1288
 
1251
- this.messages = [
1252
- ...this.messages,
1253
- {
1254
- role: 'tool',
1255
- content,
1256
- toolCallId: result.toolCallId,
1257
- },
1258
- ]
1289
+ // If a placeholder tool message exists for this toolCallId (created by
1290
+ // uiMessageToModelMessages for an approval-responded part with no
1291
+ // output yet), replace it with the real result. Otherwise the LLM sees
1292
+ // both messages — and since the Anthropic adapter dedupes tool_result
1293
+ // blocks by tool_use_id keeping the first match, the placeholder wins
1294
+ // and the real result is dropped (see issue #532).
1295
+ const placeholderIdx = this.messages.findIndex((m) => {
1296
+ if (m.role !== 'tool' || m.toolCallId !== result.toolCallId) {
1297
+ return false
1298
+ }
1299
+ if (typeof m.content !== 'string') return false
1300
+ try {
1301
+ return JSON.parse(m.content)?.pendingExecution === true
1302
+ } catch {
1303
+ return false
1304
+ }
1305
+ })
1306
+
1307
+ const newToolMessage: ModelMessage = {
1308
+ role: 'tool',
1309
+ content,
1310
+ toolCallId: result.toolCallId,
1311
+ }
1312
+
1313
+ if (placeholderIdx >= 0) {
1314
+ this.messages = [
1315
+ ...this.messages.slice(0, placeholderIdx),
1316
+ newToolMessage,
1317
+ ...this.messages.slice(placeholderIdx + 1),
1318
+ ]
1319
+ } else {
1320
+ this.messages = [...this.messages, newToolMessage]
1321
+ }
1259
1322
  }
1260
1323
 
1261
1324
  return chunks
@@ -165,6 +165,7 @@ function isToolCallIncluded(part: ToolCallPart): boolean {
165
165
  function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
166
166
  const messageList: Array<ModelMessage> = []
167
167
  let current = createSegment()
168
+ let pendingThinking: Array<{ content: string; signature?: string }> = []
168
169
 
169
170
  // Track emitted tool result IDs to avoid duplicates.
170
171
  // A tool call can have BOTH an explicit tool-result part AND an output
@@ -181,7 +182,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
181
182
  role: 'assistant',
182
183
  content,
183
184
  ...(hasToolCalls && { toolCalls: current.toolCalls }),
185
+ ...(pendingThinking.length > 0 && { thinking: pendingThinking }),
184
186
  })
187
+ pendingThinking = []
185
188
  }
186
189
  current = createSegment()
187
190
  }
@@ -227,7 +230,15 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
227
230
  }
228
231
  break
229
232
 
230
- // thinking parts are skipped - they're UI-only
233
+ case 'thinking':
234
+ if (part.content) {
235
+ pendingThinking.push({
236
+ content: part.content,
237
+ ...(part.signature && { signature: part.signature }),
238
+ })
239
+ }
240
+ break
241
+
231
242
  default:
232
243
  break
233
244
  }
@@ -306,6 +317,17 @@ export function modelMessageToUIMessage(
306
317
  ): UIMessage {
307
318
  const parts: Array<MessagePart> = []
308
319
 
320
+ if (modelMessage.role === 'assistant' && modelMessage.thinking?.length) {
321
+ for (const thinking of modelMessage.thinking) {
322
+ if (!thinking.content) continue
323
+ parts.push({
324
+ type: 'thinking',
325
+ content: thinking.content,
326
+ ...(thinking.signature && { signature: thinking.signature }),
327
+ })
328
+ }
329
+ }
330
+
309
331
  // Handle tool results (when role is "tool") - only produce tool-result part,
310
332
  // not a text part (the content IS the tool result, not display text)
311
333
  if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
@@ -244,12 +244,15 @@ export function updateToolCallApprovalResponse(
244
244
  }
245
245
 
246
246
  /**
247
- * Update or add a thinking part to a message.
247
+ * Update or add a thinking part to a message, keyed by stepId.
248
+ * Each distinct stepId produces its own ThinkingPart.
248
249
  */
249
250
  export function updateThinkingPart(
250
251
  messages: Array<UIMessage>,
251
252
  messageId: string,
253
+ stepId: string,
252
254
  content: string,
255
+ signature?: string,
253
256
  ): Array<UIMessage> {
254
257
  return messages.map((msg) => {
255
258
  if (msg.id !== messageId) {
@@ -257,15 +260,19 @@ export function updateThinkingPart(
257
260
  }
258
261
 
259
262
  const parts = [...msg.parts]
260
- const thinkingPartIndex = parts.findIndex((p) => p.type === 'thinking')
263
+ const thinkingPartIndex = parts.findIndex(
264
+ (p) => p.type === 'thinking' && p.stepId === stepId,
265
+ )
261
266
 
262
267
  const thinkingPart: ThinkingPart = {
263
268
  type: 'thinking',
264
269
  content,
270
+ stepId,
271
+ ...(signature && { signature }),
265
272
  }
266
273
 
267
274
  if (thinkingPartIndex >= 0) {
268
- // Update existing thinking part
275
+ // Update existing thinking part for this step
269
276
  parts[thinkingPartIndex] = thinkingPart
270
277
  } else {
271
278
  // Add new thinking part at the end (preserve natural streaming order)
@@ -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,13 @@ 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
901
938
 
902
939
  const newToolCall: InternalToolCallState = {
903
940
  id: chunk.toolCallId,
@@ -1155,11 +1192,45 @@ export class StreamProcessor {
1155
1192
  this.events.onError?.(new Error(errorMessage))
1156
1193
  }
1157
1194
 
1195
+ /**
1196
+ * Handle STEP_STARTED event (for thinking/reasoning content).
1197
+ *
1198
+ * Records the stepId so that subsequent STEP_FINISHED deltas accumulate
1199
+ * into their own ThinkingPart. Does not create a message — the message
1200
+ * is lazily created when the first STEP_FINISHED content arrives.
1201
+ */
1202
+ private handleStepStartedEvent(
1203
+ chunk: Extract<StreamChunk, { type: 'STEP_STARTED' }>,
1204
+ ): void {
1205
+ const stepId = chunk.stepId ?? generateMessageId()
1206
+ const activeId = this.getActiveAssistantMessageId()
1207
+ if (activeId) {
1208
+ const state = this.getMessageState(activeId)
1209
+ if (state) {
1210
+ state.currentThinkingStepId = stepId
1211
+ if (!state.thinkingSteps.has(stepId)) {
1212
+ state.thinkingSteps.set(stepId, '')
1213
+ state.thinkingStepOrder.push(stepId)
1214
+ }
1215
+ // Clear any pending stepId from a prior STEP_STARTED that fired
1216
+ // before the assistant message existed. Now that we're tracking
1217
+ // the step directly on message state, the pending value is stale
1218
+ // and must not leak into the next STEP_FINISHED (which would
1219
+ // misattribute its delta to the stale step).
1220
+ this.pendingThinkingStepId = null
1221
+ return
1222
+ }
1223
+ }
1224
+
1225
+ // No active message yet — defer until ensureAssistantMessage in STEP_FINISHED
1226
+ this.pendingThinkingStepId = stepId
1227
+ }
1228
+
1158
1229
  /**
1159
1230
  * Handle STEP_FINISHED event (for thinking/reasoning content).
1160
1231
  *
1161
- * Accumulates delta into thinkingContent and updates a single ThinkingPart
1162
- * in the UIMessage (replaced in-place, not appended).
1232
+ * Accumulates delta into the current thinking step's content and updates
1233
+ * the corresponding ThinkingPart in the UIMessage.
1163
1234
  *
1164
1235
  * @see docs/chat-architecture.md#thinkingreasoning-content — Thinking flow
1165
1236
  */
@@ -1175,10 +1246,38 @@ export class StreamProcessor {
1175
1246
  // REASONING_MESSAGE_CONTENT events for this message, skip the duplicate
1176
1247
  // thinking content from STEP_FINISHED to avoid doubled content.
1177
1248
  if (state.hasSeenReasoningEvents) {
1249
+ if (chunk.signature) {
1250
+ const stepId = state.currentThinkingStepId ?? chunk.stepId
1251
+ if (!stepId) return
1252
+ const thinking = state.thinkingSteps.get(stepId)
1253
+ if (thinking !== undefined) {
1254
+ state.thinkingStepSignatures.set(stepId, chunk.signature)
1255
+ this.messages = updateThinkingPart(
1256
+ this.messages,
1257
+ messageId,
1258
+ stepId,
1259
+ thinking,
1260
+ chunk.signature,
1261
+ )
1262
+ this.emitMessagesChange()
1263
+ }
1264
+ }
1178
1265
  return
1179
1266
  }
1180
1267
 
1181
- const previous = state.thinkingContent
1268
+ this.consumePendingThinkingStep(state)
1269
+
1270
+ const stepId =
1271
+ state.currentThinkingStepId ?? chunk.stepId ?? generateMessageId()
1272
+
1273
+ // Auto-initialize if no prior STEP_STARTED (backward compat)
1274
+ if (!state.thinkingSteps.has(stepId)) {
1275
+ state.thinkingSteps.set(stepId, '')
1276
+ state.thinkingStepOrder.push(stepId)
1277
+ state.currentThinkingStepId = stepId
1278
+ }
1279
+
1280
+ const previous = state.thinkingSteps.get(stepId)!
1182
1281
  let nextThinking = previous
1183
1282
 
1184
1283
  // Prefer delta over content
@@ -1194,25 +1293,31 @@ export class StreamProcessor {
1194
1293
  }
1195
1294
  }
1196
1295
 
1197
- state.thinkingContent = nextThinking
1296
+ state.thinkingSteps.set(stepId, nextThinking)
1297
+
1298
+ if (chunk.signature) {
1299
+ state.thinkingStepSignatures.set(stepId, chunk.signature)
1300
+ }
1198
1301
 
1199
1302
  // Update UIMessage
1200
1303
  this.messages = updateThinkingPart(
1201
1304
  this.messages,
1202
1305
  messageId,
1203
- state.thinkingContent,
1306
+ stepId,
1307
+ nextThinking,
1308
+ state.thinkingStepSignatures.get(stepId),
1204
1309
  )
1205
1310
  this.emitMessagesChange()
1206
1311
 
1207
1312
  // Emit granular event
1208
- this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
1313
+ this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
1209
1314
  }
1210
1315
 
1211
1316
  /**
1212
1317
  * Handle REASONING_MESSAGE_CONTENT event (AG-UI reasoning protocol).
1213
1318
  *
1214
- * Accumulates reasoning delta into thinkingContent and updates the ThinkingPart
1215
- * in the UIMessage.
1319
+ * Accumulates reasoning delta into thinking content and updates the
1320
+ * corresponding ThinkingPart in the UIMessage.
1216
1321
  */
1217
1322
  private handleReasoningMessageContentEvent(
1218
1323
  chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
@@ -1223,16 +1328,29 @@ export class StreamProcessor {
1223
1328
 
1224
1329
  state.hasSeenReasoningEvents = true
1225
1330
  const delta = chunk.delta || ''
1226
- state.thinkingContent = state.thinkingContent + delta
1331
+
1332
+ this.consumePendingThinkingStep(state)
1333
+
1334
+ const stepId = state.currentThinkingStepId ?? chunk.messageId
1335
+ if (!state.thinkingSteps.has(stepId)) {
1336
+ state.thinkingSteps.set(stepId, '')
1337
+ state.thinkingStepOrder.push(stepId)
1338
+ state.currentThinkingStepId = stepId
1339
+ }
1340
+
1341
+ const nextThinking = (state.thinkingSteps.get(stepId) ?? '') + delta
1342
+ state.thinkingSteps.set(stepId, nextThinking)
1227
1343
 
1228
1344
  this.messages = updateThinkingPart(
1229
1345
  this.messages,
1230
1346
  messageId,
1231
- state.thinkingContent,
1347
+ stepId,
1348
+ nextThinking,
1349
+ state.thinkingStepSignatures.get(stepId),
1232
1350
  )
1233
1351
  this.emitMessagesChange()
1234
1352
 
1235
- this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
1353
+ this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
1236
1354
  }
1237
1355
 
1238
1356
  /**
@@ -1518,7 +1636,9 @@ export class StreamProcessor {
1518
1636
 
1519
1637
  for (const state of this.messageStates.values()) {
1520
1638
  content += state.totalTextContent
1521
- thinking += state.thinkingContent
1639
+ for (const stepId of state.thinkingStepOrder) {
1640
+ thinking += state.thinkingSteps.get(stepId) ?? ''
1641
+ }
1522
1642
  }
1523
1643
 
1524
1644
  return {
@@ -1540,7 +1660,9 @@ export class StreamProcessor {
1540
1660
 
1541
1661
  for (const state of this.messageStates.values()) {
1542
1662
  content += state.totalTextContent
1543
- thinking += state.thinkingContent
1663
+ for (const stepId of state.thinkingStepOrder) {
1664
+ thinking += state.thinkingSteps.get(stepId) ?? ''
1665
+ }
1544
1666
  for (const [id, tc] of state.toolCalls) {
1545
1667
  toolCalls.set(id, tc)
1546
1668
  }
@@ -1586,6 +1708,7 @@ export class StreamProcessor {
1586
1708
  this.activeRuns.clear()
1587
1709
  this.toolCallToMessage.clear()
1588
1710
  this.pendingManualMessageId = null
1711
+ this.pendingThinkingStepId = null
1589
1712
  this.finishReason = null
1590
1713
  this.hasError = false
1591
1714
  this.isDone = false
@@ -56,8 +56,11 @@ export interface MessageStreamState {
56
56
  totalTextContent: string
57
57
  currentSegmentText: string
58
58
  lastEmittedText: string
59
- thinkingContent: string
60
59
  hasSeenReasoningEvents: boolean
60
+ thinkingSteps: Map<string, string>
61
+ thinkingStepSignatures: Map<string, string>
62
+ thinkingStepOrder: Array<string>
63
+ currentThinkingStepId: string | null
61
64
  toolCalls: Map<string, InternalToolCallState>
62
65
  toolCallOrder: Array<string>
63
66
  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',
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared error-narrowing helper for activities that convert thrown values
3
+ * into structured `RUN_ERROR` events.
4
+ *
5
+ * Accepts Error instances, objects with string-ish `message`/`code`, or bare
6
+ * strings; always returns a shape safe to serialize. Never leaks the full
7
+ * error object (which may carry request/response state from an SDK).
8
+ */
9
+ export function toRunErrorPayload(
10
+ error: unknown,
11
+ fallbackMessage = 'Unknown error occurred',
12
+ ): { message: string; code: string | undefined } {
13
+ if (error instanceof Error) {
14
+ const codeField = (error as Error & { code?: unknown }).code
15
+ return {
16
+ message: error.message || fallbackMessage,
17
+ code: typeof codeField === 'string' ? codeField : undefined,
18
+ }
19
+ }
20
+ if (typeof error === 'object' && error !== null) {
21
+ const messageField = (error as { message?: unknown }).message
22
+ const codeField = (error as { code?: unknown }).code
23
+ return {
24
+ message:
25
+ typeof messageField === 'string' && messageField.length > 0
26
+ ? messageField
27
+ : fallbackMessage,
28
+ code: typeof codeField === 'string' ? codeField : undefined,
29
+ }
30
+ }
31
+ if (typeof error === 'string' && error.length > 0) {
32
+ return { message: error, code: undefined }
33
+ }
34
+ return { message: fallbackMessage, code: undefined }
35
+ }
@@ -0,0 +1,89 @@
1
+ import type { AudioGenerationOptions, AudioGenerationResult } from '../../types'
2
+
3
+ /**
4
+ * Configuration for audio generation adapter instances
5
+ */
6
+ export interface AudioAdapterConfig {
7
+ apiKey?: string
8
+ baseUrl?: string
9
+ timeout?: number
10
+ maxRetries?: number
11
+ headers?: Record<string, string>
12
+ }
13
+
14
+ /**
15
+ * Audio generation adapter interface with pre-resolved generics.
16
+ *
17
+ * An adapter is created by a provider function: `provider('model')` → `adapter`
18
+ * All type resolution happens at the provider call site, not in this interface.
19
+ *
20
+ * Generic parameters:
21
+ * - TModel: The specific model name (e.g., 'fal-ai/diffrhythm')
22
+ * - TProviderOptions: Provider-specific options (already resolved)
23
+ */
24
+ export interface AudioAdapter<
25
+ TModel extends string = string,
26
+ TProviderOptions extends object = Record<string, unknown>,
27
+ > {
28
+ /** Discriminator for adapter kind - used to determine API shape */
29
+ readonly kind: 'audio'
30
+ /** Adapter name identifier */
31
+ readonly name: string
32
+ /** The model this adapter is configured for */
33
+ readonly model: TModel
34
+
35
+ /**
36
+ * @internal Type-only properties for inference. Not assigned at runtime.
37
+ */
38
+ '~types': {
39
+ providerOptions: TProviderOptions
40
+ }
41
+
42
+ /**
43
+ * Generate audio from a text prompt
44
+ */
45
+ generateAudio: (
46
+ options: AudioGenerationOptions<TProviderOptions>,
47
+ ) => Promise<AudioGenerationResult>
48
+ }
49
+
50
+ /**
51
+ * An AudioAdapter with any/unknown type parameters.
52
+ * Useful as a constraint in generic functions and interfaces.
53
+ */
54
+ export type AnyAudioAdapter = AudioAdapter<any, any>
55
+
56
+ /**
57
+ * Abstract base class for audio generation adapters.
58
+ * Extend this class to implement an audio adapter for a specific provider.
59
+ *
60
+ * Generic parameters match AudioAdapter - all pre-resolved by the provider function.
61
+ */
62
+ export abstract class BaseAudioAdapter<
63
+ TModel extends string = string,
64
+ TProviderOptions extends object = Record<string, unknown>,
65
+ > implements AudioAdapter<TModel, TProviderOptions> {
66
+ readonly kind = 'audio' as const
67
+ abstract readonly name: string
68
+ readonly model: TModel
69
+
70
+ // Type-only property - never assigned at runtime
71
+ declare '~types': {
72
+ providerOptions: TProviderOptions
73
+ }
74
+
75
+ protected config: AudioAdapterConfig
76
+
77
+ constructor(model: TModel, config: AudioAdapterConfig = {}) {
78
+ this.config = config
79
+ this.model = model
80
+ }
81
+
82
+ abstract generateAudio(
83
+ options: AudioGenerationOptions<TProviderOptions>,
84
+ ): Promise<AudioGenerationResult>
85
+
86
+ protected generateId(): string {
87
+ return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`
88
+ }
89
+ }