@ai-sdk/langchain 2.0.210 → 2.0.212

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/src/utils.ts CHANGED
@@ -16,12 +16,13 @@ import {
16
16
  type UserContent,
17
17
  } from 'ai';
18
18
 
19
- import {
20
- type LangGraphEventState,
21
- type ReasoningContentBlock,
22
- type ThinkingContentBlock,
23
- type GPT5ReasoningOutput,
24
- type ImageGenerationOutput,
19
+ import type {
20
+ LangGraphEventState,
21
+ LangGraphMessageSeen,
22
+ ReasoningContentBlock,
23
+ ThinkingContentBlock,
24
+ GPT5ReasoningOutput,
25
+ ImageGenerationOutput,
25
26
  } from './types';
26
27
 
27
28
  /**
@@ -954,6 +955,38 @@ export function extractImageOutputs(
954
955
  return toolOutputs.filter(isImageGenerationOutput);
955
956
  }
956
957
 
958
+ /**
959
+ * Returns per-message bookkeeping, creating it on first use.
960
+ * Map keys keep remote-controlled message IDs like "__proto__" from touching Object.prototype.
961
+ */
962
+ function getOrCreateMessageSeen(
963
+ messageSeen: LangGraphEventState['messageSeen'],
964
+ msgId: string,
965
+ ): LangGraphMessageSeen {
966
+ let seen = messageSeen.get(msgId);
967
+ if (!seen) {
968
+ seen = {};
969
+ messageSeen.set(msgId, seen);
970
+ }
971
+ return seen;
972
+ }
973
+
974
+ /**
975
+ * Returns the per-index tool call metadata for a message, creating it on first use.
976
+ * Later streamed chunks use this to recover tool call IDs/names from earlier chunks.
977
+ */
978
+ function getOrCreateToolCallInfoByIndex(
979
+ toolCallInfoByIndex: LangGraphEventState['toolCallInfoByIndex'],
980
+ msgId: string,
981
+ ): Map<number, { id: string; name: string }> {
982
+ let toolCallInfo = toolCallInfoByIndex.get(msgId);
983
+ if (!toolCallInfo) {
984
+ toolCallInfo = new Map();
985
+ toolCallInfoByIndex.set(msgId, toolCallInfo);
986
+ }
987
+ return toolCallInfo;
988
+ }
989
+
957
990
  /**
958
991
  * Processes a LangGraph event and emits UI message chunks.
959
992
  *
@@ -1036,16 +1069,16 @@ export function processLangGraphEvent(
1036
1069
  : null;
1037
1070
  if (langgraphStep !== null && langgraphStep !== state.currentStep) {
1038
1071
  if (state.currentStep !== null) {
1039
- for (const [id, seen] of Object.entries(messageSeen)) {
1072
+ for (const [id, seen] of messageSeen) {
1040
1073
  if (seen.text) {
1041
1074
  controller.enqueue({ type: 'text-end', id });
1042
1075
  }
1043
1076
  if (seen.reasoning) {
1044
1077
  controller.enqueue({ type: 'reasoning-end', id });
1045
1078
  }
1046
- delete messageSeen[id];
1047
- delete messageConcat[id];
1048
- delete messageReasoningIds[id];
1079
+ messageSeen.delete(id);
1080
+ messageConcat.delete(id);
1081
+ messageReasoningIds.delete(id);
1049
1082
  }
1050
1083
  controller.enqueue({ type: 'finish-step' });
1051
1084
  }
@@ -1058,17 +1091,19 @@ export function processLangGraphEvent(
1058
1091
  * Note: Only works for actual class instances, not serialized messages
1059
1092
  */
1060
1093
  if (AIMessageChunk.isInstance(msg)) {
1061
- if (messageConcat[msgId]) {
1062
- messageConcat[msgId] = messageConcat[msgId].concat(
1063
- msg,
1064
- ) as AIMessageChunk;
1094
+ const existingMessage = messageConcat.get(msgId);
1095
+ if (existingMessage) {
1096
+ messageConcat.set(
1097
+ msgId,
1098
+ existingMessage.concat(msg) as AIMessageChunk,
1099
+ );
1065
1100
  } else {
1066
- messageConcat[msgId] = msg;
1101
+ messageConcat.set(msgId, msg);
1067
1102
  }
1068
1103
  }
1069
1104
 
1070
1105
  if (isAIMessageChunk(msg)) {
1071
- const concatChunk = messageConcat[msgId];
1106
+ const concatChunk = messageConcat.get(msgId);
1072
1107
 
1073
1108
  /**
1074
1109
  * Handle image generation outputs from additional_kwargs.tool_outputs
@@ -1127,22 +1162,25 @@ export function processLangGraphEvent(
1127
1162
  * If this chunk has an id, store it for future lookups by index
1128
1163
  */
1129
1164
  if (toolCallChunk.id) {
1130
- toolCallInfoByIndex[msgId] ??= {};
1131
- toolCallInfoByIndex[msgId][idx] = {
1132
- id: toolCallChunk.id,
1133
- name:
1134
- toolCallChunk.name ||
1135
- concatChunk?.tool_call_chunks?.[idx]?.name ||
1136
- 'unknown',
1137
- };
1165
+ getOrCreateToolCallInfoByIndex(toolCallInfoByIndex, msgId).set(
1166
+ idx,
1167
+ {
1168
+ id: toolCallChunk.id,
1169
+ name:
1170
+ toolCallChunk.name ||
1171
+ concatChunk?.tool_call_chunks?.[idx]?.name ||
1172
+ 'unknown',
1173
+ },
1174
+ );
1138
1175
  }
1139
1176
 
1140
1177
  /**
1141
1178
  * Get the tool call ID from the chunk, stored info, or accumulated chunks
1142
1179
  */
1180
+ const storedToolCallInfo = toolCallInfoByIndex.get(msgId)?.get(idx);
1143
1181
  const toolCallId =
1144
1182
  toolCallChunk.id ||
1145
- toolCallInfoByIndex[msgId]?.[idx]?.id ||
1183
+ storedToolCallInfo?.id ||
1146
1184
  concatChunk?.tool_call_chunks?.[idx]?.id;
1147
1185
 
1148
1186
  /**
@@ -1154,7 +1192,7 @@ export function processLangGraphEvent(
1154
1192
 
1155
1193
  const toolName =
1156
1194
  toolCallChunk.name ||
1157
- toolCallInfoByIndex[msgId]?.[idx]?.name ||
1195
+ storedToolCallInfo?.name ||
1158
1196
  concatChunk?.tool_call_chunks?.[idx]?.name ||
1159
1197
  'unknown';
1160
1198
 
@@ -1163,18 +1201,21 @@ export function processLangGraphEvent(
1163
1201
  * (even if args is empty - the first chunk often has empty args)
1164
1202
  * Set dynamic: true to enable HITL approval requests
1165
1203
  */
1166
- if (!messageSeen[msgId]?.tool?.[toolCallId]) {
1167
- controller.enqueue({
1168
- type: 'tool-input-start',
1169
- toolCallId: toolCallId,
1170
- toolName: toolName,
1171
- dynamic: true,
1172
- });
1204
+ const seen = messageSeen.get(msgId);
1205
+ if (!seen?.tool?.has(toolCallId)) {
1206
+ const updatedSeen = getOrCreateMessageSeen(messageSeen, msgId);
1207
+ updatedSeen.tool ??= new Set();
1208
+ updatedSeen.tool.add(toolCallId);
1173
1209
 
1174
- messageSeen[msgId] ??= {};
1175
- messageSeen[msgId].tool ??= {};
1176
- messageSeen[msgId].tool![toolCallId] = true;
1177
- emittedToolCalls.add(toolCallId);
1210
+ if (!emittedToolCalls.has(toolCallId)) {
1211
+ emittedToolCalls.add(toolCallId);
1212
+ controller.enqueue({
1213
+ type: 'tool-input-start',
1214
+ toolCallId: toolCallId,
1215
+ toolName: toolName,
1216
+ dynamic: true,
1217
+ });
1218
+ }
1178
1219
  }
1179
1220
 
1180
1221
  /**
@@ -1205,8 +1246,8 @@ export function processLangGraphEvent(
1205
1246
  // Capture reasoning ID when we first see it (even if no content yet)
1206
1247
  const chunkReasoningId = extractReasoningId(msg);
1207
1248
  if (chunkReasoningId) {
1208
- if (!messageReasoningIds[msgId]) {
1209
- messageReasoningIds[msgId] = chunkReasoningId;
1249
+ if (!messageReasoningIds.has(msgId)) {
1250
+ messageReasoningIds.set(msgId, chunkReasoningId);
1210
1251
  }
1211
1252
  // Immediately mark as emitted to prevent values from duplicating
1212
1253
  // This must happen as soon as we see the ID, before content arrives
@@ -1217,12 +1258,12 @@ export function processLangGraphEvent(
1217
1258
  if (reasoning) {
1218
1259
  // Use stored reasoning ID, or current chunk's ID, or fall back to message ID
1219
1260
  const reasoningId =
1220
- messageReasoningIds[msgId] ?? chunkReasoningId ?? msgId;
1261
+ messageReasoningIds.get(msgId) ?? chunkReasoningId ?? msgId;
1221
1262
 
1222
- if (!messageSeen[msgId]?.reasoning) {
1263
+ const seen = messageSeen.get(msgId);
1264
+ if (!seen?.reasoning) {
1223
1265
  controller.enqueue({ type: 'reasoning-start', id: msgId });
1224
- messageSeen[msgId] ??= {};
1225
- messageSeen[msgId].reasoning = true;
1266
+ getOrCreateMessageSeen(messageSeen, msgId).reasoning = true;
1226
1267
  }
1227
1268
 
1228
1269
  // Streaming chunks have delta text, emit directly without slicing
@@ -1240,10 +1281,10 @@ export function processLangGraphEvent(
1240
1281
  */
1241
1282
  const text = getMessageText(msg);
1242
1283
  if (text) {
1243
- if (!messageSeen[msgId]?.text) {
1284
+ const seen = messageSeen.get(msgId);
1285
+ if (!seen?.text) {
1244
1286
  controller.enqueue({ type: 'text-start', id: msgId });
1245
- messageSeen[msgId] ??= {};
1246
- messageSeen[msgId].text = true;
1287
+ getOrCreateMessageSeen(messageSeen, msgId).text = true;
1247
1288
  }
1248
1289
 
1249
1290
  controller.enqueue({
@@ -1294,16 +1335,16 @@ export function processLangGraphEvent(
1294
1335
  /**
1295
1336
  * Finalize all pending message chunks
1296
1337
  */
1297
- for (const [id, seen] of Object.entries(messageSeen)) {
1338
+ for (const [id, seen] of messageSeen) {
1298
1339
  if (seen.text) controller.enqueue({ type: 'text-end', id });
1299
1340
  if (seen.tool) {
1300
- for (const [toolCallId, toolCallSeen] of Object.entries(seen.tool)) {
1301
- const concatMsg = messageConcat[id];
1341
+ for (const toolCallId of seen.tool) {
1342
+ const concatMsg = messageConcat.get(id);
1302
1343
  const toolCall = concatMsg?.tool_calls?.find(
1303
1344
  call => call.id === toolCallId,
1304
1345
  );
1305
1346
 
1306
- if (toolCallSeen && toolCall) {
1347
+ if (toolCall) {
1307
1348
  emittedToolCalls.add(toolCallId);
1308
1349
  // Store mapping for HITL interrupt lookup
1309
1350
  const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
@@ -1323,9 +1364,9 @@ export function processLangGraphEvent(
1323
1364
  controller.enqueue({ type: 'reasoning-end', id });
1324
1365
  }
1325
1366
 
1326
- delete messageSeen[id];
1327
- delete messageConcat[id];
1328
- delete messageReasoningIds[id];
1367
+ messageSeen.delete(id);
1368
+ messageConcat.delete(id);
1369
+ messageReasoningIds.delete(id);
1329
1370
  }
1330
1371
 
1331
1372
  /**
@@ -1498,7 +1539,7 @@ export function processLangGraphEvent(
1498
1539
  * AND tool_calls. We skip those to avoid duplicate reasoning entries.)
1499
1540
  */
1500
1541
  const reasoningId = extractReasoningId(msg);
1501
- const wasStreamedThisRequest = !!messageSeen[msgId];
1542
+ const wasStreamedThisRequest = messageSeen.has(msgId);
1502
1543
  const hasToolCalls = toolCalls && toolCalls.length > 0;
1503
1544
 
1504
1545
  /**