@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.
- package/dist/esm/activities/chat/adapter.d.ts +6 -3
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.js +61 -9
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +26 -3
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +4 -2
- package/dist/esm/activities/chat/stream/message-updaters.js +10 -4
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +21 -5
- package/dist/esm/activities/chat/stream/processor.js +119 -19
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/types.d.ts +9 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +3 -4
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +75 -0
- package/dist/esm/middlewares/otel.js +624 -0
- package/dist/esm/middlewares/otel.js.map +1 -0
- package/dist/esm/types.d.ts +23 -7
- package/package.json +15 -2
- package/src/activities/chat/adapter.ts +8 -2
- package/src/activities/chat/index.ts +74 -11
- package/src/activities/chat/messages.ts +29 -1
- package/src/activities/chat/stream/message-updaters.ts +18 -3
- package/src/activities/chat/stream/processor.ts +152 -17
- package/src/activities/chat/stream/types.ts +9 -1
- package/src/activities/chat/tools/tool-calls.ts +4 -4
- package/src/middlewares/index.ts +5 -0
- package/src/middlewares/otel.ts +861 -0
- 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?: (
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
1162
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1354
|
+
stepId,
|
|
1355
|
+
nextThinking,
|
|
1356
|
+
state.thinkingStepSignatures.get(stepId),
|
|
1232
1357
|
)
|
|
1233
1358
|
this.emitMessagesChange()
|
|
1234
1359
|
|
|
1235
|
-
this.events.onThinkingUpdate?.(messageId,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
105
|
-
providerMetadata: event.providerMetadata,
|
|
106
|
-
}),
|
|
106
|
+
...(event.metadata !== undefined && { metadata: event.metadata }),
|
|
107
107
|
})
|
|
108
108
|
}
|
|
109
109
|
|
package/src/middlewares/index.ts
CHANGED
|
@@ -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).
|