@librechat/agents 3.1.74 → 3.1.75

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.
Files changed (31) hide show
  1. package/README.md +66 -0
  2. package/dist/cjs/agents/AgentContext.cjs +84 -37
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -1
  5. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  6. package/dist/cjs/messages/cache.cjs +37 -3
  7. package/dist/cjs/messages/cache.cjs.map +1 -1
  8. package/dist/esm/agents/AgentContext.mjs +85 -38
  9. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  10. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -1
  11. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  12. package/dist/esm/messages/cache.mjs +37 -3
  13. package/dist/esm/messages/cache.mjs.map +1 -1
  14. package/dist/types/agents/AgentContext.d.ts +14 -4
  15. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
  16. package/dist/types/types/graph.d.ts +3 -1
  17. package/dist/types/types/run.d.ts +2 -0
  18. package/package.json +1 -1
  19. package/src/agents/AgentContext.ts +123 -44
  20. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
  21. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
  22. package/src/agents/__tests__/AgentContext.test.ts +155 -2
  23. package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
  24. package/src/llm/anthropic/utils/message_inputs.ts +6 -1
  25. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
  26. package/src/messages/cache.test.ts +104 -3
  27. package/src/messages/cache.ts +54 -3
  28. package/src/specs/anthropic.simple.test.ts +61 -0
  29. package/src/specs/summarization.test.ts +7 -3
  30. package/src/types/graph.ts +3 -1
  31. package/src/types/run.ts +2 -0
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  AIMessage,
3
3
  BaseMessage,
4
- ToolMessage,
5
4
  HumanMessage,
5
+ SystemMessage,
6
+ ToolMessage,
6
7
  MessageContentComplex,
7
8
  } from '@langchain/core/messages';
8
9
  import type Anthropic from '@anthropic-ai/sdk';
@@ -404,7 +405,107 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
404
405
  expect(first[1]).toEqual({ cachePoint: { type: 'default' } });
405
406
  });
406
407
 
