@ai-sdk/langchain 0.0.0-70e0935a-20260114150030 → 0.0.0-98261322-20260122142521

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.
@@ -0,0 +1,196 @@
1
+ import {
2
+ createCallbacksTransformer,
3
+ StreamCallbacks,
4
+ } from './stream-callbacks';
5
+ import { describe, it, expect, vi } from 'vitest';
6
+
7
+ describe('createCallbacksTransformer', () => {
8
+ async function processStream(
9
+ input: string[],
10
+ callbacks?: StreamCallbacks,
11
+ ): Promise<string[]> {
12
+ const transformer = createCallbacksTransformer(callbacks);
13
+ const readable = new ReadableStream({
14
+ start(controller) {
15
+ for (const chunk of input) {
16
+ controller.enqueue(chunk);
17
+ }
18
+ controller.close();
19
+ },
20
+ });
21
+
22
+ const output: string[] = [];
23
+ const writable = new WritableStream({
24
+ write(chunk) {
25
+ output.push(chunk);
26
+ },
27
+ });
28
+
29
+ await readable.pipeThrough(transformer).pipeTo(writable);
30
+ return output;
31
+ }
32
+
33
+ it('should pass through messages without callbacks', async () => {
34
+ const input = ['Hello', ' ', 'World'];
35
+ const output = await processStream(input);
36
+
37
+ expect(output).toEqual(input);
38
+ });
39
+
40
+ it('should pass through messages with empty callbacks object', async () => {
41
+ const input = ['Hello', ' ', 'World'];
42
+ const output = await processStream(input, {});
43
+
44
+ expect(output).toEqual(input);
45
+ });
46
+
47
+ it('should call onStart once at the beginning', async () => {
48
+ const onStart = vi.fn();
49
+ const input = ['Hello', ' ', 'World'];
50
+
51
+ await processStream(input, { onStart });
52
+
53
+ expect(onStart).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it('should call async onStart', async () => {
57
+ const onStart = vi.fn().mockResolvedValue(undefined);
58
+ const input = ['Hello'];
59
+
60
+ await processStream(input, { onStart });
61
+
62
+ expect(onStart).toHaveBeenCalledTimes(1);
63
+ });
64
+
65
+ it('should call onToken for each message', async () => {
66
+ const onToken = vi.fn();
67
+ const input = ['Hello', ' ', 'World'];
68
+
69
+ await processStream(input, { onToken });
70
+
71
+ expect(onToken).toHaveBeenCalledTimes(3);
72
+ expect(onToken).toHaveBeenNthCalledWith(1, 'Hello');
73
+ expect(onToken).toHaveBeenNthCalledWith(2, ' ');
74
+ expect(onToken).toHaveBeenNthCalledWith(3, 'World');
75
+ });
76
+
77
+ it('should call async onToken', async () => {
78
+ const onToken = vi.fn().mockResolvedValue(undefined);
79
+ const input = ['Hello', 'World'];
80
+
81
+ await processStream(input, { onToken });
82
+
83
+ expect(onToken).toHaveBeenCalledTimes(2);
84
+ });
85
+
86
+ it('should call onText for each string message', async () => {
87
+ const onText = vi.fn();
88
+ const input = ['Hello', ' ', 'World'];
89
+
90
+ await processStream(input, { onText });
91
+
92
+ expect(onText).toHaveBeenCalledTimes(3);
93
+ expect(onText).toHaveBeenNthCalledWith(1, 'Hello');
94
+ expect(onText).toHaveBeenNthCalledWith(2, ' ');
95
+ expect(onText).toHaveBeenNthCalledWith(3, 'World');
96
+ });
97
+
98
+ it('should call async onText', async () => {
99
+ const onText = vi.fn().mockResolvedValue(undefined);
100
+ const input = ['Hello'];
101
+
102
+ await processStream(input, { onText });
103
+
104
+ expect(onText).toHaveBeenCalledTimes(1);
105
+ });
106
+
107
+ it('should call onFinal with aggregated response', async () => {
108
+ const onFinal = vi.fn();
109
+ const input = ['Hello', ' ', 'World'];
110
+
111
+ await processStream(input, { onFinal });
112
+
113
+ expect(onFinal).toHaveBeenCalledTimes(1);
114
+ expect(onFinal).toHaveBeenCalledWith('Hello World');
115
+ });
116
+
117
+ it('should call async onFinal', async () => {
118
+ const onFinal = vi.fn().mockResolvedValue(undefined);
119
+ const input = ['Hello', 'World'];
120
+
121
+ await processStream(input, { onFinal });
122
+
123
+ expect(onFinal).toHaveBeenCalledTimes(1);
124
+ expect(onFinal).toHaveBeenCalledWith('HelloWorld');
125
+ });
126
+
127
+ it('should call onFinal with empty string when no messages', async () => {
128
+ const onFinal = vi.fn();
129
+ const input: string[] = [];
130
+
131
+ await processStream(input, { onFinal });
132
+
133
+ expect(onFinal).toHaveBeenCalledTimes(1);
134
+ expect(onFinal).toHaveBeenCalledWith('');
135
+ });
136
+
137
+ it('should call all callbacks in correct order', async () => {
138
+ const callOrder: string[] = [];
139
+
140
+ const callbacks: StreamCallbacks = {
141
+ onStart: () => {
142
+ callOrder.push('start');
143
+ },
144
+ onToken: token => {
145
+ callOrder.push(`token:${token}`);
146
+ },
147
+ onText: text => {
148
+ callOrder.push(`text:${text}`);
149
+ },
150
+ onFinal: completion => {
151
+ callOrder.push(`final:${completion}`);
152
+ },
153
+ };
154
+
155
+ const input = ['A', 'B'];
156
+ await processStream(input, callbacks);
157
+
158
+ expect(callOrder).toEqual([
159
+ 'start',
160
+ 'token:A',
161
+ 'text:A',
162
+ 'token:B',
163
+ 'text:B',
164
+ 'final:AB',
165
+ ]);
166
+ });
167
+
168
+ it('should handle single character messages', async () => {
169
+ const onToken = vi.fn();
170
+ const onFinal = vi.fn();
171
+ const input = ['a', 'b', 'c'];
172
+
173
+ await processStream(input, { onToken, onFinal });
174
+
175
+ expect(onToken).toHaveBeenCalledTimes(3);
176
+ expect(onFinal).toHaveBeenCalledWith('abc');
177
+ });
178
+
179
+ it('should handle messages with special characters', async () => {
180
+ const onFinal = vi.fn();
181
+ const input = ['Hello\n', 'World\t', '!'];
182
+
183
+ await processStream(input, { onFinal });
184
+
185
+ expect(onFinal).toHaveBeenCalledWith('Hello\nWorld\t!');
186
+ });
187
+
188
+ it('should handle unicode characters', async () => {
189
+ const onFinal = vi.fn();
190
+ const input = ['こんにちは', ' ', '🌍'];
191
+
192
+ await processStream(input, { onFinal });
193
+
194
+ expect(onFinal).toHaveBeenCalledWith('こんにちは 🌍');
195
+ });
196
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Configuration options and helper callback methods for stream lifecycle events.
3
+ */
4
+ export interface StreamCallbacks {
5
+ /** `onStart`: Called once when the stream is initialized. */
6
+ onStart?: () => Promise<void> | void;
7
+
8
+ /** `onFinal`: Called once when the stream is closed with the final completion message. */
9
+ onFinal?: (completion: string) => Promise<void> | void;
10
+
11
+ /** `onToken`: Called for each tokenized message. */
12
+ onToken?: (token: string) => Promise<void> | void;
13
+
14
+ /** `onText`: Called for each text chunk. */
15
+ onText?: (text: string) => Promise<void> | void;
16
+ }
17
+
18
+ /**
19
+ * Creates a transform stream that encodes input messages and invokes optional callback functions.
20
+ * The transform stream uses the provided callbacks to execute custom logic at different stages of the stream's lifecycle.
21
+ * - `onStart`: Called once when the stream is initialized.
22
+ * - `onToken`: Called for each tokenized message.
23
+ * - `onFinal`: Called once when the stream is closed with the final completion message.
24
+ *
25
+ * This function is useful when you want to process a stream of messages and perform specific actions during the stream's lifecycle.
26
+ *
27
+ * @param {StreamCallbacks} [callbacks] - An object containing the callback functions.
28
+ * @return {TransformStream<string, string>} A transform stream that allows the execution of custom logic through callbacks.
29
+ *
30
+ * @example
31
+ * const callbacks = {
32
+ * onStart: async () => console.log('Stream started'),
33
+ * onToken: async (token) => console.log(`Token: ${token}`),
34
+ * onFinal: async () => data.close()
35
+ * };
36
+ * const transformer = createCallbacksTransformer(callbacks);
37
+ */
38
+ export function createCallbacksTransformer(
39
+ callbacks: StreamCallbacks | undefined = {},
40
+ ): TransformStream<string, string> {
41
+ let aggregatedResponse = '';
42
+
43
+ return new TransformStream({
44
+ async start(): Promise<void> {
45
+ if (callbacks.onStart) await callbacks.onStart();
46
+ },
47
+
48
+ async transform(message, controller): Promise<void> {
49
+ controller.enqueue(message);
50
+
51
+ aggregatedResponse += message;
52
+
53
+ if (callbacks.onToken) await callbacks.onToken(message);
54
+ if (callbacks.onText && typeof message === 'string') {
55
+ await callbacks.onText(message);
56
+ }
57
+ },
58
+
59
+ async flush(): Promise<void> {
60
+ if (callbacks.onFinal) {
61
+ await callbacks.onFinal(aggregatedResponse);
62
+ }
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,41 @@
1
+ import { LangSmithDeploymentTransport } from './transport';
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ describe('LangSmithDeploymentTransport', () => {
5
+ it('should create transport with options', () => {
6
+ const transport = new LangSmithDeploymentTransport({
7
+ url: 'https://test.langsmith.app',
8
+ apiKey: 'test-key',
9
+ });
10
+
11
+ expect('sendMessages' in transport).toBe(true);
12
+ expect('reconnectToStream' in transport).toBe(true);
13
+ });
14
+
15
+ it('should create transport with only url', () => {
16
+ const transport = new LangSmithDeploymentTransport({
17
+ url: 'https://test.langsmith.app',
18
+ });
19
+
20
+ expect('sendMessages' in transport).toBe(true);
21
+ });
22
+
23
+ it('should create transport with custom graphId', () => {
24
+ const transport = new LangSmithDeploymentTransport({
25
+ url: 'https://test.langsmith.app',
26
+ graphId: 'custom-agent',
27
+ });
28
+
29
+ expect('sendMessages' in transport).toBe(true);
30
+ });
31
+
32
+ it('should throw error for reconnectToStream', async () => {
33
+ const transport = new LangSmithDeploymentTransport({
34
+ url: 'https://test.langsmith.app',
35
+ });
36
+
37
+ await expect(
38
+ transport.reconnectToStream({ chatId: 'chat-1' }),
39
+ ).rejects.toThrow('Method not implemented.');
40
+ });
41
+ });
@@ -0,0 +1,88 @@
1
+ import { AIMessageChunk } from '@langchain/core/messages';
2
+ import {
3
+ type UIMessage,
4
+ type UIMessageChunk,
5
+ type ChatTransport,
6
+ type ChatRequestOptions,
7
+ } from 'ai';
8
+ import {
9
+ RemoteGraph,
10
+ type RemoteGraphParams,
11
+ } from '@langchain/langgraph/remote';
12
+ import { toBaseMessages, toUIMessageStream } from './adapter';
13
+
14
+ /**
15
+ * Options for configuring a LangSmith deployment transport.
16
+ * Extends RemoteGraphParams but makes graphId optional (defaults to 'agent').
17
+ */
18
+ export type LangSmithDeploymentTransportOptions = Omit<
19
+ RemoteGraphParams,
20
+ 'graphId'
21
+ > & {
22
+ /**
23
+ * The ID of the graph to connect to.
24
+ * @default 'agent'
25
+ */
26
+ graphId?: string;
27
+ };
28
+
29
+ /**
30
+ * A ChatTransport implementation for LangSmith/LangGraph deployments.
31
+ *
32
+ * This transport enables seamless integration between the AI SDK's useChat hook
33
+ * and LangSmith deployed LangGraph agents.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { LangSmithDeploymentTransport } from '@ai-sdk/langchain';
38
+ *
39
+ * // Use with useChat
40
+ * const { messages, input, handleSubmit } = useChat({
41
+ * transport: new LangSmithDeploymentTransport({
42
+ * url: 'https://your-deployment.us.langgraph.app',
43
+ * apiKey: 'my-api-key',
44
+ * }),
45
+ * });
46
+ * ```
47
+ */
48
+ export class LangSmithDeploymentTransport<UI_MESSAGE extends UIMessage>
49
+ implements ChatTransport<UI_MESSAGE>
50
+ {
51
+ protected graph: RemoteGraph;
52
+
53
+ constructor(options: LangSmithDeploymentTransportOptions) {
54
+ this.graph = new RemoteGraph({
55
+ ...options,
56
+ graphId: options.graphId ?? 'agent',
57
+ });
58
+ }
59
+
60
+ async sendMessages(
61
+ options: {
62
+ trigger: 'submit-message' | 'regenerate-message';
63
+ chatId: string;
64
+ messageId: string | undefined;
65
+ messages: UI_MESSAGE[];
66
+ abortSignal: AbortSignal | undefined;
67
+ } & ChatRequestOptions,
68
+ ): Promise<ReadableStream<UIMessageChunk>> {
69
+ const baseMessages = await toBaseMessages(options.messages);
70
+
71
+ const stream = await this.graph.stream(
72
+ { messages: baseMessages },
73
+ { streamMode: ['values', 'messages'] },
74
+ );
75
+
76
+ return toUIMessageStream(
77
+ stream as AsyncIterable<AIMessageChunk> | ReadableStream,
78
+ );
79
+ }
80
+
81
+ async reconnectToStream(
82
+ _options: {
83
+ chatId: string;
84
+ } & ChatRequestOptions,
85
+ ): Promise<ReadableStream<UIMessageChunk> | null> {
86
+ throw new Error('Method not implemented.');
87
+ }
88
+ }
package/src/types.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { type AIMessageChunk } from '@langchain/core/messages';
2
+
3
+ /**
4
+ * State for LangGraph event processing
5
+ */
6
+ export interface LangGraphEventState {
7
+ /** Tracks which message IDs have been seen */
8
+ messageSeen: Record<
9
+ string,
10
+ { text?: boolean; reasoning?: boolean; tool?: Record<string, boolean> }
11
+ >;
12
+ /** Accumulates message chunks for later reference */
13
+ messageConcat: Record<string, AIMessageChunk>;
14
+ /** Maps tool call IDs to their message IDs (for chunks that don't include the ID) */
15
+ emittedToolCalls: Set<string>;
16
+ /** Maps image IDs to their message IDs (for chunks that don't include the ID) */
17
+ emittedImages: Set<string>;
18
+ /** Maps reasoning block IDs to their message IDs (for chunks that don't include the ID) */
19
+ emittedReasoningIds: Set<string>;
20
+ /** Maps message IDs to their reasoning block IDs (for chunks that don't include the ID) */
21
+ messageReasoningIds: Record<string, string>;
22
+ /** Maps message ID + tool call index to tool call info (for streaming chunks without ID) */
23
+ toolCallInfoByIndex: Record<
24
+ string,
25
+ Record<number, { id: string; name: string }>
26
+ >;
27
+ /** Tracks the current LangGraph step for start-step/finish-step events */
28
+ currentStep: number | null;
29
+ /** Maps tool call key (name:argsJson) to tool call ID for HITL interrupt handling */
30
+ emittedToolCallsByKey: Map<string, string>;
31
+ }
32
+
33
+ /**
34
+ * Type for reasoning content block from LangChain
35
+ */
36
+ export interface ReasoningContentBlock {
37
+ type: 'reasoning';
38
+ reasoning: string;
39
+ }
40
+
41
+ /**
42
+ * Type for thinking content block from LangChain (Anthropic-style)
43
+ */
44
+ export interface ThinkingContentBlock {
45
+ type: 'thinking';
46
+ thinking: string;
47
+ signature?: string;
48
+ }
49
+
50
+ /**
51
+ * Type for GPT-5 reasoning output block
52
+ */
53
+ export interface GPT5ReasoningOutput {
54
+ id: string;
55
+ type: 'reasoning';
56
+ summary: {
57
+ type: 'summary_text';
58
+ text: string;
59
+ }[];
60
+ }
61
+
62
+ /**
63
+ * Type for image generation tool outputs from LangChain/OpenAI
64
+ */
65
+ export interface ImageGenerationOutput {
66
+ id: string;
67
+ type: 'image_generation_call';
68
+ status: string;
69
+ result?: string; // base64 image data
70
+ revised_prompt?: string;
71
+ size?: string;
72
+ output_format?: string;
73
+ quality?: string;
74
+ background?: string;
75
+ }