@ai-sdk/langchain 3.0.0-beta.17 → 3.0.0-beta.177
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/CHANGELOG.md +1180 -0
- package/README.md +5 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +391 -120
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
- package/src/adapter.ts +74 -17
- package/src/transport.ts +9 -9
- package/src/types.ts +28 -12
- package/src/utils.ts +498 -94
- package/dist/index.d.mts +0 -159
- package/dist/index.mjs +0 -1087
- package/dist/index.mjs.map +0 -1
package/src/utils.ts
CHANGED
|
@@ -2,26 +2,30 @@ import {
|
|
|
2
2
|
AIMessage,
|
|
3
3
|
HumanMessage,
|
|
4
4
|
ToolMessage,
|
|
5
|
-
BaseMessage,
|
|
6
5
|
AIMessageChunk,
|
|
7
|
-
BaseMessageChunk,
|
|
8
|
-
ToolCallChunk,
|
|
9
6
|
type ContentBlock,
|
|
10
7
|
type ToolCall,
|
|
8
|
+
type BaseMessage,
|
|
9
|
+
type BaseMessageChunk,
|
|
10
|
+
type ToolCallChunk,
|
|
11
11
|
} from '@langchain/core/messages';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
import type {
|
|
13
|
+
UIMessageChunk,
|
|
14
|
+
ToolResultPart,
|
|
15
|
+
AssistantContent,
|
|
16
|
+
UserContent,
|
|
17
|
+
ProviderMetadata,
|
|
18
|
+
JSONValue,
|
|
17
19
|
} from 'ai';
|
|
18
20
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
import type {
|
|
22
|
+
LangGraphEventState,
|
|
23
|
+
LangGraphMessageSeen,
|
|
24
|
+
ReasoningContentBlock,
|
|
25
|
+
ThinkingContentBlock,
|
|
26
|
+
GPT5ReasoningOutput,
|
|
27
|
+
ImageGenerationOutput,
|
|
28
|
+
NormalizedCitation,
|
|
25
29
|
} from './types';
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -114,8 +118,8 @@ function getDefaultFilename(
|
|
|
114
118
|
mediaType: string,
|
|
115
119
|
prefix: string = 'file',
|
|
116
120
|
): string {
|
|
117
|
-
const
|
|
118
|
-
return `${prefix}.${
|
|
121
|
+
const fileExtension = mediaType.split('/')[1] || 'bin';
|
|
122
|
+
return `${prefix}.${fileExtension}`;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/**
|
|
@@ -220,13 +224,53 @@ export function convertUserContent(content: UserContent): HumanMessage {
|
|
|
220
224
|
});
|
|
221
225
|
}
|
|
222
226
|
} else if (part.type === 'file') {
|
|
223
|
-
const
|
|
227
|
+
const rawFilePart = part as {
|
|
224
228
|
type: 'file';
|
|
225
|
-
data:
|
|
229
|
+
data:
|
|
230
|
+
| string
|
|
231
|
+
| Uint8Array
|
|
232
|
+
| URL
|
|
233
|
+
| ArrayBuffer
|
|
234
|
+
| { type: 'data'; data: string | Uint8Array | ArrayBuffer }
|
|
235
|
+
| { type: 'url'; url: URL }
|
|
236
|
+
| { type: 'reference'; reference: Record<string, string> }
|
|
237
|
+
| { type: 'text'; text: string };
|
|
226
238
|
mediaType: string;
|
|
227
239
|
filename?: string;
|
|
228
240
|
};
|
|
229
241
|
|
|
242
|
+
// Normalize tagged data shape into the legacy bare value this code expects.
|
|
243
|
+
const normalizedData: string | Uint8Array | URL | ArrayBuffer = (() => {
|
|
244
|
+
const data = rawFilePart.data;
|
|
245
|
+
if (
|
|
246
|
+
typeof data === 'object' &&
|
|
247
|
+
data !== null &&
|
|
248
|
+
!(data instanceof URL) &&
|
|
249
|
+
!(data instanceof Uint8Array) &&
|
|
250
|
+
!(data instanceof ArrayBuffer) &&
|
|
251
|
+
'type' in data
|
|
252
|
+
) {
|
|
253
|
+
switch (data.type) {
|
|
254
|
+
case 'data':
|
|
255
|
+
return data.data;
|
|
256
|
+
case 'url':
|
|
257
|
+
return data.url;
|
|
258
|
+
case 'text':
|
|
259
|
+
return data.text;
|
|
260
|
+
default:
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return data as string | Uint8Array | URL | ArrayBuffer;
|
|
265
|
+
})();
|
|
266
|
+
|
|
267
|
+
const filePart = {
|
|
268
|
+
type: 'file' as const,
|
|
269
|
+
data: normalizedData,
|
|
270
|
+
mediaType: rawFilePart.mediaType,
|
|
271
|
+
filename: rawFilePart.filename,
|
|
272
|
+
};
|
|
273
|
+
|
|
230
274
|
/**
|
|
231
275
|
* Check if this is an image file - if so, use OpenAI's image_url format
|
|
232
276
|
*/
|
|
@@ -393,6 +437,7 @@ export function processModelChunk(
|
|
|
393
437
|
/** Track the ID used for text-start to ensure text-end uses the same ID */
|
|
394
438
|
textMessageId?: string | null;
|
|
395
439
|
emittedImages?: Set<string>;
|
|
440
|
+
emittedSourceIds?: Set<string>;
|
|
396
441
|
},
|
|
397
442
|
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
398
443
|
): void {
|
|
@@ -403,6 +448,10 @@ export function processModelChunk(
|
|
|
403
448
|
state.emittedImages = new Set<string>();
|
|
404
449
|
}
|
|
405
450
|
|
|
451
|
+
if (!state.emittedSourceIds) {
|
|
452
|
+
state.emittedSourceIds = new Set<string>();
|
|
453
|
+
}
|
|
454
|
+
|
|
406
455
|
/**
|
|
407
456
|
* Get the message ID from the chunk if available
|
|
408
457
|
*/
|
|
@@ -506,6 +555,16 @@ export function processModelChunk(
|
|
|
506
555
|
id: state.textMessageId ?? state.messageId,
|
|
507
556
|
});
|
|
508
557
|
}
|
|
558
|
+
|
|
559
|
+
const citations = extractCitationsFromContentBlocks(chunk);
|
|
560
|
+
if (citations.length > 0) {
|
|
561
|
+
emitSourceChunks(
|
|
562
|
+
citations,
|
|
563
|
+
state.messageId,
|
|
564
|
+
state.emittedSourceIds,
|
|
565
|
+
controller,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
509
568
|
}
|
|
510
569
|
|
|
511
570
|
/**
|
|
@@ -562,7 +621,8 @@ export function getMessageId(msg: unknown): string | undefined {
|
|
|
562
621
|
/**
|
|
563
622
|
* Checks if a message is an AI message chunk (works for both class instances and plain objects).
|
|
564
623
|
* For class instances, only AIMessageChunk is matched (not AIMessage).
|
|
565
|
-
* For plain objects from RemoteGraph API, matches type === 'ai'
|
|
624
|
+
* For plain objects from RemoteGraph API, matches type === 'ai' (TypeScript langchain-core)
|
|
625
|
+
* or type === 'AIMessageChunk' (Python langchain-core).
|
|
566
626
|
* For serialized LangChain messages, matches type === 'constructor' with AIMessageChunk in id path.
|
|
567
627
|
*
|
|
568
628
|
* @param msg - The message to check.
|
|
@@ -579,18 +639,25 @@ export function isAIMessageChunk(
|
|
|
579
639
|
* Plain object from RemoteGraph API (not a LangChain class instance)
|
|
580
640
|
*/
|
|
581
641
|
if (isPlainMessageObject(msg)) {
|
|
582
|
-
const
|
|
642
|
+
const messageRecord = msg as Record<string, unknown>;
|
|
583
643
|
/**
|
|
584
|
-
* Direct type
|
|
644
|
+
* Direct type from RemoteGraph format. TypeScript langchain-core emits
|
|
645
|
+
* type === 'ai'; Python langchain-core emits type === 'AIMessageChunk'.
|
|
585
646
|
*/
|
|
586
|
-
if (
|
|
647
|
+
if (
|
|
648
|
+
'type' in messageRecord &&
|
|
649
|
+
(messageRecord.type === 'ai' || messageRecord.type === 'AIMessageChunk')
|
|
650
|
+
) {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
587
653
|
/**
|
|
588
654
|
* Serialized LangChain message format: { lc: 1, type: "constructor", id: ["...", "AIMessageChunk"], kwargs: {...} }
|
|
589
655
|
*/
|
|
590
656
|
if (
|
|
591
|
-
|
|
592
|
-
Array.isArray(
|
|
593
|
-
(
|
|
657
|
+
messageRecord.type === 'constructor' &&
|
|
658
|
+
Array.isArray(messageRecord.id) &&
|
|
659
|
+
(messageRecord.id.includes('AIMessageChunk') ||
|
|
660
|
+
messageRecord.id.includes('AIMessage'))
|
|
594
661
|
) {
|
|
595
662
|
return true;
|
|
596
663
|
}
|
|
@@ -612,18 +679,18 @@ export function isToolMessageType(
|
|
|
612
679
|
* Plain object from RemoteGraph API (not a LangChain class instance)
|
|
613
680
|
*/
|
|
614
681
|
if (isPlainMessageObject(msg)) {
|
|
615
|
-
const
|
|
682
|
+
const messageRecord = msg as Record<string, unknown>;
|
|
616
683
|
/**
|
|
617
684
|
* Direct type === 'tool' (RemoteGraph format)
|
|
618
685
|
*/
|
|
619
|
-
if ('type' in
|
|
686
|
+
if ('type' in messageRecord && messageRecord.type === 'tool') return true;
|
|
620
687
|
/**
|
|
621
688
|
* Serialized LangChain message format
|
|
622
689
|
*/
|
|
623
690
|
if (
|
|
624
|
-
|
|
625
|
-
Array.isArray(
|
|
626
|
-
|
|
691
|
+
messageRecord.type === 'constructor' &&
|
|
692
|
+
Array.isArray(messageRecord.id) &&
|
|
693
|
+
messageRecord.id.includes('ToolMessage')
|
|
627
694
|
) {
|
|
628
695
|
return true;
|
|
629
696
|
}
|
|
@@ -916,6 +983,152 @@ export function extractReasoningFromValuesMessage(
|
|
|
916
983
|
return undefined;
|
|
917
984
|
}
|
|
918
985
|
|
|
986
|
+
export function isCitationContentBlock(
|
|
987
|
+
obj: unknown,
|
|
988
|
+
): obj is ContentBlock.Citation {
|
|
989
|
+
return (
|
|
990
|
+
obj != null &&
|
|
991
|
+
typeof obj === 'object' &&
|
|
992
|
+
'type' in obj &&
|
|
993
|
+
(obj as { type: unknown }).type === 'citation'
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* LangChain Core standardizes citations as `{ type: 'citation', ... }` entries in
|
|
999
|
+
* the `annotations` array of a text content block. The text extractors elsewhere
|
|
1000
|
+
* in this file only read the `text` field, so without this the citations would be
|
|
1001
|
+
* dropped instead of surfaced as AI SDK source parts.
|
|
1002
|
+
*
|
|
1003
|
+
* Handles both class instances and serialized LangChain message objects.
|
|
1004
|
+
*/
|
|
1005
|
+
export function extractCitationsFromContentBlocks(
|
|
1006
|
+
msg: unknown,
|
|
1007
|
+
): NormalizedCitation[] {
|
|
1008
|
+
if (msg == null || typeof msg !== 'object') return [];
|
|
1009
|
+
|
|
1010
|
+
const msgObj = msg as Record<string, unknown>;
|
|
1011
|
+
const kwargs =
|
|
1012
|
+
msgObj.kwargs && typeof msgObj.kwargs === 'object'
|
|
1013
|
+
? (msgObj.kwargs as Record<string, unknown>)
|
|
1014
|
+
: msgObj;
|
|
1015
|
+
|
|
1016
|
+
// `contentBlocks` is the normalized view derived from `content`; reading both
|
|
1017
|
+
// would double-count annotations, so prefer it and only fall back to `content`.
|
|
1018
|
+
const contentBlocks = (kwargs as { contentBlocks?: unknown }).contentBlocks;
|
|
1019
|
+
const content = (kwargs as { content?: unknown }).content;
|
|
1020
|
+
const blockSources: unknown[] = Array.isArray(contentBlocks)
|
|
1021
|
+
? contentBlocks
|
|
1022
|
+
: Array.isArray(content)
|
|
1023
|
+
? content
|
|
1024
|
+
: [];
|
|
1025
|
+
|
|
1026
|
+
const citations: NormalizedCitation[] = [];
|
|
1027
|
+
|
|
1028
|
+
for (const block of blockSources) {
|
|
1029
|
+
if (block == null || typeof block !== 'object') continue;
|
|
1030
|
+
|
|
1031
|
+
const annotations = (block as { annotations?: unknown }).annotations;
|
|
1032
|
+
if (!Array.isArray(annotations)) continue;
|
|
1033
|
+
|
|
1034
|
+
for (const annotation of annotations) {
|
|
1035
|
+
if (!isCitationContentBlock(annotation)) continue;
|
|
1036
|
+
|
|
1037
|
+
citations.push({
|
|
1038
|
+
url: annotation.url,
|
|
1039
|
+
title: annotation.title,
|
|
1040
|
+
source: annotation.source,
|
|
1041
|
+
citedText: annotation.citedText,
|
|
1042
|
+
startIndex: annotation.startIndex,
|
|
1043
|
+
endIndex: annotation.endIndex,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return citations;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function buildSourceProviderMetadata(
|
|
1052
|
+
citation: NormalizedCitation,
|
|
1053
|
+
): ProviderMetadata | undefined {
|
|
1054
|
+
const langchain: Record<string, JSONValue> = {};
|
|
1055
|
+
|
|
1056
|
+
if (typeof citation.citedText === 'string') {
|
|
1057
|
+
langchain.citedText = citation.citedText;
|
|
1058
|
+
}
|
|
1059
|
+
if (typeof citation.startIndex === 'number') {
|
|
1060
|
+
langchain.startIndex = citation.startIndex;
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof citation.endIndex === 'number') {
|
|
1063
|
+
langchain.endIndex = citation.endIndex;
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof citation.source === 'string') {
|
|
1066
|
+
langchain.source = citation.source;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (Object.keys(langchain).length === 0) {
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return { langchain };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Emits AI SDK source chunks (`source-url` / `source-document`) for the given
|
|
1078
|
+
* citations.
|
|
1079
|
+
*
|
|
1080
|
+
* Citations with a `url` become `source-url` parts keyed by that url; the rest
|
|
1081
|
+
* become `source-document` parts. Citation metadata that the source parts cannot
|
|
1082
|
+
* represent natively (`citedText`, `startIndex`, `endIndex`, `source`) is preserved
|
|
1083
|
+
* under `providerMetadata.langchain`.
|
|
1084
|
+
*
|
|
1085
|
+
* `emittedSourceIds` dedupes across the LangGraph messages and values events, which
|
|
1086
|
+
* can each surface the same citation. URL-less citations are keyed by their content
|
|
1087
|
+
* rather than position, so a differing citation subset/order between those two events
|
|
1088
|
+
* does not cause an id collision.
|
|
1089
|
+
*/
|
|
1090
|
+
export function emitSourceChunks(
|
|
1091
|
+
citations: NormalizedCitation[],
|
|
1092
|
+
messageId: string,
|
|
1093
|
+
emittedSourceIds: Set<string>,
|
|
1094
|
+
controller: ReadableStreamDefaultController<UIMessageChunk>,
|
|
1095
|
+
): void {
|
|
1096
|
+
for (const citation of citations) {
|
|
1097
|
+
if (citation.url) {
|
|
1098
|
+
if (emittedSourceIds.has(citation.url)) continue;
|
|
1099
|
+
emittedSourceIds.add(citation.url);
|
|
1100
|
+
|
|
1101
|
+
const providerMetadata = buildSourceProviderMetadata(citation);
|
|
1102
|
+
controller.enqueue({
|
|
1103
|
+
type: 'source-url',
|
|
1104
|
+
sourceId: citation.url,
|
|
1105
|
+
url: citation.url,
|
|
1106
|
+
...(citation.title ? { title: citation.title } : {}),
|
|
1107
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
1108
|
+
});
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// A citation with no url and no human-readable label carries no information a
|
|
1113
|
+
// UI could render, so skip it rather than emit a placeholder source.
|
|
1114
|
+
const title = citation.title ?? citation.source;
|
|
1115
|
+
if (!title) continue;
|
|
1116
|
+
|
|
1117
|
+
const sourceId = `${messageId}:${title}:${citation.citedText ?? ''}:${citation.startIndex ?? ''}:${citation.endIndex ?? ''}`;
|
|
1118
|
+
if (emittedSourceIds.has(sourceId)) continue;
|
|
1119
|
+
emittedSourceIds.add(sourceId);
|
|
1120
|
+
|
|
1121
|
+
const providerMetadata = buildSourceProviderMetadata(citation);
|
|
1122
|
+
controller.enqueue({
|
|
1123
|
+
type: 'source-document',
|
|
1124
|
+
sourceId,
|
|
1125
|
+
mediaType: 'text/plain',
|
|
1126
|
+
title,
|
|
1127
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
919
1132
|
/**
|
|
920
1133
|
* Checks if an object is an image generation output
|
|
921
1134
|
*
|
|
@@ -950,6 +1163,50 @@ export function extractImageOutputs(
|
|
|
950
1163
|
return toolOutputs.filter(isImageGenerationOutput);
|
|
951
1164
|
}
|
|
952
1165
|
|
|
1166
|
+
function formatToolError(error: unknown): string {
|
|
1167
|
+
if (error instanceof Error) return error.message;
|
|
1168
|
+
if (typeof error === 'string') return error;
|
|
1169
|
+
|
|
1170
|
+
try {
|
|
1171
|
+
const serialized = JSON.stringify(error);
|
|
1172
|
+
return serialized ?? String(error);
|
|
1173
|
+
} catch {
|
|
1174
|
+
return String(error);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Returns per-message bookkeeping, creating it on first use.
|
|
1180
|
+
* Map keys keep remote-controlled message IDs like "__proto__" from touching Object.prototype.
|
|
1181
|
+
*/
|
|
1182
|
+
function getOrCreateMessageSeen(
|
|
1183
|
+
messageSeen: LangGraphEventState['messageSeen'],
|
|
1184
|
+
msgId: string,
|
|
1185
|
+
): LangGraphMessageSeen {
|
|
1186
|
+
let seen = messageSeen.get(msgId);
|
|
1187
|
+
if (!seen) {
|
|
1188
|
+
seen = {};
|
|
1189
|
+
messageSeen.set(msgId, seen);
|
|
1190
|
+
}
|
|
1191
|
+
return seen;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Returns the per-index tool call metadata for a message, creating it on first use.
|
|
1196
|
+
* Later streamed chunks use this to recover tool call IDs/names from earlier chunks.
|
|
1197
|
+
*/
|
|
1198
|
+
function getOrCreateToolCallInfoByIndex(
|
|
1199
|
+
toolCallInfoByIndex: LangGraphEventState['toolCallInfoByIndex'],
|
|
1200
|
+
msgId: string,
|
|
1201
|
+
): Map<number, { id: string; name: string }> {
|
|
1202
|
+
let toolCallInfo = toolCallInfoByIndex.get(msgId);
|
|
1203
|
+
if (!toolCallInfo) {
|
|
1204
|
+
toolCallInfo = new Map();
|
|
1205
|
+
toolCallInfoByIndex.set(msgId, toolCallInfo);
|
|
1206
|
+
}
|
|
1207
|
+
return toolCallInfo;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
953
1210
|
/**
|
|
954
1211
|
* Processes a LangGraph event and emits UI message chunks.
|
|
955
1212
|
*
|
|
@@ -971,6 +1228,7 @@ export function processLangGraphEvent(
|
|
|
971
1228
|
messageReasoningIds,
|
|
972
1229
|
toolCallInfoByIndex,
|
|
973
1230
|
emittedToolCallsByKey,
|
|
1231
|
+
emittedToolInputs,
|
|
974
1232
|
} = state;
|
|
975
1233
|
const [type, data] = parseLangGraphEvent(event);
|
|
976
1234
|
|
|
@@ -1021,7 +1279,10 @@ export function processLangGraphEvent(
|
|
|
1021
1279
|
if (!msgId) return;
|
|
1022
1280
|
|
|
1023
1281
|
/**
|
|
1024
|
-
* Track LangGraph step changes and emit start-step/finish-step events
|
|
1282
|
+
* Track LangGraph step changes and emit start-step/finish-step events.
|
|
1283
|
+
* Before emitting finish-step, close any open text/reasoning parts so
|
|
1284
|
+
* the client does not receive orphaned deltas after its
|
|
1285
|
+
* activeReasoningParts / activeTextParts have been cleared.
|
|
1025
1286
|
*/
|
|
1026
1287
|
const langgraphStep =
|
|
1027
1288
|
typeof metadata?.langgraph_step === 'number'
|
|
@@ -1029,6 +1290,17 @@ export function processLangGraphEvent(
|
|
|
1029
1290
|
: null;
|
|
1030
1291
|
if (langgraphStep !== null && langgraphStep !== state.currentStep) {
|
|
1031
1292
|
if (state.currentStep !== null) {
|
|
1293
|
+
for (const [id, seen] of messageSeen) {
|
|
1294
|
+
if (seen.text) {
|
|
1295
|
+
controller.enqueue({ type: 'text-end', id });
|
|
1296
|
+
}
|
|
1297
|
+
if (seen.reasoning) {
|
|
1298
|
+
controller.enqueue({ type: 'reasoning-end', id });
|
|
1299
|
+
}
|
|
1300
|
+
messageSeen.delete(id);
|
|
1301
|
+
messageConcat.delete(id);
|
|
1302
|
+
messageReasoningIds.delete(id);
|
|
1303
|
+
}
|
|
1032
1304
|
controller.enqueue({ type: 'finish-step' });
|
|
1033
1305
|
}
|
|
1034
1306
|
controller.enqueue({ type: 'start-step' });
|
|
@@ -1040,17 +1312,19 @@ export function processLangGraphEvent(
|
|
|
1040
1312
|
* Note: Only works for actual class instances, not serialized messages
|
|
1041
1313
|
*/
|
|
1042
1314
|
if (AIMessageChunk.isInstance(msg)) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1315
|
+
const existingMessage = messageConcat.get(msgId);
|
|
1316
|
+
if (existingMessage) {
|
|
1317
|
+
messageConcat.set(
|
|
1318
|
+
msgId,
|
|
1319
|
+
existingMessage.concat(msg) as AIMessageChunk,
|
|
1320
|
+
);
|
|
1047
1321
|
} else {
|
|
1048
|
-
messageConcat
|
|
1322
|
+
messageConcat.set(msgId, msg);
|
|
1049
1323
|
}
|
|
1050
1324
|
}
|
|
1051
1325
|
|
|
1052
1326
|
if (isAIMessageChunk(msg)) {
|
|
1053
|
-
const concatChunk = messageConcat
|
|
1327
|
+
const concatChunk = messageConcat.get(msgId);
|
|
1054
1328
|
|
|
1055
1329
|
/**
|
|
1056
1330
|
* Handle image generation outputs from additional_kwargs.tool_outputs
|
|
@@ -1103,29 +1377,34 @@ export function processLangGraphEvent(
|
|
|
1103
1377
|
| undefined;
|
|
1104
1378
|
if (toolCallChunks?.length) {
|
|
1105
1379
|
for (const toolCallChunk of toolCallChunks) {
|
|
1106
|
-
const
|
|
1380
|
+
const toolCallIndex = toolCallChunk.index ?? 0;
|
|
1107
1381
|
|
|
1108
1382
|
/**
|
|
1109
1383
|
* If this chunk has an id, store it for future lookups by index
|
|
1110
1384
|
*/
|
|
1111
1385
|
if (toolCallChunk.id) {
|
|
1112
|
-
toolCallInfoByIndex
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1386
|
+
getOrCreateToolCallInfoByIndex(toolCallInfoByIndex, msgId).set(
|
|
1387
|
+
toolCallIndex,
|
|
1388
|
+
{
|
|
1389
|
+
id: toolCallChunk.id,
|
|
1390
|
+
name:
|
|
1391
|
+
toolCallChunk.name ||
|
|
1392
|
+
concatChunk?.tool_call_chunks?.[toolCallIndex]?.name ||
|
|
1393
|
+
'unknown',
|
|
1394
|
+
},
|
|
1395
|
+
);
|
|
1120
1396
|
}
|
|
1121
1397
|
|
|
1122
1398
|
/**
|
|
1123
1399
|
* Get the tool call ID from the chunk, stored info, or accumulated chunks
|
|
1124
1400
|
*/
|
|
1401
|
+
const storedToolCallInfo = toolCallInfoByIndex
|
|
1402
|
+
.get(msgId)
|
|
1403
|
+
?.get(toolCallIndex);
|
|
1125
1404
|
const toolCallId =
|
|
1126
1405
|
toolCallChunk.id ||
|
|
1127
|
-
|
|
1128
|
-
concatChunk?.tool_call_chunks?.[
|
|
1406
|
+
storedToolCallInfo?.id ||
|
|
1407
|
+
concatChunk?.tool_call_chunks?.[toolCallIndex]?.id;
|
|
1129
1408
|
|
|
1130
1409
|
/**
|
|
1131
1410
|
* Skip if we don't have a proper tool call ID - we'll handle it in values
|
|
@@ -1136,8 +1415,8 @@ export function processLangGraphEvent(
|
|
|
1136
1415
|
|
|
1137
1416
|
const toolName =
|
|
1138
1417
|
toolCallChunk.name ||
|
|
1139
|
-
|
|
1140
|
-
concatChunk?.tool_call_chunks?.[
|
|
1418
|
+
storedToolCallInfo?.name ||
|
|
1419
|
+
concatChunk?.tool_call_chunks?.[toolCallIndex]?.name ||
|
|
1141
1420
|
'unknown';
|
|
1142
1421
|
|
|
1143
1422
|
/**
|
|
@@ -1145,18 +1424,21 @@ export function processLangGraphEvent(
|
|
|
1145
1424
|
* (even if args is empty - the first chunk often has empty args)
|
|
1146
1425
|
* Set dynamic: true to enable HITL approval requests
|
|
1147
1426
|
*/
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
dynamic: true,
|
|
1154
|
-
});
|
|
1427
|
+
const seen = messageSeen.get(msgId);
|
|
1428
|
+
if (!seen?.tool?.has(toolCallId)) {
|
|
1429
|
+
const updatedSeen = getOrCreateMessageSeen(messageSeen, msgId);
|
|
1430
|
+
updatedSeen.tool ??= new Set();
|
|
1431
|
+
updatedSeen.tool.add(toolCallId);
|
|
1155
1432
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1433
|
+
if (!emittedToolCalls.has(toolCallId)) {
|
|
1434
|
+
emittedToolCalls.add(toolCallId);
|
|
1435
|
+
controller.enqueue({
|
|
1436
|
+
type: 'tool-input-start',
|
|
1437
|
+
toolCallId: toolCallId,
|
|
1438
|
+
toolName: toolName,
|
|
1439
|
+
dynamic: true,
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1160
1442
|
}
|
|
1161
1443
|
|
|
1162
1444
|
/**
|
|
@@ -1187,8 +1469,8 @@ export function processLangGraphEvent(
|
|
|
1187
1469
|
// Capture reasoning ID when we first see it (even if no content yet)
|
|
1188
1470
|
const chunkReasoningId = extractReasoningId(msg);
|
|
1189
1471
|
if (chunkReasoningId) {
|
|
1190
|
-
if (!messageReasoningIds
|
|
1191
|
-
messageReasoningIds
|
|
1472
|
+
if (!messageReasoningIds.has(msgId)) {
|
|
1473
|
+
messageReasoningIds.set(msgId, chunkReasoningId);
|
|
1192
1474
|
}
|
|
1193
1475
|
// Immediately mark as emitted to prevent values from duplicating
|
|
1194
1476
|
// This must happen as soon as we see the ID, before content arrives
|
|
@@ -1199,12 +1481,12 @@ export function processLangGraphEvent(
|
|
|
1199
1481
|
if (reasoning) {
|
|
1200
1482
|
// Use stored reasoning ID, or current chunk's ID, or fall back to message ID
|
|
1201
1483
|
const reasoningId =
|
|
1202
|
-
messageReasoningIds
|
|
1484
|
+
messageReasoningIds.get(msgId) ?? chunkReasoningId ?? msgId;
|
|
1203
1485
|
|
|
1204
|
-
|
|
1486
|
+
const seen = messageSeen.get(msgId);
|
|
1487
|
+
if (!seen?.reasoning) {
|
|
1205
1488
|
controller.enqueue({ type: 'reasoning-start', id: msgId });
|
|
1206
|
-
messageSeen
|
|
1207
|
-
messageSeen[msgId].reasoning = true;
|
|
1489
|
+
getOrCreateMessageSeen(messageSeen, msgId).reasoning = true;
|
|
1208
1490
|
}
|
|
1209
1491
|
|
|
1210
1492
|
// Streaming chunks have delta text, emit directly without slicing
|
|
@@ -1222,10 +1504,10 @@ export function processLangGraphEvent(
|
|
|
1222
1504
|
*/
|
|
1223
1505
|
const text = getMessageText(msg);
|
|
1224
1506
|
if (text) {
|
|
1225
|
-
|
|
1507
|
+
const seen = messageSeen.get(msgId);
|
|
1508
|
+
if (!seen?.text) {
|
|
1226
1509
|
controller.enqueue({ type: 'text-start', id: msgId });
|
|
1227
|
-
messageSeen
|
|
1228
|
-
messageSeen[msgId].text = true;
|
|
1510
|
+
getOrCreateMessageSeen(messageSeen, msgId).text = true;
|
|
1229
1511
|
}
|
|
1230
1512
|
|
|
1231
1513
|
controller.enqueue({
|
|
@@ -1234,6 +1516,16 @@ export function processLangGraphEvent(
|
|
|
1234
1516
|
id: msgId,
|
|
1235
1517
|
});
|
|
1236
1518
|
}
|
|
1519
|
+
|
|
1520
|
+
const citations = extractCitationsFromContentBlocks(msg);
|
|
1521
|
+
if (citations.length > 0) {
|
|
1522
|
+
emitSourceChunks(
|
|
1523
|
+
citations,
|
|
1524
|
+
msgId,
|
|
1525
|
+
state.emittedSourceIds,
|
|
1526
|
+
controller,
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1237
1529
|
} else if (isToolMessageType(msg)) {
|
|
1238
1530
|
// Handle both direct properties and serialized messages (kwargs)
|
|
1239
1531
|
const msgObj = msg as unknown as Record<string, unknown>;
|
|
@@ -1272,31 +1564,122 @@ export function processLangGraphEvent(
|
|
|
1272
1564
|
return;
|
|
1273
1565
|
}
|
|
1274
1566
|
|
|
1567
|
+
case 'tools': {
|
|
1568
|
+
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const payload = data as {
|
|
1573
|
+
event?: unknown;
|
|
1574
|
+
name?: unknown;
|
|
1575
|
+
input?: unknown;
|
|
1576
|
+
data?: unknown;
|
|
1577
|
+
output?: unknown;
|
|
1578
|
+
error?: unknown;
|
|
1579
|
+
toolCallId?: unknown;
|
|
1580
|
+
};
|
|
1581
|
+
const toolCallId =
|
|
1582
|
+
typeof payload.toolCallId === 'string' ? payload.toolCallId : undefined;
|
|
1583
|
+
const toolName =
|
|
1584
|
+
typeof payload.name === 'string' ? payload.name : 'unknown';
|
|
1585
|
+
|
|
1586
|
+
if (!toolCallId) return;
|
|
1587
|
+
|
|
1588
|
+
const ensureToolInputLifecycle = () => {
|
|
1589
|
+
if (!emittedToolCalls.has(toolCallId)) {
|
|
1590
|
+
emittedToolCalls.add(toolCallId);
|
|
1591
|
+
controller.enqueue({
|
|
1592
|
+
type: 'tool-input-start',
|
|
1593
|
+
toolCallId,
|
|
1594
|
+
toolName,
|
|
1595
|
+
dynamic: true,
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (!emittedToolInputs.has(toolCallId)) {
|
|
1600
|
+
emittedToolInputs.add(toolCallId);
|
|
1601
|
+
controller.enqueue({
|
|
1602
|
+
type: 'tool-input-available',
|
|
1603
|
+
toolCallId,
|
|
1604
|
+
toolName,
|
|
1605
|
+
input: payload.input,
|
|
1606
|
+
dynamic: true,
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
switch (payload.event) {
|
|
1612
|
+
case 'on_tool_start': {
|
|
1613
|
+
const toolCallKey = `${toolName}:${JSON.stringify(payload.input)}`;
|
|
1614
|
+
emittedToolCallsByKey.set(toolCallKey, toolCallId);
|
|
1615
|
+
|
|
1616
|
+
ensureToolInputLifecycle();
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
case 'on_tool_event': {
|
|
1621
|
+
ensureToolInputLifecycle();
|
|
1622
|
+
controller.enqueue({
|
|
1623
|
+
type: 'tool-output-available',
|
|
1624
|
+
toolCallId,
|
|
1625
|
+
output: payload.data,
|
|
1626
|
+
preliminary: true,
|
|
1627
|
+
});
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
case 'on_tool_end': {
|
|
1632
|
+
ensureToolInputLifecycle();
|
|
1633
|
+
controller.enqueue({
|
|
1634
|
+
type: 'tool-output-available',
|
|
1635
|
+
toolCallId,
|
|
1636
|
+
output: payload.output,
|
|
1637
|
+
});
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
case 'on_tool_error': {
|
|
1642
|
+
ensureToolInputLifecycle();
|
|
1643
|
+
controller.enqueue({
|
|
1644
|
+
type: 'tool-output-error',
|
|
1645
|
+
toolCallId,
|
|
1646
|
+
errorText: formatToolError(payload.error),
|
|
1647
|
+
});
|
|
1648
|
+
break;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1275
1655
|
case 'values': {
|
|
1276
1656
|
/**
|
|
1277
1657
|
* Finalize all pending message chunks
|
|
1278
1658
|
*/
|
|
1279
|
-
for (const [id, seen] of
|
|
1659
|
+
for (const [id, seen] of messageSeen) {
|
|
1280
1660
|
if (seen.text) controller.enqueue({ type: 'text-end', id });
|
|
1281
1661
|
if (seen.tool) {
|
|
1282
|
-
for (const
|
|
1283
|
-
const concatMsg = messageConcat
|
|
1662
|
+
for (const toolCallId of seen.tool) {
|
|
1663
|
+
const concatMsg = messageConcat.get(id);
|
|
1284
1664
|
const toolCall = concatMsg?.tool_calls?.find(
|
|
1285
1665
|
call => call.id === toolCallId,
|
|
1286
1666
|
);
|
|
1287
1667
|
|
|
1288
|
-
if (
|
|
1668
|
+
if (toolCall) {
|
|
1289
1669
|
emittedToolCalls.add(toolCallId);
|
|
1290
1670
|
// Store mapping for HITL interrupt lookup
|
|
1291
1671
|
const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
|
|
1292
1672
|
emittedToolCallsByKey.set(toolCallKey, toolCallId);
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1673
|
+
if (!emittedToolInputs.has(toolCallId)) {
|
|
1674
|
+
emittedToolInputs.add(toolCallId);
|
|
1675
|
+
controller.enqueue({
|
|
1676
|
+
type: 'tool-input-available',
|
|
1677
|
+
toolCallId,
|
|
1678
|
+
toolName: toolCall.name,
|
|
1679
|
+
input: toolCall.args,
|
|
1680
|
+
dynamic: true,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1300
1683
|
}
|
|
1301
1684
|
}
|
|
1302
1685
|
}
|
|
@@ -1305,9 +1688,9 @@ export function processLangGraphEvent(
|
|
|
1305
1688
|
controller.enqueue({ type: 'reasoning-end', id });
|
|
1306
1689
|
}
|
|
1307
1690
|
|
|
1308
|
-
delete
|
|
1309
|
-
delete
|
|
1310
|
-
delete
|
|
1691
|
+
messageSeen.delete(id);
|
|
1692
|
+
messageConcat.delete(id);
|
|
1693
|
+
messageReasoningIds.delete(id);
|
|
1311
1694
|
}
|
|
1312
1695
|
|
|
1313
1696
|
/**
|
|
@@ -1367,21 +1750,25 @@ export function processLangGraphEvent(
|
|
|
1367
1750
|
/**
|
|
1368
1751
|
* For plain objects from RemoteGraph API or serialized LangChain messages
|
|
1369
1752
|
*/
|
|
1370
|
-
const
|
|
1753
|
+
const messageRecord = msg as Record<string, unknown>;
|
|
1371
1754
|
|
|
1372
1755
|
/**
|
|
1373
1756
|
* Determine the data source (handle both direct and serialized formats)
|
|
1374
1757
|
*/
|
|
1375
1758
|
const isSerializedFormat =
|
|
1376
|
-
|
|
1377
|
-
Array.isArray(
|
|
1378
|
-
((
|
|
1379
|
-
(
|
|
1759
|
+
messageRecord.type === 'constructor' &&
|
|
1760
|
+
Array.isArray(messageRecord.id) &&
|
|
1761
|
+
((messageRecord.id as string[]).includes('AIMessageChunk') ||
|
|
1762
|
+
(messageRecord.id as string[]).includes('AIMessage'));
|
|
1380
1763
|
const dataSource = isSerializedFormat
|
|
1381
|
-
? (
|
|
1382
|
-
:
|
|
1383
|
-
|
|
1384
|
-
if (
|
|
1764
|
+
? (messageRecord.kwargs as Record<string, unknown>)
|
|
1765
|
+
: messageRecord;
|
|
1766
|
+
|
|
1767
|
+
if (
|
|
1768
|
+
messageRecord.type === 'ai' ||
|
|
1769
|
+
messageRecord.type === 'AIMessageChunk' ||
|
|
1770
|
+
isSerializedFormat
|
|
1771
|
+
) {
|
|
1385
1772
|
/**
|
|
1386
1773
|
* Try tool_calls first (normalized format)
|
|
1387
1774
|
*/
|
|
@@ -1454,6 +1841,7 @@ export function processLangGraphEvent(
|
|
|
1454
1841
|
toolName: toolCall.name,
|
|
1455
1842
|
dynamic: true,
|
|
1456
1843
|
});
|
|
1844
|
+
emittedToolInputs.add(toolCall.id);
|
|
1457
1845
|
controller.enqueue({
|
|
1458
1846
|
type: 'tool-input-available',
|
|
1459
1847
|
toolCallId: toolCall.id,
|
|
@@ -1461,6 +1849,11 @@ export function processLangGraphEvent(
|
|
|
1461
1849
|
input: toolCall.args,
|
|
1462
1850
|
dynamic: true,
|
|
1463
1851
|
});
|
|
1852
|
+
} else if (toolCall.id && emittedToolCalls.has(toolCall.id)) {
|
|
1853
|
+
// Register key mapping for tool calls already emitted via messages mode
|
|
1854
|
+
// so that __interrupt__ handling can match them by key
|
|
1855
|
+
const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
|
|
1856
|
+
emittedToolCallsByKey.set(toolCallKey, toolCall.id);
|
|
1464
1857
|
}
|
|
1465
1858
|
}
|
|
1466
1859
|
}
|
|
@@ -1476,7 +1869,7 @@ export function processLangGraphEvent(
|
|
|
1476
1869
|
* AND tool_calls. We skip those to avoid duplicate reasoning entries.)
|
|
1477
1870
|
*/
|
|
1478
1871
|
const reasoningId = extractReasoningId(msg);
|
|
1479
|
-
const wasStreamedThisRequest =
|
|
1872
|
+
const wasStreamedThisRequest = messageSeen.has(msgId);
|
|
1480
1873
|
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
|
1481
1874
|
|
|
1482
1875
|
/**
|
|
@@ -1509,6 +1902,16 @@ export function processLangGraphEvent(
|
|
|
1509
1902
|
emittedReasoningIds.add(reasoningId);
|
|
1510
1903
|
}
|
|
1511
1904
|
}
|
|
1905
|
+
|
|
1906
|
+
const valuesCitations = extractCitationsFromContentBlocks(msg);
|
|
1907
|
+
if (valuesCitations.length > 0) {
|
|
1908
|
+
emitSourceChunks(
|
|
1909
|
+
valuesCitations,
|
|
1910
|
+
msgId,
|
|
1911
|
+
state.emittedSourceIds,
|
|
1912
|
+
controller,
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1512
1915
|
}
|
|
1513
1916
|
}
|
|
1514
1917
|
}
|
|
@@ -1572,6 +1975,7 @@ export function processLangGraphEvent(
|
|
|
1572
1975
|
toolName,
|
|
1573
1976
|
dynamic: true,
|
|
1574
1977
|
});
|
|
1978
|
+
emittedToolInputs.add(toolCallId);
|
|
1575
1979
|
controller.enqueue({
|
|
1576
1980
|
type: 'tool-input-available',
|
|
1577
1981
|
toolCallId,
|