@tanstack/ai 0.47.2 → 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 +126 -31
- 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 +197 -110
- 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 +59 -54
- 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 +12 -5
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/stream-to-response.js +13 -6
- 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/errors.d.ts +9 -0
- package/dist/esm/utilities/errors.js +13 -1
- package/dist/esm/utilities/errors.js.map +1 -1
- 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 +3 -3
- package/src/activities/chat/adapter.ts +4 -4
- package/src/activities/chat/index.ts +398 -400
- package/src/activities/chat/messages.ts +183 -36
- package/src/activities/chat/stream/processor.ts +313 -239
- 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 +98 -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 +16 -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/errors.ts +23 -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
|
@@ -22,8 +22,15 @@ import {
|
|
|
22
22
|
generateMessageId,
|
|
23
23
|
uiMessageToModelMessages,
|
|
24
24
|
} from '../messages.js'
|
|
25
|
-
import {
|
|
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'
|
|
33
|
+
import { normalizeToolResult } from '../../../utilities/tool-result'
|
|
27
34
|
import { defaultJSONParser } from './json-parser'
|
|
28
35
|
import {
|
|
29
36
|
appendStructuredOutputDelta,
|
|
@@ -55,6 +62,7 @@ import type {
|
|
|
55
62
|
MessagePart,
|
|
56
63
|
ModelMessage,
|
|
57
64
|
StreamChunk,
|
|
65
|
+
ThinkingPart,
|
|
58
66
|
ToolCall,
|
|
59
67
|
ToolCallPart,
|
|
60
68
|
ToolResultPart,
|
|
@@ -238,6 +246,7 @@ export class StreamProcessor {
|
|
|
238
246
|
*
|
|
239
247
|
* @param content - The message content (string or array of content parts)
|
|
240
248
|
* @param id - Optional custom message ID (generated if not provided)
|
|
249
|
+
* @param metadata - Optional AG-UI metadata bag
|
|
241
250
|
* @returns The created UIMessage
|
|
242
251
|
*
|
|
243
252
|
* @example
|
|
@@ -255,7 +264,11 @@ export class StreamProcessor {
|
|
|
255
264
|
* processor.addUserMessage('Hello!', 'custom-id-123')
|
|
256
265
|
* ```
|
|
257
266
|
*/
|
|
258
|
-
addUserMessage(
|
|
267
|
+
addUserMessage(
|
|
268
|
+
content: string | Array<ContentPart>,
|
|
269
|
+
id?: string,
|
|
270
|
+
metadata?: UIMessage['metadata'],
|
|
271
|
+
): UIMessage {
|
|
259
272
|
// Convert content to message parts
|
|
260
273
|
const parts: Array<MessagePart> =
|
|
261
274
|
typeof content === 'string'
|
|
@@ -270,6 +283,7 @@ export class StreamProcessor {
|
|
|
270
283
|
role: 'user',
|
|
271
284
|
parts,
|
|
272
285
|
createdAt: new Date(),
|
|
286
|
+
...(metadata != null ? { metadata } : {}),
|
|
273
287
|
}
|
|
274
288
|
|
|
275
289
|
this.messages = [...this.messages, userMessage]
|
|
@@ -622,6 +636,12 @@ export class StreamProcessor {
|
|
|
622
636
|
)
|
|
623
637
|
break
|
|
624
638
|
|
|
639
|
+
case 'REASONING_ENCRYPTED_VALUE':
|
|
640
|
+
this.handleReasoningEncryptedValueEvent(
|
|
641
|
+
chunk as Extract<StreamChunk, { type: 'REASONING_ENCRYPTED_VALUE' }>,
|
|
642
|
+
)
|
|
643
|
+
break
|
|
644
|
+
|
|
625
645
|
case 'TOOL_CALL_RESULT':
|
|
626
646
|
this.handleToolCallResultEvent(
|
|
627
647
|
chunk as Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
|
|
@@ -783,6 +803,55 @@ export class StreamProcessor {
|
|
|
783
803
|
// Event Handlers
|
|
784
804
|
// ============================================
|
|
785
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
|
+
|
|
786
855
|
/**
|
|
787
856
|
* Handle TEXT_MESSAGE_START event
|
|
788
857
|
*/
|
|
@@ -819,6 +888,15 @@ export class StreamProcessor {
|
|
|
819
888
|
// Update activeMessageIds
|
|
820
889
|
this.activeMessageIds.delete(pendingId)
|
|
821
890
|
this.activeMessageIds.add(messageId)
|
|
891
|
+
|
|
892
|
+
// TOOL_CALL_ARGS/END route through toolCallToMessage. Keep those
|
|
893
|
+
// entries on the remapped id so later args still accumulate
|
|
894
|
+
// (interleaved text can arrive as a full START/CONTENT/END block).
|
|
895
|
+
for (const [toolCallId, mappedMessageId] of this.toolCallToMessage) {
|
|
896
|
+
if (mappedMessageId === pendingId) {
|
|
897
|
+
this.toolCallToMessage.set(toolCallId, messageId)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
822
900
|
}
|
|
823
901
|
|
|
824
902
|
// Ensure state exists
|
|
@@ -827,6 +905,7 @@ export class StreamProcessor {
|
|
|
827
905
|
this.activeMessageIds.add(messageId)
|
|
828
906
|
}
|
|
829
907
|
|
|
908
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
830
909
|
this.emitMessagesChange()
|
|
831
910
|
return
|
|
832
911
|
}
|
|
@@ -852,6 +931,7 @@ export class StreamProcessor {
|
|
|
852
931
|
existingState.hasToolCallsSinceTextStart = false
|
|
853
932
|
}
|
|
854
933
|
}
|
|
934
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
855
935
|
return
|
|
856
936
|
}
|
|
857
937
|
|
|
@@ -867,6 +947,7 @@ export class StreamProcessor {
|
|
|
867
947
|
this.createMessageState(messageId, uiRole)
|
|
868
948
|
this.activeMessageIds.add(messageId)
|
|
869
949
|
|
|
950
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
870
951
|
this.events.onStreamStart?.()
|
|
871
952
|
this.emitMessagesChange()
|
|
872
953
|
}
|
|
@@ -878,6 +959,7 @@ export class StreamProcessor {
|
|
|
878
959
|
chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_END' }>,
|
|
879
960
|
): void {
|
|
880
961
|
const { messageId } = chunk
|
|
962
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
881
963
|
const state = this.getMessageState(messageId)
|
|
882
964
|
if (!state) return
|
|
883
965
|
if (state.isComplete) return
|
|
@@ -886,9 +968,6 @@ export class StreamProcessor {
|
|
|
886
968
|
if (state.currentSegmentText !== state.lastEmittedText) {
|
|
887
969
|
this.emitTextUpdateForMessage(messageId)
|
|
888
970
|
}
|
|
889
|
-
|
|
890
|
-
// Complete all tool calls for this message
|
|
891
|
-
this.completeAllToolCallsForMessage(messageId)
|
|
892
971
|
}
|
|
893
972
|
|
|
894
973
|
/**
|
|
@@ -914,8 +993,19 @@ export class StreamProcessor {
|
|
|
914
993
|
// unreconstructable metadata, reconcile the normalized snapshot against
|
|
915
994
|
// the pre-snapshot state; see `reconcileSnapshotToolCalls`.
|
|
916
995
|
const prevMessages = this.messages
|
|
917
|
-
const
|
|
918
|
-
|
|
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
|
+
})
|
|
919
1009
|
this.emitMessagesChange()
|
|
920
1010
|
}
|
|
921
1011
|
|
|
@@ -942,6 +1032,54 @@ export class StreamProcessor {
|
|
|
942
1032
|
* matching `tool-call` (and prefer pre-snapshot complete/output when the
|
|
943
1033
|
* snapshot is poorer) so server tools keep the same UI shape as client tools.
|
|
944
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
|
+
|
|
945
1083
|
private reconcileSnapshotToolCalls(
|
|
946
1084
|
snapshot: Array<UIMessage>,
|
|
947
1085
|
prevMessages: Array<UIMessage>,
|
|
@@ -1138,33 +1276,10 @@ export class StreamProcessor {
|
|
|
1138
1276
|
chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
|
|
1139
1277
|
): void {
|
|
1140
1278
|
const { messageId, state } = this.ensureAssistantMessage(chunk.messageId)
|
|
1141
|
-
|
|
1142
|
-
// Content arriving means all current tool calls for this message are complete
|
|
1143
|
-
this.completeAllToolCallsForMessage(messageId)
|
|
1279
|
+
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
1144
1280
|
|
|
1145
1281
|
if (this.structuredMessageIds.has(messageId)) {
|
|
1146
|
-
|
|
1147
|
-
// (mirrors what the plain-text branch handles below). Reconcile against
|
|
1148
|
-
// the existing raw buffer so adapters that emit cumulative content
|
|
1149
|
-
// don't duplicate the JSON.
|
|
1150
|
-
let delta = chunk.delta || ''
|
|
1151
|
-
if (delta === '' && chunk.content !== undefined && chunk.content !== '') {
|
|
1152
|
-
const existingRaw = (
|
|
1153
|
-
this.messages
|
|
1154
|
-
.find((m) => m.id === messageId)
|
|
1155
|
-
?.parts.find(
|
|
1156
|
-
(p): p is Extract<MessagePart, { type: 'structured-output' }> =>
|
|
1157
|
-
p.type === 'structured-output',
|
|
1158
|
-
) ?? { raw: '' }
|
|
1159
|
-
).raw
|
|
1160
|
-
if (chunk.content.startsWith(existingRaw)) {
|
|
1161
|
-
delta = chunk.content.slice(existingRaw.length)
|
|
1162
|
-
} else if (existingRaw.startsWith(chunk.content)) {
|
|
1163
|
-
delta = ''
|
|
1164
|
-
} else {
|
|
1165
|
-
delta = chunk.content
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1282
|
+
const delta = chunk.delta || ''
|
|
1168
1283
|
if (delta !== '') {
|
|
1169
1284
|
this.messages = appendStructuredOutputDelta(
|
|
1170
1285
|
this.messages,
|
|
@@ -1198,31 +1313,15 @@ export class StreamProcessor {
|
|
|
1198
1313
|
}
|
|
1199
1314
|
|
|
1200
1315
|
const currentText = state.currentSegmentText
|
|
1201
|
-
let nextText = currentText
|
|
1202
|
-
|
|
1203
|
-
// Prefer delta over content - delta is the incremental change
|
|
1204
|
-
// Normalize to empty string to avoid "undefined" string concatenation
|
|
1205
1316
|
const delta = chunk.delta || ''
|
|
1206
|
-
|
|
1207
|
-
nextText = currentText + delta
|
|
1208
|
-
} else if (chunk.content !== undefined && chunk.content !== '') {
|
|
1209
|
-
// Fallback: use content if delta is not provided
|
|
1210
|
-
if (chunk.content.startsWith(currentText)) {
|
|
1211
|
-
nextText = chunk.content
|
|
1212
|
-
} else if (currentText.startsWith(chunk.content)) {
|
|
1213
|
-
nextText = currentText
|
|
1214
|
-
} else {
|
|
1215
|
-
nextText = currentText + chunk.content
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1317
|
+
const nextText = delta !== '' ? currentText + delta : currentText
|
|
1218
1318
|
|
|
1219
1319
|
// Calculate the delta for totalTextContent
|
|
1220
1320
|
const textDelta = nextText.slice(currentText.length)
|
|
1221
1321
|
state.currentSegmentText = nextText
|
|
1222
1322
|
state.totalTextContent += textDelta
|
|
1223
1323
|
|
|
1224
|
-
|
|
1225
|
-
const chunkPortion = chunk.delta || chunk.content || ''
|
|
1324
|
+
const chunkPortion = chunk.delta || ''
|
|
1226
1325
|
const shouldEmit = this.chunkStrategy.shouldEmit(
|
|
1227
1326
|
chunkPortion,
|
|
1228
1327
|
state.currentSegmentText,
|
|
@@ -1265,13 +1364,7 @@ export class StreamProcessor {
|
|
|
1265
1364
|
// New tool call starting
|
|
1266
1365
|
const initialState: ToolCallState = 'awaiting-input'
|
|
1267
1366
|
|
|
1268
|
-
|
|
1269
|
-
// in types.ts). Accept either so chunks from older code paths or any
|
|
1270
|
-
// adapter that only sets the deprecated field still produce a named part.
|
|
1271
|
-
// The type marks both as required strings, but in practice some emitters
|
|
1272
|
-
// only set one — fall back via the runtime value rather than the type.
|
|
1273
|
-
const toolName =
|
|
1274
|
-
(chunk as { toolCallName?: string }).toolCallName ?? chunk.toolName
|
|
1367
|
+
const toolName = chunk.toolCallName
|
|
1275
1368
|
|
|
1276
1369
|
// Capture provider metadata that arrived on TOOL_CALL_START so it
|
|
1277
1370
|
// round-trips back through the assistant message on the next turn
|
|
@@ -1284,7 +1377,7 @@ export class StreamProcessor {
|
|
|
1284
1377
|
arguments: '',
|
|
1285
1378
|
state: initialState,
|
|
1286
1379
|
parsedArguments: undefined,
|
|
1287
|
-
index:
|
|
1380
|
+
index: state.toolCalls.size,
|
|
1288
1381
|
...(chunkMetadata !== undefined && { metadata: chunkMetadata }),
|
|
1289
1382
|
}
|
|
1290
1383
|
|
|
@@ -1373,17 +1466,15 @@ export class StreamProcessor {
|
|
|
1373
1466
|
}
|
|
1374
1467
|
|
|
1375
1468
|
/**
|
|
1376
|
-
* Handle TOOL_CALL_END event —
|
|
1469
|
+
* Handle TOOL_CALL_END event — arguments are finalized (input-complete).
|
|
1470
|
+
* Tool output arrives on TOOL_CALL_RESULT, not on this event.
|
|
1377
1471
|
*
|
|
1378
|
-
*
|
|
1379
|
-
* -
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
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.
|
|
1382
1477
|
*
|
|
1383
|
-
* If `input` is provided, it overrides the accumulated string parse as the
|
|
1384
|
-
* canonical parsed arguments.
|
|
1385
|
-
*
|
|
1386
|
-
* @see docs/chat-architecture.md#tool-results-and-the-tool_call_end-dual-role — Full explanation
|
|
1387
1478
|
* @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
|
|
1388
1479
|
*/
|
|
1389
1480
|
private handleToolCallEndEvent(
|
|
@@ -1395,15 +1486,22 @@ export class StreamProcessor {
|
|
|
1395
1486
|
const msgState = this.getMessageState(messageId)
|
|
1396
1487
|
if (!msgState) return
|
|
1397
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
|
+
|
|
1398
1496
|
// Transition the tool call to input-complete (the authoritative completion signal)
|
|
1399
1497
|
const existingToolCall = msgState.toolCalls.get(chunk.toolCallId)
|
|
1400
1498
|
if (existingToolCall && existingToolCall.state !== 'input-complete') {
|
|
1401
|
-
//
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
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) {
|
|
1405
1503
|
try {
|
|
1406
|
-
existingToolCall.arguments = JSON.stringify(
|
|
1504
|
+
existingToolCall.arguments = JSON.stringify(input)
|
|
1407
1505
|
} catch {
|
|
1408
1506
|
// circular refs, BigInt, etc. — leave arguments empty rather than
|
|
1409
1507
|
// aborting stream processing
|
|
@@ -1412,20 +1510,18 @@ export class StreamProcessor {
|
|
|
1412
1510
|
|
|
1413
1511
|
const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
|
|
1414
1512
|
this.completeToolCall(messageId, index, existingToolCall)
|
|
1415
|
-
|
|
1416
|
-
//
|
|
1417
|
-
//
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
if (chunk.input !== undefined) {
|
|
1422
|
-
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
|
|
1423
1519
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
1424
1520
|
id: existingToolCall.id,
|
|
1425
1521
|
name: existingToolCall.name,
|
|
1426
1522
|
arguments: existingToolCall.arguments,
|
|
1427
1523
|
state: 'input-complete',
|
|
1428
|
-
input
|
|
1524
|
+
input,
|
|
1429
1525
|
...(existingToolCall.metadata !== undefined && {
|
|
1430
1526
|
metadata: existingToolCall.metadata,
|
|
1431
1527
|
}),
|
|
@@ -1433,43 +1529,6 @@ export class StreamProcessor {
|
|
|
1433
1529
|
this.emitMessagesChange()
|
|
1434
1530
|
}
|
|
1435
1531
|
}
|
|
1436
|
-
|
|
1437
|
-
// Update UIMessage if there's a result
|
|
1438
|
-
if (chunk.result) {
|
|
1439
|
-
// Step 1: Update the tool-call part's output field (for UI consistency
|
|
1440
|
-
// with client tools — see GitHub issue #176)
|
|
1441
|
-
let output: unknown
|
|
1442
|
-
if (Array.isArray(chunk.result)) {
|
|
1443
|
-
output = chunk.result
|
|
1444
|
-
} else {
|
|
1445
|
-
try {
|
|
1446
|
-
output = JSON.parse(chunk.result)
|
|
1447
|
-
} catch {
|
|
1448
|
-
output = chunk.result
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
this.messages = updateToolCallWithOutput(
|
|
1452
|
-
this.messages,
|
|
1453
|
-
chunk.toolCallId,
|
|
1454
|
-
output,
|
|
1455
|
-
chunk.state === 'output-error' ? 'error' : undefined,
|
|
1456
|
-
)
|
|
1457
|
-
|
|
1458
|
-
// Step 2: Create/update the tool-result part (for LLM conversation history)
|
|
1459
|
-
const resultState: ToolResultState =
|
|
1460
|
-
chunk.state === 'output-error' ? 'error' : 'complete'
|
|
1461
|
-
this.messages = updateToolResultPart(
|
|
1462
|
-
this.messages,
|
|
1463
|
-
messageId,
|
|
1464
|
-
chunk.toolCallId,
|
|
1465
|
-
chunk.result,
|
|
1466
|
-
resultState,
|
|
1467
|
-
resultState === 'error'
|
|
1468
|
-
? this.extractToolResultError(output)
|
|
1469
|
-
: undefined,
|
|
1470
|
-
)
|
|
1471
|
-
this.emitMessagesChange()
|
|
1472
|
-
}
|
|
1473
1532
|
}
|
|
1474
1533
|
|
|
1475
1534
|
private extractToolResultError(output: unknown): string {
|
|
@@ -1494,9 +1553,27 @@ export class StreamProcessor {
|
|
|
1494
1553
|
private handleToolCallResultEvent(
|
|
1495
1554
|
chunk: Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
|
|
1496
1555
|
): void {
|
|
1497
|
-
|
|
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
|
|
1498
1570
|
if (!messageId) return
|
|
1499
1571
|
|
|
1572
|
+
const extra = chunk as AdapterYieldChunk
|
|
1573
|
+
const isOutputError =
|
|
1574
|
+
extra.state === 'output-error' ||
|
|
1575
|
+
tanstackMetadata(chunk)?.state === 'output-error'
|
|
1576
|
+
|
|
1500
1577
|
// Step 1: Update the tool-call part's output field
|
|
1501
1578
|
let output: unknown
|
|
1502
1579
|
try {
|
|
@@ -1508,12 +1585,11 @@ export class StreamProcessor {
|
|
|
1508
1585
|
this.messages,
|
|
1509
1586
|
chunk.toolCallId,
|
|
1510
1587
|
output,
|
|
1511
|
-
|
|
1588
|
+
isOutputError ? 'error' : undefined,
|
|
1512
1589
|
)
|
|
1513
1590
|
|
|
1514
1591
|
// Step 2: Create/update the tool-result part
|
|
1515
|
-
const resultState: ToolResultState =
|
|
1516
|
-
chunk.state === 'output-error' ? 'error' : 'complete'
|
|
1592
|
+
const resultState: ToolResultState = isOutputError ? 'error' : 'complete'
|
|
1517
1593
|
this.messages = updateToolResultPart(
|
|
1518
1594
|
this.messages,
|
|
1519
1595
|
messageId,
|
|
@@ -1550,7 +1626,11 @@ export class StreamProcessor {
|
|
|
1550
1626
|
private handleRunFinishedEvent(
|
|
1551
1627
|
chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
|
|
1552
1628
|
): void {
|
|
1553
|
-
|
|
1629
|
+
const extra = chunk as AdapterYieldChunk
|
|
1630
|
+
this.finishReason =
|
|
1631
|
+
extra.finishReason !== undefined
|
|
1632
|
+
? extra.finishReason
|
|
1633
|
+
: (tanstackMetadata(chunk)?.finishReason ?? null)
|
|
1554
1634
|
this.activeRuns.delete(chunk.runId)
|
|
1555
1635
|
|
|
1556
1636
|
if (chunk.outcome?.type === 'interrupt') {
|
|
@@ -1643,10 +1723,7 @@ export class StreamProcessor {
|
|
|
1643
1723
|
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
1644
1724
|
): void {
|
|
1645
1725
|
this.hasError = true
|
|
1646
|
-
const runId =
|
|
1647
|
-
'runId' in chunk && typeof chunk.runId === 'string'
|
|
1648
|
-
? chunk.runId
|
|
1649
|
-
: undefined
|
|
1726
|
+
const runId = getChunkRunId(chunk)
|
|
1650
1727
|
if (runId) {
|
|
1651
1728
|
this.activeRuns.delete(runId)
|
|
1652
1729
|
} else {
|
|
@@ -1656,9 +1733,8 @@ export class StreamProcessor {
|
|
|
1656
1733
|
// Prefer spec field `message`; fall back to deprecated `error.message`.
|
|
1657
1734
|
// If neither is set, the chunk still carries debug context (provider
|
|
1658
1735
|
// error codes, request ids, etc.) — log it so the failure isn't silent.
|
|
1659
|
-
const errorMessage =
|
|
1660
|
-
|
|
1661
|
-
if (!chunk.message && !chunk.error?.message) {
|
|
1736
|
+
const errorMessage = chunk.message || 'An error occurred'
|
|
1737
|
+
if (!chunk.message) {
|
|
1662
1738
|
console.error(
|
|
1663
1739
|
'[StreamProcessor] RUN_ERROR with no message; original chunk:',
|
|
1664
1740
|
chunk,
|
|
@@ -1681,28 +1757,20 @@ export class StreamProcessor {
|
|
|
1681
1757
|
// the surfaced Error so consumers can recover the upstream detail that the
|
|
1682
1758
|
// RUN_ERROR's `message` alone discards. Both are optional and added only
|
|
1683
1759
|
// when present, keeping the Error backward compatible.
|
|
1684
|
-
|
|
1685
|
-
const code = chunk.code ?? chunk.error?.code
|
|
1686
|
-
if (code !== undefined) {
|
|
1687
|
-
Object.assign(error, { code })
|
|
1688
|
-
}
|
|
1689
|
-
if (chunk.rawEvent !== undefined) {
|
|
1690
|
-
Object.assign(error, { rawEvent: chunk.rawEvent })
|
|
1691
|
-
}
|
|
1692
|
-
this.events.onError?.(error)
|
|
1760
|
+
this.events.onError?.(runErrorEventToError(chunk))
|
|
1693
1761
|
}
|
|
1694
1762
|
|
|
1695
1763
|
/**
|
|
1696
1764
|
* Handle STEP_STARTED event (for thinking/reasoning content).
|
|
1697
1765
|
*
|
|
1698
|
-
* Records the stepId so
|
|
1766
|
+
* Records the stepId so later REASONING_MESSAGE_CONTENT deltas accumulate
|
|
1699
1767
|
* into their own ThinkingPart. Does not create a message — the message
|
|
1700
|
-
* is lazily created when the first
|
|
1768
|
+
* is lazily created when the first REASONING_MESSAGE_CONTENT arrives.
|
|
1701
1769
|
*/
|
|
1702
1770
|
private handleStepStartedEvent(
|
|
1703
1771
|
chunk: Extract<StreamChunk, { type: 'STEP_STARTED' }>,
|
|
1704
1772
|
): void {
|
|
1705
|
-
const stepId = chunk.
|
|
1773
|
+
const stepId = chunk.stepName || generateMessageId()
|
|
1706
1774
|
const activeId = this.getActiveAssistantMessageId()
|
|
1707
1775
|
if (activeId) {
|
|
1708
1776
|
const state = this.getMessageState(activeId)
|
|
@@ -1715,102 +1783,49 @@ export class StreamProcessor {
|
|
|
1715
1783
|
// Clear any pending stepId from a prior STEP_STARTED that fired
|
|
1716
1784
|
// before the assistant message existed. Now that we're tracking
|
|
1717
1785
|
// the step directly on message state, the pending value is stale
|
|
1718
|
-
// and must not leak into the next
|
|
1719
|
-
// misattribute its delta to the stale step).
|
|
1786
|
+
// and must not leak into the next REASONING_MESSAGE_CONTENT.
|
|
1720
1787
|
this.pendingThinkingStepId = null
|
|
1721
1788
|
return
|
|
1722
1789
|
}
|
|
1723
1790
|
}
|
|
1724
1791
|
|
|
1725
|
-
// No active message yet — defer until ensureAssistantMessage in
|
|
1792
|
+
// No active message yet — defer until ensureAssistantMessage in
|
|
1793
|
+
// REASONING_MESSAGE_CONTENT
|
|
1726
1794
|
this.pendingThinkingStepId = stepId
|
|
1727
1795
|
}
|
|
1728
1796
|
|
|
1729
1797
|
/**
|
|
1730
|
-
* Handle STEP_FINISHED event
|
|
1731
|
-
*
|
|
1732
|
-
* Accumulates delta into the current thinking step's content and updates
|
|
1733
|
-
* the corresponding ThinkingPart in the UIMessage.
|
|
1798
|
+
* Handle STEP_FINISHED event.
|
|
1734
1799
|
*
|
|
1735
|
-
*
|
|
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.
|
|
1736
1804
|
*/
|
|
1737
1805
|
private handleStepFinishedEvent(
|
|
1738
1806
|
chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
1739
1807
|
): void {
|
|
1808
|
+
const extra = chunk as AdapterYieldChunk
|
|
1809
|
+
const signature = extra.signature
|
|
1810
|
+
if (!signature) return
|
|
1811
|
+
|
|
1740
1812
|
const { messageId, state } = this.ensureAssistantMessage(
|
|
1741
1813
|
this.getActiveAssistantMessageId() ?? undefined,
|
|
1742
1814
|
)
|
|
1815
|
+
const stepId = state.currentThinkingStepId ?? extra.stepId
|
|
1816
|
+
if (!stepId) return
|
|
1817
|
+
const thinking = state.thinkingSteps.get(stepId)
|
|
1818
|
+
if (thinking === undefined) return
|
|
1743
1819
|
|
|
1744
|
-
|
|
1745
|
-
// REASONING_MESSAGE_CONTENT with the same delta. If we've already processed
|
|
1746
|
-
// REASONING_MESSAGE_CONTENT events for this message, skip the duplicate
|
|
1747
|
-
// thinking content from STEP_FINISHED to avoid doubled content.
|
|
1748
|
-
if (state.hasSeenReasoningEvents) {
|
|
1749
|
-
if (chunk.signature) {
|
|
1750
|
-
const stepId = state.currentThinkingStepId ?? chunk.stepId
|
|
1751
|
-
if (!stepId) return
|
|
1752
|
-
const thinking = state.thinkingSteps.get(stepId)
|
|
1753
|
-
if (thinking !== undefined) {
|
|
1754
|
-
state.thinkingStepSignatures.set(stepId, chunk.signature)
|
|
1755
|
-
this.messages = updateThinkingPart(
|
|
1756
|
-
this.messages,
|
|
1757
|
-
messageId,
|
|
1758
|
-
stepId,
|
|
1759
|
-
thinking,
|
|
1760
|
-
chunk.signature,
|
|
1761
|
-
)
|
|
1762
|
-
this.emitMessagesChange()
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
return
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
this.consumePendingThinkingStep(state)
|
|
1769
|
-
|
|
1770
|
-
const stepId =
|
|
1771
|
-
state.currentThinkingStepId ?? chunk.stepId ?? generateMessageId()
|
|
1772
|
-
|
|
1773
|
-
// Auto-initialize if no prior STEP_STARTED (backward compat)
|
|
1774
|
-
if (!state.thinkingSteps.has(stepId)) {
|
|
1775
|
-
state.thinkingSteps.set(stepId, '')
|
|
1776
|
-
state.thinkingStepOrder.push(stepId)
|
|
1777
|
-
state.currentThinkingStepId = stepId
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
const previous = state.thinkingSteps.get(stepId) ?? ''
|
|
1781
|
-
let nextThinking = previous
|
|
1782
|
-
|
|
1783
|
-
// Prefer delta over content
|
|
1784
|
-
if (chunk.delta && chunk.delta !== '') {
|
|
1785
|
-
nextThinking = previous + chunk.delta
|
|
1786
|
-
} else if (chunk.content && chunk.content !== '') {
|
|
1787
|
-
if (chunk.content.startsWith(previous)) {
|
|
1788
|
-
nextThinking = chunk.content
|
|
1789
|
-
} else if (previous.startsWith(chunk.content)) {
|
|
1790
|
-
nextThinking = previous
|
|
1791
|
-
} else {
|
|
1792
|
-
nextThinking = previous + chunk.content
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
state.thinkingSteps.set(stepId, nextThinking)
|
|
1797
|
-
|
|
1798
|
-
if (chunk.signature) {
|
|
1799
|
-
state.thinkingStepSignatures.set(stepId, chunk.signature)
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
// Update UIMessage
|
|
1820
|
+
state.thinkingStepSignatures.set(stepId, signature)
|
|
1803
1821
|
this.messages = updateThinkingPart(
|
|
1804
1822
|
this.messages,
|
|
1805
1823
|
messageId,
|
|
1806
1824
|
stepId,
|
|
1807
|
-
|
|
1808
|
-
|
|
1825
|
+
thinking,
|
|
1826
|
+
signature,
|
|
1809
1827
|
)
|
|
1810
1828
|
this.emitMessagesChange()
|
|
1811
|
-
|
|
1812
|
-
// Emit granular event
|
|
1813
|
-
this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
|
|
1814
1829
|
}
|
|
1815
1830
|
|
|
1816
1831
|
/**
|
|
@@ -1853,6 +1868,74 @@ export class StreamProcessor {
|
|
|
1853
1868
|
this.events.onThinkingUpdate?.(messageId, stepId, nextThinking)
|
|
1854
1869
|
}
|
|
1855
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
|
+
|
|
1856
1939
|
/**
|
|
1857
1940
|
* Handle CUSTOM event.
|
|
1858
1941
|
*
|
|
@@ -2026,22 +2109,10 @@ export class StreamProcessor {
|
|
|
2026
2109
|
* Detect if an incoming content chunk represents a NEW text segment
|
|
2027
2110
|
*/
|
|
2028
2111
|
private isNewTextSegment(
|
|
2029
|
-
|
|
2030
|
-
|
|
2112
|
+
_chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
|
|
2113
|
+
_previous: string,
|
|
2031
2114
|
): boolean {
|
|
2032
|
-
|
|
2033
|
-
if (chunk.content !== undefined) {
|
|
2034
|
-
if (chunk.content.length < previous.length) {
|
|
2035
|
-
return true
|
|
2036
|
-
}
|
|
2037
|
-
if (
|
|
2038
|
-
!chunk.content.startsWith(previous) &&
|
|
2039
|
-
!previous.startsWith(chunk.content)
|
|
2040
|
-
) {
|
|
2041
|
-
return true
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
return false
|
|
2115
|
+
return true
|
|
2045
2116
|
}
|
|
2046
2117
|
|
|
2047
2118
|
/**
|
|
@@ -2087,8 +2158,17 @@ export class StreamProcessor {
|
|
|
2087
2158
|
// counts as a completed tool call in getCompletedToolCalls()/getState().
|
|
2088
2159
|
toolCall.state = 'input-complete'
|
|
2089
2160
|
|
|
2090
|
-
//
|
|
2091
|
-
|
|
2161
|
+
// Only surface `input` from a strict parse. The streaming partial-JSON
|
|
2162
|
+
// parser closes unterminated strings, so truncated arguments would become
|
|
2163
|
+
// a plausible but wrong object (GitHub issue #1017). If parse fails,
|
|
2164
|
+
// `input` stays unset and consumers use the raw `arguments` string.
|
|
2165
|
+
let strictParseSucceeded = false
|
|
2166
|
+
try {
|
|
2167
|
+
toolCall.parsedArguments = JSON.parse(toolCall.arguments)
|
|
2168
|
+
strictParseSucceeded = true
|
|
2169
|
+
} catch {
|
|
2170
|
+
toolCall.parsedArguments = undefined
|
|
2171
|
+
}
|
|
2092
2172
|
|
|
2093
2173
|
// Don't downgrade the rendered part of a call that already reached the
|
|
2094
2174
|
// terminal 'error' state (e.g. an output-error TOOL_CALL_RESULT arrived
|
|
@@ -2106,19 +2186,13 @@ export class StreamProcessor {
|
|
|
2106
2186
|
}
|
|
2107
2187
|
|
|
2108
2188
|
// Update UIMessage. The arguments are complete now, so surface the parsed
|
|
2109
|
-
// input on the part
|
|
2110
|
-
// string was back-filled from TOOL_CALL_END.input, so this parse matches
|
|
2111
|
-
// the canonical input. If a TOOL_CALL_END.input diverges from the
|
|
2112
|
-
// accumulated args, handleToolCallEndEvent re-updates the part with the
|
|
2113
|
-
// canonical value after this call.
|
|
2189
|
+
// input on the part from the accumulated TOOL_CALL_ARGS deltas.
|
|
2114
2190
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
2115
2191
|
id: toolCall.id,
|
|
2116
2192
|
name: toolCall.name,
|
|
2117
2193
|
arguments: toolCall.arguments,
|
|
2118
2194
|
state: 'input-complete',
|
|
2119
|
-
...(toolCall.parsedArguments
|
|
2120
|
-
input: toolCall.parsedArguments,
|
|
2121
|
-
}),
|
|
2195
|
+
...(strictParseSucceeded && { input: toolCall.parsedArguments }),
|
|
2122
2196
|
...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
|
|
2123
2197
|
})
|
|
2124
2198
|
this.emitMessagesChange()
|