@ai-sdk/langchain 3.0.65 → 3.0.67

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": "3.0.65",
3
+ "version": "3.0.67",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -25,7 +25,7 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "ai": "7.0.65"
28
+ "ai": "7.0.67"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@langchain/core": "^1.1.46",
package/src/adapter.ts CHANGED
@@ -458,14 +458,16 @@ export function toUIMessageStream<TState = unknown>(
458
458
  messageSeen: new Map(),
459
459
  messageNamespaces: new Map(),
460
460
  messageConcat: new Map(),
461
+ messageIdsInCurrentStepByNamespace: new Map(),
461
462
  emittedToolCalls: new Set<string>(),
463
+ emittedToolCallsInCurrentStepByNamespace: new Map(),
462
464
  emittedToolInputs: new Set<string>(),
465
+ emittedToolInputsInCurrentStepByNamespace: new Map(),
463
466
  emittedImages: new Set<string>(),
464
467
  emittedReasoningIds: new Set<string>(),
465
468
  messageReasoningIds: new Map(),
466
469
  toolCallInfoByIndex: new Map(),
467
- currentStep: null as number | null,
468
- stepNamespace: null,
470
+ currentStepsByNamespace: new Map(),
469
471
  emittedToolCallsByKey: new Map<string, string>(),
470
472
  emittedSourceIds: new Set<string>(),
471
473
  };
