@librechat/agents 3.1.39 → 3.1.40

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.
@@ -15,6 +15,7 @@ import {
15
15
  getCurrentTaskInput,
16
16
  messagesStateReducer,
17
17
  } from '@langchain/langgraph';
18
+ import type { LangGraphRunnableConfig } from '@langchain/langgraph';
18
19
  import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
19
20
  import type { ToolRunnableConfig } from '@langchain/core/tools';
20
21
  import type * as t from '@/types';
@@ -745,7 +746,8 @@ export class MultiAgentGraph extends StandardGraph {
745
746
 
746
747
  /** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
747
748
  const agentWrapper = async (
748
- state: t.MultiAgentGraphState
749
+ state: t.MultiAgentGraphState,
750
+ config?: LangGraphRunnableConfig
749
751
  ): Promise<t.MultiAgentGraphState | Command> => {
750
752
  let result: t.MultiAgentGraphState;
751
753
 
@@ -817,7 +819,7 @@ export class MultiAgentGraph extends StandardGraph {
817
819
  ...state,
818
820
  messages: messagesForAgent,
819
821
  };
820
- result = await agentSubgraph.invoke(transformedState);
822
+ result = await agentSubgraph.invoke(transformedState, config);
821
823
  result = {
822
824
  ...result,
823
825
  agentMessages: [],
@@ -863,14 +865,14 @@ export class MultiAgentGraph extends StandardGraph {
863
865
  ...state,
864
866
  messages: state.agentMessages,
865
867
  };
866
- result = await agentSubgraph.invoke(transformedState);
868
+ result = await agentSubgraph.invoke(transformedState, config);
867
869
  result = {
868
870
  ...result,
869
871
  /** Clear agentMessages for next agent */
870
872
  agentMessages: [],
871
873
  };
872
874
  } else {
873
- result = await agentSubgraph.invoke(state);
875
+ result = await agentSubgraph.invoke(state, config);
874
876
  }
875
877
 
876
878
  /** If agent has both handoff and direct edges, use Command for exclusive routing */
package/src/run.ts CHANGED
@@ -4,6 +4,7 @@ import { CallbackHandler } from '@langfuse/langchain';
4
4
  import { PromptTemplate } from '@langchain/core/prompts';
5
5
  import { RunnableLambda } from '@langchain/core/runnables';
6
6
  import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
