@librechat/agents 3.0.17 → 3.0.18
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/graphs/Graph.cjs +78 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +63 -0
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/stream.cjs +3 -2
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +5 -5
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +78 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/format.mjs +64 -2
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/stream.mjs +3 -2
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +5 -5
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +19 -2
- package/dist/types/messages/format.d.ts +11 -1
- package/dist/types/tools/handlers.d.ts +2 -1
- package/dist/types/types/stream.d.ts +1 -0
- package/package.json +8 -8
- package/src/graphs/Graph.ts +95 -2
- package/src/messages/ensureThinkingBlock.test.ts +393 -0
- package/src/messages/format.ts +81 -0
- package/src/scripts/test-multi-agent-list-handoff.ts +53 -3
- package/src/scripts/test-thinking-handoff-bedrock.ts +153 -0
- package/src/scripts/test-thinking-handoff.ts +147 -0
- package/src/specs/thinking-handoff.test.ts +620 -0
- package/src/stream.ts +19 -10
- package/src/tools/handlers.ts +36 -18
- package/src/types/stream.ts +1 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// src/specs/thinking-handoff.test.ts
|
|
2
|
+
import { HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
4
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
5
|
+
import type * as t from '@/types';
|
|
6
|
+
import { Providers, Constants } from '@/common';
|
|
7
|
+
import { StandardGraph } from '@/graphs/Graph';
|
|
8
|
+
import { Run } from '@/run';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test suite for Thinking-Enabled Agent Handoff Edge Case
|
|
12
|
+
*
|
|
13
|
+
* Tests the specific edge case where:
|
|
14
|
+
* - An agent without thinking blocks (e.g., OpenAI) makes a tool call
|
|
15
|
+
* - Control is handed off to an agent with thinking enabled (e.g., Anthropic/Bedrock)
|
|
16
|
+
* - The system should handle the transition without errors
|
|
17
|
+
*
|
|
18
|
+
* Background:
|
|
19
|
+
* When Anthropic's extended thinking is enabled, the API requires that any assistant
|
|
20
|
+
* message with tool_use content must start with a thinking or redacted_thinking block.
|
|
21
|
+
* When switching from a non-thinking agent to a thinking-enabled agent, previous
|
|
22
|
+
* messages may not have these blocks, causing API errors.
|
|
23
|
+
*
|
|
24
|
+
* Solution:
|
|
25
|
+
* The ensureThinkingBlockInMessages() function converts AI messages with tool calls
|
|
26
|
+
* (that lack thinking blocks) into HumanMessages with buffer strings, avoiding the
|
|
27
|
+
* thinking block requirement while preserving context.
|
|
28
|
+
*/
|
|
29
|
+
describe('Thinking-Enabled Agent Handoff Tests', () => {
|
|
30
|
+
jest.setTimeout(30000);
|
|
31
|
+
|
|
32
|
+
const createTestConfig = (
|
|
33
|
+
agents: t.AgentInputs[],
|
|
34
|
+
edges: t.GraphEdge[]
|
|
35
|
+
): t.RunConfig => ({
|
|
36
|
+
runId: `thinking-handoff-test-${Date.now()}-${Math.random()}`,
|
|
37
|
+
graphConfig: {
|
|
38
|
+
type: 'multi-agent',
|
|
39
|
+
agents,
|
|
40
|
+
edges,
|
|
41
|
+
},
|
|
42
|
+
returnContent: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('OpenAI to Anthropic with Thinking', () => {
|
|
46
|
+
it('should successfully handoff from OpenAI to Anthropic with thinking enabled', async () => {
|
|
47
|
+
const agents: t.AgentInputs[] = [
|
|
48
|
+
{
|
|
49
|
+
agentId: 'supervisor',
|
|
50
|
+
provider: Providers.OPENAI,
|
|
51
|
+
clientOptions: {
|
|
52
|
+
modelName: 'gpt-4o-mini',
|
|
53
|
+
apiKey: 'test-key',
|
|
54
|
+
},
|
|
55
|
+
instructions:
|
|
56
|
+
'You are a supervisor. Use transfer_to_specialist when asked.',
|
|
57
|
+
maxContextTokens: 8000,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
agentId: 'specialist',
|
|
61
|
+
provider: Providers.ANTHROPIC,
|
|
62
|
+
clientOptions: {
|
|
63
|
+
modelName: 'claude-3-7-sonnet-20250219',
|
|
64
|
+
apiKey: 'test-key',
|
|
65
|
+
thinking: {
|
|
66
|
+
type: 'enabled',
|
|
67
|
+
budget_tokens: 2000,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
instructions: 'You are a specialist. Provide detailed answers.',
|
|
71
|
+
maxContextTokens: 8000,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const edges: t.GraphEdge[] = [
|
|
76
|
+
{
|
|
77
|
+
from: 'supervisor',
|
|
78
|
+
to: 'specialist',
|
|
79
|
+
edgeType: 'handoff',
|
|
80
|
+
description: 'Transfer to specialist for detailed analysis',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
85
|
+
|
|
86
|
+
// Simulate supervisor using handoff tool
|
|
87
|
+
run.Graph?.overrideTestModel(
|
|
88
|
+
[
|
|
89
|
+
'Let me transfer you to our specialist',
|
|
90
|
+
'As a specialist, let me analyze this carefully...',
|
|
91
|
+
],
|
|
92
|
+
10,
|
|
93
|
+
[
|
|
94
|
+
{
|
|
95
|
+
id: 'tool_call_1',
|
|
96
|
+
name: `${Constants.LC_TRANSFER_TO_}specialist`,
|
|
97
|
+
args: {},
|
|
98
|
+
} as ToolCall,
|
|
99
|
+
]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const messages = [new HumanMessage('I need expert analysis')];
|
|
103
|
+
|
|
104
|
+
const config: Partial<RunnableConfig> & {
|
|
105
|
+
version: 'v1' | 'v2';
|
|
106
|
+
streamMode: string;
|
|
107
|
+
} = {
|
|
108
|
+
configurable: {
|
|
109
|
+
thread_id: 'test-thinking-handoff-thread',
|
|
110
|
+
},
|
|
111
|
+
streamMode: 'values',
|
|
112
|
+
version: 'v2' as const,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Should not throw despite thinking requirement
|
|
116
|
+
await expect(
|
|
117
|
+
run.processStream({ messages }, config)
|
|
118
|
+
).resolves.not.toThrow();
|
|
119
|
+
|
|
120
|
+
const finalMessages = run.getRunMessages();
|
|
121
|
+
expect(finalMessages).toBeDefined();
|
|
122
|
+
expect(finalMessages!.length).toBeGreaterThan(1);
|
|
123
|
+
|
|
124
|
+
// Should have successful handoff
|
|
125
|
+
const toolMessages = finalMessages!.filter(
|
|
126
|
+
(msg) => msg.getType() === 'tool'
|
|
127
|
+
) as ToolMessage[];
|
|
128
|
+
|
|
129
|
+
const handoffMessage = toolMessages.find(
|
|
130
|
+
(msg) => msg.name === `${Constants.LC_TRANSFER_TO_}specialist`
|
|
131
|
+
);
|
|
132
|
+
expect(handoffMessage).toBeDefined();
|
|
133
|
+
expect(handoffMessage?.content).toContain('transferred to specialist');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should convert tool sequence to HumanMessage for thinking-enabled agent', async () => {
|
|
137
|
+
const agents: t.AgentInputs[] = [
|
|
138
|
+
{
|
|
139
|
+
agentId: 'agent_a',
|
|
140
|
+
provider: Providers.OPENAI,
|
|
141
|
+
clientOptions: {
|
|
142
|
+
modelName: 'gpt-4o-mini',
|
|
143
|
+
apiKey: 'test-key',
|
|
144
|
+
},
|
|
145
|
+
instructions: 'You are agent A',
|
|
146
|
+
maxContextTokens: 8000,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
agentId: 'agent_b',
|
|
150
|
+
provider: Providers.ANTHROPIC,
|
|
151
|
+
clientOptions: {
|
|
152
|
+
modelName: 'claude-3-7-sonnet-20250219',
|
|
153
|
+
apiKey: 'test-key',
|
|
154
|
+
thinking: {
|
|
155
|
+
type: 'enabled',
|
|
156
|
+
budget_tokens: 2000,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
instructions: 'You are agent B with thinking enabled',
|
|
160
|
+
maxContextTokens: 8000,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const edges: t.GraphEdge[] = [
|
|
165
|
+
{
|
|
166
|
+
from: 'agent_a',
|
|
167
|
+
to: 'agent_b',
|
|
168
|
+
edgeType: 'handoff',
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
173
|
+
|
|
174
|
+
// Check that agent B's context is set up correctly
|
|
175
|
+
const agentBContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
176
|
+
'agent_b'
|
|
177
|
+
);
|
|
178
|
+
expect(agentBContext).toBeDefined();
|
|
179
|
+
|
|
180
|
+
// Verify thinking is enabled
|
|
181
|
+
const thinkingConfig = (
|
|
182
|
+
agentBContext?.clientOptions as t.AnthropicClientOptions
|
|
183
|
+
).thinking;
|
|
184
|
+
expect(thinkingConfig).toBeDefined();
|
|
185
|
+
expect(thinkingConfig?.type).toBe('enabled');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Bedrock with Thinking', () => {
|
|
190
|
+
it('should handle handoff from Bedrock without thinking to Bedrock with thinking', async () => {
|
|
191
|
+
const agents: t.AgentInputs[] = [
|
|
192
|
+
{
|
|
193
|
+
agentId: 'coordinator',
|
|
194
|
+
provider: Providers.BEDROCK,
|
|
195
|
+
clientOptions: {
|
|
196
|
+
region: 'us-east-1',
|
|
197
|
+
model: 'anthropic.claude-3-5-haiku-20241022-v1:0',
|
|
198
|
+
// No thinking config
|
|
199
|
+
},
|
|
200
|
+
instructions: 'You are a coordinator',
|
|
201
|
+
maxContextTokens: 8000,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
agentId: 'analyst',
|
|
205
|
+
provider: Providers.BEDROCK,
|
|
206
|
+
clientOptions: {
|
|
207
|
+
region: 'us-east-1',
|
|
208
|
+
model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
|
209
|
+
additionalModelRequestFields: {
|
|
210
|
+
thinking: {
|
|
211
|
+
type: 'enabled',
|
|
212
|
+
budget_tokens: 2000,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
instructions: 'You are an analyst with extended thinking',
|
|
217
|
+
maxContextTokens: 8000,
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const edges: t.GraphEdge[] = [
|
|
222
|
+
{
|
|
223
|
+
from: 'coordinator',
|
|
224
|
+
to: 'analyst',
|
|
225
|
+
edgeType: 'handoff',
|
|
226
|
+
description: 'Transfer to analyst for deep analysis',
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
231
|
+
|
|
232
|
+
run.Graph?.overrideTestModel(
|
|
233
|
+
['Transferring to analyst', 'Deep analysis results...'],
|
|
234
|
+
10,
|
|
235
|
+
[
|
|
236
|
+
{
|
|
237
|
+
id: 'tool_call_1',
|
|
238
|
+
name: `${Constants.LC_TRANSFER_TO_}analyst`,
|
|
239
|
+
args: {},
|
|
240
|
+
} as ToolCall,
|
|
241
|
+
]
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const messages = [new HumanMessage('Analyze this data')];
|
|
245
|
+
|
|
246
|
+
const config: Partial<RunnableConfig> & {
|
|
247
|
+
version: 'v1' | 'v2';
|
|
248
|
+
streamMode: string;
|
|
249
|
+
} = {
|
|
250
|
+
configurable: {
|
|
251
|
+
thread_id: 'test-bedrock-thinking-thread',
|
|
252
|
+
},
|
|
253
|
+
streamMode: 'values',
|
|
254
|
+
version: 'v2' as const,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
await expect(
|
|
258
|
+
run.processStream({ messages }, config)
|
|
259
|
+
).resolves.not.toThrow();
|
|
260
|
+
|
|
261
|
+
const finalMessages = run.getRunMessages();
|
|
262
|
+
expect(finalMessages).toBeDefined();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should verify Bedrock thinking configuration is properly detected', async () => {
|
|
266
|
+
const agents: t.AgentInputs[] = [
|
|
267
|
+
{
|
|
268
|
+
agentId: 'agent_a',
|
|
269
|
+
provider: Providers.OPENAI,
|
|
270
|
+
clientOptions: {
|
|
271
|
+
modelName: 'gpt-4o-mini',
|
|
272
|
+
apiKey: 'test-key',
|
|
273
|
+
},
|
|
274
|
+
instructions: 'You are agent A',
|
|
275
|
+
maxContextTokens: 8000,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
agentId: 'agent_b',
|
|
279
|
+
provider: Providers.BEDROCK,
|
|
280
|
+
clientOptions: {
|
|
281
|
+
region: 'us-east-1',
|
|
282
|
+
model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
|
283
|
+
additionalModelRequestFields: {
|
|
284
|
+
thinking: {
|
|
285
|
+
type: 'enabled',
|
|
286
|
+
budget_tokens: 3000,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
instructions: 'You are agent B with Bedrock thinking',
|
|
291
|
+
maxContextTokens: 8000,
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const edges: t.GraphEdge[] = [
|
|
296
|
+
{
|
|
297
|
+
from: 'agent_a',
|
|
298
|
+
to: 'agent_b',
|
|
299
|
+
edgeType: 'handoff',
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
304
|
+
|
|
305
|
+
const agentBContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
306
|
+
'agent_b'
|
|
307
|
+
);
|
|
308
|
+
expect(agentBContext).toBeDefined();
|
|
309
|
+
expect(agentBContext?.provider).toBe(Providers.BEDROCK);
|
|
310
|
+
|
|
311
|
+
// Verify thinking configuration in additionalModelRequestFields
|
|
312
|
+
const bedrockOptions =
|
|
313
|
+
agentBContext?.clientOptions as t.BedrockAnthropicInput;
|
|
314
|
+
expect(bedrockOptions.additionalModelRequestFields).toBeDefined();
|
|
315
|
+
expect(
|
|
316
|
+
bedrockOptions.additionalModelRequestFields?.thinking
|
|
317
|
+
).toBeDefined();
|
|
318
|
+
|
|
319
|
+
const thinkingConfig = bedrockOptions.additionalModelRequestFields
|
|
320
|
+
?.thinking as {
|
|
321
|
+
type: string;
|
|
322
|
+
budget_tokens: number;
|
|
323
|
+
};
|
|
324
|
+
expect(thinkingConfig.type).toBe('enabled');
|
|
325
|
+
expect(thinkingConfig.budget_tokens).toBe(3000);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should handle OpenAI to Bedrock with thinking handoff', async () => {
|
|
329
|
+
const agents: t.AgentInputs[] = [
|
|
330
|
+
{
|
|
331
|
+
agentId: 'supervisor',
|
|
332
|
+
provider: Providers.OPENAI,
|
|
333
|
+
clientOptions: {
|
|
334
|
+
modelName: 'gpt-4o-mini',
|
|
335
|
+
apiKey: 'test-key',
|
|
336
|
+
},
|
|
337
|
+
instructions: 'You are a supervisor',
|
|
338
|
+
maxContextTokens: 8000,
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
agentId: 'bedrock_specialist',
|
|
342
|
+
provider: Providers.BEDROCK,
|
|
343
|
+
clientOptions: {
|
|
344
|
+
region: 'us-east-1',
|
|
345
|
+
model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
|
346
|
+
additionalModelRequestFields: {
|
|
347
|
+
thinking: {
|
|
348
|
+
type: 'enabled',
|
|
349
|
+
budget_tokens: 2000,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
instructions: 'You are a Bedrock specialist with thinking',
|
|
354
|
+
maxContextTokens: 8000,
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const edges: t.GraphEdge[] = [
|
|
359
|
+
{
|
|
360
|
+
from: 'supervisor',
|
|
361
|
+
to: 'bedrock_specialist',
|
|
362
|
+
edgeType: 'handoff',
|
|
363
|
+
description: 'Transfer to Bedrock specialist',
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
368
|
+
|
|
369
|
+
run.Graph?.overrideTestModel(['Transferring', 'Analysis complete'], 10, [
|
|
370
|
+
{
|
|
371
|
+
id: 'tool_call_1',
|
|
372
|
+
name: `${Constants.LC_TRANSFER_TO_}bedrock_specialist`,
|
|
373
|
+
args: {},
|
|
374
|
+
} as ToolCall,
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
const messages = [new HumanMessage('Analyze this')];
|
|
378
|
+
|
|
379
|
+
const config: Partial<RunnableConfig> & {
|
|
380
|
+
version: 'v1' | 'v2';
|
|
381
|
+
streamMode: string;
|
|
382
|
+
} = {
|
|
383
|
+
configurable: {
|
|
384
|
+
thread_id: 'test-openai-bedrock-thread',
|
|
385
|
+
},
|
|
386
|
+
streamMode: 'values',
|
|
387
|
+
version: 'v2' as const,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await expect(
|
|
391
|
+
run.processStream({ messages }, config)
|
|
392
|
+
).resolves.not.toThrow();
|
|
393
|
+
|
|
394
|
+
const finalMessages = run.getRunMessages();
|
|
395
|
+
expect(finalMessages).toBeDefined();
|
|
396
|
+
|
|
397
|
+
const toolMessages = finalMessages!.filter(
|
|
398
|
+
(msg) => msg.getType() === 'tool'
|
|
399
|
+
) as ToolMessage[];
|
|
400
|
+
|
|
401
|
+
const handoffMessage = toolMessages.find(
|
|
402
|
+
(msg) => msg.name === `${Constants.LC_TRANSFER_TO_}bedrock_specialist`
|
|
403
|
+
);
|
|
404
|
+
expect(handoffMessage).toBeDefined();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('Multiple Handoffs with Mixed Thinking Configurations', () => {
|
|
409
|
+
it('should handle chain of handoffs with varying thinking configurations', async () => {
|
|
410
|
+
const agents: t.AgentInputs[] = [
|
|
411
|
+
{
|
|
412
|
+
agentId: 'router',
|
|
413
|
+
provider: Providers.OPENAI,
|
|
414
|
+
clientOptions: {
|
|
415
|
+
modelName: 'gpt-4o-mini',
|
|
416
|
+
apiKey: 'test-key',
|
|
417
|
+
},
|
|
418
|
+
instructions: 'You route requests',
|
|
419
|
+
maxContextTokens: 8000,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
agentId: 'processor',
|
|
423
|
+
provider: Providers.ANTHROPIC,
|
|
424
|
+
clientOptions: {
|
|
425
|
+
modelName: 'claude-haiku-4-5',
|
|
426
|
+
apiKey: 'test-key',
|
|
427
|
+
// No thinking
|
|
428
|
+
},
|
|
429
|
+
instructions: 'You process requests',
|
|
430
|
+
maxContextTokens: 8000,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
agentId: 'reviewer',
|
|
434
|
+
provider: Providers.ANTHROPIC,
|
|
435
|
+
clientOptions: {
|
|
436
|
+
modelName: 'claude-3-7-sonnet-20250219',
|
|
437
|
+
apiKey: 'test-key',
|
|
438
|
+
thinking: {
|
|
439
|
+
type: 'enabled',
|
|
440
|
+
budget_tokens: 2000,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
instructions: 'You review with deep thinking',
|
|
444
|
+
maxContextTokens: 8000,
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const edges: t.GraphEdge[] = [
|
|
449
|
+
{
|
|
450
|
+
from: 'router',
|
|
451
|
+
to: 'processor',
|
|
452
|
+
edgeType: 'handoff',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
from: 'processor',
|
|
456
|
+
to: 'reviewer',
|
|
457
|
+
edgeType: 'handoff',
|
|
458
|
+
},
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
462
|
+
|
|
463
|
+
// Verify all agents are created with correct configurations
|
|
464
|
+
const routerContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
465
|
+
'router'
|
|
466
|
+
);
|
|
467
|
+
const processorContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
468
|
+
'processor'
|
|
469
|
+
);
|
|
470
|
+
const reviewerContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
471
|
+
'reviewer'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(routerContext).toBeDefined();
|
|
475
|
+
expect(processorContext).toBeDefined();
|
|
476
|
+
expect(reviewerContext).toBeDefined();
|
|
477
|
+
|
|
478
|
+
// Verify thinking configuration on reviewer
|
|
479
|
+
const reviewerThinking = (
|
|
480
|
+
reviewerContext?.clientOptions as t.AnthropicClientOptions
|
|
481
|
+
).thinking;
|
|
482
|
+
expect(reviewerThinking).toBeDefined();
|
|
483
|
+
expect(reviewerThinking?.type).toBe('enabled');
|
|
484
|
+
|
|
485
|
+
// Verify handoff tools exist
|
|
486
|
+
expect(
|
|
487
|
+
routerContext?.tools?.find(
|
|
488
|
+
(tool) =>
|
|
489
|
+
(tool as { name?: string }).name ===
|
|
490
|
+
`${Constants.LC_TRANSFER_TO_}processor`
|
|
491
|
+
)
|
|
492
|
+
).toBeDefined();
|
|
493
|
+
expect(
|
|
494
|
+
processorContext?.tools?.find(
|
|
495
|
+
(tool) =>
|
|
496
|
+
(tool as { name?: string }).name ===
|
|
497
|
+
`${Constants.LC_TRANSFER_TO_}reviewer`
|
|
498
|
+
)
|
|
499
|
+
).toBeDefined();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe('Edge Cases', () => {
|
|
504
|
+
it('should not modify messages when agent already uses thinking', async () => {
|
|
505
|
+
const agents: t.AgentInputs[] = [
|
|
506
|
+
{
|
|
507
|
+
agentId: 'agent_a',
|
|
508
|
+
provider: Providers.ANTHROPIC,
|
|
509
|
+
clientOptions: {
|
|
510
|
+
modelName: 'claude-3-7-sonnet-20250219',
|
|
511
|
+
apiKey: 'test-key',
|
|
512
|
+
thinking: {
|
|
513
|
+
type: 'enabled',
|
|
514
|
+
budget_tokens: 2000,
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
instructions: 'You are agent A with thinking',
|
|
518
|
+
maxContextTokens: 8000,
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
agentId: 'agent_b',
|
|
522
|
+
provider: Providers.ANTHROPIC,
|
|
523
|
+
clientOptions: {
|
|
524
|
+
modelName: 'claude-3-7-sonnet-20250219',
|
|
525
|
+
apiKey: 'test-key',
|
|
526
|
+
thinking: {
|
|
527
|
+
type: 'enabled',
|
|
528
|
+
budget_tokens: 2000,
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
instructions: 'You are agent B with thinking',
|
|
532
|
+
maxContextTokens: 8000,
|
|
533
|
+
},
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
const edges: t.GraphEdge[] = [
|
|
537
|
+
{
|
|
538
|
+
from: 'agent_a',
|
|
539
|
+
to: 'agent_b',
|
|
540
|
+
edgeType: 'handoff',
|
|
541
|
+
},
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
545
|
+
|
|
546
|
+
run.Graph?.overrideTestModel(['Transferring', 'Received handoff'], 10, [
|
|
547
|
+
{
|
|
548
|
+
id: 'tool_call_1',
|
|
549
|
+
name: `${Constants.LC_TRANSFER_TO_}agent_b`,
|
|
550
|
+
args: {},
|
|
551
|
+
} as ToolCall,
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
const messages = [new HumanMessage('Test message')];
|
|
555
|
+
|
|
556
|
+
const config: Partial<RunnableConfig> & {
|
|
557
|
+
version: 'v1' | 'v2';
|
|
558
|
+
streamMode: string;
|
|
559
|
+
} = {
|
|
560
|
+
configurable: {
|
|
561
|
+
thread_id: 'test-both-thinking-thread',
|
|
562
|
+
},
|
|
563
|
+
streamMode: 'values',
|
|
564
|
+
version: 'v2' as const,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// Should work fine when both agents use thinking
|
|
568
|
+
await expect(
|
|
569
|
+
run.processStream({ messages }, config)
|
|
570
|
+
).resolves.not.toThrow();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should handle empty conversation history', async () => {
|
|
574
|
+
const agents: t.AgentInputs[] = [
|
|
575
|
+
{
|
|
576
|
+
agentId: 'agent_a',
|
|
577
|
+
provider: Providers.OPENAI,
|
|
578
|
+
clientOptions: {
|
|
579
|
+
modelName: 'gpt-4o-mini',
|
|
580
|
+
apiKey: 'test-key',
|
|
581
|
+
},
|
|
582
|
+
instructions: 'You are agent A',
|
|
583
|
+
maxContextTokens: 8000,
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
agentId: 'agent_b',
|
|
587
|
+
provider: Providers.ANTHROPIC,
|
|
588
|
+
clientOptions: {
|
|
589
|
+
modelName: 'claude-3-7-sonnet-20250219',
|
|
590
|
+
apiKey: 'test-key',
|
|
591
|
+
thinking: {
|
|
592
|
+
type: 'enabled',
|
|
593
|
+
budget_tokens: 2000,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
instructions: 'You are agent B',
|
|
597
|
+
maxContextTokens: 8000,
|
|
598
|
+
},
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
const edges: t.GraphEdge[] = [
|
|
602
|
+
{
|
|
603
|
+
from: 'agent_a',
|
|
604
|
+
to: 'agent_b',
|
|
605
|
+
edgeType: 'handoff',
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
610
|
+
|
|
611
|
+
expect(run.Graph).toBeDefined();
|
|
612
|
+
|
|
613
|
+
// Just verify the graph was created correctly
|
|
614
|
+
const agentBContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
615
|
+
'agent_b'
|
|
616
|
+
);
|
|
617
|
+
expect(agentBContext).toBeDefined();
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|
package/src/stream.ts
CHANGED
|
@@ -194,6 +194,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
194
194
|
graph,
|
|
195
195
|
stepKey,
|
|
196
196
|
toolCallChunks: chunk.tool_call_chunks,
|
|
197
|
+
metadata,
|
|
197
198
|
});
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -203,12 +204,16 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
203
204
|
|
|
204
205
|
const message_id = getMessageId(stepKey, graph) ?? '';
|
|
205
206
|
if (message_id) {
|
|
206
|
-
await graph.dispatchRunStep(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
await graph.dispatchRunStep(
|
|
208
|
+
stepKey,
|
|
209
|
+
{
|
|
210
|
+
type: StepTypes.MESSAGE_CREATION,
|
|
211
|
+
message_creation: {
|
|
212
|
+
message_id,
|
|
213
|
+
},
|
|
210
214
|
},
|
|
211
|
-
|
|
215
|
+
metadata
|
|
216
|
+
);
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
const stepId = graph.getStepIdByKey(stepKey);
|
|
@@ -267,12 +272,16 @@ hasToolCallChunks: ${hasToolCallChunks}
|
|
|
267
272
|
agentContext.tokenTypeSwitch = 'content';
|
|
268
273
|
const newStepKey = graph.getStepKey(metadata);
|
|
269
274
|
const message_id = getMessageId(newStepKey, graph) ?? '';
|
|
270
|
-
await graph.dispatchRunStep(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
275
|
+
await graph.dispatchRunStep(
|
|
276
|
+
newStepKey,
|
|
277
|
+
{
|
|
278
|
+
type: StepTypes.MESSAGE_CREATION,
|
|
279
|
+
message_creation: {
|
|
280
|
+
message_id,
|
|
281
|
+
},
|
|
274
282
|
},
|
|
275
|
-
|
|
283
|
+
metadata
|
|
284
|
+
);
|
|
276
285
|
|
|
277
286
|
const newStepId = graph.getStepIdByKey(newStepKey);
|
|
278
287
|
await graph.dispatchMessageDelta(newStepId, {
|