@librechat/agents 3.0.0-rc9 → 3.0.0

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.
Files changed (195) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +6 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +23 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +5 -5
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +21 -0
  8. package/dist/cjs/instrumentation.cjs.map +1 -0
  9. package/dist/cjs/llm/anthropic/index.cjs +21 -2
  10. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/index.cjs +3 -0
  12. package/dist/cjs/llm/google/index.cjs.map +1 -1
  13. package/dist/cjs/llm/google/utils/common.cjs +13 -0
  14. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  15. package/dist/cjs/llm/ollama/index.cjs +3 -0
  16. package/dist/cjs/llm/ollama/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/index.cjs +18 -3
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openai/utils/index.cjs +6 -1
  20. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs +5 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/llm/vertexai/index.cjs +1 -1
  24. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  25. package/dist/cjs/main.cjs +8 -2
  26. package/dist/cjs/main.cjs.map +1 -1
  27. package/dist/cjs/messages/cache.cjs +49 -0
  28. package/dist/cjs/messages/cache.cjs.map +1 -0
  29. package/dist/cjs/messages/content.cjs +53 -0
  30. package/dist/cjs/messages/content.cjs.map +1 -0
  31. package/dist/cjs/messages/core.cjs +5 -1
  32. package/dist/cjs/messages/core.cjs.map +1 -1
  33. package/dist/cjs/messages/format.cjs +50 -59
  34. package/dist/cjs/messages/format.cjs.map +1 -1
  35. package/dist/cjs/messages/prune.cjs +28 -0
  36. package/dist/cjs/messages/prune.cjs.map +1 -1
  37. package/dist/cjs/run.cjs +57 -5
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +7 -0
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolNode.cjs +2 -0
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/search/firecrawl.cjs +3 -1
  44. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  45. package/dist/cjs/tools/search/rerankers.cjs +8 -6
  46. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  47. package/dist/cjs/tools/search/search.cjs +5 -5
  48. package/dist/cjs/tools/search/search.cjs.map +1 -1
  49. package/dist/cjs/tools/search/serper-scraper.cjs +132 -0
  50. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -0
  51. package/dist/cjs/tools/search/tool.cjs +46 -9
  52. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  53. package/dist/cjs/utils/handlers.cjs +70 -0
  54. package/dist/cjs/utils/handlers.cjs.map +1 -0
  55. package/dist/cjs/utils/misc.cjs +8 -1
  56. package/dist/cjs/utils/misc.cjs.map +1 -1
  57. package/dist/cjs/utils/title.cjs +54 -25
  58. package/dist/cjs/utils/title.cjs.map +1 -1
  59. package/dist/esm/agents/AgentContext.mjs +6 -2
  60. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  61. package/dist/esm/graphs/Graph.mjs +23 -2
  62. package/dist/esm/graphs/Graph.mjs.map +1 -1
  63. package/dist/esm/graphs/MultiAgentGraph.mjs +5 -5
  64. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  65. package/dist/esm/instrumentation.mjs +19 -0
  66. package/dist/esm/instrumentation.mjs.map +1 -0
  67. package/dist/esm/llm/anthropic/index.mjs +21 -2
  68. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  69. package/dist/esm/llm/google/index.mjs +3 -0
  70. package/dist/esm/llm/google/index.mjs.map +1 -1
  71. package/dist/esm/llm/google/utils/common.mjs +13 -0
  72. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  73. package/dist/esm/llm/ollama/index.mjs +3 -0
  74. package/dist/esm/llm/ollama/index.mjs.map +1 -1
  75. package/dist/esm/llm/openai/index.mjs +18 -3
  76. package/dist/esm/llm/openai/index.mjs.map +1 -1
  77. package/dist/esm/llm/openai/utils/index.mjs +6 -1
  78. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  79. package/dist/esm/llm/openrouter/index.mjs +5 -1
  80. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  81. package/dist/esm/llm/vertexai/index.mjs +1 -1
  82. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  83. package/dist/esm/main.mjs +5 -2
  84. package/dist/esm/main.mjs.map +1 -1
  85. package/dist/esm/messages/cache.mjs +47 -0
  86. package/dist/esm/messages/cache.mjs.map +1 -0
  87. package/dist/esm/messages/content.mjs +51 -0
  88. package/dist/esm/messages/content.mjs.map +1 -0
  89. package/dist/esm/messages/core.mjs +5 -1
  90. package/dist/esm/messages/core.mjs.map +1 -1
  91. package/dist/esm/messages/format.mjs +50 -58
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/prune.mjs +28 -0
  94. package/dist/esm/messages/prune.mjs.map +1 -1
  95. package/dist/esm/run.mjs +57 -5
  96. package/dist/esm/run.mjs.map +1 -1
  97. package/dist/esm/stream.mjs +7 -0
  98. package/dist/esm/stream.mjs.map +1 -1
  99. package/dist/esm/tools/ToolNode.mjs +2 -0
  100. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  101. package/dist/esm/tools/search/firecrawl.mjs +3 -1
  102. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  103. package/dist/esm/tools/search/rerankers.mjs +8 -6
  104. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  105. package/dist/esm/tools/search/search.mjs +5 -5
  106. package/dist/esm/tools/search/search.mjs.map +1 -1
  107. package/dist/esm/tools/search/serper-scraper.mjs +129 -0
  108. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -0
  109. package/dist/esm/tools/search/tool.mjs +46 -9
  110. package/dist/esm/tools/search/tool.mjs.map +1 -1
  111. package/dist/esm/utils/handlers.mjs +68 -0
  112. package/dist/esm/utils/handlers.mjs.map +1 -0
  113. package/dist/esm/utils/misc.mjs +8 -2
  114. package/dist/esm/utils/misc.mjs.map +1 -1
  115. package/dist/esm/utils/title.mjs +54 -25
  116. package/dist/esm/utils/title.mjs.map +1 -1
  117. package/dist/types/agents/AgentContext.d.ts +4 -1
  118. package/dist/types/instrumentation.d.ts +1 -0
  119. package/dist/types/llm/anthropic/index.d.ts +3 -0
  120. package/dist/types/llm/google/index.d.ts +1 -0
  121. package/dist/types/llm/ollama/index.d.ts +1 -0
  122. package/dist/types/llm/openai/index.d.ts +4 -0
  123. package/dist/types/llm/openrouter/index.d.ts +4 -2
  124. package/dist/types/llm/vertexai/index.d.ts +1 -1
  125. package/dist/types/messages/cache.d.ts +8 -0
  126. package/dist/types/messages/content.d.ts +7 -0
  127. package/dist/types/messages/format.d.ts +22 -25
  128. package/dist/types/messages/index.d.ts +2 -0
  129. package/dist/types/run.d.ts +2 -1
  130. package/dist/types/tools/search/firecrawl.d.ts +2 -1
  131. package/dist/types/tools/search/rerankers.d.ts +4 -1
  132. package/dist/types/tools/search/search.d.ts +1 -2
  133. package/dist/types/tools/search/serper-scraper.d.ts +59 -0
  134. package/dist/types/tools/search/tool.d.ts +25 -4
  135. package/dist/types/tools/search/types.d.ts +31 -1
  136. package/dist/types/types/graph.d.ts +3 -1
  137. package/dist/types/types/messages.d.ts +4 -0
  138. package/dist/types/utils/handlers.d.ts +34 -0
  139. package/dist/types/utils/index.d.ts +1 -0
  140. package/dist/types/utils/misc.d.ts +1 -0
  141. package/package.json +6 -2
  142. package/src/agents/AgentContext.ts +8 -0
  143. package/src/graphs/Graph.ts +31 -2
  144. package/src/graphs/MultiAgentGraph.ts +5 -5
  145. package/src/instrumentation.ts +22 -0
  146. package/src/llm/anthropic/index.ts +23 -2
  147. package/src/llm/anthropic/llm.spec.ts +1 -1
  148. package/src/llm/google/index.ts +4 -0
  149. package/src/llm/google/utils/common.ts +14 -0
  150. package/src/llm/ollama/index.ts +3 -0
  151. package/src/llm/openai/index.ts +17 -4
  152. package/src/llm/openai/utils/index.ts +7 -1
  153. package/src/llm/openrouter/index.ts +15 -6
  154. package/src/llm/vertexai/index.ts +2 -2
  155. package/src/messages/cache.test.ts +262 -0
  156. package/src/messages/cache.ts +56 -0
  157. package/src/messages/content.test.ts +362 -0
  158. package/src/messages/content.ts +63 -0
  159. package/src/messages/core.ts +5 -2
  160. package/src/messages/format.ts +65 -71
  161. package/src/messages/formatMessage.test.ts +418 -2
  162. package/src/messages/index.ts +2 -0
  163. package/src/messages/prune.ts +51 -0
  164. package/src/run.ts +82 -10
  165. package/src/scripts/ant_web_search.ts +1 -1
  166. package/src/scripts/handoff-test.ts +1 -1
  167. package/src/scripts/multi-agent-chain.ts +4 -4
  168. package/src/scripts/multi-agent-conditional.ts +4 -4
  169. package/src/scripts/multi-agent-document-review-chain.ts +4 -4
  170. package/src/scripts/multi-agent-parallel.ts +10 -8
  171. package/src/scripts/multi-agent-sequence.ts +3 -3
  172. package/src/scripts/multi-agent-supervisor.ts +5 -3
  173. package/src/scripts/multi-agent-test.ts +2 -2
  174. package/src/scripts/search.ts +5 -1
  175. package/src/scripts/simple.ts +8 -0
  176. package/src/scripts/test-custom-prompt-key.ts +4 -4
  177. package/src/scripts/test-handoff-input.ts +3 -3
  178. package/src/scripts/test-multi-agent-list-handoff.ts +2 -2
  179. package/src/scripts/tools.ts +4 -1
  180. package/src/specs/agent-handoffs.test.ts +889 -0
  181. package/src/stream.ts +9 -2
  182. package/src/tools/search/firecrawl.ts +5 -2
  183. package/src/tools/search/jina-reranker.test.ts +126 -0
  184. package/src/tools/search/rerankers.ts +11 -5
  185. package/src/tools/search/search.ts +6 -8
  186. package/src/tools/search/serper-scraper.ts +155 -0
  187. package/src/tools/search/tool.ts +49 -8
  188. package/src/tools/search/types.ts +46 -0
  189. package/src/types/graph.ts +6 -1
  190. package/src/types/messages.ts +4 -0
  191. package/src/utils/handlers.ts +107 -0
  192. package/src/utils/index.ts +2 -1
  193. package/src/utils/llmConfig.ts +35 -1
  194. package/src/utils/misc.ts +33 -21
  195. package/src/utils/title.ts +80 -40