7
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
7
8
  import type {
8
9
  MessageContentComplex,
9
10
  BaseMessage,
@@ -240,9 +241,14 @@ export class Run<_T extends t.BaseGraphState> {
240
241
  ? this.getCallbacks(streamOptions.callbacks)
241
242
  : [];
242
243
 
243
- config.callbacks = baseCallbacks.concat(streamCallbacks).concat({
244
+ const customHandler = BaseCallbackHandler.fromMethods({
244
245
  [Callback.CUSTOM_EVENT]: customEventCallback,
245
246
  });
247
+ customHandler.awaitHandlers = true;
248
+
249
+ config.callbacks = baseCallbacks
250
+ .concat(streamCallbacks)
251
+ .concat(customHandler);
246
252
 
247
253
  if (
248
254
  isPresent(process.env.LANGFUSE_SECRET_KEY) &&
@@ -0,0 +1,215 @@
1
+ import { HumanMessage } from '@langchain/core/messages';
2
+ import type * as t from '@/types';
3
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
4
+ import { ContentTypes, GraphEvents, Providers } from '@/common';
5
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
6
+ import { Run } from '@/run';
7
+
8
+ describe('Custom event handler awaitHandlers behavior', () => {
9
+ jest.setTimeout(15000);
10
+
11
+ const llmConfig: t.LLMConfig = {
12
+ provider: Providers.OPENAI,
13
+ streaming: true,
14
+ streamUsage: false,
15
+ };
16
+
17
+ const config = {
18
+ configurable: {
19
+ thread_id: 'test-thread',
20
+ },
21
+ streamMode: 'values' as const,
22
+ version: 'v2' as const,
23
+ };
24
+
25
+ it('should fully aggregate all content before processStream returns', async () => {
26
+ const longResponse =
27
+ 'The quick brown fox jumps over the lazy dog and then runs across the field to find shelter from the rain';
28
+
29
+ let aggregateCallCount = 0;
30
+ const { contentParts, aggregateContent } = createContentAggregator();
31
+
32
+ const wrappedAggregate: t.ContentAggregator = (params) => {
33
+ aggregateCallCount++;
34
+ aggregateContent(params);
35
+ };
36
+
37
+ let messageDeltaCount = 0;
38
+
39
+ const customHandlers: Record<string | GraphEvents, t.EventHandler> = {
40
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
41
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
42
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
43
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
44
+ handle: (
45
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
46
+ data: t.StreamEventData
47
+ ) => {
48
+ wrappedAggregate({
49
+ event,
50
+ data: data as unknown as { result: t.ToolEndEvent },
51
+ });
52
+ },
53
+ },
54
+ [GraphEvents.ON_RUN_STEP]: {
55
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData) => {
56
+ wrappedAggregate({ event, data: data as t.RunStep });
57
+ },
58
+ },
59
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
60
+ handle: (
61
+ event: GraphEvents.ON_RUN_STEP_DELTA,
62
+ data: t.StreamEventData
63
+ ) => {
64
+ wrappedAggregate({ event, data: data as t.RunStepDeltaEvent });
65
+ },
66
+ },
67
+ [GraphEvents.ON_MESSAGE_DELTA]: {
68
+ handle: async (
69
+ event: GraphEvents.ON_MESSAGE_DELTA,
70
+ data: t.StreamEventData
71
+ ) => {
72
+ messageDeltaCount++;
73
+ wrappedAggregate({ event, data: data as t.MessageDeltaEvent });
74
+ },
75
+ },
76
+ [GraphEvents.ON_REASONING_DELTA]: {
77
+ handle: (
78
+ event: GraphEvents.ON_REASONING_DELTA,
79
+ data: t.StreamEventData
80
+ ) => {
81
+ wrappedAggregate({ event, data: data as t.ReasoningDeltaEvent });
82
+ },
83
+ },
84
+ };
85
+
86
+ const run = await Run.create<t.IState>({
87
+ runId: 'test-await-handlers',
88
+ graphConfig: {
89
+ type: 'standard',
90
+ llmConfig,
91
+ },
92
+ returnContent: true,
93
+ customHandlers,
94
+ });
95
+
96
+ run.Graph!.overrideTestModel([longResponse]);
97
+
98
+ const inputs = { messages: [new HumanMessage('hello')] };
99
+ const finalContentParts = await run.processStream(inputs, config);
100
+
101
+ expect(finalContentParts).toBeDefined();
102
+ expect(finalContentParts!.length).toBeGreaterThan(0);
103
+
104
+ expect(messageDeltaCount).toBeGreaterThan(0);
105
+ expect(aggregateCallCount).toBeGreaterThan(0);
106
+
107
+ const typedParts = contentParts as t.MessageContentComplex[];
108
+ const textParts = typedParts.filter(
109
+ (p: t.MessageContentComplex | undefined) =>
110
+ p !== undefined && p.type === ContentTypes.TEXT
111
+ );
112
+ expect(textParts.length).toBeGreaterThan(0);
113
+
114
+ const aggregatedText = textParts
115
+ .map(
116
+ (p) =>
117
+ (p as { type: string; [ContentTypes.TEXT]: string })[
118
+ ContentTypes.TEXT
119
+ ]
120
+ )
121
+ .join('');
122
+ expect(aggregatedText).toBe(longResponse);
123
+ });
124
+
125
+ it('should aggregate content from async handlers before processStream returns', async () => {
126
+ const response =
127
+ 'This is a test of async handler aggregation with multiple tokens';
128
+
129
+ const { contentParts, aggregateContent } = createContentAggregator();
130
+
131
+ let asyncHandlerCompletions = 0;
132
+
133
+ const customHandlers: Record<string | GraphEvents, t.EventHandler> = {
134
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
135
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
136
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
137
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
138
+ handle: (
139
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
140
+ data: t.StreamEventData
141
+ ) => {
142
+ aggregateContent({
143
+ event,
144
+ data: data as unknown as { result: t.ToolEndEvent },
145
+ });
146
+ },
147
+ },
148
+ [GraphEvents.ON_RUN_STEP]: {
149
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData) => {
150
+ aggregateContent({ event, data: data as t.RunStep });
151
+ },
152
+ },
153
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
154
+ handle: (
155
+ event: GraphEvents.ON_RUN_STEP_DELTA,
156
+ data: t.StreamEventData
157
+ ) => {
158
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
159
+ },
160
+ },
161
+ [GraphEvents.ON_MESSAGE_DELTA]: {
162
+ handle: async (
163
+ event: GraphEvents.ON_MESSAGE_DELTA,
164
+ data: t.StreamEventData
165
+ ) => {
166
+ await new Promise<void>((resolve) => setTimeout(resolve, 5));
167
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
168
+ asyncHandlerCompletions++;
169
+ },
170
+ },
171
+ [GraphEvents.ON_REASONING_DELTA]: {
172
+ handle: (
173
+ event: GraphEvents.ON_REASONING_DELTA,
174
+ data: t.StreamEventData
175
+ ) => {
176
+ aggregateContent({ event, data: data as t.ReasoningDeltaEvent });
177
+ },
178
+ },
179
+ };
180
+
181
+ const run = await Run.create<t.IState>({
182
+ runId: 'test-async-handlers',
183
+ graphConfig: {
184
+ type: 'standard',
185
+ llmConfig,
186
+ },
187
+ returnContent: true,
188
+ customHandlers,
189
+ });
190
+
191
+ run.Graph!.overrideTestModel([response]);
192
+
193
+ const inputs = { messages: [new HumanMessage('hello')] };
194
+ await run.processStream(inputs, config);
195
+
196
+ expect(asyncHandlerCompletions).toBeGreaterThan(0);
197
+
198
+ const typedParts = contentParts as t.MessageContentComplex[];
199
+ const textParts = typedParts.filter(
200
+ (p: t.MessageContentComplex | undefined) =>
201
+ p !== undefined && p.type === ContentTypes.TEXT
202
+ );
203
+ expect(textParts.length).toBeGreaterThan(0);
204
+
205
+ const aggregatedText = textParts
206
+ .map(
207
+ (p) =>
208
+ (p as { type: string; [ContentTypes.TEXT]: string })[
209
+ ContentTypes.TEXT
210
+ ]
211
+ )
212
+ .join('');
213
+ expect(aggregatedText).toBe(response);
214
+ });
215
+ });