@tanstack/ai 0.47.3 → 0.48.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 +2 -1
- package/dist/esm/activities/chat/adapter.d.ts +5 -4
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +4 -7
- package/dist/esm/activities/chat/index.js +201 -243
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +115 -25
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +38 -17
- package/dist/esm/activities/chat/stream/processor.js +186 -101
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +3 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +15 -10
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +6 -6
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +7 -8
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +56 -53
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/client.d.ts +8 -1
- package/dist/esm/client.js +7 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +6 -2
- package/dist/esm/middlewares/otel.js +11 -4
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/stream-to-response.js +8 -5
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.js +4 -2
- package/dist/esm/stream-to-websocket.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.d.ts +10 -13
- package/dist/esm/strip-to-spec-middleware.js +24 -22
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/types.d.ts +82 -305
- package/dist/esm/utilities/adapter-yield-chunk.d.ts +31 -0
- package/dist/esm/utilities/ag-ui-usage.d.ts +24 -0
- package/dist/esm/utilities/ag-ui-usage.js +66 -0
- package/dist/esm/utilities/ag-ui-usage.js.map +1 -0
- package/dist/esm/utilities/ag-ui-wire.d.ts +14 -7
- package/dist/esm/utilities/ag-ui-wire.js +71 -30
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +3 -3
- package/dist/esm/utilities/chat-params.js +10 -30
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/chunk-ids.d.ts +5 -0
- package/dist/esm/utilities/chunk-ids.js +25 -0
- package/dist/esm/utilities/chunk-ids.js.map +1 -0
- package/dist/esm/utilities/merge-metadata.d.ts +14 -0
- package/dist/esm/utilities/merge-metadata.js +43 -0
- package/dist/esm/utilities/merge-metadata.js.map +1 -0
- package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
- package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
- package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
- package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
- package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
- package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
- package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
- package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
- package/dist/esm/utilities/spec-event-keys.js +56 -0
- package/dist/esm/utilities/spec-event-keys.js.map +1 -0
- package/dist/esm/utilities/structured-output-events.d.ts +3 -3
- package/dist/esm/utilities/structured-output-events.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/chat/adapter.ts +4 -4
- package/src/activities/chat/index.ts +398 -400
- package/src/activities/chat/messages.ts +174 -34
- package/src/activities/chat/stream/processor.ts +289 -218
- package/src/activities/chat/tools/tool-calls.ts +23 -22
- package/src/activities/generateVideo/index.ts +7 -6
- package/src/activities/stream-generation-result.ts +8 -12
- package/src/activities/summarize/chat-stream-summarize.ts +94 -70
- package/src/adapter-internals.ts +2 -0
- package/src/client.ts +13 -0
- package/src/index.ts +6 -0
- package/src/middlewares/otel.ts +16 -8
- package/src/stream-to-response.ts +11 -4
- package/src/stream-to-websocket.ts +3 -1
- package/src/strip-to-spec-middleware.ts +48 -24
- package/src/types.ts +109 -393
- package/src/utilities/adapter-yield-chunk.ts +30 -0
- package/src/utilities/ag-ui-usage.test.ts +194 -0
- package/src/utilities/ag-ui-usage.ts +148 -0
- package/src/utilities/ag-ui-wire.ts +149 -31
- package/src/utilities/chat-params.ts +22 -39
- package/src/utilities/chunk-ids.ts +24 -0
- package/src/utilities/merge-metadata.test.ts +117 -0
- package/src/utilities/merge-metadata.ts +59 -0
- package/src/utilities/normalize-stream-chunk.test.ts +423 -0
- package/src/utilities/normalize-stream-chunk.ts +186 -0
- package/src/utilities/reasoning-encrypted-value.ts +18 -0
- package/src/utilities/restore-inbound-chunk.test.ts +133 -0
- package/src/utilities/restore-inbound-chunk.ts +72 -0
- package/src/utilities/spec-event-keys.test.ts +34 -0
- package/src/utilities/spec-event-keys.ts +74 -0
- package/src/utilities/structured-output-events.ts +3 -3
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
} from '../messages.js'
|
|
25
25
|
import { runErrorEventToError } from '../../../utilities/errors'
|
|
26
26
|
import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
|
|
27
|
+
import {
|
|
28
|
+
mergeMetadata,
|
|
29
|
+
tanstackMetadata,
|
|
30
|
+
} from '../../../utilities/merge-metadata'
|
|
31
|
+
import { getChunkRunId } from '../../../utilities/chunk-ids'
|
|
32
|
+
import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
|
|
27
33
|
import { normalizeToolResult } from '../../../utilities/tool-result'
|
|
28
34
|
import { defaultJSONParser } from './json-parser'
|
|
29
35
|
import {
|
|
@@ -56,6 +62,7 @@ import type {
|
|
|
56
62
|
MessagePart,
|
|
57
63
|
ModelMessage,
|
|
58
64
|
StreamChunk,
|
|
65
|
+
ThinkingPart,
|
|
59
66
|
ToolCall,
|
|
60
67
|
ToolCallPart,
|
|
61
68
|
ToolResultPart,
|
|
@@ -239,6 +246,7 @@ export class StreamProcessor {
|
|
|
239
246
|
*
|
|
240
247
|
* @param content - The message content (string or array of content parts)
|
|
241
248
|
* @param id - Optional custom message ID (generated if not provided)
|
|
249
|
+
* @param metadata - Optional AG-UI metadata bag
|
|
242
250
|
* @returns The created UIMessage
|
|
243
251
|
*
|
|
244
252
|
* @example
|
|
@@ -256,7 +264,11 @@ export class StreamProcessor {
|
|
|
256
264
|
* processor.addUserMessage('Hello!', 'custom-id-123')
|
|
257
265
|
* ```
|
|
258
266
|
*/
|
|
259
|
-
addUserMessage(
|
|
267
|
+
addUserMessage(
|
|
268
|
+
content: string | Array<ContentPart>,
|
|
269
|
+
id?: string,
|
|
270
|
+
metadata?: UIMessage['metadata'],
|
|
271
|
+
): UIMessage {
|
|
260
272
|
// Convert content to message parts
|
|
261
273
|
const parts: Array<MessagePart> =
|
|
262
274
|
typeof content === 'string'
|
|
@@ -271,6 +283,7 @@ export class StreamProcessor {
|
|
|
271
283
|
role: 'user',
|
|
272
284
|
parts,
|
|
273
285
|
createdAt: new Date(),
|
|
286
|
+
...(metadata != null ? { metadata } : {}),
|
|
274
287
|
}
|
|
275
288
|
|
|
276
289
|
this.messages = [...this.messages, userMessage]
|
|
@@ -623,6 +636,12 @@ export class StreamProcessor {
|
|
|
623
636
|
)
|
|
624
637
|
break
|
|
625
638
|
|
|
639
|
+
case 'REASONING_ENCRYPTED_VALUE':
|
|
640
|
+
this.handleReasoningEncryptedValueEvent(
|
|
641
|
+
chunk as Extract<StreamChunk, { type: 'REASONING_ENCRYPTED_VALUE' }>,
|
|
642
|
+
)
|
|
643
|
+
break
|
|
644
|
+
|
|
626
645
|
case 'TOOL_CALL_RESULT':
|
|
627
646
|
this.handleToolCallResultEvent(
|
|
628
647
|
chunk as Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
|
|
@@ -784,6 +803,55 @@ export class StreamProcessor {
|
|
|
784
803
|
// Event Handlers
|
|
785
804
|
// ============================================
|
|
786
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Merge event metadata onto a UIMessage. `tanstack` is deep-merged so a
|
|
808
|
+
* later delta does not wipe `tanstack.model`. High-frequency leftover
|
|
809
|
+
* keys (`content`, `args`) never stamp onto the message.
|
|
810
|
+
* Rebuilds `createdAt` when `tanstack.createdAt` is an ISO string.
|
|
811
|
+
*/
|
|
812
|
+
private mergeMessageMetadata(messageId: string, incoming: unknown): void {
|
|
813
|
+
if (
|
|
814
|
+
incoming == null ||
|
|
815
|
+
typeof incoming !== 'object' ||
|
|
816
|
+
Array.isArray(incoming)
|
|
817
|
+
) {
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
const message = this.messages.find((msg) => msg.id === messageId)
|
|
821
|
+
if (!message) return
|
|
822
|
+
|
|
823
|
+
const incomingRecord = incoming as NonNullable<UIMessage['metadata']>
|
|
824
|
+
const incomingTanstack = tanstackMetadata(incomingRecord)
|
|
825
|
+
const toMerge =
|
|
826
|
+
incomingTanstack != null &&
|
|
827
|
+
('content' in incomingTanstack || 'args' in incomingTanstack)
|
|
828
|
+
? {
|
|
829
|
+
...incomingRecord,
|
|
830
|
+
tanstack: Object.fromEntries(
|
|
831
|
+
Object.entries(incomingTanstack).filter(
|
|
832
|
+
([key]) => key !== 'content' && key !== 'args',
|
|
833
|
+
),
|
|
834
|
+
),
|
|
835
|
+
}
|
|
836
|
+
: incomingRecord
|
|
837
|
+
const metadata = mergeMetadata(message.metadata, toMerge)
|
|
838
|
+
const createdAtRaw = tanstackMetadata(incomingRecord)?.createdAt
|
|
839
|
+
const createdAt =
|
|
840
|
+
typeof createdAtRaw === 'string' ? new Date(createdAtRaw) : undefined
|
|
841
|
+
const createdAtValid =
|
|
842
|
+
createdAt !== undefined && !Number.isNaN(createdAt.getTime())
|
|
843
|
+
this.messages = this.messages.map((msg) =>
|
|
844
|
+
msg.id === messageId
|
|
845
|
+
? {
|
|
846
|
+
...msg,
|
|
847
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
848
|
+
...(createdAtValid ? { createdAt } : {}),
|
|
849
|
+
}
|
|
850
|
+
: msg,
|
|
851
|
+
)
|
|
852
|
+
this.emitMessagesChange()
|
|
853
|
+
}
|
|
854
|
+
|
|
787
855
|
/**
|
|
788
856
|
* Handle TEXT_MESSAGE_START event
|
|
789
857
|
*/
|
|
@@ -837,6 +905,7 @@ export class StreamProcessor {
|
|
|
837
905
|
this.activeMessageIds.add(messageId)
|
|
838
906
|
}
|
|
839
907
|
|
|
908
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
840
909
|
this.emitMessagesChange()
|
|
841
910
|
return
|
|
842
911
|
}
|
|
@@ -862,6 +931,7 @@ export class StreamProcessor {
|
|
|
862
931
|
existingState.hasToolCallsSinceTextStart = false
|
|
863
932
|
}
|
|
864
933
|
}
|
|
934
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
865
935
|
return
|
|
866
936
|
}
|
|
867
937
|
|
|
@@ -877,6 +947,7 @@ export class StreamProcessor {
|
|
|
877
947
|
this.createMessageState(messageId, uiRole)
|
|
878
948
|
this.activeMessageIds.add(messageId)
|
|
879
949
|
|
|
950
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
880
951
|
this.events.onStreamStart?.()
|
|
881
952
|
this.emitMessagesChange()
|
|
882
953
|
}
|
|
@@ -888,6 +959,7 @@ export class StreamProcessor {
|
|
|
888
959
|
chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_END' }>,
|
|
889
960
|
): void {
|
|
890
961
|
const { messageId } = chunk
|
|
962
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
891
963
|
const state = this.getMessageState(messageId)
|
|
892
964
|
if (!state) return
|
|
893
965
|
if (state.isComplete) return
|
|
@@ -921,8 +993,19 @@ export class StreamProcessor {
|
|
|
921
993
|
// unreconstructable metadata, reconcile the normalized snapshot against
|
|
922
994
|
// the pre-snapshot state; see `reconcileSnapshotToolCalls`.
|
|
923
995
|
const prevMessages = this.messages
|
|
924
|
-
const
|
|
925
|
-
|
|
996
|
+
const prevById = new Map(prevMessages.map((msg) => [msg.id, msg]))
|
|
997
|
+
const normalized = this.mergeReasoningFanOut(
|
|
998
|
+
chunk.messages.map(aguiSnapshotMessageToUIMessage),
|
|
999
|
+
)
|
|
1000
|
+
this.messages = this.reconcileSnapshotToolCalls(
|
|
1001
|
+
normalized,
|
|
1002
|
+
prevMessages,
|
|
1003
|
+
).map((msg) => {
|
|
1004
|
+
if (msg.metadata != null) return msg
|
|
1005
|
+
const prev = prevById.get(msg.id)
|
|
1006
|
+
if (prev?.metadata == null) return msg
|
|
1007
|
+
return { ...msg, metadata: prev.metadata }
|
|
1008
|
+
})
|
|
926
1009
|
this.emitMessagesChange()
|
|
927
1010
|
}
|
|
928
1011
|
|
|
@@ -949,6 +1032,54 @@ export class StreamProcessor {
|
|
|
949
1032
|
* matching `tool-call` (and prefer pre-snapshot complete/output when the
|
|
950
1033
|
* snapshot is poorer) so server tools keep the same UI shape as client tools.
|
|
951
1034
|
*/
|
|
1035
|
+
/**
|
|
1036
|
+
* Wire order is reasoning fan-outs, then the assistant anchor.
|
|
1037
|
+
* Snapshot conversion turns each reasoning row into its own assistant
|
|
1038
|
+
* message. Fold leading thinking-only messages into the next real
|
|
1039
|
+
* assistant. Do not fold into a tool-result-only message (`role: 'tool'`
|
|
1040
|
+
* on the wire). `reconcileSnapshotToolCalls` anchors those results.
|
|
1041
|
+
*/
|
|
1042
|
+
private mergeReasoningFanOut(messages: Array<UIMessage>): Array<UIMessage> {
|
|
1043
|
+
const out: Array<UIMessage> = []
|
|
1044
|
+
let pending: Array<UIMessage> = []
|
|
1045
|
+
const thinkingParts = (msg: UIMessage) =>
|
|
1046
|
+
msg.parts.filter((part): part is ThinkingPart => part.type === 'thinking')
|
|
1047
|
+
const isThinkingOnly = (msg: UIMessage) =>
|
|
1048
|
+
msg.role === 'assistant' &&
|
|
1049
|
+
msg.parts.length > 0 &&
|
|
1050
|
+
msg.parts.every((part) => part.type === 'thinking')
|
|
1051
|
+
const isToolResultOnly = (msg: UIMessage) =>
|
|
1052
|
+
msg.role === 'assistant' &&
|
|
1053
|
+
msg.parts.length === 1 &&
|
|
1054
|
+
msg.parts[0]?.type === 'tool-result'
|
|
1055
|
+
const flushPending = () => {
|
|
1056
|
+
out.push(...pending)
|
|
1057
|
+
pending = []
|
|
1058
|
+
}
|
|
1059
|
+
for (const msg of messages) {
|
|
1060
|
+
if (isThinkingOnly(msg)) {
|
|
1061
|
+
pending.push(msg)
|
|
1062
|
+
continue
|
|
1063
|
+
}
|
|
1064
|
+
if (
|
|
1065
|
+
msg.role === 'assistant' &&
|
|
1066
|
+
pending.length > 0 &&
|
|
1067
|
+
!isToolResultOnly(msg)
|
|
1068
|
+
) {
|
|
1069
|
+
out.push({
|
|
1070
|
+
...msg,
|
|
1071
|
+
parts: [...pending.flatMap(thinkingParts), ...msg.parts],
|
|
1072
|
+
})
|
|
1073
|
+
pending = []
|
|
1074
|
+
continue
|
|
1075
|
+
}
|
|
1076
|
+
flushPending()
|
|
1077
|
+
out.push(msg)
|
|
1078
|
+
}
|
|
1079
|
+
flushPending()
|
|
1080
|
+
return out
|
|
1081
|
+
}
|
|
1082
|
+
|
|
952
1083
|
private reconcileSnapshotToolCalls(
|
|
953
1084
|
snapshot: Array<UIMessage>,
|
|
954
1085
|
prevMessages: Array<UIMessage>,
|
|
@@ -1145,30 +1276,10 @@ export class StreamProcessor {
|
|
|
1145
1276
|
chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
|
|
1146
1277
|
): void {
|
|
1147
1278
|
const { messageId, state } = this.ensureAssistantMessage(chunk.messageId)
|
|
1279
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
1148
1280
|
|
|
1149
1281
|
if (this.structuredMessageIds.has(messageId)) {
|
|
1150
|
-
|
|
1151
|
-
// (mirrors what the plain-text branch handles below). Reconcile against
|
|
1152
|
-
// the existing raw buffer so adapters that emit cumulative content
|
|
1153
|
-
// don't duplicate the JSON.
|
|
1154
|
-
let delta = chunk.delta || ''
|
|
1155
|
-
if (delta === '' && chunk.content !== undefined && chunk.content !== '') {
|
|
1156
|
-
const existingRaw = (
|
|
1157
|
-
this.messages
|
|
1158
|
-
.find((m) => m.id === messageId)
|
|
1159
|
-
?.parts.find(
|
|
1160
|
-
(p): p is Extract<MessagePart, { type: 'structured-output' }> =>
|
|
1161
|
-
p.type === 'structured-output',
|
|
1162
|
-
) ?? { raw: '' }
|
|
1163
|
-
).raw
|
|
1164
|
-
if (chunk.content.startsWith(existingRaw)) {
|
|
1165
|
-
delta = chunk.content.slice(existingRaw.length)
|
|
1166
|
-
} else if (existingRaw.startsWith(chunk.content)) {
|
|
1167
|
-
delta = ''
|
|
1168
|
-
} else {
|
|
1169
|
-
delta = chunk.content
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1282
|
+
const delta = chunk.delta || ''
|
|
1172
1283
|
if (delta !== '') {
|
|
1173
1284
|
this.messages = appendStructuredOutputDelta(
|
|
1174
1285
|
this.messages,
|
|
@@ -1202,31 +1313,15 @@ export class StreamProcessor {
|
|
|
1202
1313
|
}
|
|
1203
1314
|
|
|
1204
1315
|
const currentText = state.currentSegmentText
|
|
1205
|
-
let nextText = currentText
|
|
1206
|
-
|
|
1207
|
-
// Prefer delta over content - delta is the incremental change
|
|
1208
|
-
// Normalize to empty string to avoid "undefined" string concatenation
|
|
1209
1316
|
const delta = chunk.delta || ''
|
|
1210
|
-
|
|
1211
|
-
nextText = currentText + delta
|
|
1212
|
-
} else if (chunk.content !== undefined && chunk.content !== '') {
|
|
1213
|
-
// Fallback: use content if delta is not provided
|
|
1214
|
-
if (chunk.content.startsWith(currentText)) {
|
|
1215
|
-
nextText = chunk.content
|
|
1216
|
-
} else if (currentText.startsWith(chunk.content)) {
|
|
1217
|
-
nextText = currentText
|
|
1218
|
-
} else {
|
|
1219
|
-
nextText = currentText + chunk.content
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1317
|
+
const nextText = delta !== '' ? currentText + delta : currentText
|
|
1222
1318
|
|
|
1223
1319
|
// Calculate the delta for totalTextContent
|
|
1224
1320
|
const textDelta = nextText.slice(currentText.length)
|
|
1225
1321
|
state.currentSegmentText = nextText
|
|
1226
1322
|
state.totalTextContent += textDelta
|
|
1227
1323
|
|
|
1228
|
-
|
|
1229
|
-
const chunkPortion = chunk.delta || chunk.content || ''
|
|
1324
|
+
const chunkPortion = chunk.delta || ''
|
|
1230
1325
|
const shouldEmit = this.chunkStrategy.shouldEmit(
|
|
1231
1326
|
chunkPortion,
|
|
1232
1327
|
state.currentSegmentText,
|
|
@@ -1269,13 +1364,7 @@ export class StreamProcessor {
|
|
|
1269
1364
|
// New tool call starting
|
|
1270
1365
|
const initialState: ToolCallState = 'awaiting-input'
|
|
1271
1366
|
|
|
1272
|
-
|
|
1273
|
-
// in types.ts). Accept either so chunks from older code paths or any
|
|
1274
|
-
// adapter that only sets the deprecated field still produce a named part.
|
|
1275
|
-
// The type marks both as required strings, but in practice some emitters
|
|
1276
|
-
// only set one — fall back via the runtime value rather than the type.
|
|
1277
|
-
const toolName =
|
|
1278
|
-
(chunk as { toolCallName?: string }).toolCallName ?? chunk.toolName
|
|
1367
|
+
const toolName = chunk.toolCallName
|
|
1279
1368
|
|
|
1280
1369
|
// Capture provider metadata that arrived on TOOL_CALL_START so it
|
|
1281
1370
|
// round-trips back through the assistant message on the next turn
|
|
@@ -1288,7 +1377,7 @@ export class StreamProcessor {
|
|
|
1288
1377
|
arguments: '',
|
|
1289
1378
|
state: initialState,
|
|
1290
1379
|
parsedArguments: undefined,
|
|
1291
|
-
index:
|
|
1380
|
+
index: state.toolCalls.size,
|
|
1292
1381
|
...(chunkMetadata !== undefined && { metadata: chunkMetadata }),
|
|
1293
1382
|
}
|
|
1294
1383
|
|
|
@@ -1377,17 +1466,15 @@ export class StreamProcessor {
|
|
|
1377
1466
|
}
|
|
1378
1467
|
|
|
1379
1468
|
/**
|
|
1380
|
-
* Handle TOOL_CALL_END event —
|
|
1381
|
-
*
|
|
1382
|
-
* This event has a DUAL ROLE:
|
|
1383
|
-
* - Without `result`: Signals arguments are done (from adapter). Transitions to input-complete.
|
|
1384
|
-
* - With `result`: Signals tool was executed and result is available (from TextEngine).
|
|
1385
|
-
* Creates both output on the tool-call part AND a tool-result part.
|
|
1469
|
+
* Handle TOOL_CALL_END event — arguments are finalized (input-complete).
|
|
1470
|
+
* Tool output arrives on TOOL_CALL_RESULT, not on this event.
|
|
1386
1471
|
*
|
|
1387
|
-
* If `input
|
|
1388
|
-
*
|
|
1472
|
+
* If TOOL_CALL_END carries parsed `input`, use it as the canonical arguments:
|
|
1473
|
+
* back-fill the accumulated string when no TOOL_CALL_ARGS deltas were seen
|
|
1474
|
+
* (adapters that deliver the whole input on END — e.g. Anthropic
|
|
1475
|
+
* server_tool_use / web_search — issue #839) and override the rendered part's
|
|
1476
|
+
* `input` with the canonical value.
|
|
1389
1477
|
*
|
|
1390
|
-
* @see docs/chat-architecture.md#tool-results-and-the-tool_call_end-dual-role — Full explanation
|
|
1391
1478
|
* @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
|
|
1392
1479
|
*/
|
|
1393
1480
|
private handleToolCallEndEvent(
|
|
@@ -1399,15 +1486,22 @@ export class StreamProcessor {
|
|
|
1399
1486
|
const msgState = this.getMessageState(messageId)
|
|
1400
1487
|
if (!msgState) return
|
|
1401
1488
|
|
|
1489
|
+
// The parsed input can ride on the spec `input` field or, for adapters
|
|
1490
|
+
// that only stamp it into TanStack metadata, on `metadata.tanstack.input`.
|
|
1491
|
+
const input =
|
|
1492
|
+
chunk.input !== undefined
|
|
1493
|
+
? chunk.input
|
|
1494
|
+
: (tanstackMetadata(chunk)?.input as unknown)
|
|
1495
|
+
|
|
1402
1496
|
// Transition the tool call to input-complete (the authoritative completion signal)
|
|
1403
1497
|
const existingToolCall = msgState.toolCalls.get(chunk.toolCallId)
|
|
1404
1498
|
if (existingToolCall && existingToolCall.state !== 'input-complete') {
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
if (
|
|
1499
|
+
// Back-fill the arguments string from the parsed input when no
|
|
1500
|
+
// TOOL_CALL_ARGS deltas were received, so completeToolCall's strict parse
|
|
1501
|
+
// surfaces the correct value on the ToolCallPart.
|
|
1502
|
+
if (input !== undefined && !existingToolCall.arguments) {
|
|
1409
1503
|
try {
|
|
1410
|
-
existingToolCall.arguments = JSON.stringify(
|
|
1504
|
+
existingToolCall.arguments = JSON.stringify(input)
|
|
1411
1505
|
} catch {
|
|
1412
1506
|
// circular refs, BigInt, etc. — leave arguments empty rather than
|
|
1413
1507
|
// aborting stream processing
|
|
@@ -1416,20 +1510,18 @@ export class StreamProcessor {
|
|
|
1416
1510
|
|
|
1417
1511
|
const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
|
|
1418
1512
|
this.completeToolCall(messageId, index, existingToolCall)
|
|
1419
|
-
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
//
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
if (chunk.input !== undefined) {
|
|
1426
|
-
existingToolCall.parsedArguments = chunk.input
|
|
1513
|
+
|
|
1514
|
+
// Canonicalize on the parsed input: overrides the accumulated-args parse
|
|
1515
|
+
// that completeToolCall wrote (adapters may coerce values differently
|
|
1516
|
+
// between streamed args and the final structured input).
|
|
1517
|
+
if (input !== undefined) {
|
|
1518
|
+
existingToolCall.parsedArguments = input
|
|
1427
1519
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
1428
1520
|
id: existingToolCall.id,
|
|
1429
1521
|
name: existingToolCall.name,
|
|
1430
1522
|
arguments: existingToolCall.arguments,
|
|
1431
1523
|
state: 'input-complete',
|
|
1432
|
-
input
|
|
1524
|
+
input,
|
|
1433
1525
|
...(existingToolCall.metadata !== undefined && {
|
|
1434
1526
|
metadata: existingToolCall.metadata,
|
|
1435
1527
|
}),
|
|
@@ -1437,43 +1529,6 @@ export class StreamProcessor {
|
|
|
1437
1529
|
this.emitMessagesChange()
|
|
1438
1530
|
}
|
|
1439
1531
|
}
|
|
1440
|
-
|
|
1441
|
-
// Update UIMessage if there's a result
|
|
1442
|
-
if (chunk.result) {
|
|
1443
|
-
// Step 1: Update the tool-call part's output field (for UI consistency
|
|
1444
|
-
// with client tools — see GitHub issue #176)
|
|
1445
|
-
let output: unknown
|
|
1446
|
-
if (Array.isArray(chunk.result)) {
|
|
1447
|
-
output = chunk.result
|
|
1448
|
-
} else {
|
|
1449
|
-
try {
|
|
1450
|
-
output = JSON.parse(chunk.result)
|
|
1451
|
-
} catch {
|
|
1452
|
-
output = chunk.result
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
this.messages = updateToolCallWithOutput(
|
|
1456
|
-
this.messages,
|
|
1457
|
-
chunk.toolCallId,
|
|
1458
|
-
output,
|
|
1459
|
-
chunk.state === 'output-error' ? 'error' : undefined,
|
|
1460
|
-
)
|
|
1461
|
-
|
|
1462
|
-
// Step 2: Create/update the tool-result part (for LLM conversation history)
|
|
1463
|
-
const resultState: ToolResultState =
|
|
1464
|
-
chunk.state === 'output-error' ? 'error' : 'complete'
|
|
1465
|
-
this.messages = updateToolResultPart(
|
|
1466
|
-
this.messages,
|
|
1467
|
-
messageId,
|
|
1468
|
-
chunk.toolCallId,
|
|
1469
|
-
chunk.result,
|
|
1470
|
-
resultState,
|
|
1471
|
-
resultState === 'error'
|
|
1472
|
-
? this.extractToolResultError(output)
|
|
1473
|
-
: undefined,
|
|
1474
|
-
)
|
|
1475
|
-
this.emitMessagesChange()
|
|
1476
|
-
}
|
|
1477
1532
|
}
|
|
1478
1533
|
|
|
1479
1534
|
private extractToolResultError(output: unknown): string {
|
|
@@ -1498,9 +1553,27 @@ export class StreamProcessor {
|
|
|
1498
1553
|
private handleToolCallResultEvent(
|
|
1499
1554
|
chunk: Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
|
|
1500
1555
|
): void {
|
|
1501
|
-
|
|
1556
|
+
// A resume stream delivers TOOL_CALL_RESULT for a tool call that was
|
|
1557
|
+
// started in a PRIOR run. A preceding MESSAGES_SNAPSHOT resets stream state
|
|
1558
|
+
// (clearing `toolCallToMessage`), so fall back to locating the message that
|
|
1559
|
+
// owns the tool-call part — matching `addToolResult`/`handleInterrupts`.
|
|
1560
|
+
// Without this the result is dropped, the follow-up request omits the tool
|
|
1561
|
+
// message, and the still-"pending" tool call re-interrupts (issue #532).
|
|
1562
|
+
const messageId =
|
|
1563
|
+
this.toolCallToMessage.get(chunk.toolCallId) ??
|
|
1564
|
+
this.messages.find((m) =>
|
|
1565
|
+
m.parts.some(
|
|
1566
|
+
(p): p is ToolCallPart =>
|
|
1567
|
+
p.type === 'tool-call' && p.id === chunk.toolCallId,
|
|
1568
|
+
),
|
|
1569
|
+
)?.id
|
|
1502
1570
|
if (!messageId) return
|
|
1503
1571
|
|
|
1572
|
+
const extra = chunk as AdapterYieldChunk
|
|
1573
|
+
const isOutputError =
|
|
1574
|
+
extra.state === 'output-error' ||
|
|
1575
|
+
tanstackMetadata(chunk)?.state === 'output-error'
|
|
1576
|
+
|
|
1504
1577
|
// Step 1: Update the tool-call part's output field
|
|
1505
1578
|
let output: unknown
|
|
1506
1579
|
try {
|
|
@@ -1512,12 +1585,11 @@ export class StreamProcessor {
|
|
|
1512
1585
|
this.messages,
|
|
1513
1586
|
chunk.toolCallId,
|
|
1514
1587
|
output,
|
|
1515
|
-
|
|
1588
|
+
isOutputError ? 'error' : undefined,
|
|
1516
1589
|
)
|
|
1517
1590
|
|
|
1518
1591
|
// Step 2: Create/update the tool-result part
|
|
1519
|
-
const resultState: ToolResultState =
|
|
1520
|
-
chunk.state === 'output-error' ? 'error' : 'complete'
|
|
1592
|
+
const resultState: ToolResultState = isOutputError ? 'error' : 'complete'
|
|
1521
1593
|
this.messages = updateToolResultPart(
|
|
1522
1594
|
this.messages,
|
|
1523
1595
|
messageId,
|
|
@@ -1554,7 +1626,11 @@ export class StreamProcessor {
|
|
|
1554
1626
|
private handleRunFinishedEvent(
|
|
1555
1627
|
chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
|
|
1556
1628
|
): void {
|
|
1557
|
-
|
|
1629
|
+
const extra = chunk as AdapterYieldChunk
|
|
1630
|
+
this.finishReason =
|
|
1631
|
+
extra.finishReason !== undefined
|
|
1632
|
+
? extra.finishReason
|
|
1633
|
+
: (tanstackMetadata(chunk)?.finishReason ?? null)
|
|
1558
1634
|
this.activeRuns.delete(chunk.runId)
|
|
1559
1635
|
|
|
1560
1636
|
if (chunk.outcome?.type === 'interrupt') {
|
|
@@ -1647,10 +1723,7 @@ export class StreamProcessor {
|
|
|
1647
1723
|
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
1648
1724
|
): void {
|
|
1649
1725
|
this.hasError = true
|
|
1650
|
-
const runId =
|
|
1651
|
-
'runId' in chunk && typeof chunk.runId === 'string'
|
|
1652
|
-
? chunk.runId
|
|
1653
|
-
: undefined
|
|
1726
|
+
const runId = getChunkRunId(chunk)
|
|
1654
1727
|
if (runId) {
|
|
1655
1728
|
this.activeRuns.delete(runId)
|
|
1656
1729
|
} else {
|
|
@@ -1660,9 +1733,8 @@ export class StreamProcessor {
|
|
|
1660
1733
|
// Prefer spec field `message`; fall back to deprecated `error.message`.
|
|
1661
1734
|
// If neither is set, the chunk still carries debug context (provider
|
|
1662
1735
|
// error codes, request ids, etc.) — log it so the failure isn't silent.
|
|
1663
|
-
const errorMessage =
|
|
1664
|
-
|
|
1665
|
-
if (!chunk.message && !chunk.error?.message) {
|
|
1736
|
+
const errorMessage = chunk.message || 'An error occurred'
|
|
1737
|
+
if (!chunk.message) {
|
|
1666
1738
|
console.error(
|
|
1667
1739
|
'[StreamProcessor] RUN_ERROR with no message; original chunk:',
|
|
1668
1740
|
chunk,
|
|
@@ -1691,14 +1763,14 @@ export class StreamProcessor {
|
|
|
1691
1763
|
/**
|
|
1692
1764
|
* Handle STEP_STARTED event (for thinking/reasoning content).
|
|
1693
1765
|
*
|
|
1694
|
-
* Records the stepId so
|
|
1766
|
+
* Records the stepId so later REASONING_MESSAGE_CONTENT deltas accumulate
|
|
1695
1767
|
* into their own ThinkingPart. Does not create a message — the message
|
|
1696
|
-
* is lazily created when the first
|
|
1768
|
+
* is lazily created when the first REASONING_MESSAGE_CONTENT arrives.
|
|
1697
1769
|
*/
|
|
1698
1770
|
private handleStepStartedEvent(
|
|
1699
1771
|
chunk: Extract<StreamChunk, { type: 'STEP_STARTED' }>,
|
|
1700
1772
|
): void {
|
|
1701
|
-
const stepId = chunk.
|
|
1773
|
+
const stepId = chunk.stepName || generateMessageId()
|
|
1702
1774
|
const activeId = this.getActiveAssistantMessageId()
|
|
1703
1775
|
if (activeId) {
|
|
1704
1776
|
const state = this.getMessageState(activeId)
|
|
@@ -1711,102 +1783,49 @@ export class StreamProcessor {
|
|
|
1711
1783
|
// Clear any pending stepId from a prior STEP_STARTED that fired
|
|
1712
1784
|
// before the assistant message existed. Now that we're tracking
|
|
1713
1785
|
// the step directly on message state, the pending value is stale
|
|
1714
|
-
// and must not leak into the next
|
|
1715
|
-
// misattribute its delta to the stale step).
|
|
1786
|
+
// and must not leak into the next REASONING_MESSAGE_CONTENT.
|
|
1716
1787
|
this.pendingThinkingStepId = null
|
|
1717
1788
|
return
|
|
1718
1789
|
}
|
|
1719
1790
|
}
|
|
1720
1791
|
|
|
1721
|
-
// No active message yet — defer until ensureAssistantMessage in
|
|
1792
|
+
// No active message yet — defer until ensureAssistantMessage in
|
|
1793
|
+
// REASONING_MESSAGE_CONTENT
|
|
1722
1794
|
this.pendingThinkingStepId = stepId
|
|
1723
1795
|
}
|
|
1724
1796
|
|
|
1725
1797
|
/**
|
|
1726
|
-
* Handle STEP_FINISHED event
|
|
1798
|
+
* Handle STEP_FINISHED event.
|
|
1727
1799
|
*
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1800
|
+
* Thinking *content* comes from REASONING_MESSAGE_CONTENT, not STEP_FINISHED.
|
|
1801
|
+
* But some adapters (e.g. BytePlus thinking-summary) carry the provider
|
|
1802
|
+
* signature blob ONLY on the STEP_FINISHED event, so still extract that here
|
|
1803
|
+
* and attach it to the thinking step the reasoning events already built.
|
|
1732
1804
|
*/
|
|
1733
1805
|
private handleStepFinishedEvent(
|
|
1734
1806
|
chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
1735
1807
|
): void {
|
|
1808
|
+
const extra = chunk as AdapterYieldChunk
|
|
1809
|
+
const signature = extra.signature
|
|
1810
|
+
if (!signature) return
|
|
1811
|
+
|
|
1736
1812
|
const { messageId, state } = this.ensureAssistantMessage(
|
|
1737
1813
|
this.getActiveAssistantMessageId() ?? undefined,
|
|
1738
1814
|
)
|
|
1815
|
+
const stepId = state.currentThinkingStepId ?? extra.stepId
|
|
1816
|
+
if (!stepId) return
|
|
1817
|
+
const thinking = state.thinkingSteps.get(stepId)
|
|
1818
|
+
if (thinking === undefined) return
|
|
1739
1819
|
|
|
1740
|
-
|
|
1741
|
-
// REASONING_MESSAGE_CONTENT with the same delta. If we've already processed
|
|
1742
|
-
// REASONING_MESSAGE_CONTENT events for this message, skip the duplicate
|
|
1743
|
-
// thinking content from STEP_FINISHED to avoid doubled content.
|
|
1744
|
-
if (state.hasSeenReasoningEvents) {
|
|
1745
|
-
if (chunk.signature) {
|
|
1746
|
-
const stepId = state.currentThinkingStepId ?? chunk.stepId
|
|
1747
|
-
if (!stepId) return
|
|
1748
|
-
const thinking = state.thinkingSteps.get(stepId)
|
|
1749
|
-
if (thinking !== undefined) {
|
|
1750
|
-
state.thinkingStepSignatures.set(stepId, chunk.signature)
|
|
1751
|
-
this.messages = updateThinkingPart(
|
|
1752
|
-
this.messages,
|
|
1753
|
-
messageId,
|
|
1754
|
-
stepId,
|
|
1755
|
-
thinking,
|
|
1756
|
-
chunk.signature,
|
|
1757
|
-
)
|
|
1758
|
-
this.emitMessagesChange()
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
return
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
this.consumePendingThinkingStep(state)
|
|
1765
|
-
|
|
1766
|
-
const stepId =
|
|
1767
|
-
state.currentThinkingStepId ?? chunk.stepId ?? generateMessageId()
|
|
1768
|
-
|
|
1769
|
-
// Auto-initialize if no prior STEP_STARTED (backward compat)
|
|
1770
|
-
if (!state.thinkingSteps.has(stepId)) {
|
|
1771
|
-
state.thinkingSteps.set(stepId, '')
|
|
1772
|
-
state.thinkingStepOrder.push(stepId)
|
|
1773
|
-
state.currentThinkingStepId = stepId
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
const previous = state.thinkingSteps.get(stepId) ?? ''
|
|
1777
|
-
let nextThinking = previous
|
|
1778
|
-
|
|
1779
|
-
// Prefer delta over content
|
|
1780
|
-
if (chunk.delta && chunk.delta !== '') {
|
|
1781
|
-
nextThinking = previous + chunk.delta
|
|
1782
|
-
} else if (chunk.content && chunk.content !== '') {
|
|
1783
|
-
if (chunk.content.startsWith(previous)) {
|
|
1784
|
-
nextThinking = chunk.content
|
|
1785
|
-
} else if (previous.startsWith(chunk.content)) {
|
|
1786
|
-
nextThinking = previous
|
|
1787
|
-
} else {
|
|
1788
|
-
nextThinking = previous + chunk.content
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
state.thinkingSteps.set(stepId, nextThinking)
|
|
1793
|
-
|
|
1794
|
-
if (chunk.signature) {
|
|
1795
|
-
state.thinkingStepSignatures.set(stepId, chunk.signature)
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// Update UIMessage
|
|
1820
|
+
state.thinkingStepSignatures.set(stepId, signature)
|
|
1799
1821
|
this.messages = updateThinkingPart(
|
|
1800
1822
|
this.messages,
|
|
1801
1823
|
messageId,
|
|
1802
1824
|
stepId,
|
|
1803
|
-
|
|
1804
|
-
|
|
1825
|
+
thinking,
|
|
1826
|
+
signature,
|
|
1805
1827
|
)
|
|
1806
1828
|
this.emitMessagesChange()
|
|
1807
|
-
|
|
1808
|
-
// Emit granular event
|
|
1809
|
-
this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
|
|
1810
1829
|
}
|
|
1811
1830
|
|
|
1812
1831
|
/**
|
|
@@ -1849,6 +1868,74 @@ export class StreamProcessor {
|
|
|
1849
1868
|
this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
|
|
1850
1869
|
}
|
|
1851
1870
|
|
|
1871
|
+
/**
|
|
1872
|
+
* Attach a provider signature blob from REASONING_ENCRYPTED_VALUE.
|
|
1873
|
+
* `subtype: 'message'` updates ThinkingPart.signature.
|
|
1874
|
+
* `subtype: 'tool-call'` stores Gemini thoughtSignature on the tool-call part.
|
|
1875
|
+
*/
|
|
1876
|
+
private handleReasoningEncryptedValueEvent(
|
|
1877
|
+
chunk: Extract<StreamChunk, { type: 'REASONING_ENCRYPTED_VALUE' }>,
|
|
1878
|
+
): void {
|
|
1879
|
+
const encryptedValue = chunk.encryptedValue
|
|
1880
|
+
if (typeof encryptedValue !== 'string' || encryptedValue === '') return
|
|
1881
|
+
|
|
1882
|
+
if (chunk.subtype === 'tool-call') {
|
|
1883
|
+
this.attachToolCallSignature(chunk.entityId, encryptedValue)
|
|
1884
|
+
return
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
const { messageId, state } = this.ensureAssistantMessage(
|
|
1888
|
+
this.getActiveAssistantMessageId() ?? undefined,
|
|
1889
|
+
)
|
|
1890
|
+
const stepId = state.currentThinkingStepId ?? chunk.entityId
|
|
1891
|
+
state.thinkingStepSignatures.set(stepId, encryptedValue)
|
|
1892
|
+
const content = state.thinkingSteps.get(stepId) ?? ''
|
|
1893
|
+
if (!state.thinkingSteps.has(stepId)) {
|
|
1894
|
+
state.thinkingSteps.set(stepId, content)
|
|
1895
|
+
state.thinkingStepOrder.push(stepId)
|
|
1896
|
+
}
|
|
1897
|
+
this.messages = updateThinkingPart(
|
|
1898
|
+
this.messages,
|
|
1899
|
+
messageId,
|
|
1900
|
+
stepId,
|
|
1901
|
+
content,
|
|
1902
|
+
encryptedValue,
|
|
1903
|
+
)
|
|
1904
|
+
this.emitMessagesChange()
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
private attachToolCallSignature(
|
|
1908
|
+
toolCallId: string,
|
|
1909
|
+
thoughtSignature: string,
|
|
1910
|
+
): void {
|
|
1911
|
+
this.messages = this.messages.map((msg) => {
|
|
1912
|
+
let changed = false
|
|
1913
|
+
const parts = msg.parts.map((part) => {
|
|
1914
|
+
if (part.type !== 'tool-call' || part.id !== toolCallId) return part
|
|
1915
|
+
changed = true
|
|
1916
|
+
return {
|
|
1917
|
+
...part,
|
|
1918
|
+
metadata: {
|
|
1919
|
+
...(part.metadata != null && typeof part.metadata === 'object'
|
|
1920
|
+
? part.metadata
|
|
1921
|
+
: {}),
|
|
1922
|
+
thoughtSignature,
|
|
1923
|
+
},
|
|
1924
|
+
}
|
|
1925
|
+
})
|
|
1926
|
+
return changed ? { ...msg, parts } : msg
|
|
1927
|
+
})
|
|
1928
|
+
for (const state of this.messageStates.values()) {
|
|
1929
|
+
const call = state.toolCalls.get(toolCallId)
|
|
1930
|
+
if (!call) continue
|
|
1931
|
+
call.metadata = {
|
|
1932
|
+
...(call.metadata ?? {}),
|
|
1933
|
+
thoughtSignature,
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
this.emitMessagesChange()
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1852
1939
|
/**
|
|
1853
1940
|
* Handle CUSTOM event.
|
|
1854
1941
|
*
|
|
@@ -2022,22 +2109,10 @@ export class StreamProcessor {
|
|
|
2022
2109
|
* Detect if an incoming content chunk represents a NEW text segment
|
|
2023
2110
|
*/
|
|
2024
2111
|
private isNewTextSegment(
|
|
2025
|
-
|
|
2026
|
-
|
|
2112
|
+
_chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
|
|
2113
|
+
_previous: string,
|
|
2027
2114
|
): boolean {
|
|
2028
|
-
|
|
2029
|
-
if (chunk.content !== undefined) {
|
|
2030
|
-
if (chunk.content.length < previous.length) {
|
|
2031
|
-
return true
|
|
2032
|
-
}
|
|
2033
|
-
if (
|
|
2034
|
-
!chunk.content.startsWith(previous) &&
|
|
2035
|
-
!previous.startsWith(chunk.content)
|
|
2036
|
-
) {
|
|
2037
|
-
return true
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
return false
|
|
2115
|
+
return true
|
|
2041
2116
|
}
|
|
2042
2117
|
|
|
2043
2118
|
/**
|
|
@@ -2111,11 +2186,7 @@ export class StreamProcessor {
|
|
|
2111
2186
|
}
|
|
2112
2187
|
|
|
2113
2188
|
// Update UIMessage. The arguments are complete now, so surface the parsed
|
|
2114
|
-
// input on the part
|
|
2115
|
-
// string was back-filled from TOOL_CALL_END.input, so this parse matches
|
|
2116
|
-
// the canonical input. If a TOOL_CALL_END.input diverges from the
|
|
2117
|
-
// accumulated args, handleToolCallEndEvent re-updates the part with the
|
|
2118
|
-
// canonical value after this call.
|
|
2189
|
+
// input on the part from the accumulated TOOL_CALL_ARGS deltas.
|
|
2119
2190
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
2120
2191
|
id: toolCall.id,
|
|
2121
2192
|
name: toolCall.name,
|