@@ -0,0 +1,889 @@
1
+ // src/specs/agent-handoffs.test.ts
2
+ import { z } from 'zod';
3
+ import { DynamicStructuredTool } from '@langchain/core/tools';
4
+ import { HumanMessage, ToolMessage } from '@langchain/core/messages';
5
+ import type { ToolCall } from '@langchain/core/messages/tool';
6
+ import type { RunnableConfig } from '@langchain/core/runnables';
7
+ import type * as t from '@/types';
8
+ import { Providers, Constants } from '@/common';
9
+ import { StandardGraph } from '@/graphs/Graph';
10
+ import { Run } from '@/run';
11
+
12
+ /**
13
+ * Helper to safely get tool name from tool object
14
+ */
15
+ const getToolName = (tool: t.GraphTools[0]): string | undefined => {
16
+ return (tool as { name?: string }).name;
17
+ };
18
+
19
+ /**
20
+ * Helper to safely get tool description from tool object
21
+ */
22
+ const getToolDescription = (tool: t.GraphTools[0]): string | undefined => {
23
+ return (tool as { description?: string }).description;
24
+ };
25
+
26
+ /**
27
+ * Helper to safely get tool schema from tool object
28
+ */
29
+ const getToolSchema = (tool: t.GraphTools[0]): unknown => {
30
+ return (tool as { schema?: unknown }).schema;
31
+ };
32
+
33
+ /**
34
+ * Helper to find tool by name
35
+ */
36
+ const findToolByName = (
37
+ tools: t.GraphTools | undefined,
38
+ name: string
39
+ ): t.GraphTools[0] | undefined => {
40
+ return tools?.find((tool) => getToolName(tool) === name);
41
+ };
42
+
43
+ /**
44
+ * Test suite for Agent Handoffs feature
45
+ *
46
+ * Tests cover:
47
+ * - Basic handoff between two agents
48
+ * - Handoffs with custom descriptions
49
+ * - Handoffs with prompts and prompt keys
50
+ * - Sequential handoffs (A -> B -> C)
51
+ * - Bidirectional handoffs (A <-> B)
52
+ * - Multiple handoff options from single agent
53
+ * - Handoff tool creation and execution
54
+ * - Error cases and edge conditions
55
+ */
56
+ describe('Agent Handoffs Tests', () => {
57
+ jest.setTimeout(30000);
58
+
59
+ const createTestConfig = (
60
+ agents: t.AgentInputs[],
61
+ edges: t.GraphEdge[]
62
+ ): t.RunConfig => ({
63
+ runId: `handoff-test-${Date.now()}-${Math.random()}`,
64
+ graphConfig: {
65
+ type: 'multi-agent',
66
+ agents,
67
+ edges,
68
+ },
69
+ returnContent: true,
70
+ });
71
+
72
+ const createBasicAgent = (
73
+ agentId: string,
74
+ instructions: string
75
+ ): t.AgentInputs => ({
76
+ agentId,
77
+ provider: Providers.ANTHROPIC,
78
+ clientOptions: {
79
+ modelName: 'claude-haiku-4-5',
80
+ apiKey: 'test-key',
81
+ },
82
+ instructions,
83
+ maxContextTokens: 28000,
84
+ });
85
+
86
+ describe('Basic Handoff Tests', () => {
87
+ it('should create handoff tool for agent with outgoing handoff edge', async () => {
88
+ const agents: t.AgentInputs[] = [
89
+ createBasicAgent('agent_a', 'You are agent A'),
90
+ createBasicAgent('agent_b', 'You are agent B'),
91
+ ];
92
+
93
+ const edges: t.GraphEdge[] = [
94
+ {
95
+ from: 'agent_a',
96
+ to: 'agent_b',
97
+ edgeType: 'handoff',
98
+ description: 'Transfer to agent B',
99
+ },
100
+ ];
101
+
102
+ const run = await Run.create(createTestConfig(agents, edges));
103
+
104
+ expect(run.Graph).toBeDefined();
105
+
106
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
107
+ 'agent_a'
108
+ );
109
+ expect(agentAContext).toBeDefined();
110
+ expect(agentAContext?.tools).toBeDefined();
111
+
112
+ // Check that handoff tool was created
113
+ const handoffTool = findToolByName(
114
+ agentAContext?.tools,
115
+ `${Constants.LC_TRANSFER_TO_}agent_b`
116
+ );
117
+ expect(handoffTool).toBeDefined();
118
+ expect(getToolDescription(handoffTool!)).toBe('Transfer to agent B');
119
+ });
120
+
121
+ it('should successfully handoff from agent A to agent B', async () => {
122
+ const agents: t.AgentInputs[] = [
123
+ createBasicAgent('agent_a', 'You are agent A. Transfer to agent B.'),
124
+ createBasicAgent('agent_b', 'You are agent B. Respond to the user.'),
125
+ ];
126
+
127
+ const edges: t.GraphEdge[] = [
128
+ {
129
+ from: 'agent_a',
130
+ to: 'agent_b',
131
+ edgeType: 'handoff',
132
+ description: 'Transfer to agent B when needed',
133
+ },
134
+ ];
135
+
136
+ const run = await Run.create(createTestConfig(agents, edges));
137
+
138
+ // Override models to simulate handoff behavior
139
+ run.Graph?.overrideTestModel(
140
+ [
141
+ 'Transferring to agent B', // Agent A response
142
+ 'Hello from agent B', // Agent B response
143
+ ],
144
+ 10,
145
+ [
146
+ {
147
+ id: 'tool_call_1',
148
+ name: `${Constants.LC_TRANSFER_TO_}agent_b`,
149
+ args: {},
150
+ } as ToolCall,
151
+ ]
152
+ );
153
+
154
+ const messages = [new HumanMessage('Hello')];
155
+
156
+ const config: Partial<RunnableConfig> & {
157
+ version: 'v1' | 'v2';
158
+ streamMode: string;
159
+ } = {
160
+ configurable: {
161
+ thread_id: 'test-handoff-thread',
162
+ },
163
+ streamMode: 'values',
164
+ version: 'v2' as const,
165
+ };
166
+
167
+ await run.processStream({ messages }, config);
168
+
169
+ const finalMessages = run.getRunMessages();
170
+ expect(finalMessages).toBeDefined();
171
+ expect(finalMessages!.length).toBeGreaterThan(1);
172
+
173
+ // Check for tool message indicating handoff
174
+ const toolMessages = finalMessages!.filter(
175
+ (msg) => msg.getType() === 'tool'
176
+ ) as ToolMessage[];
177
+
178
+ const handoffToolMessage = toolMessages.find(
179
+ (msg) => msg.name === `${Constants.LC_TRANSFER_TO_}agent_b`
180
+ );
181
+ expect(handoffToolMessage).toBeDefined();
182
+ expect(handoffToolMessage?.content).toContain('transferred to agent_b');
183
+ });
184
+
185
+ it('should not create handoff tool for agent without outgoing edges', async () => {
186
+ const agents: t.AgentInputs[] = [
187
+ createBasicAgent('agent_a', 'You are agent A'),
188
+ createBasicAgent('agent_b', 'You are agent B'),
189
+ ];
190
+
191
+ const edges: t.GraphEdge[] = [
192
+ {
193
+ from: 'agent_a',
194
+ to: 'agent_b',
195
+ edgeType: 'handoff',
196
+ },
197
+ ];
198
+
199
+ const run = await Run.create(createTestConfig(agents, edges));
200
+
201
+ const agentBContext = (run.Graph as StandardGraph).agentContexts.get(
202
+ 'agent_b'
203
+ );
204
+ expect(agentBContext).toBeDefined();
205
+
206
+ // Agent B should not have handoff tools (no outgoing edges)
207
+ const handoffTools = agentBContext?.tools?.filter((tool) => {
208
+ const name = getToolName(tool);
209
+ return name?.startsWith(Constants.LC_TRANSFER_TO_) ?? false;
210
+ });
211
+ expect(handoffTools?.length ?? 0).toBe(0);
212
+ });
213
+ });
214
+
215
+ describe('Bidirectional Handoffs', () => {
216
+ it('should create handoff tools for both agents in bidirectional setup', async () => {
217
+ const agents: t.AgentInputs[] = [
218
+ createBasicAgent('agent_a', 'You are agent A'),
219
+ createBasicAgent('agent_b', 'You are agent B'),
220
+ ];
221
+
222
+ const edges: t.GraphEdge[] = [
223
+ {
224
+ from: 'agent_a',
225
+ to: 'agent_b',
226
+ edgeType: 'handoff',
227
+ description: 'Transfer to agent B',
228
+ },
229
+ {
230
+ from: 'agent_b',
231
+ to: 'agent_a',
232
+ edgeType: 'handoff',
233
+ description: 'Transfer to agent A',
234
+ },
235
+ ];
236
+
237
+ const run = await Run.create(createTestConfig(agents, edges));
238
+
239
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
240
+ 'agent_a'
241
+ );
242
+ const agentBContext = (run.Graph as StandardGraph).agentContexts.get(
243
+ 'agent_b'
244
+ );
245
+
246
+ // Agent A should have tool to transfer to B
247
+ const agentAHandoffTool = findToolByName(
248
+ agentAContext?.tools,
249
+ `${Constants.LC_TRANSFER_TO_}agent_b`
250
+ );
251
+ expect(agentAHandoffTool).toBeDefined();
252
+
253
+ // Agent B should have tool to transfer to A
254
+ const agentBHandoffTool = findToolByName(
255
+ agentBContext?.tools,
256
+ `${Constants.LC_TRANSFER_TO_}agent_a`
257
+ );
258
+ expect(agentBHandoffTool).toBeDefined();
259
+ });
260
+
261
+ it('should handle handoff from A to B in bidirectional setup', async () => {
262
+ const agents: t.AgentInputs[] = [
263
+ createBasicAgent('agent_a', 'You are agent A'),
264
+ createBasicAgent('agent_b', 'You are agent B'),
265
+ ];
266
+
267
+ const edges: t.GraphEdge[] = [
268
+ {
269
+ from: 'agent_a',
270
+ to: 'agent_b',
271
+ edgeType: 'handoff',
272
+ },
273
+ {
274
+ from: 'agent_b',
275
+ to: 'agent_a',
276
+ edgeType: 'handoff',
277
+ },
278
+ ];
279
+
280
+ const run = await Run.create(createTestConfig(agents, edges));
281
+
282
+ // Simulate single handoff from A to B
283
+ run.Graph?.overrideTestModel(
284
+ ['Transferring to B', 'Response from B'],
285
+ 10,
286
+ [
287
+ {
288
+ id: 'tool_call_1',
289
+ name: `${Constants.LC_TRANSFER_TO_}agent_b`,
290
+ args: {},
291
+ } as ToolCall,
292
+ ]
293
+ );
294
+
295
+ const messages = [new HumanMessage('Start conversation')];
296
+
297
+ const config: Partial<RunnableConfig> & {
298
+ version: 'v1' | 'v2';
299
+ streamMode: string;
300
+ } = {
301
+ configurable: {
302
+ thread_id: 'test-bidirectional-thread',
303
+ },
304
+ streamMode: 'values',
305
+ version: 'v2' as const,
306
+ };
307
+
308
+ await run.processStream({ messages }, config);
309
+
310
+ const finalMessages = run.getRunMessages();
311
+ expect(finalMessages).toBeDefined();
312
+
313
+ // Should have a handoff tool message
314
+ const toolMessages = finalMessages!.filter(
315
+ (msg) => msg.getType() === 'tool'
316
+ ) as ToolMessage[];
317
+
318
+ const handoffMessage = toolMessages.find(
319
+ (msg) => msg.name === `${Constants.LC_TRANSFER_TO_}agent_b`
320
+ );
321
+ expect(handoffMessage).toBeDefined();
322
+ });
323
+ });
324
+
325
+ describe('Sequential Handoffs (Chain)', () => {
326
+ it('should create handoff tools for chain of agents A -> B -> C', async () => {
327
+ const agents: t.AgentInputs[] = [
328
+ createBasicAgent('agent_a', 'You are agent A'),
329
+ createBasicAgent('agent_b', 'You are agent B'),
330
+ createBasicAgent('agent_c', 'You are agent C'),
331
+ ];
332
+
333
+ const edges: t.GraphEdge[] = [
334
+ {
335
+ from: 'agent_a',
336
+ to: 'agent_b',
337
+ edgeType: 'handoff',
338
+ description: 'Transfer to agent B',
339
+ },
340
+ {
341
+ from: 'agent_b',
342
+ to: 'agent_c',
343
+ edgeType: 'handoff',
344
+ description: 'Transfer to agent C',
345
+ },
346
+ ];
347
+
348
+ const run = await Run.create(createTestConfig(agents, edges));
349
+
350
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
351
+ 'agent_a'
352
+ );
353
+ const agentBContext = (run.Graph as StandardGraph).agentContexts.get(
354
+ 'agent_b'
355
+ );
356
+ const agentCContext = (run.Graph as StandardGraph).agentContexts.get(
357
+ 'agent_c'
358
+ );
359
+
360
+ // Agent A should have tool to transfer to B
361
+ expect(
362
+ findToolByName(
363
+ agentAContext?.tools,
364
+ `${Constants.LC_TRANSFER_TO_}agent_b`
365
+ )
366
+ ).toBeDefined();
367
+
368
+ // Agent B should have tool to transfer to C
369
+ expect(
370
+ findToolByName(
371
+ agentBContext?.tools,
372
+ `${Constants.LC_TRANSFER_TO_}agent_c`
373
+ )
374
+ ).toBeDefined();
375
+
376
+ // Agent C should have no handoff tools
377
+ const agentCHandoffTools = agentCContext?.tools?.filter((tool) => {
378
+ const name = getToolName(tool);
379
+ return name?.startsWith(Constants.LC_TRANSFER_TO_) ?? false;
380
+ });
381
+ expect(agentCHandoffTools?.length ?? 0).toBe(0);
382
+ });
383
+ });
384
+
385
+ describe('Multiple Handoff Options', () => {
386
+ it('should create multiple handoff tools when agent has multiple outgoing edges', async () => {
387
+ const agents: t.AgentInputs[] = [
388
+ createBasicAgent('router', 'You are a router agent'),
389
+ createBasicAgent('agent_a', 'You are agent A'),
390
+ createBasicAgent('agent_b', 'You are agent B'),
391
+ createBasicAgent('agent_c', 'You are agent C'),
392
+ ];
393
+
394
+ const edges: t.GraphEdge[] = [
395
+ {
396
+ from: 'router',
397
+ to: 'agent_a',
398
+ edgeType: 'handoff',
399
+ description: 'Transfer to agent A for task A',
400
+ },
401
+ {
402
+ from: 'router',
403
+ to: 'agent_b',
404
+ edgeType: 'handoff',
405
+ description: 'Transfer to agent B for task B',
406
+ },
407
+ {
408
+ from: 'router',
409
+ to: 'agent_c',
410
+ edgeType: 'handoff',
411
+ description: 'Transfer to agent C for task C',
412
+ },
413
+ ];
414
+
415
+ const run = await Run.create(createTestConfig(agents, edges));
416
+
417
+ const routerContext = (run.Graph as StandardGraph).agentContexts.get(
418
+ 'router'
419
+ );
420
+ expect(routerContext).toBeDefined();
421
+
422
+ // Router should have 3 handoff tools
423
+ const handoffTools = routerContext?.tools?.filter((tool) => {
424
+ const name = getToolName(tool);
425
+ return name?.startsWith(Constants.LC_TRANSFER_TO_) ?? false;
426
+ });
427
+ expect(handoffTools?.length).toBe(3);
428
+
429
+ // Verify each tool exists
430
+ expect(
431
+ findToolByName(handoffTools, `${Constants.LC_TRANSFER_TO_}agent_a`)
432
+ ).toBeDefined();
433
+ expect(
434
+ findToolByName(handoffTools, `${Constants.LC_TRANSFER_TO_}agent_b`)
435
+ ).toBeDefined();
436
+ expect(
437
+ findToolByName(handoffTools, `${Constants.LC_TRANSFER_TO_}agent_c`)
438
+ ).toBeDefined();
439
+ });
440
+
441
+ it('should route to correct agent based on handoff tool used', async () => {
442
+ const agents: t.AgentInputs[] = [
443
+ createBasicAgent('router', 'You are a router'),
444
+ createBasicAgent('agent_a', 'You are agent A'),
445
+ createBasicAgent('agent_b', 'You are agent B'),
446
+ ];
447
+
448
+ const edges: t.GraphEdge[] = [
449
+ {
450
+ from: 'router',
451
+ to: 'agent_a',
452
+ edgeType: 'handoff',
453
+ description: 'Transfer to agent A',
454
+ },
455
+ {
456
+ from: 'router',
457
+ to: 'agent_b',
458
+ edgeType: 'handoff',
459
+ description: 'Transfer to agent B',
460
+ },
461
+ ];
462
+
463
+ const run = await Run.create(createTestConfig(agents, edges));
464
+
465
+ // Router chooses agent_b
466
+ run.Graph?.overrideTestModel(
467
+ ['Routing to agent B', 'Hello from agent B'],
468
+ 10,
469
+ [
470
+ {
471
+ id: 'tool_call_1',
472
+ name: `${Constants.LC_TRANSFER_TO_}agent_b`,
473
+ args: {},
474
+ } as ToolCall,
475
+ ]
476
+ );
477
+
478
+ const messages = [new HumanMessage('Route this message')];
479
+
480
+ const config: Partial<RunnableConfig> & {
481
+ version: 'v1' | 'v2';
482
+ streamMode: string;
483
+ } = {
484
+ configurable: {
485
+ thread_id: 'test-routing-thread',
486
+ },
487
+ streamMode: 'values',
488
+ version: 'v2' as const,
489
+ };
490
+
491
+ await run.processStream({ messages }, config);
492
+
493
+ const finalMessages = run.getRunMessages();
494
+ const toolMessages = finalMessages!.filter(
495
+ (msg) => msg.getType() === 'tool'
496
+ ) as ToolMessage[];
497
+
498
+ // Should have handoff to agent_b, not agent_a
499
+ const handoffToB = toolMessages.find(
500
+ (msg) => msg.name === `${Constants.LC_TRANSFER_TO_}agent_b`
501
+ );
502
+ expect(handoffToB).toBeDefined();
503
+
504
+ const handoffToA = toolMessages.find(
505
+ (msg) => msg.name === `${Constants.LC_TRANSFER_TO_}agent_a`
506
+ );
507
+ expect(handoffToA).toBeUndefined();
508
+ });
509
+ });
510
+
511
+ describe('Handoffs with Prompts', () => {
512
+ it('should create handoff tool with prompt parameter when prompt is specified', async () => {
513
+ const agents: t.AgentInputs[] = [
514
+ createBasicAgent('agent_a', 'You are agent A'),
515
+ createBasicAgent('agent_b', 'You are agent B'),
516
+ ];
517
+
518
+ const edges: t.GraphEdge[] = [
519
+ {
520
+ from: 'agent_a',
521
+ to: 'agent_b',
522
+ edgeType: 'handoff',
523
+ description: 'Transfer to agent B with instructions',
524
+ prompt: 'Provide specific instructions for agent B',
525
+ promptKey: 'instructions',
526
+ },
527
+ ];
528
+
529
+ const run = await Run.create(createTestConfig(agents, edges));
530
+
531
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
532
+ 'agent_a'
533
+ );
534
+ const handoffTool = findToolByName(
535
+ agentAContext?.tools,
536
+ `${Constants.LC_TRANSFER_TO_}agent_b`
537
+ );
538
+
539
+ expect(handoffTool).toBeDefined();
540
+ // Tool should accept parameters (schema should be defined)
541
+ expect(getToolSchema(handoffTool!)).toBeDefined();
542
+ });
543
+
544
+ it('should use default promptKey when not specified', async () => {
545
+ const agents: t.AgentInputs[] = [
546
+ createBasicAgent('agent_a', 'You are agent A'),
547
+ createBasicAgent('agent_b', 'You are agent B'),
548
+ ];
549
+
550
+ const edges: t.GraphEdge[] = [
551
+ {
552
+ from: 'agent_a',
553
+ to: 'agent_b',
554
+ edgeType: 'handoff',
555
+ prompt: 'Instructions for handoff',
556
+ // promptKey not specified, should default to 'instructions'
557
+ },
558
+ ];
559
+
560
+ const run = await Run.create(createTestConfig(agents, edges));
561
+
562
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
563
+ 'agent_a'
564
+ );
565
+ const handoffTool = findToolByName(
566
+ agentAContext?.tools,
567
+ `${Constants.LC_TRANSFER_TO_}agent_b`
568
+ );
569
+
570
+ expect(handoffTool).toBeDefined();
571
+ expect(getToolSchema(handoffTool!)).toBeDefined();
572
+ });
573
+
574
+ it('should include prompt content in handoff tool message', async () => {
575
+ const agents: t.AgentInputs[] = [
576
+ createBasicAgent('agent_a', 'You are agent A'),
577
+ createBasicAgent('agent_b', 'You are agent B'),
578
+ ];
579
+
580
+ const edges: t.GraphEdge[] = [
581
+ {
582
+ from: 'agent_a',
583
+ to: 'agent_b',
584
+ edgeType: 'handoff',
585
+ description: 'Transfer to agent B',
586
+ prompt: 'Additional context for agent B',
587
+ promptKey: 'context',
588
+ },
589
+ ];
590
+
591
+ const run = await Run.create(createTestConfig(agents, edges));
592
+
593
+ run.Graph?.overrideTestModel(['Transferring with context'], 10, [
594
+ {
595
+ id: 'tool_call_1',
596
+ name: `${Constants.LC_TRANSFER_TO_}agent_b`,
597
+ args: { context: 'User needs help with booking' },
598
+ } as ToolCall,
599
+ ]);
600
+
601
+ const messages = [new HumanMessage('Help me')];
602
+
603
+ const config: Partial<RunnableConfig> & {
604
+ version: 'v1' | 'v2';
605
+ streamMode: string;
606
+ } = {
607
+ configurable: {
608
+ thread_id: 'test-prompt-thread',
609
+ },
610
+ streamMode: 'values',
611
+ version: 'v2' as const,
612
+ };
613
+
614
+ await run.processStream({ messages }, config);
615
+
616
+ const finalMessages = run.getRunMessages();
617
+ const toolMessages = finalMessages!.filter(
618
+ (msg) => msg.getType() === 'tool'
619
+ ) as ToolMessage[];
620
+
621
+ const handoffMessage = toolMessages.find(
622
+ (msg) => msg.name === `${Constants.LC_TRANSFER_TO_}agent_b`
623
+ );
624
+
625
+ expect(handoffMessage).toBeDefined();
626
+ // Tool message should contain the prompt key and value
627
+ expect(handoffMessage?.content).toContain('Context:');
628
+ });
629
+ });
630
+
631
+ describe('Edge Cases and Error Handling', () => {
632
+ it('should handle self-referential edge gracefully', async () => {
633
+ const agents: t.AgentInputs[] = [
634
+ createBasicAgent('agent_a', 'You are agent A'),
635
+ ];
636
+
637
+ const edges: t.GraphEdge[] = [
638
+ {
639
+ from: 'agent_a',
640
+ to: 'agent_a',
641
+ edgeType: 'handoff',
642
+ description: 'Self-handoff (should be allowed but unusual)',
643
+ },
644
+ ];
645
+
646
+ // Should not throw during creation
647
+ expect(async () => {
648
+ await Run.create(createTestConfig(agents, edges));
649
+ }).not.toThrow();
650
+ });
651
+
652
+ it('should handle empty edges array', async () => {
653
+ const agents: t.AgentInputs[] = [
654
+ createBasicAgent('agent_a', 'You are agent A'),
655
+ createBasicAgent('agent_b', 'You are agent B'),
656
+ ];
657
+
658
+ const edges: t.GraphEdge[] = [];
659
+
660
+ const run = await Run.create(createTestConfig(agents, edges));
661
+
662
+ expect(run.Graph).toBeDefined();
663
+
664
+ // Agents should have no handoff tools
665
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
666
+ 'agent_a'
667
+ );
668
+ const handoffTools = agentAContext?.tools?.filter((tool) => {
669
+ const name = getToolName(tool);
670
+ return name?.startsWith(Constants.LC_TRANSFER_TO_) ?? false;
671
+ });
672
+ expect(handoffTools?.length ?? 0).toBe(0);
673
+ });
674
+
675
+ it('should start from first agent when no edges are defined', async () => {
676
+ const agents: t.AgentInputs[] = [
677
+ createBasicAgent('agent_a', 'You are agent A'),
678
+ createBasicAgent('agent_b', 'You are agent B'),
679
+ ];
680
+
681
+ const edges: t.GraphEdge[] = [];
682
+
683
+ const run = await Run.create(createTestConfig(agents, edges));
684
+
685
+ run.Graph?.overrideTestModel(['Response from first agent'], 10);
686
+
687
+ const messages = [new HumanMessage('Hello')];
688
+
689
+ const config: Partial<RunnableConfig> & {
690
+ version: 'v1' | 'v2';
691
+ streamMode: string;
692
+ } = {
693
+ configurable: {
694
+ thread_id: 'test-no-edges-thread',
695
+ },
696
+ streamMode: 'values',
697
+ version: 'v2' as const,
698
+ };
699
+
700
+ await run.processStream({ messages }, config);
701
+
702
+ const finalMessages = run.getRunMessages();
703
+ expect(finalMessages).toBeDefined();
704
+ expect(finalMessages!.length).toBeGreaterThan(0);
705
+ });
706
+
707
+ it('should handle agents with existing tools alongside handoff tools', async () => {
708
+ const customTool = new DynamicStructuredTool({
709
+ name: 'custom_tool',
710
+ description: 'A custom tool',
711
+ schema: z.object({}),
712
+ func: async (): Promise<string> => 'Tool result',
713
+ });
714
+
715
+ const agents: t.AgentInputs[] = [
716
+ {
717
+ ...createBasicAgent('agent_a', 'You are agent A'),
718
+ tools: [customTool],
719
+ },
720
+ createBasicAgent('agent_b', 'You are agent B'),
721
+ ];
722
+
723
+ const edges: t.GraphEdge[] = [
724
+ {
725
+ from: 'agent_a',
726
+ to: 'agent_b',
727
+ edgeType: 'handoff',
728
+ description: 'Transfer to agent B',
729
+ },
730
+ ];
731
+
732
+ const run = await Run.create(createTestConfig(agents, edges));
733
+
734
+ const agentAContext = (run.Graph as StandardGraph).agentContexts.get(
735
+ 'agent_a'
736
+ );
737
+
738
+ // Agent A should have both custom tool and handoff tool
739
+ expect(agentAContext?.tools?.length).toBeGreaterThanOrEqual(2);
740
+
741
+ expect(findToolByName(agentAContext?.tools, 'custom_tool')).toBeDefined();
742
+
743
+ expect(
744
+ findToolByName(
745
+ agentAContext?.tools,
746
+ `${Constants.LC_TRANSFER_TO_}agent_b`
747
+ )
748
+ ).toBeDefined();
749
+ });
750
+ });
751
+
752
+ describe('Graph Structure Analysis', () => {
753
+ it('should correctly identify starting nodes with no incoming edges', async () => {
754
+ const agents: t.AgentInputs[] = [
755
+ createBasicAgent('agent_a', 'Starting agent'),
756
+ createBasicAgent('agent_b', 'Middle agent'),
757
+ createBasicAgent('agent_c', 'End agent'),
758
+ ];
759
+
760
+ const edges: t.GraphEdge[] = [
761
+ {
762
+ from: 'agent_a',
763
+ to: 'agent_b',
764
+ edgeType: 'handoff',
765
+ },
766
+ {
767
+ from: 'agent_b',
768
+ to: 'agent_c',
769
+ edgeType: 'handoff',
770
+ },
771
+ ];
772
+
773
+ const run = await Run.create(createTestConfig(agents, edges));
774
+
775
+ // agent_a should be the starting node (no incoming edges)
776
+ expect(run.Graph).toBeDefined();
777
+ // This is internal behavior, but we can test via execution
778
+ run.Graph?.overrideTestModel(['Response from agent A'], 10);
779
+
780
+ const messages = [new HumanMessage('Start')];
781
+
782
+ const config: Partial<RunnableConfig> & {
783
+ version: 'v1' | 'v2';
784
+ streamMode: string;
785
+ } = {
786
+ configurable: {
787
+ thread_id: 'test-starting-node-thread',
788
+ },
789
+ streamMode: 'values',
790
+ version: 'v2' as const,
791
+ };
792
+
793
+ // Should start from agent_a
794
+ await run.processStream({ messages }, config);
795
+
796
+ const finalMessages = run.getRunMessages();
797
+ expect(finalMessages).toBeDefined();
798
+ });
799
+
800
+ it('should handle multiple starting nodes (parallel entry points)', async () => {
801
+ const agents: t.AgentInputs[] = [
802
+ createBasicAgent('agent_a', 'Starting agent A'),
803
+ createBasicAgent('agent_b', 'Starting agent B'),
804
+ createBasicAgent('agent_c', 'Shared destination'),
805
+ ];
806
+
807
+ const edges: t.GraphEdge[] = [
808
+ {
809
+ from: 'agent_a',
810
+ to: 'agent_c',
811
+ edgeType: 'handoff',
812
+ },
813
+ {
814
+ from: 'agent_b',
815
+ to: 'agent_c',
816
+ edgeType: 'handoff',
817
+ },
818
+ ];
819
+
820
+ // Both agent_a and agent_b have no incoming edges, so both are starting nodes
821
+ const run = await Run.create(createTestConfig(agents, edges));
822
+
823
+ expect(run.Graph).toBeDefined();
824
+ });
825
+ });
826
+
827
+ describe('Handoff Tool Naming', () => {
828
+ it('should use correct naming convention for handoff tools', async () => {
829
+ const agents: t.AgentInputs[] = [
830
+ createBasicAgent('flight_assistant', 'You handle flights'),
831
+ createBasicAgent('hotel_assistant', 'You handle hotels'),
832
+ ];
833
+
834
+ const edges: t.GraphEdge[] = [
835
+ {
836
+ from: 'flight_assistant',
837
+ to: 'hotel_assistant',
838
+ edgeType: 'handoff',
839
+ description: 'Transfer to hotel booking',
840
+ },
841
+ ];
842
+
843
+ const run = await Run.create(createTestConfig(agents, edges));
844
+
845
+ const flightContext = (run.Graph as StandardGraph).agentContexts.get(
846
+ 'flight_assistant'
847
+ );
848
+ const handoffTool = findToolByName(
849
+ flightContext?.tools,
850
+ `${Constants.LC_TRANSFER_TO_}hotel_assistant`
851
+ );
852
+
853
+ expect(handoffTool).toBeDefined();
854
+ expect(getToolName(handoffTool!)).toBe(
855
+ `${Constants.LC_TRANSFER_TO_}hotel_assistant`
856
+ );
857
+ });
858
+
859
+ it('should preserve agent ID format in tool names', async () => {
860
+ const agents: t.AgentInputs[] = [
861
+ createBasicAgent('agent_with_underscores', 'Agent with underscores'),
862
+ createBasicAgent('AgentWithCamelCase', 'Agent with camel case'),
863
+ ];
864
+
865
+ const edges: t.GraphEdge[] = [
866
+ {
867
+ from: 'agent_with_underscores',
868
+ to: 'AgentWithCamelCase',
869
+ edgeType: 'handoff',
870
+ },
871
+ ];
872
+
873
+ const run = await Run.create(createTestConfig(agents, edges));
874
+
875
+ const agentContext = (run.Graph as StandardGraph).agentContexts.get(
876
+ 'agent_with_underscores'
877
+ );
878
+ const handoffTool = findToolByName(
879
+ agentContext?.tools,
880
+ `${Constants.LC_TRANSFER_TO_}AgentWithCamelCase`
881
+ );
882
+
883
+ expect(handoffTool).toBeDefined();
884
+ expect(getToolName(handoffTool!)).toBe(
885
+ `${Constants.LC_TRANSFER_TO_}AgentWithCamelCase`
886
+ );
887
+ });
888
+ });
889
+ });