@@ -638,7 +640,7 @@ export function toUIMessageStream<TState = unknown>(
638
640
  /**
639
641
  * Emit finish-step if a step was started
640
642
  */
641
- if (langGraphState.currentStep !== null) {
643
+ if (langGraphState.currentStepsByNamespace.size > 0) {
642
644
  controller.enqueue({ type: 'finish-step' });
643
645
  }
644
646
  if (sendFinish) {
package/src/types.ts CHANGED
@@ -16,10 +16,16 @@ 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
  /** Tracks which tool call IDs have emitted complete tool inputs */
22
26
  emittedToolInputs: Set<string>;
27
+ /** Tracks complete tool inputs emitted in each namespace's current step */
28
+ emittedToolInputsInCurrentStepByNamespace: Map<string, Set<string>>;
23
29
  /** Maps image IDs to their message IDs (for chunks that don't include the ID) */
24
30
  emittedImages: Set<string>;
25
31
  /** Maps reasoning block IDs to their message IDs (for chunks that don't include the ID) */
@@ -28,10 +34,8 @@ export interface LangGraphEventState {
28
34
  messageReasoningIds: Map<string, string>;
29
35
  /** Maps message ID + tool call index to tool call info (for streaming chunks without ID) */
30
36
  toolCallInfoByIndex: Map<string, Map<number, { id: string; name: string }>>;
31
- /** Tracks the current LangGraph step for start-step/finish-step events */
32
- currentStep: number | null;
33
- /** Namespace whose step counter drives the global UI step lifecycle */
34
- stepNamespace: string | null;
37
+ /** Tracks each namespace's current LangGraph step */
38
+ currentStepsByNamespace: Map<string, number>;
35
39
  /** Maps tool call key (name:argsJson) to tool call ID for HITL interrupt handling */
36
40
  emittedToolCallsByKey: Map<string, string>;
37
41
  /** Tracks source IDs already emitted to avoid duplicates across messages/values events */
package/src/utils.ts CHANGED
@@ -1252,10 +1252,89 @@ function getLangGraphNamespaceKey(namespace: string[] | undefined): string {
1252
1252
  return JSON.stringify(namespace ?? []);
1253
1253
  }
1254
1254
 
1255
+ function getOrCreateNamespaceSet(
1256
+ setsByNamespace: Map<string, Set<string>>,
1257
+ namespace: string,
1258
+ ): Set<string> {
1259
+ let values = setsByNamespace.get(namespace);
1260
+ if (!values) {
1261
+ values = new Set();
1262
+ setsByNamespace.set(namespace, values);
1263
+ }
1264
+ return values;
1265
+ }
1266
+
1267
+ function hasEmittedToolCallInCurrentStep(
1268
+ state: LangGraphEventState,
1269
+ toolCallId: string,
1270
+ namespace: string,
1271
+ ): boolean {
1272
+ return !state.currentStepsByNamespace.has(namespace)
1273
+ ? state.emittedToolCalls.has(toolCallId)
1274
+ : state.emittedToolCallsInCurrentStepByNamespace
1275
+ .get(namespace)
1276
+ ?.has(toolCallId) === true;
1277
+ }
1278
+
1279
+ function markToolCallEmitted(
1280
+ state: LangGraphEventState,
1281
+ toolCallId: string,
1282
+ namespace: string,
1283
+ ): void {
1284
+ state.emittedToolCalls.add(toolCallId);
1285
+ if (state.currentStepsByNamespace.has(namespace)) {
1286
+ getOrCreateNamespaceSet(
1287
+ state.emittedToolCallsInCurrentStepByNamespace,
1288
+ namespace,
1289
+ ).add(toolCallId);
1290
+ }
1291
+ }
1292
+
1293
+ function hasEmittedToolInputInCurrentStep(
1294
+ state: LangGraphEventState,
1295
+ toolCallId: string,
1296
+ namespace: string,
1297
+ ): boolean {
1298
+ return !state.currentStepsByNamespace.has(namespace)
1299
+ ? state.emittedToolInputs.has(toolCallId)
1300
+ : state.emittedToolInputsInCurrentStepByNamespace
1301
+ .get(namespace)
1302
+ ?.has(toolCallId) === true;
1303
+ }
1304
+
1305
+ function markToolInputEmitted(
1306
+ state: LangGraphEventState,
1307
+ toolCallId: string,
1308
+ namespace: string,
1309
+ ): void {
1310
+ state.emittedToolInputs.add(toolCallId);
1311
+ if (state.currentStepsByNamespace.has(namespace)) {
1312
+ getOrCreateNamespaceSet(
1313
+ state.emittedToolInputsInCurrentStepByNamespace,
1314
+ namespace,
1315
+ ).add(toolCallId);
1316
+ }
1317
+ }
1318
+
1319
+ function findMessageCurrentStepNamespace(
1320
+ state: LangGraphEventState,
1321
+ messageId: string,
1322
+ ): string | undefined {
1323
+ for (const [
1324
+ namespace,
1325
+ messageIds,
1326
+ ] of state.messageIdsInCurrentStepByNamespace) {
1327
+ if (messageIds.has(messageId)) {
1328
+ return namespace;
1329
+ }
1330
+ }
1331
+ return undefined;
1332
+ }
1333
+
1255
1334
  /**
1256
- * Ends and clears message state for the namespace that drives the global UI
1257
- * step lifecycle. Returns whether another namespace still has active text or
1258
- * reasoning that would be invalidated by a global finish-step chunk.
1335
+ * Ends and clears message state for an advancing namespace. Returns whether
1336
+ * another namespace still has active text or reasoning that would be
1337
+ * invalidated by a global finish-step chunk.
1259
1338
  */
1260
1339
  function closeStepNamespaceMessages(
1261
1340
  state: LangGraphEventState,
@@ -1312,7 +1391,6 @@ export function processLangGraphEvent(
1312
1391
  messageReasoningIds,
1313
1392
  toolCallInfoByIndex,
1314
1393
  emittedToolCallsByKey,
1315
- emittedToolInputs,
1316
1394
  } = state;
1317
1395
  const [rawNamespace, type, data] = parseLangGraphEvent(event);
1318
1396
  const namespace =
@@ -1320,6 +1398,7 @@ export function processLangGraphEvent(
1320
1398
  rawNamespace.every(segment => typeof segment === 'string')
1321
1399
  ? rawNamespace
1322
1400
  : undefined;
1401
+ const eventNamespace = getLangGraphNamespaceKey(namespace);
1323
1402
  controller = createLangGraphNamespaceController(controller, state, namespace);
1324
1403
 
1325
1404
  switch (type) {
@@ -1373,41 +1452,73 @@ export function processLangGraphEvent(
1373
1452
  }
1374
1453
 
1375
1454
  /**
1376
- * The first namespace with a step counter drives the global UI step
1377
- * lifecycle. This is the root namespace for complete subgraph streams and
1378
- * the selected namespace for streams filtered to one subgraph.
1379
- *
1380
- * Other namespaces have independent counters and must not change this
1381
- * cursor. A finish-step chunk clears every active UI text/reasoning part,
1382
- * so suppress the global boundary when another namespace is still active.
1455
+ * Each namespace has an independent LangGraph step counter. Advancing a
1456
+ * namespace starts a new reducer scope so provider-scoped tool call IDs
1457
+ * can be reused in that namespace. A finish-step chunk clears every
1458
+ * active UI text/reasoning part, so omit it when another namespace is
1459
+ * still active.
1383
1460
  */
1384
1461
  const langgraphStep =
1385
1462
  typeof metadata?.langgraph_step === 'number'
1386
1463
  ? metadata.langgraph_step
1387
1464
  : null;
1388
- const eventNamespace = getLangGraphNamespaceKey(namespace);
1389
- if (langgraphStep !== null && state.stepNamespace === null) {
1390
- state.stepNamespace = eventNamespace;
1391
- }
1392
- if (
1393
- langgraphStep !== null &&
1394
- eventNamespace === state.stepNamespace &&
1395
- langgraphStep !== state.currentStep
1396
- ) {
1397
- if (state.currentStep !== null) {
1465
+ if (langgraphStep !== null) {
1466
+ const currentStep =
1467
+ state.currentStepsByNamespace.get(eventNamespace) ?? null;
1468
+ if (currentStep === null) {
1469
+ if (state.currentStepsByNamespace.size === 0) {
1470
+ controller.enqueue({ type: 'start-step' });
1471
+ }
1472
+ state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
1473
+ state.messageIdsInCurrentStepByNamespace.set(
1474
+ eventNamespace,
1475
+ new Set(),
1476
+ );
1477
+ state.emittedToolCallsInCurrentStepByNamespace.set(
1478
+ eventNamespace,
1479
+ new Set(),
1480
+ );
1481
+ state.emittedToolInputsInCurrentStepByNamespace.set(
1482
+ eventNamespace,
1483
+ new Set(),
1484
+ );
1485
+ } else if (langgraphStep !== currentStep) {
1398
1486
  const hasConcurrentMessageParts = closeStepNamespaceMessages(
1399
1487
  state,
1400
- state.stepNamespace,
1488
+ eventNamespace,
1401
1489
  controller,
1402
1490
  );
1403
1491
  if (!hasConcurrentMessageParts) {
1404
1492
  controller.enqueue({ type: 'finish-step' });
1405
- controller.enqueue({ type: 'start-step' });
1406
1493
  }
1407
- } else {
1494
+
1495
+ /**
1496
+ * A concurrent namespace prevents finish-step because it would close
1497
+ * that namespace's active text/reasoning parts. Still emit start-step
1498
+ * so tool calls in the advancing namespace have a distinct reducer
1499
+ * scope while the concurrent message lifecycle remains active.
1500
+ */
1408
1501
  controller.enqueue({ type: 'start-step' });
1502
+ state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
1503
+ state.messageIdsInCurrentStepByNamespace.set(
1504
+ eventNamespace,
1505
+ new Set(),
1506
+ );
1507
+ state.emittedToolCallsInCurrentStepByNamespace.set(
1508
+ eventNamespace,
1509
+ new Set(),
1510
+ );
1511
+ state.emittedToolInputsInCurrentStepByNamespace.set(
1512
+ eventNamespace,
1513
+ new Set(),
1514
+ );
1409
1515
  }
1410
- state.currentStep = langgraphStep;
1516
+ }
1517
+ if (state.currentStepsByNamespace.has(eventNamespace)) {
1518
+ getOrCreateNamespaceSet(
1519
+ state.messageIdsInCurrentStepByNamespace,
1520
+ eventNamespace,
1521
+ ).add(msgId);
1411
1522
  }
1412
1523
 
1413
1524
  /**
@@ -1533,8 +1644,14 @@ export function processLangGraphEvent(
1533
1644
  updatedSeen.tool ??= new Set();
1534
1645
  updatedSeen.tool.add(toolCallId);
1535
1646
 
1536
- if (!emittedToolCalls.has(toolCallId)) {
1537
- emittedToolCalls.add(toolCallId);
1647
+ if (
1648
+ !hasEmittedToolCallInCurrentStep(
1649
+ state,
1650
+ toolCallId,
1651
+ eventNamespace,
1652
+ )
1653
+ ) {
1654
+ markToolCallEmitted(state, toolCallId, eventNamespace);
1538
1655
  controller.enqueue({
1539
1656
  type: 'tool-input-start',
1540
1657
  toolCallId: toolCallId,
@@ -1693,9 +1810,19 @@ export function processLangGraphEvent(
1693
1810
 
1694
1811
  if (!toolCallId) return;
1695
1812
 
1696
- const ensureToolInputLifecycle = () => {
1697
- if (!emittedToolCalls.has(toolCallId)) {
1698
- emittedToolCalls.add(toolCallId);
1813
+ const ensureToolInputLifecycle = ({
1814
+ allowPreviousStep,
1815
+ }: {
1816
+ allowPreviousStep: boolean;
1817
+ }) => {
1818
+ if (
1819
+ !hasEmittedToolCallInCurrentStep(state, toolCallId, eventNamespace)
1820
+ ) {
1821
+ if (allowPreviousStep && emittedToolCalls.has(toolCallId)) {
1822
+ return;
1823
+ }
1824
+
1825
+ markToolCallEmitted(state, toolCallId, eventNamespace);
1699
1826
  controller.enqueue({
1700
1827
  type: 'tool-input-start',
1701
1828
  toolCallId,
@@ -1704,8 +1831,10 @@ export function processLangGraphEvent(
1704
1831
  });
1705
1832
  }
1706
1833
 
1707
- if (!emittedToolInputs.has(toolCallId)) {
1708
- emittedToolInputs.add(toolCallId);
1834
+ if (
1835
+ !hasEmittedToolInputInCurrentStep(state, toolCallId, eventNamespace)
1836
+ ) {
1837
+ markToolInputEmitted(state, toolCallId, eventNamespace);
1709
1838
  controller.enqueue({
1710
1839
  type: 'tool-input-available',
1711
1840
  toolCallId,
@@ -1721,12 +1850,12 @@ export function processLangGraphEvent(
1721
1850
  const toolCallKey = `${toolName}:${JSON.stringify(payload.input)}`;
1722
1851
  emittedToolCallsByKey.set(toolCallKey, toolCallId);
1723
1852
 
1724
- ensureToolInputLifecycle();
1853
+ ensureToolInputLifecycle({ allowPreviousStep: false });
1725
1854
  break;
1726
1855
  }
1727
1856
 
1728
1857
  case 'on_tool_event': {
1729
- ensureToolInputLifecycle();
1858
+ ensureToolInputLifecycle({ allowPreviousStep: true });
1730
1859
  controller.enqueue({
1731
1860
  type: 'tool-output-available',
1732
1861
  toolCallId,
@@ -1737,7 +1866,7 @@ export function processLangGraphEvent(
1737
1866
  }
1738
1867
 
1739
1868
  case 'on_tool_end': {
1740
- ensureToolInputLifecycle();
1869
+ ensureToolInputLifecycle({ allowPreviousStep: true });
1741
1870
  controller.enqueue({
1742
1871
  type: 'tool-output-available',
1743
1872
  toolCallId,
@@ -1747,7 +1876,7 @@ export function processLangGraphEvent(
1747
1876
  }
1748
1877
 
1749
1878
  case 'on_tool_error': {
1750
- ensureToolInputLifecycle();
1879
+ ensureToolInputLifecycle({ allowPreviousStep: true });
1751
1880
  controller.enqueue({
1752
1881
  type: 'tool-output-error',
1753
1882
  toolCallId,
@@ -1765,6 +1894,9 @@ export function processLangGraphEvent(
1765
1894
  * Finalize all pending message chunks
1766
1895
  */
1767
1896
  for (const [id, seen] of messageSeen) {
1897
+ const messageNamespace = getLangGraphNamespaceKey(
1898
+ state.messageNamespaces.get(id),
1899
+ );
1768
1900
  if (seen.text) controller.enqueue({ type: 'text-end', id });
1769
1901
  if (seen.tool) {
1770
1902
  for (const toolCallId of seen.tool) {
@@ -1774,12 +1906,18 @@ export function processLangGraphEvent(
1774
1906
  );
1775
1907
 
1776
1908
  if (toolCall) {
1777
- emittedToolCalls.add(toolCallId);
1909
+ markToolCallEmitted(state, toolCallId, messageNamespace);
1778
1910
  // Store mapping for HITL interrupt lookup
1779
1911
  const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1780
1912
  emittedToolCallsByKey.set(toolCallKey, toolCallId);
1781
- if (!emittedToolInputs.has(toolCallId)) {
1782
- emittedToolInputs.add(toolCallId);
1913
+ if (
1914
+ !hasEmittedToolInputInCurrentStep(
1915
+ state,
1916
+ toolCallId,
1917
+ messageNamespace,
1918
+ )
1919
+ ) {
1920
+ markToolInputEmitted(state, toolCallId, messageNamespace);
1783
1921
  controller.enqueue({
1784
1922
  type: 'tool-input-available',
1785
1923
  toolCallId,
@@ -1924,18 +2062,30 @@ export function processLangGraphEvent(
1924
2062
 
1925
2063
  if (toolCalls && toolCalls.length > 0) {
1926
2064
  for (const toolCall of toolCalls) {
2065
+ const messageNamespace = findMessageCurrentStepNamespace(
2066
+ state,
2067
+ msgId,
2068
+ );
2069
+ const lifecycleNamespace = messageNamespace ?? eventNamespace;
2070
+ const wasObservedInCurrentStep = messageNamespace !== undefined;
1927
2071
  /**
1928
- * Only emit if we haven't already processed this tool call
1929
- * AND if it's not a historical tool call that already has a ToolMessage response.
1930
- * Historical completed tool calls should not be re-emitted as this would create
1931
- * orphaned tool parts in the UI without corresponding outputs.
2072
+ * Emit tool calls recovered from a message in the current step,
2073
+ * even when a prior step used the same provider-scoped ID.
2074
+ * Otherwise, preserve stream-wide suppression for historical
2075
+ * completed calls and values-only streams without step metadata.
1932
2076
  */
1933
2077
  if (
1934
2078
  toolCall.id &&
1935
- !emittedToolCalls.has(toolCall.id) &&
1936
- !completedToolCallIds.has(toolCall.id)
2079
+ !hasEmittedToolCallInCurrentStep(
2080
+ state,
2081
+ toolCall.id,
2082
+ lifecycleNamespace,
2083
+ ) &&
2084
+ (wasObservedInCurrentStep ||
2085
+ (!emittedToolCalls.has(toolCall.id) &&
2086
+ !completedToolCallIds.has(toolCall.id)))
1937
2087
  ) {
1938
- emittedToolCalls.add(toolCall.id);
2088
+ markToolCallEmitted(state, toolCall.id, lifecycleNamespace);
1939
2089
  // Store mapping for HITL interrupt lookup
1940
2090
  const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1941
2091
  emittedToolCallsByKey.set(toolCallKey, toolCall.id);
@@ -1950,7 +2100,7 @@ export function processLangGraphEvent(
1950
2100
  toolName: toolCall.name,
1951
2101
  dynamic: true,
1952
2102
  });
1953
- emittedToolInputs.add(toolCall.id);
2103
+ markToolInputEmitted(state, toolCall.id, lifecycleNamespace);
1954
2104
  controller.enqueue({
1955
2105
  type: 'tool-input-available',
1956
2106
  toolCallId: toolCall.id,
@@ -2076,7 +2226,7 @@ export function processLangGraphEvent(
2076
2226
  * so the UI knows what tool is being called with proper lifecycle
2077
2227
  */
2078
2228
  if (!emittedToolCalls.has(toolCallId)) {
2079
- emittedToolCalls.add(toolCallId);
2229
+ markToolCallEmitted(state, toolCallId, eventNamespace);
2080
2230
  emittedToolCallsByKey.set(toolCallKey, toolCallId);
2081
2231
  controller.enqueue({
2082
2232
  type: 'tool-input-start',
@@ -2084,7 +2234,7 @@ export function processLangGraphEvent(
2084
2234
  toolName,
2085
2235
  dynamic: true,
2086
2236
  });
2087
- emittedToolInputs.add(toolCallId);
2237
+ markToolInputEmitted(state, toolCallId, eventNamespace);
2088
2238
  controller.enqueue({
2089
2239
  type: 'tool-input-available',
2090
2240
  toolCallId,