@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.
@@ -9,6 +9,9 @@ type MessageWithContent = {
9
9
 
10
10
  /**
11
11
  * Anthropic API: Adds cache control to the appropriate user messages in the payload.
12
+ * Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
13
+ * then adds fresh cache control to the last 2 user messages in a single backward pass.
14
+ * This ensures we don't accumulate stale cache points across multiple turns.
12
15
  * @param messages - The array of message objects.
13
16
  * @returns - The updated array of message objects with cache control added.
14
17
  */
@@ -22,15 +25,26 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
22
25
  const updatedMessages = [...messages];
23
26
  let userMessagesModified = 0;
24
27
 
25
- for (
26
- let i = updatedMessages.length - 1;
27
- i >= 0 && userMessagesModified < 2;
28
- i--
29
- ) {
28
+ for (let i = updatedMessages.length - 1; i >= 0; i--) {
30
29
  const message = updatedMessages[i];
31
- if ('getType' in message && message.getType() !== 'human') {
32
- continue;
33
- } else if ('role' in message && message.role !== 'user') {
30
+ const isUserMessage =
31
+ ('getType' in message && message.getType() === 'human') ||
32
+ ('role' in message && message.role === 'user');
33
+
34
+ if (Array.isArray(message.content)) {
35
+ message.content = message.content.filter(
36
+ (block) => !isCachePoint(block as MessageContentComplex)
37
+ ) as typeof message.content;
38
+
39
+ for (let j = 0; j < message.content.length; j++) {
40
+ const block = message.content[j] as Record<string, unknown>;
41
+ if ('cache_control' in block) {
42
+ delete block.cache_control;
43
+ }
44
+ }
45
+ }
46
+
47
+ if (userMessagesModified >= 2 || !isUserMessage) {
34
48
  continue;
35
49
  }
36
50
 
@@ -60,10 +74,77 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
60
74
  return updatedMessages;
61
75
  }
62
76
 
77
+ /**
78
+ * Checks if a content block is a cache point
79
+ */
80
+ function isCachePoint(block: MessageContentComplex): boolean {
81
+ return 'cachePoint' in block && !('type' in block);
82
+ }
83
+
84
+ /**
85
+ * Removes all Anthropic cache_control fields from messages
86
+ * Used when switching from Anthropic to Bedrock provider
87
+ */
88
+ export function stripAnthropicCacheControl<T extends MessageWithContent>(
89
+ messages: T[]
90
+ ): T[] {
91
+ if (!Array.isArray(messages)) {
92
+ return messages;
93
+ }
94
+
95
+ const updatedMessages = [...messages];
96
+
97
+ for (let i = 0; i < updatedMessages.length; i++) {
98
+ const message = updatedMessages[i];
99
+ const content = message.content;
100
+
101
+ if (Array.isArray(content)) {
102
+ for (let j = 0; j < content.length; j++) {
103
+ const block = content[j] as Record<string, unknown>;
104
+ if ('cache_control' in block) {
105
+ delete block.cache_control;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ return updatedMessages;
112
+ }
113
+
114
+ /**
115
+ * Removes all Bedrock cachePoint blocks from messages
116
+ * Used when switching from Bedrock to Anthropic provider
117
+ */
118
+ export function stripBedrockCacheControl<T extends MessageWithContent>(
119
+ messages: T[]
120
+ ): T[] {
121
+ if (!Array.isArray(messages)) {
122
+ return messages;
123
+ }
124
+
125
+ const updatedMessages = [...messages];
126
+
127
+ for (let i = 0; i < updatedMessages.length; i++) {
128
+ const message = updatedMessages[i];
129
+ const content = message.content;
130
+
131
+ if (Array.isArray(content)) {
132
+ message.content = content.filter(
133
+ (block) => !isCachePoint(block as MessageContentComplex)
134
+ ) as typeof content;
135
+ }
136
+ }
137
+
138
+ return updatedMessages;
139
+ }
140
+
63
141
  /**
64
142
  * Adds Bedrock Converse API cache points to the last two messages.
65
143
  * Inserts `{ cachePoint: { type: 'default' } }` as a separate content block
66
144
  * immediately after the last text block in each targeted message.
145
+ * Strips ALL existing cache control (both Bedrock and Anthropic formats) from all messages,
146
+ * then adds fresh cache points to the last 2 messages in a single backward pass.
147
+ * This ensures we don't accumulate stale cache points across multiple turns.
67
148
  * @param messages - The array of message objects.
68
149
  * @returns - The updated array of message objects with cache points added.
69
150
  */
@@ -77,23 +158,32 @@ export function addBedrockCacheControl<
77
158
  const updatedMessages: T[] = messages.slice();
78
159
  let messagesModified = 0;
79
160
 
80
- for (
81
- let i = updatedMessages.length - 1;
82
- i >= 0 && messagesModified < 2;
83
- i--
84
- ) {
161
+ for (let i = updatedMessages.length - 1; i >= 0; i--) {
85
162
  const message = updatedMessages[i];
86
-
87
- if (
163
+ const isToolMessage =
88
164
  'getType' in message &&
89
165
  typeof message.getType === 'function' &&
90
- message.getType() === 'tool'
91
- ) {
92
- continue;
93
- }
166
+ message.getType() === 'tool';
94
167
 
95
168
  const content = message.content;
96
169
 
170
+ if (Array.isArray(content)) {
171
+ message.content = content.filter(
172
+ (block) => !isCachePoint(block)
173
+ ) as typeof content;
174
+
175
+ for (let j = 0; j < message.content.length; j++) {
176
+ const block = message.content[j] as Record<string, unknown>;
177
+ if ('cache_control' in block) {
178
+ delete block.cache_control;
179
+ }
180
+ }
181
+ }
182
+
183
+ if (messagesModified >= 2 || isToolMessage) {
184
+ continue;
185
+ }
186
+
97
187
  if (typeof content === 'string' && content === '') {
98
188
  continue;
99
189
  }
@@ -107,9 +197,9 @@ export function addBedrockCacheControl<
107
197
  continue;
108
198
  }
109
199
 
110
- if (Array.isArray(content)) {
200
+ if (Array.isArray(message.content)) {
111
201
  let hasCacheableContent = false;
112
- for (const block of content) {
202
+ for (const block of message.content) {
113
203
  if (block.type === ContentTypes.TEXT) {
114
204
  if (typeof block.text === 'string' && block.text !== '') {
115
205
  hasCacheableContent = true;
@@ -123,15 +213,15 @@ export function addBedrockCacheControl<
123
213
  }
124
214
 
125
215
  let inserted = false;
126
- for (let j = content.length - 1; j >= 0; j--) {
127
- const block = content[j] as MessageContentComplex;
216
+ for (let j = message.content.length - 1; j >= 0; j--) {
217
+ const block = message.content[j] as MessageContentComplex;
128
218
  const type = (block as { type?: string }).type;
129
219
  if (type === ContentTypes.TEXT || type === 'text') {
130
220
  const text = (block as { text?: string }).text;
131
221
  if (text === '' || text === undefined) {
132
222
  continue;
133
223
  }
134
- content.splice(j + 1, 0, {
224
+ message.content.splice(j + 1, 0, {
135
225
  cachePoint: { type: 'default' },
136
226
  } as MessageContentComplex);
137
227
  inserted = true;
@@ -139,7 +229,7 @@ export function addBedrockCacheControl<
139
229
  }
140
230
  }
141
231
  if (!inserted) {
142
- content.push({
232
+ message.content.push({
143
233
  cachePoint: { type: 'default' },
144
234
  } as MessageContentComplex);
145
235
  }
@@ -13,6 +13,8 @@ import type { ToolCall } from '@langchain/core/messages/tool';
13
13
  import type {
14
14
  ExtendedMessageContent,
15
15
  MessageContentComplex,
16
+ ReasoningContentText,
17
+ ToolCallContent,
16
18
  ToolCallPart,
17
19
  TPayload,
18
20
  TMessage,
@@ -308,12 +310,12 @@ function formatAssistantMessage(
308
310
  }
309
311
  // Create a new AIMessage with this text and prepare for tool calls
310
312
  lastAIMessage = new AIMessage({
311
- content: part.text || '',
313
+ content: part.text != null ? part.text : '',
312
314
  });
313
315
  formattedMessages.push(lastAIMessage);
314
316
  } else if (part.type === ContentTypes.TOOL_CALL) {
315
317
  // Skip malformed tool call entries without tool_call property
316
- if (!part.tool_call) {
318
+ if (part.tool_call == null) {
317
319
  continue;
318
320
  }
319
321
 
@@ -325,7 +327,10 @@ function formatAssistantMessage(
325
327
  } = part.tool_call as ToolCallPart;
326
328
 
327
329
  // Skip invalid tool calls that have no name AND no output
328
- if (!_tool_call.name && (output == null || output === '')) {
330
+ if (
331
+ _tool_call.name == null ||
332
+ (_tool_call.name === '' && (output == null || output === ''))
333
+ ) {
329
334
  continue;
330
335
  }
331
336
 
@@ -358,7 +363,7 @@ function formatAssistantMessage(
358
363
  new ToolMessage({
359
364
  tool_call_id: tool_call.id ?? '',
360
365
  name: tool_call.name,
361
- content: output || '',
366
+ content: output != null ? output : '',
362
367
  })
363
368
  );
364
369
  } else if (part.type === ContentTypes.THINK) {
@@ -395,6 +400,226 @@ function formatAssistantMessage(
395
400
  return formattedMessages;
396
401
  }
397
402
 
403
+ /**
404
+ * Labels all agent content for parallel patterns (fan-out/fan-in)
405
+ * Groups consecutive content by agent and wraps with clear labels
406
+ */
407
+ function labelAllAgentContent(
408
+ contentParts: MessageContentComplex[],
409
+ agentIdMap: Record<number, string>,
410
+ agentNames?: Record<string, string>
411
+ ): MessageContentComplex[] {
412
+ const result: MessageContentComplex[] = [];
413
+ let currentAgentId: string | undefined;
414
+ let agentContentBuffer: MessageContentComplex[] = [];
415
+
416
+ const flushAgentBuffer = (): void => {
417
+ if (agentContentBuffer.length === 0) {
418
+ return;
419
+ }
420
+
421
+ if (currentAgentId != null && currentAgentId !== '') {
422
+ const agentName = (agentNames?.[currentAgentId] ?? '') || currentAgentId;
423
+ const formattedParts: string[] = [];
424
+
425
+ formattedParts.push(`--- ${agentName} ---`);
426
+
427
+ for (const part of agentContentBuffer) {
428
+ if (part.type === ContentTypes.THINK) {
429
+ const thinkContent = (part as ReasoningContentText).think || '';
430
+ if (thinkContent) {
431
+ formattedParts.push(
432
+ `${agentName}: ${JSON.stringify({
433
+ type: 'think',
434
+ think: thinkContent,
435
+ })}`
436
+ );
437
+ }
438
+ } else if (part.type === ContentTypes.TEXT) {
439
+ const textContent: string = part.text ?? '';
440
+ if (textContent) {
441
+ formattedParts.push(`${agentName}: ${textContent}`);
442
+ }
443
+ } else if (part.type === ContentTypes.TOOL_CALL) {
444
+ formattedParts.push(
445
+ `${agentName}: ${JSON.stringify({
446
+ type: 'tool_call',
447
+ tool_call: (part as ToolCallContent).tool_call,
448
+ })}`
449
+ );
450
+ }
451
+ }
452
+
453
+ formattedParts.push(`--- End of ${agentName} ---`);
454
+
455
+ // Create a single text content part with all agent content
456
+ result.push({
457
+ type: ContentTypes.TEXT,
458
+ text: formattedParts.join('\n\n'),
459
+ } as MessageContentComplex);
460
+ } else {
461
+ // No agent ID, pass through as-is
462
+ result.push(...agentContentBuffer);
463
+ }
464
+
465
+ agentContentBuffer = [];
466
+ };
467
+
468
+ for (let i = 0; i < contentParts.length; i++) {
469
+ const part = contentParts[i];
470
+ const agentId = agentIdMap[i];
471
+
472
+ // If agent changed, flush previous buffer
473
+ if (agentId !== currentAgentId && currentAgentId !== undefined) {
474
+ flushAgentBuffer();
475
+ }
476
+
477
+ currentAgentId = agentId;
478
+ agentContentBuffer.push(part);
479
+ }
480
+
481
+ // Flush any remaining content
482
+ flushAgentBuffer();
483
+
484
+ return result;
485
+ }
486
+
487
+ /**
488
+ * Groups content parts by agent and formats them with agent labels
489
+ * This preprocesses multi-agent content to prevent identity confusion
490
+ *
491
+ * @param contentParts - The content parts from a run
492
+ * @param agentIdMap - Map of content part index to agent ID
493
+ * @param agentNames - Optional map of agent ID to display name
494
+ * @param options - Configuration options
495
+ * @param options.labelNonTransferContent - If true, labels all agent transitions (for parallel patterns)
496
+ * @returns Modified content parts with agent labels where appropriate
497
+ */
498
+ export const labelContentByAgent = (
499
+ contentParts: MessageContentComplex[],
500
+ agentIdMap?: Record<number, string>,
501
+ agentNames?: Record<string, string>,
502
+ options?: { labelNonTransferContent?: boolean }
503
+ ): MessageContentComplex[] => {
504
+ if (!agentIdMap || Object.keys(agentIdMap).length === 0) {
505
+ return contentParts;
506
+ }
507
+
508
+ // If labelNonTransferContent is true, use a different strategy for parallel patterns
509
+ if (options?.labelNonTransferContent === true) {
510
+ return labelAllAgentContent(contentParts, agentIdMap, agentNames);
511
+ }
512
+
513
+ const result: MessageContentComplex[] = [];
514
+ let currentAgentId: string | undefined;
515
+ let agentContentBuffer: MessageContentComplex[] = [];
516
+ let transferToolCallIndex: number | undefined;
517
+ let transferToolCallId: string | undefined;
518
+
519
+ const flushAgentBuffer = (): void => {
520
+ if (agentContentBuffer.length === 0) {
521
+ return;
522
+ }
523
+
524
+ // If this is content from a transferred agent, format it specially
525
+ if (
526
+ currentAgentId != null &&
527
+ currentAgentId !== '' &&
528
+ transferToolCallIndex !== undefined
529
+ ) {
530
+ const agentName = (agentNames?.[currentAgentId] ?? '') || currentAgentId;
531
+ const formattedParts: string[] = [];
532
+
533
+ formattedParts.push(`--- Transfer to ${agentName} ---`);
534
+
535
+ for (const part of agentContentBuffer) {
536
+ if (part.type === ContentTypes.THINK) {
537
+ formattedParts.push(
538
+ `${agentName}: ${JSON.stringify({
539
+ type: 'think',
540
+ think: (part as ReasoningContentText).think,
541
+ })}`
542
+ );
543
+ } else if ('text' in part && part.type === ContentTypes.TEXT) {
544
+ const textContent: string = part.text ?? '';
545
+ if (textContent) {
546
+ formattedParts.push(
547
+ `${agentName}: ${JSON.stringify({
548
+ type: 'text',
549
+ text: textContent,
550
+ })}`
551
+ );
552
+ }
553
+ } else if (part.type === ContentTypes.TOOL_CALL) {
554
+ formattedParts.push(
555
+ `${agentName}: ${JSON.stringify({
556
+ type: 'tool_call',
557
+ tool_call: (part as ToolCallContent).tool_call,
558
+ })}`
559
+ );
560
+ }
561
+ }
562
+
563
+ formattedParts.push(`--- End of ${agentName} response ---`);
564
+
565
+ // Find the tool call that triggered this transfer and update its output
566
+ if (transferToolCallIndex < result.length) {
567
+ const transferToolCall = result[transferToolCallIndex];
568
+ if (
569
+ transferToolCall.type === ContentTypes.TOOL_CALL &&
570
+ transferToolCall.tool_call?.id === transferToolCallId
571
+ ) {
572
+ transferToolCall.tool_call.output = formattedParts.join('\n\n');
573
+ }
574
+ }
575
+ } else {
576
+ // Not from a transfer, add as-is
577
+ result.push(...agentContentBuffer);
578
+ }
579
+
580
+ agentContentBuffer = [];
581
+ transferToolCallIndex = undefined;
582
+ transferToolCallId = undefined;
583
+ };
584
+
585
+ for (let i = 0; i < contentParts.length; i++) {
586
+ const part = contentParts[i];
587
+ const agentId = agentIdMap[i];
588
+
589
+ // Check if this is a transfer tool call
590
+ const isTransferTool =
591
+ (part.type === ContentTypes.TOOL_CALL &&
592
+ (part as ToolCallContent).tool_call?.name?.startsWith(
593
+ 'lc_transfer_to_'
594
+ )) ??
595
+ false;
596
+
597
+ // If agent changed, flush previous buffer
598
+ if (agentId !== currentAgentId && currentAgentId !== undefined) {
599
+ flushAgentBuffer();
600
+ }
601
+
602
+ currentAgentId = agentId;
603
+
604
+ if (isTransferTool) {
605
+ // Flush any existing buffer first
606
+ flushAgentBuffer();
607
+ // Add the transfer tool call to result
608
+ result.push(part);
609
+ // Mark that the next agent's content should be captured
610
+ transferToolCallIndex = result.length - 1;
611
+ transferToolCallId = (part as ToolCallContent).tool_call?.id;
612
+ currentAgentId = undefined; // Reset to capture the next agent
613
+ } else {
614
+ agentContentBuffer.push(part);
615
+ }
616
+ }
617
+
618
+ flushAgentBuffer();
619
+
620
+ return result;
621
+ };
622
+
398
623
  /**
399
624
  * Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
400
625
  *
@@ -463,7 +688,7 @@ export const formatAgentMessages = (
463
688
  }
464
689
  // Protect against malformed tool call entries
465
690
  if (
466
- !part.tool_call ||
691
+ part.tool_call == null ||
467
692
  part.tool_call.name == null ||
468
693
  part.tool_call.name === ''
469
694
  ) {
@@ -495,7 +720,7 @@ export const formatAgentMessages = (
495
720
  // Check if this is a continuation of the tool sequence
496
721
  let isToolResponse = false;
497
722
  const content = payload[j].content;
498
- if (content && Array.isArray(content)) {
723
+ if (content != null && Array.isArray(content)) {
499
724
  for (const part of content) {
500
725
  if (part.type === ContentTypes.TOOL_CALL) {
501
726
  isToolResponse = true;