@ai-sdk/langchain 0.0.0-0219f568-20260113124214 → 0.0.0-17394c74-20260122151521

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 CHANGED
@@ -1,11 +1,118 @@
1
1
  # @ai-sdk/langchain
2
2
 
3
- ## 0.0.0-0219f568-20260113124214
3
+ ## 0.0.0-17394c74-20260122151521
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [eade408]
8
- - ai@0.0.0-0219f568-20260113124214
7
+ - Updated dependencies [4caafb2]
8
+ - ai@0.0.0-17394c74-20260122151521
9
+
10
+ ## 2.0.52
11
+
12
+ ### Patch Changes
13
+
14
+ - ai@6.0.47
15
+
16
+ ## 2.0.51
17
+
18
+ ### Patch Changes
19
+
20
+ - 8dc54db: chore: add src folders to package bundle
21
+ - ai@6.0.46
22
+
23
+ ## 2.0.50
24
+
25
+ ### Patch Changes
26
+
27
+ - ai@6.0.45
28
+
29
+ ## 2.0.49
30
+
31
+ ### Patch Changes
32
+
33
+ - ai@6.0.44
34
+
35
+ ## 2.0.48
36
+
37
+ ### Patch Changes
38
+
39
+ - Updated dependencies [2dc9bfa]
40
+ - ai@6.0.43
41
+
42
+ ## 2.0.47
43
+
44
+ ### Patch Changes
45
+
46
+ - ai@6.0.42
47
+
48
+ ## 2.0.46
49
+
50
+ ### Patch Changes
51
+
52
+ - Updated dependencies [84b6e6d]
53
+ - ai@6.0.41
54
+
55
+ ## 2.0.45
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies [ab57783]
60
+ - ai@6.0.40
61
+
62
+ ## 2.0.44
63
+
64
+ ### Patch Changes
65
+
66
+ - Updated dependencies [4e28ba0]
67
+ - ai@6.0.39
68
+
69
+ ## 2.0.43
70
+
71
+ ### Patch Changes
72
+
73
+ - ai@6.0.38
74
+
75
+ ## 2.0.42
76
+
77
+ ### Patch Changes
78
+
79
+ - Updated dependencies [b5dab9b]
80
+ - ai@6.0.37
81
+
82
+ ## 2.0.41
83
+
84
+ ### Patch Changes
85
+
86
+ - Updated dependencies [46f46e4]
87
+ - ai@6.0.36
88
+
89
+ ## 2.0.40
90
+
91
+ ### Patch Changes
92
+
93
+ - Updated dependencies [d7e7f1f]
94
+ - ai@6.0.35
95
+
96
+ ## 2.0.39
97
+
98
+ ### Patch Changes
99
+
100
+ - Updated dependencies [1b11dcb]
101
+ - ai@6.0.34
102
+
103
+ ## 2.0.38
104
+
105
+ ### Patch Changes
106
+
107
+ - Updated dependencies [0ca078c]
108
+ - ai@6.0.33
109
+
110
+ ## 2.0.37
111
+
112
+ ### Patch Changes
113
+
114
+ - Updated dependencies [ec24401]
115
+ - ai@6.0.32
9
116
 
10
117
  ## 2.0.36
11
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/langchain",
3
- "version": "0.0.0-0219f568-20260113124214",
3
+ "version": "0.0.0-17394c74-20260122151521",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -8,6 +8,11 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
10
10
  "dist/**/*",
11
+ "src",
12
+ "!src/**/*.test.ts",
13
+ "!src/**/*.test-d.ts",
14
+ "!src/**/__snapshots__",
15
+ "!src/**/__fixtures__",
11
16
  "CHANGELOG.md",
12
17
  "README.md"
13
18
  ],
@@ -20,7 +25,7 @@
20
25
  }
21
26
  },
22
27
  "dependencies": {
23
- "ai": "0.0.0-0219f568-20260113124214"
28
+ "ai": "0.0.0-17394c74-20260122151521"
24
29
  },
25
30
  "devDependencies": {
26
31
  "@langchain/core": "^1.1.5",
@@ -28,7 +33,7 @@
28
33
  "@types/node": "20.17.24",
29
34
  "tsup": "^8",
30
35
  "typescript": "5.8.3",
31
- "@ai-sdk/provider-utils": "4.0.5",
36
+ "@ai-sdk/provider-utils": "4.0.8",
32
37
  "@vercel/ai-tsconfig": "0.0.0"
33
38
  },
