@ai-sdk/langchain 2.0.267 → 2.0.268

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/langchain",
3
- "version": "2.0.267",
3
+ "version": "2.0.268",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "ai": "6.0.259"
28
+ "ai": "6.0.260"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@langchain/core": "^1.1.5",
package/src/adapter.ts CHANGED
@@ -435,13 +435,14 @@ export function toUIMessageStream<TState = unknown>(
435
435
  messageSeen: new Map(),
436
436
  messageNamespaces: new Map(),
437
437
  messageConcat: new Map(),
438
+ messageIdsInCurrentStepByNamespace: new Map(),
438
439
  emittedToolCalls: new Set<string>(),
440
+ emittedToolCallsInCurrentStepByNamespace: new Map(),
439
441
  emittedImages: new Set<string>(),
440
442
  emittedReasoningIds: new Set<string>(),
441
443
  messageReasoningIds: new Map(),
442
444
  toolCallInfoByIndex: new Map(),
443
- currentStep: null as number | null,
444
- stepNamespace: null,
445
+ currentStepsByNamespace: new Map(),
445
446
  emittedToolCallsByKey: new Map<string, string>(),
446
447
  emittedSourceIds: new Set<string>(),
447
448
  };
@@ -608,7 +609,7 @@ export function toUIMessageStream<TState = unknown>(
608
609
  /**
609
610
  * Emit finish-step if a step was started
610
611
  */
611
- if (langGraphState.currentStep !== null) {
612
+ if (langGraphState.currentStepsByNamespace.size > 0) {
612
613
  controller.enqueue({ type: 'finish-step' });
613
614
  }
614
615
  controller.enqueue({ type: 'finish' });
package/src/types.ts CHANGED
@@ -16,8 +16,12 @@ export interface LangGraphEventState {
16
16
  messageNamespaces: Map<string, string[]>;
17
17
  /** Accumulates message chunks for later reference */
18
18
  messageConcat: Map<string, AIMessageChunk>;
19
+ /** Tracks message IDs observed in each namespace's current LangGraph step */
20
+ messageIdsInCurrentStepByNamespace: Map<string, Set<string>>;
19
21
  /** Tracks which tool call IDs have emitted tool-input-start */
20
22
  emittedToolCalls: Set<string>;
23
+ /** Tracks tool-input-start chunks emitted in each namespace's current step */
24
+ emittedToolCallsInCurrentStepByNamespace: Map<string, Set<string>>;
21
25
  /** Maps image IDs to their message IDs (for chunks that don't include the ID) */
22
26
  emittedImages: Set<string>;
23
27
  /** Maps reasoning block IDs to their message IDs (for chunks that don't include the ID) */
@@ -26,10 +30,8 @@ export interface LangGraphEventState {
26
30
  messageReasoningIds: Map<string, string>;
27
31
  /** Maps message ID + tool call index to tool call info (for streaming chunks without ID) */
28
32
  toolCallInfoByIndex: Map<string, Map<number, { id: string; name: string }>>;
29
- /** Tracks the current LangGraph step for start-step/finish-step events */
30
- currentStep: number | null;
31
- /** Namespace whose step counter drives the global UI step lifecycle */
32
- stepNamespace: string | null;
33
+ /** Tracks each namespace's current LangGraph step */
34
+ currentStepsByNamespace: Map<string, number>;
33
35
  /** Maps tool call key (name:argsJson) to tool call ID for HITL interrupt handling */
34
36
  emittedToolCallsByKey: Map<string, string>;
35
37
  /** Tracks source IDs already emitted to avoid duplicates across messages/values events */
package/src/utils.ts CHANGED
@@ -1173,10 +1173,63 @@ function getLangGraphNamespaceKey(namespace: string[] | undefined): string {
1173
1173
  return JSON.stringify(namespace ?? []);
1174
1174
  }
1175
1175
 
1176
+ function getOrCreateNamespaceSet(
1177
+ setsByNamespace: Map<string, Set<string>>,
1178
+ namespace: string,
1179
+ ): Set<string> {
1180
+ let values = setsByNamespace.get(namespace);
1181
+ if (!values) {
1182
+ values = new Set();
1183
+ setsByNamespace.set(namespace, values);
1184
+ }
1185
+ return values;
1186
+ }
1187
+
1188
+ function hasEmittedToolCallInCurrentStep(
1189
+ state: LangGraphEventState,
1190
+ toolCallId: string,
1191
+ namespace: string,
1192
+ ): boolean {
1193
+ return !state.currentStepsByNamespace.has(namespace)
1194
+ ? state.emittedToolCalls.has(toolCallId)
1195
+ : state.emittedToolCallsInCurrentStepByNamespace
1196
+ .get(namespace)
1197
+ ?.has(toolCallId) === true;
1198
+ }
1199
+
1200
+ function markToolCallEmitted(
1201
+ state: LangGraphEventState,
1202
+ toolCallId: string,
1203
+ namespace: string,
1204
+ ): void {
1205
+ state.emittedToolCalls.add(toolCallId);
1206
+ if (state.currentStepsByNamespace.has(namespace)) {
1207
+ getOrCreateNamespaceSet(
1208
+ state.emittedToolCallsInCurrentStepByNamespace,
1209
+ namespace,
1210
+ ).add(toolCallId);
1211
+ }
1212
+ }
1213
+
1214
+ function findMessageCurrentStepNamespace(
1215
+ state: LangGraphEventState,
1216
+ messageId: string,
1217
+ ): string | undefined {
1218
+ for (const [
1219
+ namespace,
1220
+ messageIds,
1221
+ ] of state.messageIdsInCurrentStepByNamespace) {
1222
+ if (messageIds.has(messageId)) {
1223
+ return namespace;
1224
+ }
1225
+ }
1226
+ return undefined;
1227
+ }
1228
+
1176
1229
  /**
1177
- * Ends and clears message state for the namespace that drives the global UI
1178
- * step lifecycle. Returns whether another namespace still has active text or
1179
- * reasoning that would be invalidated by a global finish-step chunk.
1230
+ * Ends and clears message state for an advancing namespace. Returns whether
1231
+ * another namespace still has active text or reasoning that would be
1232
+ * invalidated by a global finish-step chunk.
1180
1233
  */
1181
1234
  function closeStepNamespaceMessages(
1182
1235
  state: LangGraphEventState,
@@ -1240,6 +1293,7 @@ export function processLangGraphEvent(
1240
1293
  rawNamespace.every(segment => typeof segment === 'string')
1241
1294
  ? rawNamespace
1242
1295
  : undefined;
1296
+ const eventNamespace = getLangGraphNamespaceKey(namespace);
1243
1297
  controller = createLangGraphNamespaceController(controller, state, namespace);
1244
1298
 
1245
1299
  switch (type) {
@@ -1293,41 +1347,65 @@ export function processLangGraphEvent(
1293
1347
  }
1294
1348
 
1295
1349
  /**
1296
- * The first namespace with a step counter drives the global UI step
1297
- * lifecycle. This is the root namespace for complete subgraph streams and
1298
- * the selected namespace for streams filtered to one subgraph.
1299
- *
1300
- * Other namespaces have independent counters and must not change this
1301
- * cursor. A finish-step chunk clears every active UI text/reasoning part,
1302
- * so suppress the global boundary when another namespace is still active.
1350
+ * Each namespace has an independent LangGraph step counter. Advancing a
1351
+ * namespace starts a new reducer scope so provider-scoped tool call IDs
1352
+ * can be reused in that namespace. A finish-step chunk clears every
1353
+ * active UI text/reasoning part, so omit it when another namespace is
1354
+ * still active.
1303
1355
  */
1304
1356
  const langgraphStep =
1305
1357
  typeof metadata?.langgraph_step === 'number'
1306
1358
  ? metadata.langgraph_step
1307
1359
  : null;
1308
- const eventNamespace = getLangGraphNamespaceKey(namespace);
1309
- if (langgraphStep !== null && state.stepNamespace === null) {
1310
- state.stepNamespace = eventNamespace;
1311
- }
1312
- if (
1313
- langgraphStep !== null &&
1314
- eventNamespace === state.stepNamespace &&
1315
- langgraphStep !== state.currentStep
1316
- ) {
1317
- if (state.currentStep !== null) {
1360
+ if (langgraphStep !== null) {
1361
+ const currentStep =
1362
+ state.currentStepsByNamespace.get(eventNamespace) ?? null;
1363
+ if (currentStep === null) {
1364
+ if (state.currentStepsByNamespace.size === 0) {
1365
+ controller.enqueue({ type: 'start-step' });
1366
+ }
1367
+ state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
1368
+ state.messageIdsInCurrentStepByNamespace.set(
1369
+ eventNamespace,
1370
+ new Set(),
1371
+ );
1372
+ state.emittedToolCallsInCurrentStepByNamespace.set(
1373
+ eventNamespace,
1374
+ new Set(),
1375
+ );
1376
+ } else if (langgraphStep !== currentStep) {
1318
1377
  const hasConcurrentMessageParts = closeStepNamespaceMessages(
1319
1378
  state,
1320
- state.stepNamespace,
1379
+ eventNamespace,
1321
1380
  controller,
1322
1381
  );
1323
1382
  if (!hasConcurrentMessageParts) {
1324
1383
  controller.enqueue({ type: 'finish-step' });
1325
- controller.enqueue({ type: 'start-step' });
1326
1384
  }
1327
- } else {
1385
+
1386
+ /**
1387
+ * A concurrent namespace prevents finish-step because it would close
1388
+ * that namespace's active text/reasoning parts. Still emit start-step
1389
+ * so tool calls in the advancing namespace have a distinct reducer
1390
+ * scope while the concurrent message lifecycle remains active.
1391
+ */
1328
1392
  controller.enqueue({ type: 'start-step' });
1393
+ state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
1394
+ state.messageIdsInCurrentStepByNamespace.set(
1395
+ eventNamespace,
1396
+ new Set(),
1397
+ );
1398
+ state.emittedToolCallsInCurrentStepByNamespace.set(
1399
+ eventNamespace,
1400
+ new Set(),
1401
+ );
1329
1402
  }
1330
- state.currentStep = langgraphStep;
1403
+ }
1404
+ if (state.currentStepsByNamespace.has(eventNamespace)) {
1405
+ getOrCreateNamespaceSet(
1406
+ state.messageIdsInCurrentStepByNamespace,
1407
+ eventNamespace,
1408
+ ).add(msgId);
1331
1409
  }
1332
1410
 
1333
1411
  /**
@@ -1451,8 +1529,14 @@ export function processLangGraphEvent(
1451
1529
  updatedSeen.tool ??= new Set();
1452
1530
  updatedSeen.tool.add(toolCallId);
1453
1531
 
1454
- if (!emittedToolCalls.has(toolCallId)) {
1455
- emittedToolCalls.add(toolCallId);
1532
+ if (
1533
+ !hasEmittedToolCallInCurrentStep(
1534
+ state,
1535
+ toolCallId,
1536
+ eventNamespace,
1537
+ )
1538
+ ) {
1539
+ markToolCallEmitted(state, toolCallId, eventNamespace);
1456
1540
  controller.enqueue({
1457
1541
  type: 'tool-input-start',
1458
1542
  toolCallId: toolCallId,
@@ -1595,6 +1679,9 @@ export function processLangGraphEvent(
1595
1679
  * Finalize all pending message chunks
1596
1680
  */
1597
1681
  for (const [id, seen] of messageSeen) {
1682
+ const messageNamespace = getLangGraphNamespaceKey(
1683
+ state.messageNamespaces.get(id),
1684
+ );
1598
1685
  if (seen.text) controller.enqueue({ type: 'text-end', id });
1599
1686
  if (seen.tool) {
1600
1687
  for (const toolCallId of seen.tool) {
@@ -1604,7 +1691,7 @@ export function processLangGraphEvent(
1604
1691
  );
1605
1692
 
1606
1693
  if (toolCall) {
1607
- emittedToolCalls.add(toolCallId);
1694
+ markToolCallEmitted(state, toolCallId, messageNamespace);
1608
1695
  // Store mapping for HITL interrupt lookup
1609
1696
  const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1610
1697
  emittedToolCallsByKey.set(toolCallKey, toolCallId);
@@ -1751,18 +1838,30 @@ export function processLangGraphEvent(
1751
1838
 
1752
1839
  if (toolCalls && toolCalls.length > 0) {
1753
1840
  for (const toolCall of toolCalls) {
1841
+ const messageNamespace = findMessageCurrentStepNamespace(
1842
+ state,
1843
+ msgId,
1844
+ );
1845
+ const lifecycleNamespace = messageNamespace ?? eventNamespace;
1846
+ const wasObservedInCurrentStep = messageNamespace !== undefined;
1754
1847
  /**
1755
- * Only emit if we haven't already processed this tool call
1756
- * AND if it's not a historical tool call that already has a ToolMessage response.
1757
- * Historical completed tool calls should not be re-emitted as this would create
1758
- * orphaned tool parts in the UI without corresponding outputs.
1848
+ * Emit tool calls recovered from a message in the current step,
1849
+ * even when a prior step used the same provider-scoped ID.
1850
+ * Otherwise, preserve stream-wide suppression for historical
1851
+ * completed calls and values-only streams without step metadata.
1759
1852
  */
1760
1853
  if (
1761
1854
  toolCall.id &&
1762
- !emittedToolCalls.has(toolCall.id) &&
1763
- !completedToolCallIds.has(toolCall.id)
1855
+ !hasEmittedToolCallInCurrentStep(
1856
+ state,
1857
+ toolCall.id,
1858
+ lifecycleNamespace,
1859
+ ) &&
1860
+ (wasObservedInCurrentStep ||
1861
+ (!emittedToolCalls.has(toolCall.id) &&
1862
+ !completedToolCallIds.has(toolCall.id)))
1764
1863
  ) {
1765
- emittedToolCalls.add(toolCall.id);
1864
+ markToolCallEmitted(state, toolCall.id, lifecycleNamespace);
1766
1865
  // Store mapping for HITL interrupt lookup
1767
1866
  const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1768
1867
  emittedToolCallsByKey.set(toolCallKey, toolCall.id);
@@ -1902,7 +2001,7 @@ export function processLangGraphEvent(
1902
2001
  * so the UI knows what tool is being called with proper lifecycle
1903
2002
  */
1904
2003
  if (!emittedToolCalls.has(toolCallId)) {
1905
- emittedToolCalls.add(toolCallId);
2004
+ markToolCallEmitted(state, toolCallId, eventNamespace);
1906
2005
  emittedToolCallsByKey.set(toolCallKey, toolCallId);
1907
2006
  controller.enqueue({
1908
2007
  type: 'tool-input-start',