@librechat/agents 3.0.0-rc9 → 3.0.1
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/dist/cjs/agents/AgentContext.cjs +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +23 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +5 -5
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +21 -0
- package/dist/cjs/instrumentation.cjs.map +1 -0
- package/dist/cjs/llm/anthropic/index.cjs +21 -2
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +3 -0
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +13 -0
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/ollama/index.cjs +3 -0
- package/dist/cjs/llm/ollama/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +18 -3
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +6 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +5 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +1 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +8 -2
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +49 -0
- package/dist/cjs/messages/cache.cjs.map +1 -0
- package/dist/cjs/messages/content.cjs +53 -0
- package/dist/cjs/messages/content.cjs.map +1 -0
- package/dist/cjs/messages/core.cjs +5 -1
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +50 -59
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +28 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +57 -5
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -0
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +2 -0
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +3 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +8 -6
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +5 -5
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +132 -0
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +46 -9
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs +70 -0
- package/dist/cjs/utils/handlers.cjs.map +1 -0
- package/dist/cjs/utils/misc.cjs +8 -1
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +54 -25
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +23 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +5 -5
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +19 -0
- package/dist/esm/instrumentation.mjs.map +1 -0
- package/dist/esm/llm/anthropic/index.mjs +21 -2
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +3 -0
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +13 -0
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/ollama/index.mjs +3 -0
- package/dist/esm/llm/ollama/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +18 -3
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +6 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +5 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +1 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +47 -0
- package/dist/esm/messages/cache.mjs.map +1 -0
- package/dist/esm/messages/content.mjs +51 -0
- package/dist/esm/messages/content.mjs.map +1 -0
- package/dist/esm/messages/core.mjs +5 -1
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +50 -58
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +28 -0
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +57 -5
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -0
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +2 -0
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +3 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +8 -6
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +5 -5
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +129 -0
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +46 -9
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs +68 -0
- package/dist/esm/utils/handlers.mjs.map +1 -0
- package/dist/esm/utils/misc.mjs +8 -2
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +54 -25
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/instrumentation.d.ts +1 -0
- package/dist/types/llm/anthropic/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +1 -0
- package/dist/types/llm/ollama/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openrouter/index.d.ts +4 -2
- package/dist/types/llm/vertexai/index.d.ts +1 -1
- package/dist/types/messages/cache.d.ts +8 -0
- package/dist/types/messages/content.d.ts +7 -0
- package/dist/types/messages/format.d.ts +22 -25
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/run.d.ts +2 -1
- package/dist/types/tools/search/firecrawl.d.ts +2 -1
- package/dist/types/tools/search/rerankers.d.ts +4 -1
- package/dist/types/tools/search/search.d.ts +1 -2
- package/dist/types/tools/search/serper-scraper.d.ts +59 -0
- package/dist/types/tools/search/tool.d.ts +25 -4
- package/dist/types/tools/search/types.d.ts +31 -1
- package/dist/types/types/graph.d.ts +3 -1
- package/dist/types/types/messages.d.ts +4 -0
- package/dist/types/utils/handlers.d.ts +34 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/misc.d.ts +1 -0
- package/package.json +7 -3
- package/src/agents/AgentContext.ts +8 -0
- package/src/graphs/Graph.ts +31 -2
- package/src/graphs/MultiAgentGraph.ts +5 -5
- package/src/instrumentation.ts +22 -0
- package/src/llm/anthropic/index.ts +23 -2
- package/src/llm/anthropic/llm.spec.ts +1 -1
- package/src/llm/google/index.ts +4 -0
- package/src/llm/google/utils/common.ts +14 -0
- package/src/llm/ollama/index.ts +3 -0
- package/src/llm/openai/index.ts +17 -4
- package/src/llm/openai/utils/index.ts +7 -1
- package/src/llm/openrouter/index.ts +15 -6
- package/src/llm/vertexai/index.ts +2 -2
- package/src/messages/cache.test.ts +262 -0
- package/src/messages/cache.ts +56 -0
- package/src/messages/content.test.ts +362 -0
- package/src/messages/content.ts +63 -0
- package/src/messages/core.ts +5 -2
- package/src/messages/format.ts +65 -71
- package/src/messages/formatMessage.test.ts +418 -2
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +51 -0
- package/src/run.ts +82 -10
- package/src/scripts/ant_web_search.ts +1 -1
- package/src/scripts/handoff-test.ts +1 -1
- package/src/scripts/multi-agent-chain.ts +4 -4
- package/src/scripts/multi-agent-conditional.ts +4 -4
- package/src/scripts/multi-agent-document-review-chain.ts +4 -4
- package/src/scripts/multi-agent-parallel.ts +10 -8
- package/src/scripts/multi-agent-sequence.ts +3 -3
- package/src/scripts/multi-agent-supervisor.ts +5 -3
- package/src/scripts/multi-agent-test.ts +2 -2
- package/src/scripts/search.ts +5 -1
- package/src/scripts/simple.ts +8 -0
- package/src/scripts/test-custom-prompt-key.ts +4 -4
- package/src/scripts/test-handoff-input.ts +3 -3
- package/src/scripts/test-multi-agent-list-handoff.ts +2 -2
- package/src/scripts/tools.ts +4 -1
- package/src/specs/agent-handoffs.test.ts +889 -0
- package/src/stream.ts +9 -2
- package/src/tools/search/firecrawl.ts +5 -2
- package/src/tools/search/jina-reranker.test.ts +126 -0
- package/src/tools/search/rerankers.ts +11 -5
- package/src/tools/search/search.ts +6 -8
- package/src/tools/search/serper-scraper.ts +155 -0
- package/src/tools/search/tool.ts +49 -8
- package/src/tools/search/types.ts +46 -0
- package/src/types/graph.ts +6 -1
- package/src/types/messages.ts +4 -0
- package/src/utils/handlers.ts +107 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/llmConfig.ts +35 -1
- package/src/utils/misc.ts +33 -21
- 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
|
+
});
|