@librechat/agents 3.0.18 → 3.0.20

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.
@@ -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
+ });