@librechat/agents 3.0.18 → 3.0.19
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 +6 -4
- 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 +179 -6
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +6 -4
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/format.mjs +179 -7
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/types/messages/format.d.ts +14 -0
- package/package.json +2 -1
- package/src/graphs/Graph.ts +8 -4
- package/src/messages/format.ts +231 -6
- package/src/messages/labelContentByAgent.test.ts +887 -0
- package/src/scripts/test-multi-agent-list-handoff.ts +116 -10
- package/src/scripts/test-parallel-agent-labeling.ts +325 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
import { ContentTypes } from '@/common';
|
|
2
|
+
import { labelContentByAgent } from './format';
|
|
3
|
+
import type { MessageContentComplex, ToolCallContent } from '@/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if content is ToolCallContent
|
|
7
|
+
*/
|
|
8
|
+
function isToolCallContent(
|
|
9
|
+
content: MessageContentComplex
|
|
10
|
+
): content is ToolCallContent {
|
|
11
|
+
return content.type === ContentTypes.TOOL_CALL && 'tool_call' in content;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type guard to check if content has text property
|
|
16
|
+
*/
|
|
17
|
+
function hasTextProperty(
|
|
18
|
+
content: MessageContentComplex
|
|
19
|
+
): content is MessageContentComplex & { text: string } {
|
|
20
|
+
return 'text' in content;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('labelContentByAgent', () => {
|
|
24
|
+
describe('Basic functionality', () => {
|
|
25
|
+
it('should return contentParts unchanged when no agentIdMap provided', () => {
|
|
26
|
+
const contentParts: MessageContentComplex[] = [
|
|
27
|
+
{ type: ContentTypes.TEXT, text: 'Hello world' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const result = labelContentByAgent(contentParts, undefined);
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual(contentParts);
|
|
33
|
+
expect(result.length).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return contentParts unchanged when agentIdMap is empty', () => {
|
|
37
|
+
const contentParts: MessageContentComplex[] = [
|
|
38
|
+
{ type: ContentTypes.TEXT, text: 'Hello world' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const result = labelContentByAgent(contentParts, {});
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual(contentParts);
|
|
44
|
+
expect(result.length).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle empty contentParts array', () => {
|
|
48
|
+
const contentParts: MessageContentComplex[] = [];
|
|
49
|
+
const agentIdMap = {};
|
|
50
|
+
|
|
51
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Transfer-based labeling (default)', () => {
|
|
58
|
+
it('should consolidate transferred agent content into transfer tool output', () => {
|
|
59
|
+
const contentParts: MessageContentComplex[] = [
|
|
60
|
+
{ type: ContentTypes.TEXT, text: '' },
|
|
61
|
+
{
|
|
62
|
+
type: ContentTypes.TOOL_CALL,
|
|
63
|
+
tool_call: {
|
|
64
|
+
id: 'call_123',
|
|
65
|
+
name: 'lc_transfer_to_specialist',
|
|
66
|
+
args: '',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{ type: ContentTypes.TEXT, text: 'Specialist response here' },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const agentIdMap = {
|
|
73
|
+
0: 'supervisor',
|
|
74
|
+
1: 'supervisor',
|
|
75
|
+
2: 'specialist',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const agentNames = {
|
|
79
|
+
supervisor: 'Supervisor',
|
|
80
|
+
specialist: 'Specialist Agent',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = labelContentByAgent(contentParts, agentIdMap, agentNames);
|
|
84
|
+
|
|
85
|
+
// Should have 2 items: empty text + modified transfer tool call
|
|
86
|
+
expect(result.length).toBe(2);
|
|
87
|
+
expect(result[0].type).toBe(ContentTypes.TEXT);
|
|
88
|
+
|
|
89
|
+
// The transfer tool call should have consolidated output
|
|
90
|
+
expect(result[1].type).toBe(ContentTypes.TOOL_CALL);
|
|
91
|
+
const toolCallContent = result[1] as ToolCallContent;
|
|
92
|
+
expect(toolCallContent.tool_call?.output).toContain(
|
|
93
|
+
'--- Transfer to Specialist Agent ---'
|
|
94
|
+
);
|
|
95
|
+
expect(toolCallContent.tool_call?.output).toContain('"type":"text"');
|
|
96
|
+
expect(toolCallContent.tool_call?.output).toContain(
|
|
97
|
+
'"text":"Specialist response here"'
|
|
98
|
+
);
|
|
99
|
+
expect(toolCallContent.tool_call?.output).toContain(
|
|
100
|
+
'--- End of Specialist Agent response ---'
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle multiple content types from transferred agent', () => {
|
|
105
|
+
const contentParts: MessageContentComplex[] = [
|
|
106
|
+
{
|
|
107
|
+
type: ContentTypes.TOOL_CALL,
|
|
108
|
+
tool_call: {
|
|
109
|
+
id: 'transfer_1',
|
|
110
|
+
name: 'lc_transfer_to_analyst',
|
|
111
|
+
args: '',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{ type: ContentTypes.THINK, think: 'Analyzing the problem...' },
|
|
115
|
+
{ type: ContentTypes.TEXT, text: 'Here is my analysis' },
|
|
116
|
+
{
|
|
117
|
+
type: ContentTypes.TOOL_CALL,
|
|
118
|
+
tool_call: {
|
|
119
|
+
id: 'tool_1',
|
|
120
|
+
name: 'search',
|
|
121
|
+
args: '{"query":"test"}',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const agentIdMap = {
|
|
127
|
+
0: 'supervisor',
|
|
128
|
+
1: 'analyst',
|
|
129
|
+
2: 'analyst',
|
|
130
|
+
3: 'analyst',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
134
|
+
|
|
135
|
+
expect(result.length).toBe(1);
|
|
136
|
+
expect(isToolCallContent(result[0])).toBe(true);
|
|
137
|
+
if (isToolCallContent(result[0])) {
|
|
138
|
+
expect(result[0].tool_call?.output).toContain('"type":"think"');
|
|
139
|
+
expect(result[0].tool_call?.output).toContain('"type":"text"');
|
|
140
|
+
expect(result[0].tool_call?.output).toContain('"type":"tool_call"');
|
|
141
|
+
expect(result[0].tool_call?.output).toContain(
|
|
142
|
+
'Analyzing the problem...'
|
|
143
|
+
);
|
|
144
|
+
expect(result[0].tool_call?.output).toContain('Here is my analysis');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should use agentId when agentNames not provided', () => {
|
|
149
|
+
const contentParts: MessageContentComplex[] = [
|
|
150
|
+
{
|
|
151
|
+
type: ContentTypes.TOOL_CALL,
|
|
152
|
+
tool_call: {
|
|
153
|
+
id: 'call_1',
|
|
154
|
+
name: 'lc_transfer_to_agent2',
|
|
155
|
+
args: '',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{ type: ContentTypes.TEXT, text: 'Response from agent2' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const agentIdMap = {
|
|
162
|
+
0: 'agent1',
|
|
163
|
+
1: 'agent2',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
167
|
+
|
|
168
|
+
expect(isToolCallContent(result[0])).toBe(true);
|
|
169
|
+
if (isToolCallContent(result[0])) {
|
|
170
|
+
expect(result[0].tool_call?.output).toContain(
|
|
171
|
+
'--- Transfer to agent2 ---'
|
|
172
|
+
);
|
|
173
|
+
expect(result[0].tool_call?.output).toContain('agent2:');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle sequential transfers (agent1 -> agent2 -> agent3)', () => {
|
|
178
|
+
const contentParts: MessageContentComplex[] = [
|
|
179
|
+
{ type: ContentTypes.TEXT, text: 'Starting' },
|
|
180
|
+
{
|
|
181
|
+
type: ContentTypes.TOOL_CALL,
|
|
182
|
+
tool_call: {
|
|
183
|
+
id: 'transfer_1',
|
|
184
|
+
name: 'lc_transfer_to_agent2',
|
|
185
|
+
args: '',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{ type: ContentTypes.TEXT, text: 'Agent2 response' },
|
|
189
|
+
{
|
|
190
|
+
type: ContentTypes.TOOL_CALL,
|
|
191
|
+
tool_call: {
|
|
192
|
+
id: 'transfer_2',
|
|
193
|
+
name: 'lc_transfer_to_agent3',
|
|
194
|
+
args: '',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{ type: ContentTypes.TEXT, text: 'Agent3 final response' },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const agentIdMap = {
|
|
201
|
+
0: 'agent1',
|
|
202
|
+
1: 'agent1',
|
|
203
|
+
2: 'agent2',
|
|
204
|
+
3: 'agent2',
|
|
205
|
+
4: 'agent3',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
209
|
+
|
|
210
|
+
expect(result.length).toBe(3);
|
|
211
|
+
expect(result[0].type).toBe(ContentTypes.TEXT);
|
|
212
|
+
expect(result[1].type).toBe(ContentTypes.TOOL_CALL);
|
|
213
|
+
expect(result[2].type).toBe(ContentTypes.TOOL_CALL);
|
|
214
|
+
|
|
215
|
+
// First transfer should have agent2 content
|
|
216
|
+
if (isToolCallContent(result[1])) {
|
|
217
|
+
expect(result[1].tool_call?.output).toContain('agent2');
|
|
218
|
+
expect(result[1].tool_call?.output).toContain('Agent2 response');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Second transfer should have agent3 content
|
|
222
|
+
if (isToolCallContent(result[2])) {
|
|
223
|
+
expect(result[2].tool_call?.output).toContain('agent3');
|
|
224
|
+
expect(result[2].tool_call?.output).toContain('Agent3 final response');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Full agent labeling (labelNonTransferContent: true)', () => {
|
|
230
|
+
it('should label all agent content when labelNonTransferContent is true', () => {
|
|
231
|
+
const contentParts: MessageContentComplex[] = [
|
|
232
|
+
{ type: ContentTypes.TEXT, text: 'Researcher coordinating' },
|
|
233
|
+
{ type: ContentTypes.TEXT, text: 'FINANCIAL ANALYSIS: Revenue impact' },
|
|
234
|
+
{
|
|
235
|
+
type: ContentTypes.TEXT,
|
|
236
|
+
text: 'TECHNICAL ANALYSIS: System requirements',
|
|
237
|
+
},
|
|
238
|
+
{ type: ContentTypes.TEXT, text: 'Summary of all analyses' },
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const agentIdMap = {
|
|
242
|
+
0: 'researcher',
|
|
243
|
+
1: 'analyst1',
|
|
244
|
+
2: 'analyst2',
|
|
245
|
+
3: 'summarizer',
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const agentNames = {
|
|
249
|
+
researcher: 'Research Coordinator',
|
|
250
|
+
analyst1: 'Financial Analyst',
|
|
251
|
+
analyst2: 'Technical Analyst',
|
|
252
|
+
summarizer: 'Synthesis Expert',
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const result = labelContentByAgent(contentParts, agentIdMap, agentNames, {
|
|
256
|
+
labelNonTransferContent: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Should create 4 labeled groups
|
|
260
|
+
expect(result.length).toBe(4);
|
|
261
|
+
|
|
262
|
+
// Each should be a text content part with agent labels
|
|
263
|
+
expect(result[0].type).toBe(ContentTypes.TEXT);
|
|
264
|
+
if (hasTextProperty(result[0])) {
|
|
265
|
+
expect(result[0].text).toContain('--- Research Coordinator ---');
|
|
266
|
+
expect(result[0].text).toContain(
|
|
267
|
+
'Research Coordinator: Researcher coordinating'
|
|
268
|
+
);
|
|
269
|
+
expect(result[0].text).toContain('--- End of Research Coordinator ---');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
expect(result[1].type).toBe(ContentTypes.TEXT);
|
|
273
|
+
if (hasTextProperty(result[1])) {
|
|
274
|
+
expect(result[1].text).toContain('--- Financial Analyst ---');
|
|
275
|
+
expect(result[1].text).toContain(
|
|
276
|
+
'Financial Analyst: FINANCIAL ANALYSIS: Revenue impact'
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
expect(result[2].type).toBe(ContentTypes.TEXT);
|
|
281
|
+
if (hasTextProperty(result[2])) {
|
|
282
|
+
expect(result[2].text).toContain('--- Technical Analyst ---');
|
|
283
|
+
expect(result[2].text).toContain(
|
|
284
|
+
'Technical Analyst: TECHNICAL ANALYSIS: System requirements'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
expect(result[3].type).toBe(ContentTypes.TEXT);
|
|
289
|
+
if (hasTextProperty(result[3])) {
|
|
290
|
+
expect(result[3].text).toContain('--- Synthesis Expert ---');
|
|
291
|
+
expect(result[3].text).toContain(
|
|
292
|
+
'Synthesis Expert: Summary of all analyses'
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should group consecutive content from same agent', () => {
|
|
298
|
+
const contentParts: MessageContentComplex[] = [
|
|
299
|
+
{ type: ContentTypes.TEXT, text: 'First message' },
|
|
300
|
+
{ type: ContentTypes.TEXT, text: 'Second message' },
|
|
301
|
+
{ type: ContentTypes.TEXT, text: 'Third message' },
|
|
302
|
+
{ type: ContentTypes.TEXT, text: 'Different agent' },
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const agentIdMap = {
|
|
306
|
+
0: 'agent1',
|
|
307
|
+
1: 'agent1',
|
|
308
|
+
2: 'agent1',
|
|
309
|
+
3: 'agent2',
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
313
|
+
labelNonTransferContent: true,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Should create 2 groups
|
|
317
|
+
expect(result.length).toBe(2);
|
|
318
|
+
|
|
319
|
+
// First group has all 3 messages from agent1
|
|
320
|
+
if (hasTextProperty(result[0])) {
|
|
321
|
+
expect(result[0].text).toContain('agent1: First message');
|
|
322
|
+
expect(result[0].text).toContain('agent1: Second message');
|
|
323
|
+
expect(result[0].text).toContain('agent1: Third message');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Second group has agent2 message
|
|
327
|
+
if (hasTextProperty(result[1])) {
|
|
328
|
+
expect(result[1].text).toContain('agent2: Different agent');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should handle thinking content in parallel labeling', () => {
|
|
333
|
+
const contentParts: MessageContentComplex[] = [
|
|
334
|
+
{ type: ContentTypes.THINK, think: 'Let me analyze this...' },
|
|
335
|
+
{ type: ContentTypes.TEXT, text: 'My conclusion' },
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const agentIdMap = {
|
|
339
|
+
0: 'analyst',
|
|
340
|
+
1: 'analyst',
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const agentNames = {
|
|
344
|
+
analyst: 'Expert Analyst',
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const result = labelContentByAgent(contentParts, agentIdMap, agentNames, {
|
|
348
|
+
labelNonTransferContent: true,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(result.length).toBe(1);
|
|
352
|
+
if (hasTextProperty(result[0])) {
|
|
353
|
+
expect(result[0].text).toContain('--- Expert Analyst ---');
|
|
354
|
+
expect(result[0].text).toContain('"type":"think"');
|
|
355
|
+
expect(result[0].text).toContain('Let me analyze this...');
|
|
356
|
+
expect(result[0].text).toContain('Expert Analyst: My conclusion');
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should skip empty text content in parallel labeling', () => {
|
|
361
|
+
const contentParts: MessageContentComplex[] = [
|
|
362
|
+
{ type: ContentTypes.TEXT, text: '' },
|
|
363
|
+
{ type: ContentTypes.TEXT, text: 'Valid content' },
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
const agentIdMap = {
|
|
367
|
+
0: 'agent1',
|
|
368
|
+
1: 'agent1',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
372
|
+
labelNonTransferContent: true,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(result.length).toBe(1);
|
|
376
|
+
// Should only contain the valid content, not the empty string
|
|
377
|
+
if (hasTextProperty(result[0])) {
|
|
378
|
+
expect(result[0].text).toContain('agent1: Valid content');
|
|
379
|
+
expect(result[0].text).not.toContain('agent1: \n');
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('Edge cases', () => {
|
|
385
|
+
it('should handle content parts without agentId in map', () => {
|
|
386
|
+
const contentParts: MessageContentComplex[] = [
|
|
387
|
+
{ type: ContentTypes.TEXT, text: 'Message 1' },
|
|
388
|
+
{ type: ContentTypes.TEXT, text: 'Message 2' },
|
|
389
|
+
{ type: ContentTypes.TEXT, text: 'Message 3' },
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const agentIdMap = {
|
|
393
|
+
0: 'agent1',
|
|
394
|
+
// Missing index 1
|
|
395
|
+
2: 'agent2',
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
399
|
+
labelNonTransferContent: true,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Should still process and group by available agent IDs
|
|
403
|
+
expect(result.length).toBeGreaterThan(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should handle transfer tool without subsequent agent content', () => {
|
|
407
|
+
const contentParts: MessageContentComplex[] = [
|
|
408
|
+
{
|
|
409
|
+
type: ContentTypes.TOOL_CALL,
|
|
410
|
+
tool_call: {
|
|
411
|
+
id: 'transfer_1',
|
|
412
|
+
name: 'lc_transfer_to_specialist',
|
|
413
|
+
args: '',
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
const agentIdMap = {
|
|
419
|
+
0: 'supervisor',
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
423
|
+
|
|
424
|
+
// Transfer tool should still be present, just without added content
|
|
425
|
+
expect(result.length).toBe(1);
|
|
426
|
+
expect(result[0].type).toBe(ContentTypes.TOOL_CALL);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should handle multiple transfers in sequence', () => {
|
|
430
|
+
const contentParts: MessageContentComplex[] = [
|
|
431
|
+
{
|
|
432
|
+
type: ContentTypes.TOOL_CALL,
|
|
433
|
+
tool_call: {
|
|
434
|
+
id: 'transfer_1',
|
|
435
|
+
name: 'lc_transfer_to_agent_a',
|
|
436
|
+
args: '',
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
{ type: ContentTypes.TEXT, text: 'Agent A response' },
|
|
440
|
+
{
|
|
441
|
+
type: ContentTypes.TOOL_CALL,
|
|
442
|
+
tool_call: {
|
|
443
|
+
id: 'transfer_2',
|
|
444
|
+
name: 'lc_transfer_to_agent_b',
|
|
445
|
+
args: '',
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{ type: ContentTypes.TEXT, text: 'Agent B response' },
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const agentIdMap = {
|
|
452
|
+
0: 'supervisor',
|
|
453
|
+
1: 'agent_a',
|
|
454
|
+
2: 'agent_a',
|
|
455
|
+
3: 'agent_b',
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
459
|
+
|
|
460
|
+
expect(result.length).toBe(2);
|
|
461
|
+
|
|
462
|
+
// Both transfers should have consolidated outputs
|
|
463
|
+
if (isToolCallContent(result[0])) {
|
|
464
|
+
expect(result[0].tool_call?.output).toContain('agent_a');
|
|
465
|
+
expect(result[0].tool_call?.output).toContain('Agent A response');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (isToolCallContent(result[1])) {
|
|
469
|
+
expect(result[1].tool_call?.output).toContain('agent_b');
|
|
470
|
+
expect(result[1].tool_call?.output).toContain('Agent B response');
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should preserve non-transfer tool calls unchanged', () => {
|
|
475
|
+
const contentParts: MessageContentComplex[] = [
|
|
476
|
+
{ type: ContentTypes.TEXT, text: 'Using a tool' },
|
|
477
|
+
{
|
|
478
|
+
type: ContentTypes.TOOL_CALL,
|
|
479
|
+
tool_call: {
|
|
480
|
+
id: 'tool_1',
|
|
481
|
+
name: 'search',
|
|
482
|
+
args: '{"query":"test"}',
|
|
483
|
+
output: 'Search results',
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
{ type: ContentTypes.TEXT, text: 'Here are the results' },
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
const agentIdMap = {
|
|
490
|
+
0: 'agent1',
|
|
491
|
+
1: 'agent1',
|
|
492
|
+
2: 'agent1',
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
496
|
+
|
|
497
|
+
// All content from same agent with no transfers, should pass through
|
|
498
|
+
expect(result).toEqual(contentParts);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe('Parallel patterns', () => {
|
|
503
|
+
it('should label parallel analyst contributions separately', () => {
|
|
504
|
+
const contentParts: MessageContentComplex[] = [
|
|
505
|
+
{ type: ContentTypes.TEXT, text: 'Coordinating research' },
|
|
506
|
+
{ type: ContentTypes.TEXT, text: 'FINANCIAL: Budget analysis' },
|
|
507
|
+
{ type: ContentTypes.TEXT, text: 'TECHNICAL: System design' },
|
|
508
|
+
{ type: ContentTypes.TEXT, text: 'MARKET: Competitive landscape' },
|
|
509
|
+
{ type: ContentTypes.TEXT, text: 'Integrated summary' },
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
const agentIdMap = {
|
|
513
|
+
0: 'researcher',
|
|
514
|
+
1: 'financial_analyst',
|
|
515
|
+
2: 'technical_analyst',
|
|
516
|
+
3: 'market_analyst',
|
|
517
|
+
4: 'summarizer',
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const agentNames = {
|
|
521
|
+
researcher: 'Research Coordinator',
|
|
522
|
+
financial_analyst: 'Financial Analyst',
|
|
523
|
+
technical_analyst: 'Technical Analyst',
|
|
524
|
+
market_analyst: 'Market Analyst',
|
|
525
|
+
summarizer: 'Synthesis Expert',
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const result = labelContentByAgent(contentParts, agentIdMap, agentNames, {
|
|
529
|
+
labelNonTransferContent: true,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Should have 5 labeled groups (one per agent)
|
|
533
|
+
expect(result.length).toBe(5);
|
|
534
|
+
|
|
535
|
+
// Verify each group
|
|
536
|
+
if (hasTextProperty(result[0])) {
|
|
537
|
+
expect(result[0].text).toContain('--- Research Coordinator ---');
|
|
538
|
+
expect(result[0].text).toContain('Coordinating research');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (hasTextProperty(result[1])) {
|
|
542
|
+
expect(result[1].text).toContain('--- Financial Analyst ---');
|
|
543
|
+
expect(result[1].text).toContain('FINANCIAL: Budget analysis');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (hasTextProperty(result[2])) {
|
|
547
|
+
expect(result[2].text).toContain('--- Technical Analyst ---');
|
|
548
|
+
expect(result[2].text).toContain('TECHNICAL: System design');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (hasTextProperty(result[3])) {
|
|
552
|
+
expect(result[3].text).toContain('--- Market Analyst ---');
|
|
553
|
+
expect(result[3].text).toContain('MARKET: Competitive landscape');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (hasTextProperty(result[4])) {
|
|
557
|
+
expect(result[4].text).toContain('--- Synthesis Expert ---');
|
|
558
|
+
expect(result[4].text).toContain('Integrated summary');
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should handle agent alternation in parallel mode', () => {
|
|
563
|
+
const contentParts: MessageContentComplex[] = [
|
|
564
|
+
{ type: ContentTypes.TEXT, text: 'A1' },
|
|
565
|
+
{ type: ContentTypes.TEXT, text: 'B1' },
|
|
566
|
+
{ type: ContentTypes.TEXT, text: 'A2' },
|
|
567
|
+
{ type: ContentTypes.TEXT, text: 'B2' },
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
const agentIdMap = {
|
|
571
|
+
0: 'agentA',
|
|
572
|
+
1: 'agentB',
|
|
573
|
+
2: 'agentA',
|
|
574
|
+
3: 'agentB',
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
578
|
+
labelNonTransferContent: true,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Should create 4 groups (alternating agents)
|
|
582
|
+
expect(result.length).toBe(4);
|
|
583
|
+
|
|
584
|
+
if (hasTextProperty(result[0]))
|
|
585
|
+
expect(result[0].text).toContain('agentA: A1');
|
|
586
|
+
if (hasTextProperty(result[1]))
|
|
587
|
+
expect(result[1].text).toContain('agentB: B1');
|
|
588
|
+
if (hasTextProperty(result[2]))
|
|
589
|
+
expect(result[2].text).toContain('agentA: A2');
|
|
590
|
+
if (hasTextProperty(result[3]))
|
|
591
|
+
expect(result[3].text).toContain('agentB: B2');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should handle tool calls in parallel labeling', () => {
|
|
595
|
+
const contentParts: MessageContentComplex[] = [
|
|
596
|
+
{ type: ContentTypes.TEXT, text: 'Analyzing' },
|
|
597
|
+
{
|
|
598
|
+
type: ContentTypes.TOOL_CALL,
|
|
599
|
+
tool_call: {
|
|
600
|
+
id: 'tool_1',
|
|
601
|
+
name: 'search',
|
|
602
|
+
args: '{"query":"data"}',
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
{ type: ContentTypes.TEXT, text: 'Results analyzed' },
|
|
606
|
+
];
|
|
607
|
+
|
|
608
|
+
const agentIdMap = {
|
|
609
|
+
0: 'analyst',
|
|
610
|
+
1: 'analyst',
|
|
611
|
+
2: 'analyst',
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
615
|
+
labelNonTransferContent: true,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
expect(result.length).toBe(1);
|
|
619
|
+
if (hasTextProperty(result[0])) {
|
|
620
|
+
expect(result[0].text).toContain('--- analyst ---');
|
|
621
|
+
expect(result[0].text).toContain('analyst: Analyzing');
|
|
622
|
+
expect(result[0].text).toContain('"type":"tool_call"');
|
|
623
|
+
expect(result[0].text).toContain('"name":"search"');
|
|
624
|
+
expect(result[0].text).toContain('analyst: Results analyzed');
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe('Mixed patterns', () => {
|
|
630
|
+
it('should handle content with no transfer but mixed agents (without option)', () => {
|
|
631
|
+
const contentParts: MessageContentComplex[] = [
|
|
632
|
+
{ type: ContentTypes.TEXT, text: 'Agent 1 says this' },
|
|
633
|
+
{ type: ContentTypes.TEXT, text: 'Agent 2 says this' },
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
const agentIdMap = {
|
|
637
|
+
0: 'agent1',
|
|
638
|
+
1: 'agent2',
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Default mode (transfer-based) should pass through non-transfer content
|
|
642
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
643
|
+
|
|
644
|
+
expect(result).toEqual(contentParts);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should handle hybrid: transfer followed by non-transfer agents', () => {
|
|
648
|
+
const contentParts: MessageContentComplex[] = [
|
|
649
|
+
{
|
|
650
|
+
type: ContentTypes.TOOL_CALL,
|
|
651
|
+
tool_call: {
|
|
652
|
+
id: 'transfer_1',
|
|
653
|
+
name: 'lc_transfer_to_specialist',
|
|
654
|
+
args: '',
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
{ type: ContentTypes.TEXT, text: 'Specialist work' },
|
|
658
|
+
{ type: ContentTypes.TEXT, text: 'Another agent response' },
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
const agentIdMap = {
|
|
662
|
+
0: 'supervisor',
|
|
663
|
+
1: 'specialist',
|
|
664
|
+
2: 'agent3',
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const result = labelContentByAgent(contentParts, agentIdMap);
|
|
668
|
+
|
|
669
|
+
expect(result.length).toBe(2);
|
|
670
|
+
|
|
671
|
+
// Transfer tool should have specialist content
|
|
672
|
+
if (isToolCallContent(result[0])) {
|
|
673
|
+
expect(result[0].tool_call?.output).toContain('specialist');
|
|
674
|
+
expect(result[0].tool_call?.output).toContain('Specialist work');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Non-transfer content should pass through
|
|
678
|
+
expect(result[1]).toEqual(contentParts[2]);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe('Real-world scenarios', () => {
|
|
683
|
+
it('should handle supervisor -> legal_advisor handoff pattern', () => {
|
|
684
|
+
const contentParts: MessageContentComplex[] = [
|
|
685
|
+
{ type: ContentTypes.TEXT, text: '' },
|
|
686
|
+
{
|
|
687
|
+
type: ContentTypes.TOOL_CALL,
|
|
688
|
+
tool_call: {
|
|
689
|
+
id: 'call_legal',
|
|
690
|
+
name: 'lc_transfer_to_legal_advisor',
|
|
691
|
+
args: '',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
type: ContentTypes.TEXT,
|
|
696
|
+
text: 'GPL licensing creates obligations...',
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
|
|
700
|
+
const agentIdMap = {
|
|
701
|
+
0: 'supervisor',
|
|
702
|
+
1: 'supervisor',
|
|
703
|
+
2: 'legal_advisor',
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const agentNames = {
|
|
707
|
+
supervisor: 'Supervisor',
|
|
708
|
+
legal_advisor: 'Legal Advisor',
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const result = labelContentByAgent(contentParts, agentIdMap, agentNames);
|
|
712
|
+
|
|
713
|
+
expect(result.length).toBe(2);
|
|
714
|
+
if (isToolCallContent(result[1])) {
|
|
715
|
+
expect(result[1].tool_call?.output).toContain(
|
|
716
|
+
'--- Transfer to Legal Advisor ---'
|
|
717
|
+
);
|
|
718
|
+
expect(result[1].tool_call?.output).toContain('"type":"text"');
|
|
719
|
+
expect(result[1].tool_call?.output).toContain(
|
|
720
|
+
'"text":"GPL licensing creates obligations..."'
|
|
721
|
+
);
|
|
722
|
+
expect(result[1].tool_call?.output).toContain(
|
|
723
|
+
'--- End of Legal Advisor response ---'
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should handle fan-out to 3 analysts then fan-in to summarizer', () => {
|
|
729
|
+
const contentParts: MessageContentComplex[] = [
|
|
730
|
+
{ type: ContentTypes.TEXT, text: 'Coordination brief' },
|
|
731
|
+
{ type: ContentTypes.TEXT, text: 'Financial analysis content' },
|
|
732
|
+
{ type: ContentTypes.TEXT, text: 'Technical analysis content' },
|
|
733
|
+
{ type: ContentTypes.TEXT, text: 'Market analysis content' },
|
|
734
|
+
{ type: ContentTypes.TEXT, text: 'Executive summary' },
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
const agentIdMap = {
|
|
738
|
+
0: 'researcher',
|
|
739
|
+
1: 'analyst1',
|
|
740
|
+
2: 'analyst2',
|
|
741
|
+
3: 'analyst3',
|
|
742
|
+
4: 'summarizer',
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
746
|
+
labelNonTransferContent: true,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(result.length).toBe(5);
|
|
750
|
+
|
|
751
|
+
// Each analyst's work should be clearly separated
|
|
752
|
+
if (hasTextProperty(result[0]))
|
|
753
|
+
expect(result[0].text).toContain('researcher:');
|
|
754
|
+
if (hasTextProperty(result[1]))
|
|
755
|
+
expect(result[1].text).toContain('analyst1:');
|
|
756
|
+
if (hasTextProperty(result[2]))
|
|
757
|
+
expect(result[2].text).toContain('analyst2:');
|
|
758
|
+
if (hasTextProperty(result[3]))
|
|
759
|
+
expect(result[3].text).toContain('analyst3:');
|
|
760
|
+
if (hasTextProperty(result[4]))
|
|
761
|
+
expect(result[4].text).toContain('summarizer:');
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
describe('Performance and edge cases', () => {
|
|
766
|
+
it('should handle large number of content parts efficiently', () => {
|
|
767
|
+
const contentParts: MessageContentComplex[] = [];
|
|
768
|
+
const agentIdMap: Record<number, string> = {};
|
|
769
|
+
|
|
770
|
+
// Create 100 content parts from 10 different agents
|
|
771
|
+
for (let i = 0; i < 100; i++) {
|
|
772
|
+
contentParts.push({
|
|
773
|
+
type: ContentTypes.TEXT,
|
|
774
|
+
text: `Message ${i}`,
|
|
775
|
+
});
|
|
776
|
+
agentIdMap[i] = `agent${i % 10}`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
780
|
+
labelNonTransferContent: true,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Should complete without errors
|
|
784
|
+
expect(result.length).toBeGreaterThan(0);
|
|
785
|
+
expect(result.length).toBeLessThanOrEqual(100);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('should handle all content from single agent', () => {
|
|
789
|
+
const contentParts: MessageContentComplex[] = [
|
|
790
|
+
{ type: ContentTypes.TEXT, text: 'Part 1' },
|
|
791
|
+
{ type: ContentTypes.TEXT, text: 'Part 2' },
|
|
792
|
+
{ type: ContentTypes.TEXT, text: 'Part 3' },
|
|
793
|
+
];
|
|
794
|
+
|
|
795
|
+
const agentIdMap = {
|
|
796
|
+
0: 'single_agent',
|
|
797
|
+
1: 'single_agent',
|
|
798
|
+
2: 'single_agent',
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
802
|
+
labelNonTransferContent: true,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Should create 1 group with all content
|
|
806
|
+
expect(result.length).toBe(1);
|
|
807
|
+
if (hasTextProperty(result[0])) {
|
|
808
|
+
expect(result[0].text).toContain('single_agent: Part 1');
|
|
809
|
+
expect(result[0].text).toContain('single_agent: Part 2');
|
|
810
|
+
expect(result[0].text).toContain('single_agent: Part 3');
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
describe('Content type handling', () => {
|
|
816
|
+
it('should properly format thinking content with JSON', () => {
|
|
817
|
+
const contentParts: MessageContentComplex[] = [
|
|
818
|
+
{
|
|
819
|
+
type: ContentTypes.THINK,
|
|
820
|
+
think: 'I need to consider multiple factors...',
|
|
821
|
+
},
|
|
822
|
+
];
|
|
823
|
+
|
|
824
|
+
const agentIdMap = { 0: 'analyst' };
|
|
825
|
+
|
|
826
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
827
|
+
labelNonTransferContent: true,
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
if (hasTextProperty(result[0])) {
|
|
831
|
+
const parsed = JSON.parse(
|
|
832
|
+
result[0].text.split('analyst: ')[1].split('\n')[0]
|
|
833
|
+
);
|
|
834
|
+
expect(parsed.type).toBe('think');
|
|
835
|
+
expect(parsed.think).toBe('I need to consider multiple factors...');
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('should properly format tool call content with JSON', () => {
|
|
840
|
+
const contentParts: MessageContentComplex[] = [
|
|
841
|
+
{
|
|
842
|
+
type: ContentTypes.TOOL_CALL,
|
|
843
|
+
tool_call: {
|
|
844
|
+
id: 'tool_123',
|
|
845
|
+
name: 'calculator',
|
|
846
|
+
args: { expression: '2+2' },
|
|
847
|
+
output: '4',
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
const agentIdMap = { 0: 'agent1' };
|
|
853
|
+
|
|
854
|
+
const result = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
855
|
+
labelNonTransferContent: true,
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
if (hasTextProperty(result[0])) {
|
|
859
|
+
expect(result[0].text).toContain('"type":"tool_call"');
|
|
860
|
+
expect(result[0].text).toContain('"name":"calculator"');
|
|
861
|
+
expect(result[0].text).toContain('"id":"tool_123"');
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe('Integration scenarios', () => {
|
|
867
|
+
it('should work with formatAgentMessages pipeline', () => {
|
|
868
|
+
const contentParts: MessageContentComplex[] = [
|
|
869
|
+
{ type: ContentTypes.TEXT, text: 'Agent 1 content' },
|
|
870
|
+
{ type: ContentTypes.TEXT, text: 'Agent 2 content' },
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
const agentIdMap = {
|
|
874
|
+
0: 'agent1',
|
|
875
|
+
1: 'agent2',
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const labeled = labelContentByAgent(contentParts, agentIdMap, undefined, {
|
|
879
|
+
labelNonTransferContent: true,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// Labeled content should be valid for formatAgentMessages
|
|
883
|
+
expect(labeled.length).toBeGreaterThan(0);
|
|
884
|
+
expect(labeled.every((part) => part.type != null)).toBe(true);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
});
|