@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.
- package/README.md +66 -0
- package/dist/cjs/agents/AgentContext.cjs +84 -37
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +37 -3
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +85 -38
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +37 -3
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +14 -4
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
- package/dist/types/types/graph.d.ts +3 -1
- package/dist/types/types/run.d.ts +2 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +123 -44
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
- package/src/agents/__tests__/AgentContext.test.ts +155 -2
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
- package/src/llm/anthropic/utils/message_inputs.ts +6 -1
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
- package/src/messages/cache.test.ts +104 -3
- package/src/messages/cache.ts +54 -3
- package/src/specs/anthropic.simple.test.ts +61 -0
- package/src/specs/summarization.test.ts +7 -3
- package/src/types/graph.ts +3 -1
- 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('
|
|
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
|
|
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?',
|
package/src/messages/cache.ts
CHANGED
|
@@ -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
|
|
351
|
+
const messageType =
|
|
314
352
|
'getType' in originalMessage &&
|
|
315
|
-
typeof originalMessage.getType === 'function'
|
|
316
|
-
|
|
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)) +
|
|
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
|
-
|
|
2609
|
-
|
|
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}`
|
package/src/types/graph.ts
CHANGED
|
@@ -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
|
|
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
|
|