34
39
  "peerDependencies": {
package/src/adapter.ts ADDED
@@ -0,0 +1,520 @@
1
+ import {
2
+ SystemMessage,
3
+ BaseMessage,
4
+ AIMessageChunk,
5
+ } from '@langchain/core/messages';
6
+ import {
7
+ type UIMessage,
8
+ type UIMessageChunk,
9
+ convertToModelMessages,
10
+ type ModelMessage,
11
+ } from 'ai';
12
+ import {
13
+ convertToolResultPart,
14
+ convertAssistantContent,
15
+ convertUserContent,
16
+ processModelChunk,
17
+ processLangGraphEvent,
18
+ isToolResultPart,
19
+ extractReasoningFromContentBlocks,
20
+ } from './utils';
21
+ import { type LangGraphEventState } from './types';
22
+ import { type StreamCallbacks } from './stream-callbacks';
23
+
24
+ /**
25
+ * Converts AI SDK UIMessages to LangChain BaseMessage objects.
26
+ *
27
+ * This function transforms the AI SDK's message format into LangChain's message
28
+ * format, enabling seamless integration between the two frameworks.
29
+ *
30
+ * @param messages - Array of AI SDK UIMessage objects to convert.
31
+ * @returns Promise resolving to an array of LangChain BaseMessage objects.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { toBaseMessages } from '@ai-sdk/langchain';
36
+ *
37
+ * const langchainMessages = await toBaseMessages(uiMessages);
38
+ *
39
+ * // Use with LangChain
40
+ * const response = await model.invoke(langchainMessages);
41
+ * ```
42
+ */
43
+ export async function toBaseMessages(
44
+ messages: UIMessage[],
45
+ ): Promise<BaseMessage[]> {
46
+ const modelMessages = await convertToModelMessages(messages);
47
+ return convertModelMessages(modelMessages);
48
+ }
49
+
50
+ /**
51
+ * Converts ModelMessages to LangChain BaseMessage objects.
52
+ *
53
+ * @param modelMessages - Array of ModelMessage objects from convertToModelMessages.
54
+ * @returns Array of LangChain BaseMessage objects.
55
+ */
56
+ export function convertModelMessages(
57
+ modelMessages: ModelMessage[],
58
+ ): BaseMessage[] {
59
+ const result: BaseMessage[] = [];
60
+
61
+ for (const message of modelMessages) {
62
+ switch (message.role) {
63
+ case 'tool': {
64
+ // Tool messages contain an array of tool results
65
+ for (const item of message.content) {
66
+ if (isToolResultPart(item)) {
67
+ result.push(convertToolResultPart(item));
68
+ }
69
+ }
70
+ break;
71
+ }
72
+
73
+ case 'assistant': {
74
+ result.push(convertAssistantContent(message.content));
75
+ break;
76
+ }
77
+
78
+ case 'system': {
79
+ result.push(new SystemMessage({ content: message.content }));
80
+ break;
81
+ }
82
+
83
+ case 'user': {
84
+ result.push(convertUserContent(message.content));
85
+ break;
86
+ }
87
+ }
88
+ }
89
+
90
+ return result;
91
+ }
92
+
93
+ /**
94
+ * Type guard to check if a value is a streamEvents event object.
95
+ * streamEvents produces objects with `event` and `data` properties.
96
+ *
97
+ * @param value - The value to check.
98
+ * @returns True if the value is a streamEvents event object.
99
+ */
100
+ function isStreamEventsEvent(
101
+ value: unknown,
102
+ ): value is { event: string; data: Record<string, unknown> } {
103
+ if (value == null || typeof value !== 'object') return false;
104
+ const obj = value as Record<string, unknown>;
105
+ // Check for event property being a string
106
+ if (!('event' in obj) || typeof obj.event !== 'string') return false;
107
+ // Check for data property being an object (but allow null/undefined)
108
+ if (!('data' in obj)) return false;
109
+ // data can be null in some events, treat as empty object
110
+ return obj.data === null || typeof obj.data === 'object';
111
+ }
112
+
113
+ /**
114
+ * Processes a streamEvents event and emits UI message chunks.
115
+ *
116
+ * @param event - The streamEvents event to process.
117
+ * @param state - The state for tracking stream progress.
118
+ * @param controller - The controller to emit UI message chunks.
119
+ */
120
+ function processStreamEventsEvent(
121
+ event: {
122
+ event: string;
123
+ data: Record<string, unknown> | null;
124
+ run_id?: string;
125
+ name?: string;
126
+ },
127
+ state: {
128
+ started: boolean;
129
+ messageId: string;
130
+ reasoningStarted: boolean;
131
+ textStarted: boolean;
132
+ textMessageId: string | null;
133
+ reasoningMessageId: string | null;
134
+ },
135
+ controller: ReadableStreamDefaultController<UIMessageChunk>,
136
+ ): void {
137
+ /**
138
+ * Capture run_id from event level if available (streamEvents v2 format)
139
+ */
140
+ if (event.run_id && !state.started) {
141
+ state.messageId = event.run_id;
142
+ }
143
+
144
+ /**
145
+ * Skip events with null/undefined data
146
+ */
147
+ if (!event.data) return;
148
+
149
+ switch (event.event) {
150
+ case 'on_chat_model_start': {
151
+ /**
152
+ * Handle model start - capture message metadata if available
153
+ * run_id is at event level in v2, but check data for backwards compatibility
154
+ */
155
+ const runId = event.run_id || (event.data.run_id as string | undefined);
156
+ if (runId) {
157
+ state.messageId = runId;
158
+ }
159
+ break;
160
+ }
161
+
162
+ case 'on_chat_model_stream': {
163
+ /**
164
+ * Handle streaming token chunks
165
+ */
166
+ const chunk = event.data.chunk;
167
+ if (chunk && typeof chunk === 'object') {
168
+ /**
169
+ * Get message ID from chunk if available
170
+ */
171
+ const chunkId = (chunk as { id?: string }).id;
172
+ if (chunkId) {
173
+ state.messageId = chunkId;
174
+ }
175
+
176
+ /**
177
+ * Handle reasoning content from contentBlocks
178
+ */
179
+ const reasoning = extractReasoningFromContentBlocks(chunk);
180
+ if (reasoning) {
181
+ if (!state.reasoningStarted) {
182
+ // Track the ID used for reasoning-start to ensure reasoning-end uses the same ID
183
+ state.reasoningMessageId = state.messageId;
184
+ controller.enqueue({
185
+ type: 'reasoning-start',
186
+ id: state.messageId,
187
+ });
188
+ state.reasoningStarted = true;
189
+ state.started = true;
190
+ }
191
+ controller.enqueue({
192
+ type: 'reasoning-delta',
193
+ delta: reasoning,
194
+ id: state.reasoningMessageId ?? state.messageId,
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Extract text content from chunk
200
+ */
201
+ const content = (chunk as { content?: unknown }).content;
202
+ const text =
203
+ typeof content === 'string'
204
+ ? content
205
+ : Array.isArray(content)
206
+ ? content
207
+ .filter(
208
+ (c): c is { type: 'text'; text: string } =>
209
+ typeof c === 'object' &&
210
+ c !== null &&
211
+ 'type' in c &&
212
+ c.type === 'text',
213
+ )
214
+ .map(c => c.text)
215
+ .join('')
216
+ : '';
217
+
218
+ if (text) {
219
+ /**
220
+ * If reasoning was streamed before text, close reasoning first
221
+ */
222
+ if (state.reasoningStarted && !state.textStarted) {
223
+ controller.enqueue({
224
+ type: 'reasoning-end',
225
+ id: state.reasoningMessageId ?? state.messageId,
226
+ });
227
+ state.reasoningStarted = false;
228
+ }
229
+
230
+ if (!state.textStarted) {
231
+ // Track the ID used for text-start to ensure text-end uses the same ID
232
+ state.textMessageId = state.messageId;
233
+ controller.enqueue({ type: 'text-start', id: state.messageId });
234
+ state.textStarted = true;
235
+ state.started = true;
236
+ }
237
+ controller.enqueue({
238
+ type: 'text-delta',
239
+ delta: text,
240
+ id: state.textMessageId ?? state.messageId,
241
+ });
242
+ }
243
+ }
244
+ break;
245
+ }
246
+
247
+ case 'on_tool_start': {
248
+ /**
249
+ * Handle tool call start
250
+ * run_id and name are at event level in v2, check data for backwards compatibility
251
+ */
252
+ const runId = event.run_id || (event.data.run_id as string | undefined);
253
+ const name = event.name || (event.data.name as string | undefined);
254
+
255
+ if (runId && name) {
256
+ controller.enqueue({
257
+ type: 'tool-input-start',
258
+ toolCallId: runId,
259
+ toolName: name,
260
+ dynamic: true,
261
+ });
262
+ }
263
+ break;
264
+ }
265
+
266
+ case 'on_tool_end': {
267
+ /**
268
+ * Handle tool call end
269
+ * run_id is at event level in v2, check data for backwards compatibility
270
+ */
271
+ const runId = event.run_id || (event.data.run_id as string | undefined);
272
+ const output = event.data.output;
273
+
274
+ if (runId) {
275
+ controller.enqueue({
276
+ type: 'tool-output-available',
277
+ toolCallId: runId,
278
+ output,
279
+ });
280
+ }
281
+ break;
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Converts a LangChain stream to an AI SDK UIMessageStream.
288
+ *
289
+ * This function automatically detects the stream type and handles:
290
+ * - Direct model streams (AsyncIterable from `model.stream()`)
291
+ * - LangGraph streams (ReadableStream with `streamMode: ['values', 'messages']`)
292
+ * - streamEvents streams (from `agent.streamEvents()` or `model.streamEvents()`)
293
+ *
294
+ * @param stream - A stream from LangChain model.stream(), graph.stream(), or streamEvents().
295
+ * @param callbacks - Optional callbacks for stream lifecycle events.
296
+ * @returns A ReadableStream of UIMessageChunk objects.
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * // With a direct model stream
301
+ * const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
302
+ * const stream = await model.stream(messages);
303
+ * return createUIMessageStreamResponse({
304
+ * stream: toUIMessageStream(stream),
305
+ * });
306
+ *
307
+ * // With a LangGraph stream
308
+ * const graphStream = await graph.stream(
309
+ * { messages },
310
+ * { streamMode: ['values', 'messages'] }
311
+ * );
312
+ * return createUIMessageStreamResponse({
313
+ * stream: toUIMessageStream(graphStream),
314
+ * });
315
+ *
316
+ * // With streamEvents
317
+ * const streamEvents = agent.streamEvents(
318
+ * { messages },
319
+ * { version: "v2" }
320
+ * );
321
+ * return createUIMessageStreamResponse({
322
+ * stream: toUIMessageStream(streamEvents),
323
+ * });
324
+ * ```
325
+ */
326
+ export function toUIMessageStream(
327
+ stream: AsyncIterable<AIMessageChunk> | ReadableStream,
328
+ callbacks?: StreamCallbacks,
329
+ ): ReadableStream<UIMessageChunk> {
330
+ /**
331
+ * Track text chunks for onFinal callback
332
+ */
333
+ const textChunks: string[] = [];
334
+
335
+ /**
336
+ * State for model stream handling
337
+ */
338
+ const modelState = {
339
+ started: false,
340
+ messageId: 'langchain-msg-1',
341
+ reasoningStarted: false,
342
+ textStarted: false,
343
+ /** Track the ID used for text-start to ensure text-end uses the same ID */
344
+ textMessageId: null as string | null,
345
+ /** Track the ID used for reasoning-start to ensure reasoning-end uses the same ID */
346
+ reasoningMessageId: null as string | null,
347
+ };
348
+
349
+ /**
350
+ * State for LangGraph stream handling
351
+ */
352
+ const langGraphState: LangGraphEventState = {
353
+ messageSeen: {} as Record<
354
+ string,
355
+ { text?: boolean; reasoning?: boolean; tool?: Record<string, boolean> }
356
+ >,
357
+ messageConcat: {} as Record<string, AIMessageChunk>,
358
+ emittedToolCalls: new Set<string>(),
359
+ emittedImages: new Set<string>(),
360
+ emittedReasoningIds: new Set<string>(),
361
+ messageReasoningIds: {} as Record<string, string>,
362
+ toolCallInfoByIndex: {} as Record<
363
+ string,
364
+ Record<number, { id: string; name: string }>
365
+ >,
366
+ currentStep: null as number | null,
367
+ emittedToolCallsByKey: new Map<string, string>(),
368
+ };
369
+
370
+ /**
371
+ * Track detected stream type: null = not yet detected
372
+ */
373
+ let streamType: 'model' | 'langgraph' | 'streamEvents' | null = null;
374
+
375
+ /**
376
+ * Get async iterator from the stream (works for both AsyncIterable and ReadableStream)
377
+ */
378
+ const getAsyncIterator = (): AsyncIterator<unknown> => {
379
+ if (Symbol.asyncIterator in stream) {
380
+ return (stream as AsyncIterable<unknown>)[Symbol.asyncIterator]();
381
+ }
382
+ /**
383
+ * For ReadableStream without Symbol.asyncIterator
384
+ */
385
+ const reader = (stream as ReadableStream).getReader();
386
+ return {
387
+ async next() {
388
+ const { done, value } = await reader.read();
389
+ return { done, value };
390
+ },
391
+ };
392
+ };
393
+
394
+ const iterator = getAsyncIterator();
395
+
396
+ /**
397
+ * Create a wrapper around the controller to intercept text chunks for callbacks
398
+ */
399
+ const createCallbackController = (
400
+ originalController: ReadableStreamDefaultController<UIMessageChunk>,
401
+ ): ReadableStreamDefaultController<UIMessageChunk> => {
402
+ return {
403
+ get desiredSize() {
404
+ return originalController.desiredSize;
405
+ },
406
+ close: () => originalController.close(),
407
+ error: (e?: unknown) => originalController.error(e),
408
+ enqueue: (chunk: UIMessageChunk) => {
409
+ /**
410
+ * Intercept text-delta chunks for callbacks
411
+ */
412
+ if (callbacks && chunk.type === 'text-delta' && chunk.delta) {
413
+ textChunks.push(chunk.delta);
414
+ callbacks.onToken?.(chunk.delta);
415
+ callbacks.onText?.(chunk.delta);
416
+ }
417
+ originalController.enqueue(chunk);
418
+ },
419
+ };
420
+ };
421
+
422
+ return new ReadableStream<UIMessageChunk>({
423
+ async start(controller) {
424
+ await callbacks?.onStart?.();
425
+
426
+ const wrappedController = createCallbackController(controller);
427
+ controller.enqueue({ type: 'start' });
428
+
429
+ try {
430
+ while (true) {
431
+ const { done, value } = await iterator.next();
432
+ if (done) break;
433
+
434
+ /**
435
+ * Detect stream type on first value
436
+ */
437
+ if (streamType === null) {
438
+ if (Array.isArray(value)) {
439
+ streamType = 'langgraph';
440
+ } else if (isStreamEventsEvent(value)) {
441
+ streamType = 'streamEvents';
442
+ } else {
443
+ streamType = 'model';
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Process based on detected type
449
+ */
450
+ if (streamType === 'model') {
451
+ processModelChunk(
452
+ value as AIMessageChunk,
453
+ modelState,
454
+ wrappedController,
455
+ );
456
+ } else if (streamType === 'streamEvents') {
457
+ processStreamEventsEvent(
458
+ value as {
459
+ event: string;
460
+ data: Record<string, unknown> | null;
461
+ run_id?: string;
462
+ name?: string;
463
+ },
464
+ modelState,
465
+ wrappedController,
466
+ );
467
+ } else {
468
+ processLangGraphEvent(
469
+ value as unknown[],
470
+ langGraphState,
471
+ wrappedController,
472
+ );
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Finalize based on stream type
478
+ */
479
+ if (streamType === 'model' || streamType === 'streamEvents') {
480
+ if (modelState.reasoningStarted) {
481
+ controller.enqueue({
482
+ type: 'reasoning-end',
483
+ id: modelState.reasoningMessageId ?? modelState.messageId,
484
+ });
485
+ }
486
+ if (modelState.textStarted) {
487
+ /**
488
+ * Use the same ID that was used for text-start
489
+ */
490
+ controller.enqueue({
491
+ type: 'text-end',
492
+ id: modelState.textMessageId ?? modelState.messageId,
493
+ });
494
+ }
495
+ controller.enqueue({ type: 'finish' });
496
+ } else if (streamType === 'langgraph') {
497
+ /**
498
+ * Emit finish-step if a step was started
499
+ */
500
+ if (langGraphState.currentStep !== null) {
501
+ controller.enqueue({ type: 'finish-step' });
502
+ }
503
+ controller.enqueue({ type: 'finish' });
504
+ }
505
+
506
+ /**
507
+ * Call onFinal callback with aggregated text
508
+ */
509
+ await callbacks?.onFinal?.(textChunks.join(''));
510
+ } catch (error) {
511
+ controller.enqueue({
512
+ type: 'error',
513
+ errorText: error instanceof Error ? error.message : 'Unknown error',
514
+ });
515
+ } finally {
516
+ controller.close();
517
+ }
518
+ },
519
+ });
520
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export {
2
+ toBaseMessages,
3
+ toUIMessageStream,
4
+ convertModelMessages,
5
+ } from './adapter';
6
+
7
+ export {
8
+ LangSmithDeploymentTransport,
9
+ type LangSmithDeploymentTransportOptions,
10
+ } from './transport';
11
+
12
+ export { type StreamCallbacks } from './stream-callbacks';