@librechat/agents 3.0.0-rc6 → 3.0.0-rc8

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": "@librechat/agents",
3
- "version": "3.0.00-rc6",
3
+ "version": "3.0.00-rc8",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -57,6 +57,7 @@
57
57
  "abort": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/abort.ts --provider 'openAI' --name 'Jo' --location 'New York, NY'",
58
58
  "start:cli2": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/cli2.ts --provider 'anthropic' --name 'Jo' --location 'New York, NY'",
59
59
  "multi-agent-test": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-test.ts",
60
+ "test-tools-before-handoff": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/test-tools-before-handoff.ts",
60
61
  "multi-agent-parallel": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-parallel.ts",
61
62
  "multi-agent-sequence": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-sequence.ts",
62
63
  "multi-agent-conditional": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-conditional.ts",
@@ -115,6 +116,7 @@
115
116
  "devDependencies": {
116
117
  "@anthropic-ai/vertex-sdk": "^0.12.0",
117
118
  "@eslint/compat": "^1.2.7",
119
+ "@langchain/tavily": "^0.1.5",
118
120
  "@rollup/plugin-alias": "^5.1.0",
119
121
  "@rollup/plugin-commonjs": "^28.0.3",
120
122
  "@rollup/plugin-json": "^6.1.0",
@@ -380,7 +380,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
380
380
  currentTools?: t.GraphTools;
381
381
  currentToolMap?: t.ToolMap;
382
382
  }): CustomToolNode<t.BaseGraphState> | ToolNode<t.BaseGraphState> {
383
- // return new ToolNode<t.BaseGraphState>(this.tools);
384
383
  return new CustomToolNode<t.BaseGraphState>({
385
384
  tools: (currentTools as t.GenericTool[] | undefined) ?? [],
386
385
  toolMap: currentToolMap,
@@ -125,14 +125,6 @@ export class MultiAgentGraph extends StandardGraph {
125
125
  agentContext.tools = [];
126
126
  }
127
127
  agentContext.tools.push(...handoffTools);
128
-
129
- // Update tool map
130
- for (const tool of handoffTools) {
131
- if (!agentContext.toolMap) {
132
- agentContext.toolMap = new Map();
133
- }
134
- agentContext.toolMap.set(tool.name, tool);
135
- }
136
128
  }
137
129
  }
138
130
 
@@ -1,11 +1,11 @@
1
- #!/usr/bin/env bun
2
-
3
1
  import { config } from 'dotenv';
4
2
  config();
5
3
 
6
4
  import { HumanMessage } from '@langchain/core/messages';
7
5
  import { Run } from '@/run';
8
- import { Providers } from '@/common';
6
+ import { Providers, GraphEvents } from '@/common';
7
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
8
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
9
9
  import type * as t from '@/types';
10
10
 
11
11
  /**
@@ -14,8 +14,68 @@ import type * as t from '@/types';
14
14
  */
15
15
  async function testHandoffInput() {
16
16
  console.log('Testing Handoff Input Feature...\n');
17
+ // Set up content aggregator
18
+ const { contentParts, aggregateContent } = createContentAggregator();
19
+
20
+ // Track which specialist role was selected
21
+ let selectedRole = '';
22
+ let roleInstructions = '';
23
+
24
+ // Create custom handlers
25
+ const customHandlers = {
26
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
27
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
28
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
29
+ [GraphEvents.ON_RUN_STEP]: {
30
+ handle: (
31
+ event: GraphEvents.ON_RUN_STEP,
32
+ data: t.StreamEventData
33
+ ): void => {
34
+ const runStepData = data as any;
35
+ if (runStepData?.name) {
36
+ console.log(`\n[${runStepData.name}] Processing...`);
37
+ }
38
+ aggregateContent({ event, data: data as t.RunStep });
39
+ },
40
+ },
41
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
42
+ handle: (
43
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
44
+ data: t.StreamEventData
45
+ ): void => {
46
+ aggregateContent({
47
+ event,
48
+ data: data as unknown as { result: t.ToolEndEvent },
49
+ });
50
+ },
51
+ },
52
+ [GraphEvents.ON_MESSAGE_DELTA]: {
53
+ handle: (
54
+ event: GraphEvents.ON_MESSAGE_DELTA,
55
+ data: t.StreamEventData
56
+ ): void => {
57
+ console.dir(data, { depth: null });
58
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
59
+ },
60
+ },
61
+ [GraphEvents.TOOL_START]: {
62
+ handle: (
63
+ _event: string,
64
+ data: t.StreamEventData,
65
+ metadata?: Record<string, unknown>
66
+ ): void => {
67
+ const toolData = data as any;
68
+ if (toolData?.name?.includes('transfer_to_')) {
69
+ const specialist = toolData.name.replace('transfer_to_', '');
70
+ console.log(`\n🔀 Transferring to ${specialist}...`);
71
+ selectedRole = specialist;
72
+ }
73
+ },
74
+ },
75
+ };
17
76
 
18
77
  const runConfig: t.RunConfig = {
78
+ customHandlers,
19
79
  runId: `test-handoff-input-${Date.now()}`,
20
80
  graphConfig: {
21
81
  type: 'multi-agent',
@@ -76,7 +136,7 @@ async function testHandoffInput() {
76
136
 
77
137
  // Test queries that should result in different handoffs with specific instructions
78
138
  const testQueries = [
79
- 'Analyze our Q4 sales data and identify the top 3 performing products',
139
+ // 'Analyze our Q4 sales data and identify the top 3 performing products',
80
140
  'Write a blog post about the benefits of remote work for software developers',
81
141
  ];
82
142
 
@@ -97,7 +157,7 @@ async function testHandoffInput() {
97
157
  messages: [new HumanMessage(query)],
98
158
  };
99
159
 
100
- await run.processStream(inputs, config);
160
+ const finalContentParts = await run.processStream(inputs, config);
101
161
 
102
162
  console.log(`\n${'─'.repeat(60)}`);
103
163
  console.log('Notice how the supervisor passes specific instructions');
@@ -0,0 +1,233 @@
1
+ import { config } from 'dotenv';
2
+ config();
3
+
4
+ import { TavilySearch } from '@langchain/tavily';
5
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
6
+ import { Run } from '@/run';
7
+ import { Providers, GraphEvents } from '@/common';
8
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
9
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
10
+ import type * as t from '@/types';
11
+
12
+ const conversationHistory: BaseMessage[] = [];
13
+
14
+ /**
15
+ * Test edge case: Agent performs 2 web searches before handing off
16
+ *
17
+ * This tests how the system behaves when an agent with handoff capabilities
18
+ * uses tools before transferring control to another agent.
19
+ */
20
+ async function testToolsBeforeHandoff() {
21
+ console.log('Testing Tools Before Handoff Edge Case...\n');
22
+
23
+ // Set up content aggregator
24
+ const { contentParts, aggregateContent } = createContentAggregator();
25
+
26
+ // Track tool calls and handoffs
27
+ let toolCallCount = 0;
28
+ let handoffOccurred = false;
29
+
30
+ // Create custom handlers
31
+ const customHandlers = {
32
+ [GraphEvents.TOOL_END]: new ToolEndHandler(undefined, (name?: string) => {
33
+ console.log(`\n✅ Tool completed: ${name}`);
34
+ return true;
35
+ }),
36
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
37
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
38
+ [GraphEvents.ON_RUN_STEP]: {
39
+ handle: (
40
+ event: GraphEvents.ON_RUN_STEP,
41
+ data: t.StreamEventData
42
+ ): void => {
43
+ const runStepData = data as any;
44
+ if (runStepData?.name) {
45
+ console.log(`\n[${runStepData.name}] Processing...`);
46
+ }
47
+ aggregateContent({ event, data: data as t.RunStep });
48
+ },
49
+ },
50
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
51
+ handle: (
52
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
53
+ data: t.StreamEventData
54
+ ): void => {
55
+ aggregateContent({
56
+ event,
57
+ data: data as unknown as { result: t.ToolEndEvent },
58
+ });
59
+ },
60
+ },
61
+ [GraphEvents.ON_MESSAGE_DELTA]: {
62
+ handle: (
63
+ event: GraphEvents.ON_MESSAGE_DELTA,
64
+ data: t.StreamEventData
65
+ ): void => {
66
+ // console.log('====== ON_MESSAGE_DELTA ======');
67
+ console.dir(data, { depth: null });
68
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
69
+ },
70
+ },
71
+ [GraphEvents.TOOL_START]: {
72
+ handle: (
73
+ _event: string,
74
+ data: t.StreamEventData,
75
+ metadata?: Record<string, unknown>
76
+ ): void => {
77
+ const toolData = data as any;
78
+ console.log(`\n🔧 Tool started:`);
79
+ console.dir({ toolData, metadata }, { depth: null });
80
+
81
+ if (toolData?.output?.name === 'tavily_search_results_json') {
82
+ toolCallCount++;
83
+ console.log(`📊 Search #${toolCallCount} initiated`);
84
+ } else if (toolData?.output?.name?.includes('transfer_to_')) {
85
+ handoffOccurred = true;
86
+ const specialist = toolData.name.replace('transfer_to_', '');
87
+ console.log(`\n🔀 Handoff initiated to: ${specialist}`);
88
+ }
89
+ },
90
+ },
91
+ };
92
+
93
+ // Create the graph with research agent and report writer
94
+ function createGraphWithToolsAndHandoff(): t.RunConfig {
95
+ const agents: t.AgentInputs[] = [
96
+ {
97
+ agentId: 'research_coordinator',
98
+ provider: Providers.OPENAI,
99
+ clientOptions: {
100
+ modelName: 'gpt-4.1-mini',
101
+ apiKey: process.env.OPENAI_API_KEY,
102
+ },
103
+ tools: [new TavilySearch({ maxResults: 3 })],
104
+ instructions: `You are a Research Coordinator with access to web search and a report writer specialist.
105
+
106
+ Your workflow MUST follow these steps IN ORDER:
107
+ 1. FIRST: Write an initial response acknowledging the request and outlining your research plan
108
+ - Explain what aspects you'll investigate
109
+ - Describe your search strategy
110
+ 2. SECOND: Conduct exactly 2 web searches to gather comprehensive information
111
+ - Search 1: Get general information about the topic
112
+ - Search 2: Get specific details, recent updates, or complementary data
113
+ - Note: Even if your searches are unsuccessful, you MUST still proceed to handoff after EXACTLY 2 searches
114
+ 3. FINALLY: After completing both searches, transfer to the report writer
115
+ - Provide the report writer with a summary of your findings
116
+
117
+ CRITICAL: You MUST write your initial response before ANY tool use. Then complete both searches before handoff.`,
118
+ maxContextTokens: 8000,
119
+ },
120
+ {
121
+ agentId: 'report_writer',
122
+ provider: Providers.OPENAI,
123
+ clientOptions: {
124
+ modelName: 'gpt-5-mini',
125
+ apiKey: process.env.OPENAI_API_KEY,
126
+ },
127
+ instructions: `You are a Report Writer specialist. Your role is to:
128
+ 1. Receive research findings from the Research Coordinator
129
+ 2. Create a well-structured, comprehensive report
130
+ 3. Include all key findings from the research
131
+ 4. Format the report with clear sections and bullet points
132
+ 5. Add a brief executive summary at the beginning
133
+
134
+ Focus on clarity, completeness, and professional presentation.`,
135
+ maxContextTokens: 8000,
136
+ },
137
+ ];
138
+
139
+ // Create edge from research coordinator to report writer
140
+ const edges: t.GraphEdge[] = [
141
+ {
142
+ from: 'research_coordinator',
143
+ to: 'report_writer',
144
+ description: 'Transfer to report writer after completing research',
145
+ edgeType: 'handoff',
146
+ },
147
+ ];
148
+
149
+ return {
150
+ runId: `tools-before-handoff-${Date.now()}`,
151
+ graphConfig: {
152
+ type: 'multi-agent',
153
+ agents,
154
+ edges,
155
+ },
156
+ customHandlers,
157
+ returnContent: true,
158
+ };
159
+ }
160
+
161
+ try {
162
+ // Single test query that requires research before report writing
163
+ const query = `Research the latest developments in quantum computing from 2025,
164
+ including major breakthroughs and commercial applications.
165
+ I need a comprehensive report with recent findings.`;
166
+
167
+ console.log('='.repeat(60));
168
+ console.log(`USER QUERY: "${query}"`);
169
+ console.log('='.repeat(60));
170
+
171
+ // Create the graph
172
+ const runConfig = createGraphWithToolsAndHandoff();
173
+ const run = await Run.create(runConfig);
174
+
175
+ console.log('\nExpected behavior:');
176
+ console.log('1. Research Coordinator writes initial response/plan');
177
+ console.log('2. Research Coordinator performs 2 web searches');
178
+ console.log('3. Research Coordinator hands off to Report Writer');
179
+ console.log('4. Report Writer creates final report\n');
180
+
181
+ // Process with streaming
182
+ conversationHistory.push(new HumanMessage(query));
183
+ const inputs = {
184
+ messages: conversationHistory,
185
+ };
186
+
187
+ const config = {
188
+ configurable: {
189
+ thread_id: 'tools-handoff-test-1',
190
+ },
191
+ streamMode: 'values',
192
+ version: 'v2' as const,
193
+ };
194
+
195
+ const finalContentParts = await run.processStream(inputs, config);
196
+ const finalMessages = run.getRunMessages();
197
+
198
+ if (finalMessages) {
199
+ conversationHistory.push(...finalMessages);
200
+ }
201
+
202
+ // Show results summary
203
+ console.log(`\n${'─'.repeat(60)}`);
204
+ console.log('EDGE CASE TEST RESULTS:');
205
+ console.log('─'.repeat(60));
206
+ console.log(`Tool calls before handoff: ${toolCallCount}`);
207
+ console.log(`Expected tool calls: 2`);
208
+ console.log(`Handoff occurred: ${handoffOccurred ? 'Yes ✅' : 'No ❌'}`);
209
+ console.log(
210
+ `Test status: ${toolCallCount === 2 && handoffOccurred ? 'PASSED ✅' : 'FAILED ❌'}`
211
+ );
212
+ console.log('─'.repeat(60));
213
+
214
+ // Display conversation history
215
+ console.log('\nConversation History:');
216
+ console.log('─'.repeat(60));
217
+ conversationHistory.forEach((msg, idx) => {
218
+ const role = msg.constructor.name.replace('Message', '');
219
+ console.log(`\n[${idx}] ${role}:`);
220
+ if (typeof msg.content === 'string') {
221
+ console.log(
222
+ msg.content.substring(0, 200) +
223
+ (msg.content.length > 200 ? '...' : '')
224
+ );
225
+ }
226
+ });
227
+ } catch (error) {
228
+ console.error('Error in tools-before-handoff test:', error);
229
+ }
230
+ }
231
+
232
+ // Run the test
233
+ testToolsBeforeHandoff();
package/src/stream.ts CHANGED
@@ -153,7 +153,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
153
153
  if (
154
154
  chunk.tool_calls &&
155
155
  chunk.tool_calls.length > 0 &&
156
- chunk.tool_calls.every((tc) => tc.id != null && tc.id !== '')
156
+ chunk.tool_calls.every(
157
+ (tc) =>
158
+ tc.id != null && tc.id !== '' && tc.name != null && tc.name !== ''
159
+ )
157
160
  ) {
158
161
  hasToolCalls = true;
159
162
  await handleToolCalls(chunk.tool_calls, metadata, graph);