@ai-sdk/langchain 2.0.254 → 2.0.256

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/adapter.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  extractReasoningFromContentBlocks,
21
21
  extractCitationsFromContentBlocks,
22
22
  emitSourceChunks,
23
+ getLangGraphProviderMetadata,
23
24
  } from './utils';
24
25
  import { type LangGraphEventState } from './types';
25
26
  import { type StreamCallbacks } from './stream-callbacks';
@@ -432,6 +433,7 @@ export function toUIMessageStream<TState = unknown>(
432
433
  */
433
434
  const langGraphState: LangGraphEventState = {
434
435
  messageSeen: new Map(),
436
+ messageNamespaces: new Map(),
435
437
  messageConcat: new Map(),
436
438
  emittedToolCalls: new Set<string>(),
437
439
  emittedImages: new Set<string>(),
@@ -439,6 +441,7 @@ export function toUIMessageStream<TState = unknown>(
439
441
  messageReasoningIds: new Map(),
440
442
  toolCallInfoByIndex: new Map(),
441
443
  currentStep: null as number | null,
444
+ stepNamespace: null,
442
445
  emittedToolCallsByKey: new Map<string, string>(),
443
446
  emittedSourceIds: new Set<string>(),
444
447
  };
@@ -542,7 +545,7 @@ export function toUIMessageStream<TState = unknown>(
542
545
  );
543
546
  } else {
544
547
  const eventArray = value as unknown[];
545
- const [type, data] = parseLangGraphEvent(eventArray);
548
+ const [, type, data] = parseLangGraphEvent(eventArray);
546
549
 
547
550
  if (type === 'values') {
548
551
  lastValuesData = data as TState;
@@ -583,11 +586,22 @@ export function toUIMessageStream<TState = unknown>(
583
586
  * where the values handler never ran to emit *-end events.
584
587
  */
585
588
  for (const [id, seen] of langGraphState.messageSeen) {
589
+ const providerMetadata = getLangGraphProviderMetadata(
590
+ langGraphState.messageNamespaces.get(id),
591
+ );
586
592
  if (seen.text) {
587
- controller.enqueue({ type: 'text-end', id });
593
+ controller.enqueue({
594
+ type: 'text-end',
595
+ id,
596
+ ...(providerMetadata !== undefined && { providerMetadata }),
597
+ });
588
598
  }
589
599
  if (seen.reasoning) {
590
- controller.enqueue({ type: 'reasoning-end', id });
600
+ controller.enqueue({
601
+ type: 'reasoning-end',
602
+ id,
603
+ ...(providerMetadata !== undefined && { providerMetadata }),
604
+ });
591
605
  }
592
606
  }
593
607
 
package/src/types.ts CHANGED
@@ -12,6 +12,8 @@ export interface LangGraphMessageSeen {
12
12
  export interface LangGraphEventState {
13
13
  /** Tracks which message IDs have been seen */
14
14
  messageSeen: Map<string, LangGraphMessageSeen>;
15
+ /** Maps message IDs to the LangGraph namespace that emitted them */
16
+ messageNamespaces: Map<string, string[]>;
15
17
  /** Accumulates message chunks for later reference */
16
18
  messageConcat: Map<string, AIMessageChunk>;
17
19
  /** Tracks which tool call IDs have emitted tool-input-start */
@@ -26,6 +28,8 @@ export interface LangGraphEventState {
26
28
  toolCallInfoByIndex: Map<string, Map<number, { id: string; name: string }>>;
27
29
  /** Tracks the current LangGraph step for start-step/finish-step events */
28
30
  currentStep: number | null;
31
+ /** Namespace whose step counter drives the global UI step lifecycle */
32
+ stepNamespace: string | null;
29
33
  /** Maps tool call key (name:argsJson) to tool call ID for HITL interrupt handling */
30
34
  emittedToolCallsByKey: Map<string, string>;
31
35
  /** Tracks source IDs already emitted to avoid duplicates across messages/values events */
package/src/utils.ts CHANGED
@@ -29,16 +29,104 @@ import {
29
29
  } from './types';
30
30
 
31
31
  /**
32
- * Parses a LangGraph event tuple into [type, data].
32
+ * Parses a LangGraph event tuple into [namespace, type, data].
33
33
  * Handles both 2-element [type, data] and 3-element [namespace, type, data] formats.
34
34
  *
35
35
  * @param event - The raw LangGraph event array.
36
- * @returns A tuple of [type, data].
36
+ * @returns A tuple of [namespace, type, data].
37
37
  */
38
38
  export function parseLangGraphEvent(
39
39
  event: unknown[],
40
- ): [type: unknown, data: unknown] {
41
- return event.length === 3 ? [event[1], event[2]] : [event[0], event[1]];
40
+ ): [namespace: unknown | undefined, type: unknown, data: unknown] {
41
+ return event.length === 3
42
+ ? [event[0], event[1], event[2]]
43
+ : [undefined, event[0], event[1]];
44
+ }
45
+
46
+ export function getLangGraphProviderMetadata(
47
+ namespace: string[] | undefined,
48
+ ): ProviderMetadata | undefined {
49
+ return namespace === undefined
50
+ ? undefined
51
+ : {
52
+ langchain: {
53
+ namespace,
54
+ },
55
+ };
56
+ }
57
+
58
+ function addLangGraphNamespace(
59
+ chunk: UIMessageChunk,
60
+ namespace: string[] | undefined,
61
+ ): UIMessageChunk {
62
+ if (namespace === undefined) {
63
+ return chunk;
64
+ }
65
+
66
+ switch (chunk.type) {
67
+ case 'text-start':
68
+ case 'text-delta':
69
+ case 'text-end':
70
+ case 'reasoning-start':
71
+ case 'reasoning-delta':
72
+ case 'reasoning-end':
73
+ case 'tool-input-start':
74
+ case 'tool-input-available':
75
+ case 'tool-input-error':
76
+ case 'tool-output-available':
77
+ case 'tool-output-error':
78
+ case 'source-url':
79
+ case 'source-document':
80
+ case 'file': {
81
+ return {
82
+ ...chunk,
83
+ providerMetadata: {
84
+ ...chunk.providerMetadata,
85
+ langchain: {
86
+ ...chunk.providerMetadata?.langchain,
87
+ namespace,
88
+ },
89
+ },
90
+ };
91
+ }
92
+ default:
93
+ return chunk;
94
+ }
95
+ }
96
+
97
+ function createLangGraphNamespaceController(
98
+ controller: ReadableStreamDefaultController<UIMessageChunk>,
99
+ state: LangGraphEventState,
100
+ eventNamespace: string[] | undefined,
101
+ ): ReadableStreamDefaultController<UIMessageChunk> {
102
+ return {
103
+ get desiredSize() {
104
+ return controller.desiredSize;
105
+ },
106
+ close: () => controller.close(),
107
+ error: reason => controller.error(reason),
108
+ enqueue: chunk => {
109
+ if (chunk === undefined) {
110
+ return;
111
+ }
112
+
113
+ let messageNamespace: string[] | undefined;
114
+ switch (chunk.type) {
115
+ case 'text-start':
116
+ case 'text-delta':
117
+ case 'text-end':
118
+ case 'reasoning-start':
119
+ case 'reasoning-delta':
120
+ case 'reasoning-end':
121
+ messageNamespace = state.messageNamespaces.get(chunk.id);
122
+ break;
123
+ }
124
+
125
+ controller.enqueue(
126
+ addLangGraphNamespace(chunk, messageNamespace ?? eventNamespace),
127
+ );
128
+ },
129
+ };
42
130
  }
43
131
 
44
132
  /**
@@ -1077,6 +1165,53 @@ function getOrCreateToolCallInfoByIndex(
1077
1165
  return toolCallInfo;
1078
1166
  }
1079
1167
 
1168
+ /**
1169
+ * Normalizes legacy tuples without a namespace and explicit empty root
1170
+ * namespaces to the same key.
1171
+ */
1172
+ function getLangGraphNamespaceKey(namespace: string[] | undefined): string {
1173
+ return JSON.stringify(namespace ?? []);
1174
+ }
1175
+
1176
+ /**
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.
1180
+ */
1181
+ function closeStepNamespaceMessages(
1182
+ state: LangGraphEventState,
1183
+ stepNamespace: string,
1184
+ controller: ReadableStreamDefaultController<UIMessageChunk>,
1185
+ ): boolean {
1186
+ let hasConcurrentMessageParts = false;
1187
+
1188
+ for (const [id, seen] of state.messageSeen) {
1189
+ const messageNamespace = getLangGraphNamespaceKey(
1190
+ state.messageNamespaces.get(id),
1191
+ );
1192
+
1193
+ if (messageNamespace !== stepNamespace) {
1194
+ if (seen.text || seen.reasoning) {
1195
+ hasConcurrentMessageParts = true;
1196
+ }
1197
+ continue;
1198
+ }
1199
+
1200
+ if (seen.text) {
1201
+ controller.enqueue({ type: 'text-end', id });
1202
+ }
1203
+ if (seen.reasoning) {
1204
+ controller.enqueue({ type: 'reasoning-end', id });
1205
+ }
1206
+ state.messageSeen.delete(id);
1207
+ state.messageNamespaces.delete(id);
1208
+ state.messageConcat.delete(id);
1209
+ state.messageReasoningIds.delete(id);
1210
+ }
1211
+
1212
+ return hasConcurrentMessageParts;
1213
+ }
1214
+
1080
1215
  /**
1081
1216
  * Processes a LangGraph event and emits UI message chunks.
1082
1217
  *
@@ -1099,7 +1234,13 @@ export function processLangGraphEvent(
1099
1234
  toolCallInfoByIndex,
1100
1235
  emittedToolCallsByKey,
1101
1236
  } = state;
1102
- const [type, data] = parseLangGraphEvent(event);
1237
+ const [rawNamespace, type, data] = parseLangGraphEvent(event);
1238
+ const namespace =
1239
+ Array.isArray(rawNamespace) &&
1240
+ rawNamespace.every(segment => typeof segment === 'string')
1241
+ ? rawNamespace
1242
+ : undefined;
1243
+ controller = createLangGraphNamespaceController(controller, state, namespace);
1103
1244
 
1104
1245
  switch (type) {
1105
1246
  case 'custom': {
@@ -1147,32 +1288,45 @@ export function processLangGraphEvent(
1147
1288
 
1148
1289
  if (!msgId) return;
1149
1290
 
1291
+ if (namespace !== undefined) {
1292
+ state.messageNamespaces.set(msgId, namespace);
1293
+ }
1294
+
1150
1295
  /**
1151
- * Track LangGraph step changes and emit start-step/finish-step events.
1152
- * Before emitting finish-step, close any open text/reasoning parts so
1153
- * the client does not receive orphaned deltas after its
1154
- * activeReasoningParts / activeTextParts have been cleared.
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.
1155
1303
  */
1156
1304
  const langgraphStep =
1157
1305
  typeof metadata?.langgraph_step === 'number'
1158
1306
  ? metadata.langgraph_step
1159
1307
  : null;
1160
- if (langgraphStep !== null && langgraphStep !== state.currentStep) {
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
+ ) {
1161
1317
  if (state.currentStep !== null) {
1162
- for (const [id, seen] of messageSeen) {
1163
- if (seen.text) {
1164
- controller.enqueue({ type: 'text-end', id });
1165
- }
1166
- if (seen.reasoning) {
1167
- controller.enqueue({ type: 'reasoning-end', id });
1168
- }
1169
- messageSeen.delete(id);
1170
- messageConcat.delete(id);
1171
- messageReasoningIds.delete(id);
1318
+ const hasConcurrentMessageParts = closeStepNamespaceMessages(
1319
+ state,
1320
+ state.stepNamespace,
1321
+ controller,
1322
+ );
1323
+ if (!hasConcurrentMessageParts) {
1324
+ controller.enqueue({ type: 'finish-step' });
1325
+ controller.enqueue({ type: 'start-step' });
1172
1326
  }
1173
- controller.enqueue({ type: 'finish-step' });
1327
+ } else {
1328
+ controller.enqueue({ type: 'start-step' });
1174
1329
  }
1175
- controller.enqueue({ type: 'start-step' });
1176
1330
  state.currentStep = langgraphStep;
1177
1331
  }
1178
1332
 
@@ -1470,6 +1624,7 @@ export function processLangGraphEvent(
1470
1624
  }
1471
1625
 
1472
1626
  messageSeen.delete(id);
1627
+ state.messageNamespaces.delete(id);
1473
1628
  messageConcat.delete(id);
1474
1629
  messageReasoningIds.delete(id);
1475
1630
  }