407
- it('works with the example from the langchain pr (with multi-turn behavior)', () => {
408
+ it('preserves LangChain system message content unchanged', () => {
409
+ const systemContent = [
410
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
411
+ { cachePoint: { type: 'default' } },
412
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
413
+ ] as MessageContentComplex[];
414
+ const messages: BaseMessage[] = [
415
+ new SystemMessage({ content: systemContent }),
416
+ new HumanMessage('Hello'),
417
+ new AIMessage('Hi'),
418
+ ];
419
+
420
+ const result = addBedrockCacheControl(messages);
421
+
422
+ expect(result[0]).toBe(messages[0]);
423
+ expect(result[0].content).toEqual(systemContent);
424
+ });
425
+
426
+ it('preserves serialized system message content unchanged', () => {
427
+ const systemContent = [
428
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
429
+ { cachePoint: { type: 'default' } },
430
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
431
+ ] as MessageContentComplex[];
432
+ const messages: TestMsg[] = [
433
+ { role: 'system', content: systemContent },
434
+ { role: 'user', content: 'Hello' },
435
+ { role: 'assistant', content: 'Hi' },
436
+ ];
437
+
438
+ const result = addBedrockCacheControl(messages);
439
+
440
+ expect(result[0]).toBe(messages[0]);
441
+ expect(result[0].content).toEqual(systemContent);
442
+ });
443
+
444
+ it('strips Anthropic cache_control from LangChain system messages without moving cache points', () => {
445
+ const systemContent = [
446
+ {
447
+ type: ContentTypes.TEXT,
448
+ text: 'Stable system text',
449
+ cache_control: { type: 'ephemeral' },
450
+ } as MessageContentComplex,
451
+ { cachePoint: { type: 'default' } },
452
+ {
453
+ type: ContentTypes.TEXT,
454
+ text: 'Dynamic system text',
455
+ cache_control: { type: 'ephemeral' },
456
+ } as MessageContentComplex,
457
+ ] as MessageContentComplex[];
458
+ const messages: BaseMessage[] = [
459
+ new SystemMessage({ content: systemContent }),
460
+ new HumanMessage('Hello'),
461
+ new AIMessage('Hi'),
462
+ ];
463
+
464
+ const result = addBedrockCacheControl(messages);
465
+
466
+ expect(result[0]).not.toBe(messages[0]);
467
+ expect(result[0].content).toEqual([
468
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
469
+ { cachePoint: { type: 'default' } },
470
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
471
+ ]);
472
+ expect(systemContent[0]).toHaveProperty('cache_control');
473
+ expect(systemContent[2]).toHaveProperty('cache_control');
474
+ });
475
+
476
+ it('strips Anthropic cache_control from serialized system messages without moving cache points', () => {
477
+ const systemContent = [
478
+ {
479
+ type: ContentTypes.TEXT,
480
+ text: 'Stable system text',
481
+ cache_control: { type: 'ephemeral' },
482
+ } as MessageContentComplex,
483
+ { cachePoint: { type: 'default' } },
484
+ {
485
+ type: ContentTypes.TEXT,
486
+ text: 'Dynamic system text',
487
+ cache_control: { type: 'ephemeral' },
488
+ } as MessageContentComplex,
489
+ ] as MessageContentComplex[];
490
+ const messages: TestMsg[] = [
491
+ { role: 'system', content: systemContent },
492
+ { role: 'user', content: 'Hello' },
493
+ { role: 'assistant', content: 'Hi' },
494
+ ];
495
+
496
+ const result = addBedrockCacheControl(messages);
497
+
498
+ expect(result[0]).not.toBe(messages[0]);
499
+ expect(result[0].content).toEqual([
500
+ { type: ContentTypes.TEXT, text: 'Stable system text' },
501
+ { cachePoint: { type: 'default' } },
502
+ { type: ContentTypes.TEXT, text: 'Dynamic system text' },
503
+ ]);
504
+ expect(systemContent[0]).toHaveProperty('cache_control');
505
+ expect(systemContent[2]).toHaveProperty('cache_control');
506
+ });
507
+
508
+ it('skips serialized system messages while adding cache points to non-system turns', () => {
408
509
  const messages: TestMsg[] = [
409
510
  {
410
511
  role: 'system',
@@ -429,7 +530,7 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
429
530
  type: ContentTypes.TEXT,
430
531
  text: 'You\'re an advanced AI assistant.',
431
532
  });
432
- expect(system[1]).toEqual({ cachePoint: { type: 'default' } });
533
+ expect(system).toHaveLength(1);
433
534
  expect(user[0]).toEqual({
434
535
  type: ContentTypes.TEXT,
435
536
  text: 'What is the capital of France?',
@@ -14,6 +14,10 @@ type MessageWithContent = {
14
14
  content?: string | MessageContentComplex[];
15
15
  };
16
16
 
17
+ type MessageContentWithCacheControl = MessageContentComplex & {
18
+ cache_control?: unknown;
19
+ };
20
+
17
21
  /**
18
22
  * Deep clones a message's content to prevent mutation of the original.
19
23
  */
@@ -101,6 +105,40 @@ function cloneMessage<T extends MessageWithContent>(
101
105
  return cloned;
102
106
  }
103
107
 
108
+ function stripAnthropicCacheControlFromBlocks(
109
+ content: MessageContentComplex[]
110
+ ): { content: MessageContentComplex[]; modified: boolean } {
111
+ let modified = false;
112
+ const strippedContent = content.map((block) => {
113
+ if (!('cache_control' in block)) {
114
+ return block;
115
+ }
116
+
117
+ const cloned: MessageContentWithCacheControl = { ...block };
118
+ delete cloned.cache_control;
119
+ modified = true;
120
+ return cloned;
121
+ });
122
+
123
+ return { content: strippedContent, modified };
124
+ }
125
+
126
+ function sanitizeBedrockSystemMessage<T extends MessageWithContent>(
127
+ message: T
128
+ ): T {
129
+ const content = message.content;
130
+ if (!Array.isArray(content)) {
131
+ return message;
132
+ }
133
+
134
+ const stripped = stripAnthropicCacheControlFromBlocks(content);
135
+ if (!stripped.modified) {
136
+ return message;
137
+ }
138
+
139
+ return cloneMessage(message, stripped.content);
140
+ }
141
+
104
142
  /**
105
143
  * Anthropic API: Adds cache control to the appropriate user messages in the payload.
106
144
  * Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
@@ -310,11 +348,24 @@ export function addBedrockCacheControl<
310
348
 
311
349
  for (let i = updatedMessages.length - 1; i >= 0; i--) {
312
350
  const originalMessage = updatedMessages[i];
313
- const isToolMessage =
351
+ const messageType =
314
352
  'getType' in originalMessage &&
315
- typeof originalMessage.getType === 'function' &&
316
- originalMessage.getType() === 'tool';
353
+ typeof originalMessage.getType === 'function'
354
+ ? originalMessage.getType()
355
+ : undefined;
356
+ const messageRole =
357
+ 'role' in originalMessage && typeof originalMessage.role === 'string'
358
+ ? originalMessage.role
359
+ : undefined;
360
+
361
+ const isSystemMessage =
362
+ messageType === 'system' || messageRole === 'system';
363
+ if (isSystemMessage) {
364
+ updatedMessages[i] = sanitizeBedrockSystemMessage(originalMessage);
365
+ continue;
366
+ }
317
367
 
368
+ const isToolMessage = messageType === 'tool' || messageRole === 'tool';
318
369
  const content = originalMessage.content;
319
370
  const hasArrayContent = Array.isArray(content);
320
371
  const isEmptyString = typeof content === 'string' && content === '';
@@ -376,6 +376,67 @@ describe(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
376
376
  );
377
377
  });
378
378
 
379
+ test(`${capitalizeFirstLetter(provider)}: follow-up after assistant message with only whitespace text content`, async () => {
380
+ /**
381
+ * Regression for LibreChat discussion #12806.
382
+ *
383
+ * The Anthropic API has two distinct rejection rules (verified against
384
+ * the live API):
385
+ * 1. Strict empty `text: ''` → rejected anywhere
386
+ * "messages: text content blocks must be non-empty"
387
+ * 2. Whitespace-only `text: ' '` / '\n' / '\t' → rejected when the
388
+ * assistant message has no other accepted blocks (no tool blocks,
389
+ * no non-whitespace text)
390
+ * "messages: text content blocks must contain non-whitespace text"
391
+ *
392
+ * Anthropic responses for some prompts include a whitespace-only text
393
+ * block as the sole text content. Re-sending that history on a
394
+ * follow-up turn triggers rule 2.
395
+ *
396
+ * The wire-send filter in `_formatContent` must drop any text block
397
+ * whose trimmed content is empty. The previous filter used strict
398
+ * `text === ''` only, which caught rule 1 but not rule 2.
399
+ */
400
+ const llmConfig = getLLMConfig(provider);
401
+ const customHandlers1 = setupCustomHandlers();
402
+
403
+ const followUpRun = await Run.create<t.IState>({
404
+ runId: 'repro-12806-followup',
405
+ graphConfig: {
406
+ type: 'standard',
407
+ llmConfig,
408
+ instructions: 'You are a friendly AI assistant.',
409
+ },
410
+ returnContent: true,
411
+ skipCleanup: true,
412
+ customHandlers: customHandlers1,
413
+ });
414
+
415
+ // Build history with an assistant message whose entire content array
416
+ // is a single whitespace-only text block. This is the precise shape
417
+ // the API rejects under rule 2 above.
418
+ conversationHistory = [
419
+ new HumanMessage('hi'),
420
+ new (require('@langchain/core/messages').AIMessage)({
421
+ content: [{ type: 'text', text: ' ' }],
422
+ }),
423
+ new HumanMessage('please respond with a short greeting'),
424
+ ];
425
+
426
+ // With the fix: `_formatContent` drops the whitespace text block,
427
+ // the assistant content becomes an empty array, and the API accepts.
428
+ // Without the fix: the whitespace block is forwarded and the API
429
+ // rejects with "messages: text content blocks must contain non-whitespace text".
430
+ const finalContentParts = await followUpRun.processStream(
431
+ { messages: conversationHistory },
432
+ config
433
+ );
434
+ expect(finalContentParts).toBeDefined();
435
+ const finalMessages = followUpRun.getRunMessages();
436
+ expect(finalMessages).toBeDefined();
437
+ expect(finalMessages?.length).toBeGreaterThan(0);
438
+ });
439
+
379
440
  test('should handle errors appropriately', async () => {
380
441
  // Test error scenarios
381
442
  await expect(async () => {
@@ -22,6 +22,8 @@ import { formatAgentMessages } from '@/messages/format';
22
22
  import { FakeListChatModel } from '@langchain/core/utils/testing';
23
23
  import * as providers from '@/llm/providers';
24
24
 
25
+ const SUMMARY_WRAPPER_OVERHEAD_TOKENS = 33;
26
+
25
27
  /** Extract plain text from a SummaryContentBlock's content array (test helper). */
26
28
  function getSummaryText(summary: t.SummaryContentBlock | undefined): string {
27
29
  if (!summary) return '';
@@ -1443,7 +1445,8 @@ describe('Cross-run summary lifecycle (no API keys)', () => {
1443
1445
  expect(completePayload.summary!.tokenCount ?? 0).toBeGreaterThan(0);
1444
1446
 
1445
1447
  const expectedTokenCount =
1446
- tokenCounter(new SystemMessage(KNOWN_SUMMARY)) + 33;
1448
+ tokenCounter(new SystemMessage(KNOWN_SUMMARY)) +
1449
+ SUMMARY_WRAPPER_OVERHEAD_TOKENS;
1447
1450
  expect(completePayload.summary!.tokenCount).toBe(expectedTokenCount);
1448
1451
 
1449
1452
  const summaryBlock = completePayload.summary!;
@@ -2605,8 +2608,9 @@ const hasAnyApiKey =
2605
2608
  const summaryText = getSummaryText(completePayload.summary);
2606
2609
  const reportedTokenCount = completePayload.summary!.tokenCount ?? 0;
2607
2610
 
2608
- // Count tokens locally using the same tokenizer
2609
- const localTokenCount = tokenCounter(new SystemMessage(summaryText));
2611
+ const localTokenCount =
2612
+ tokenCounter(new SystemMessage(summaryText)) +
2613
+ SUMMARY_WRAPPER_OVERHEAD_TOKENS;
2610
2614
 
2611
2615
  console.log(
2612
2616
  ` Token match: reported=${reportedTokenCount}, local=${localTokenCount}`
@@ -471,10 +471,12 @@ export interface AgentInputs {
471
471
  toolMap?: ToolMap;
472
472
  tools?: GraphTools;
473
473
  provider: Providers;
474
+ /** Stable/cacheable system instructions. */
474
475
  instructions?: string;
475
476
  streamBuffer?: number;
476
477
  maxContextTokens?: number;
477
478
  clientOptions?: ClientOptions;
479
+ /** Dynamic system tail appended after stable instructions without provider cache markers. */
478
480
  additional_instructions?: string;
479
481
  reasoningKey?: 'reasoning_content' | 'reasoning';
480
482
  /** Format content blocks as strings (for legacy compatibility i.e. Ollama/Azure Serverless) */
@@ -500,7 +502,7 @@ export interface AgentInputs {
500
502
  summarizationEnabled?: boolean;
501
503
  summarizationConfig?: SummarizationConfig;
502
504
  /** Cross-run summary from a previous run, forwarded from formatAgentMessages.
503
- * Injected into the system message via AgentContext.buildInstructionsString(). */
505
+ * Injected into the dynamic system tail via AgentContext. */
504
506
  initialSummary?: { text: string; tokenCount: number };
505
507
  contextPruningConfig?: ContextPruningConfig;
506
508
  maxToolResultChars?: number;
package/src/types/run.ts CHANGED
@@ -75,7 +75,9 @@ export interface AgentStateChannels {
75
75
  messages: BaseMessage[];
76
76
  next: string;
77
77
  [key: string]: unknown;
78
+ /** Stable/cacheable system instructions. */
78
79
  instructions?: string;
80
+ /** Dynamic system tail appended after stable instructions. */
79
81
  additional_instructions?: string;
80
82
  }
81
83