@librechat/agents 3.1.74 → 3.1.75-dev.0
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/graphs/Graph.cjs +13 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +145 -52
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +1 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +5 -4
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +468 -647
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +1 -448
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +57 -175
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +5 -3
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +39 -4
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +7 -6
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +7 -6
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/langchain.cjs +26 -0
- package/dist/cjs/messages/langchain.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +7 -6
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +5 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +85 -38
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +13 -3
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +146 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +1 -1
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +5 -4
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +469 -648
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +4 -449
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +57 -175
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +5 -3
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +39 -4
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +7 -6
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +7 -6
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/langchain.mjs +23 -0
- package/dist/esm/messages/langchain.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +7 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +5 -1
- package/dist/esm/tools/ToolNode.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/llm/anthropic/index.d.ts +22 -9
- package/dist/types/llm/anthropic/types.d.ts +5 -1
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
- package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
- package/dist/types/llm/openai/index.d.ts +21 -24
- package/dist/types/llm/openrouter/index.d.ts +11 -9
- package/dist/types/llm/vertexai/index.d.ts +1 -0
- package/dist/types/messages/cache.d.ts +4 -1
- package/dist/types/messages/langchain.d.ts +27 -0
- package/dist/types/types/graph.d.ts +26 -38
- package/dist/types/types/llm.d.ts +3 -3
- package/dist/types/types/run.d.ts +2 -0
- package/dist/types/types/stream.d.ts +1 -1
- package/package.json +17 -16
- 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/graphs/Graph.ts +24 -4
- package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
- package/src/llm/anthropic/index.ts +252 -84
- package/src/llm/anthropic/llm.spec.ts +751 -102
- package/src/llm/anthropic/types.ts +9 -1
- package/src/llm/anthropic/utils/message_inputs.ts +43 -20
- package/src/llm/anthropic/utils/message_outputs.ts +119 -101
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
- package/src/llm/bedrock/index.ts +2 -2
- package/src/llm/bedrock/llm.spec.ts +341 -0
- package/src/llm/bedrock/utils/message_inputs.ts +303 -4
- package/src/llm/bedrock/utils/message_outputs.ts +2 -1
- package/src/llm/custom-chat-models.smoke.test.ts +662 -0
- package/src/llm/google/llm.spec.ts +339 -57
- package/src/llm/google/utils/common.ts +53 -48
- package/src/llm/openai/contentBlocks.test.ts +346 -0
- package/src/llm/openai/index.ts +736 -837
- package/src/llm/openai/utils/index.ts +84 -64
- package/src/llm/openrouter/index.ts +124 -247
- package/src/llm/openrouter/reasoning.test.ts +8 -1
- package/src/llm/vertexai/index.ts +11 -5
- package/src/llm/vertexai/llm.spec.ts +28 -1
- package/src/messages/cache.test.ts +106 -4
- package/src/messages/cache.ts +57 -5
- package/src/messages/core.ts +16 -9
- package/src/messages/format.ts +9 -6
- package/src/messages/langchain.ts +39 -0
- package/src/messages/prune.ts +12 -8
- package/src/scripts/caching.ts +2 -3
- package/src/specs/anthropic.simple.test.ts +61 -0
- package/src/specs/summarization.test.ts +58 -61
- package/src/tools/ToolNode.ts +5 -1
- package/src/types/graph.ts +35 -88
- package/src/types/llm.ts +3 -3
- package/src/types/run.ts +2 -0
- package/src/types/stream.ts +1 -1
- package/src/utils/llmConfig.ts +1 -6
|
@@ -20,6 +20,16 @@ import { addCacheControl } from '@/messages/cache';
|
|
|
20
20
|
import { DEFAULT_RESERVE_RATIO } from '@/messages';
|
|
21
21
|
import { toJsonSchema } from '@/utils/schema';
|
|
22
22
|
|
|
23
|
+
type AgentSystemTextBlock = {
|
|
24
|
+
type: 'text';
|
|
25
|
+
text: string;
|
|
26
|
+
cache_control?: { type: 'ephemeral' };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type AgentSystemContentBlock =
|
|
30
|
+
| AgentSystemTextBlock
|
|
31
|
+
| { cachePoint: { type: 'default' } };
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* Encapsulates agent-specific state that can vary between agents in a multi-agent system
|
|
25
35
|
*/
|
|
@@ -249,7 +259,7 @@ export class AgentContext {
|
|
|
249
259
|
private summaryTokenCount: number = 0;
|
|
250
260
|
/**
|
|
251
261
|
* Where the summary should be injected:
|
|
252
|
-
* - `'system_prompt'`: cross-run summary, included in
|
|
262
|
+
* - `'system_prompt'`: cross-run summary, included in the dynamic system tail
|
|
253
263
|
* - `'user_message'`: mid-run compaction, injected as HumanMessage on clean slate
|
|
254
264
|
* - `'none'`: no summary present
|
|
255
265
|
*/
|
|
@@ -417,7 +427,8 @@ export class AgentContext {
|
|
|
417
427
|
|
|
418
428
|
/**
|
|
419
429
|
* Gets the system runnable, creating it lazily if needed.
|
|
420
|
-
* Includes instructions, additional instructions, and
|
|
430
|
+
* Includes stable instructions, dynamic additional instructions, and
|
|
431
|
+
* programmatic-only tools documentation.
|
|
421
432
|
* Only rebuilds when marked stale (via markToolsAsDiscovered).
|
|
422
433
|
*/
|
|
423
434
|
get systemRunnable():
|
|
@@ -431,8 +442,10 @@ export class AgentContext {
|
|
|
431
442
|
return this.cachedSystemRunnable;
|
|
432
443
|
}
|
|
433
444
|
|
|
434
|
-
|
|
435
|
-
|
|
445
|
+
this.cachedSystemRunnable = this.buildSystemRunnable({
|
|
446
|
+
stableInstructions: this.buildStableInstructionsString(),
|
|
447
|
+
dynamicInstructions: this.buildDynamicInstructionsString(),
|
|
448
|
+
});
|
|
436
449
|
this.systemRunnableStale = false;
|
|
437
450
|
return this.cachedSystemRunnable;
|
|
438
451
|
}
|
|
@@ -443,17 +456,19 @@ export class AgentContext {
|
|
|
443
456
|
*/
|
|
444
457
|
initializeSystemRunnable(): void {
|
|
445
458
|
if (this.systemRunnableStale || this.cachedSystemRunnable === undefined) {
|
|
446
|
-
|
|
447
|
-
|
|
459
|
+
this.cachedSystemRunnable = this.buildSystemRunnable({
|
|
460
|
+
stableInstructions: this.buildStableInstructionsString(),
|
|
461
|
+
dynamicInstructions: this.buildDynamicInstructionsString(),
|
|
462
|
+
});
|
|
448
463
|
this.systemRunnableStale = false;
|
|
449
464
|
}
|
|
450
465
|
}
|
|
451
466
|
|
|
452
467
|
/**
|
|
453
|
-
* Builds the
|
|
468
|
+
* Builds the cacheable instructions string (without creating SystemMessage).
|
|
454
469
|
* Includes agent identity preamble and handoff context when available.
|
|
455
470
|
*/
|
|
456
|
-
private
|
|
471
|
+
private buildStableInstructionsString(): string {
|
|
457
472
|
const parts: string[] = [];
|
|
458
473
|
|
|
459
474
|
const identityPreamble = this.buildIdentityPreamble();
|
|
@@ -465,6 +480,22 @@ export class AgentContext {
|
|
|
465
480
|
parts.push(this.instructions);
|
|
466
481
|
}
|
|
467
482
|
|
|
483
|
+
const programmaticToolsDoc = this.buildProgrammaticOnlyToolsInstructions();
|
|
484
|
+
if (programmaticToolsDoc) {
|
|
485
|
+
parts.push(programmaticToolsDoc);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return parts.join('\n\n');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Builds the dynamic system-tail string (without creating SystemMessage).
|
|
493
|
+
* Keep this out of prompt-cache-marked content so volatile context does not
|
|
494
|
+
* invalidate the stable prefix.
|
|
495
|
+
*/
|
|
496
|
+
private buildDynamicInstructionsString(): string {
|
|
497
|
+
const parts: string[] = [];
|
|
498
|
+
|
|
468
499
|
if (
|
|
469
500
|
this.additionalInstructions != null &&
|
|
470
501
|
this.additionalInstructions !== ''
|
|
@@ -472,14 +503,10 @@ export class AgentContext {
|
|
|
472
503
|
parts.push(this.additionalInstructions);
|
|
473
504
|
}
|
|
474
505
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// Cross-run summary: include in system prompt so the model has context
|
|
481
|
-
// from the prior run. Mid-run summaries are injected as a HumanMessage
|
|
482
|
-
// on the post-compaction clean slate instead (see buildSystemRunnable).
|
|
506
|
+
// Cross-run summary: include in the system tail so the model has context
|
|
507
|
+
// from the prior run without invalidating the cacheable prefix. Mid-run
|
|
508
|
+
// summaries are injected as a HumanMessage on the post-compaction clean
|
|
509
|
+
// slate instead (see buildSystemRunnable).
|
|
483
510
|
if (
|
|
484
511
|
this._summaryLocation === 'system_prompt' &&
|
|
485
512
|
this.summaryText != null &&
|
|
@@ -523,9 +550,13 @@ export class AgentContext {
|
|
|
523
550
|
* Build system runnable from pre-built instructions string.
|
|
524
551
|
* Only called when content has actually changed.
|
|
525
552
|
*/
|
|
526
|
-
private buildSystemRunnable(
|
|
527
|
-
|
|
528
|
-
|
|
553
|
+
private buildSystemRunnable({
|
|
554
|
+
stableInstructions,
|
|
555
|
+
dynamicInstructions,
|
|
556
|
+
}: {
|
|
557
|
+
stableInstructions: string;
|
|
558
|
+
dynamicInstructions: string;
|
|
559
|
+
}):
|
|
529
560
|
| Runnable<
|
|
530
561
|
BaseMessage[],
|
|
531
562
|
(BaseMessage | SystemMessage)[],
|
|
@@ -537,35 +568,17 @@ export class AgentContext {
|
|
|
537
568
|
this.summaryText != null &&
|
|
538
569
|
this.summaryText !== '';
|
|
539
570
|
|
|
540
|
-
if (!
|
|
571
|
+
if (!stableInstructions && !dynamicInstructions && !hasMidRunSummary) {
|
|
541
572
|
this.systemMessageTokens = 0;
|
|
542
573
|
return undefined;
|
|
543
574
|
}
|
|
544
575
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
| undefined;
|
|
552
|
-
if (anthropicOptions?.promptCache === true) {
|
|
553
|
-
usePromptCache = true;
|
|
554
|
-
finalInstructions = {
|
|
555
|
-
content: [
|
|
556
|
-
{
|
|
557
|
-
type: 'text',
|
|
558
|
-
text: instructionsString,
|
|
559
|
-
cache_control: { type: 'ephemeral' },
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const systemMessage = instructionsString
|
|
567
|
-
? new SystemMessage(finalInstructions)
|
|
568
|
-
: undefined;
|
|
576
|
+
const usePromptCache = this.hasAnthropicPromptCache();
|
|
577
|
+
const systemMessage = this.buildSystemMessage({
|
|
578
|
+
stableInstructions,
|
|
579
|
+
dynamicInstructions,
|
|
580
|
+
usePromptCache,
|
|
581
|
+
});
|
|
569
582
|
|
|
570
583
|
if (this.tokenCounter) {
|
|
571
584
|
this.systemMessageTokens = systemMessage
|
|
@@ -615,6 +628,72 @@ export class AgentContext {
|
|
|
615
628
|
}).withConfig({ runName: 'prompt' });
|
|
616
629
|
}
|
|
617
630
|
|
|
631
|
+
private hasAnthropicPromptCache(): boolean {
|
|
632
|
+
if (this.provider !== Providers.ANTHROPIC) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
const anthropicOptions = this.clientOptions as
|
|
636
|
+
| t.AnthropicClientOptions
|
|
637
|
+
| undefined;
|
|
638
|
+
return anthropicOptions?.promptCache === true;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private hasBedrockPromptCache(): boolean {
|
|
642
|
+
if (this.provider !== Providers.BEDROCK) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
const bedrockOptions = this.clientOptions as
|
|
646
|
+
| t.BedrockAnthropicClientOptions
|
|
647
|
+
| undefined;
|
|
648
|
+
return bedrockOptions?.promptCache === true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private buildSystemMessage({
|
|
652
|
+
stableInstructions,
|
|
653
|
+
dynamicInstructions,
|
|
654
|
+
usePromptCache,
|
|
655
|
+
}: {
|
|
656
|
+
stableInstructions: string;
|
|
657
|
+
dynamicInstructions: string;
|
|
658
|
+
usePromptCache: boolean;
|
|
659
|
+
}): SystemMessage | undefined {
|
|
660
|
+
if (!stableInstructions && !dynamicInstructions) {
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (usePromptCache) {
|
|
665
|
+
const content: AgentSystemContentBlock[] = [];
|
|
666
|
+
if (stableInstructions) {
|
|
667
|
+
content.push({
|
|
668
|
+
type: 'text',
|
|
669
|
+
text: stableInstructions,
|
|
670
|
+
cache_control: { type: 'ephemeral' },
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
if (dynamicInstructions) {
|
|
674
|
+
content.push({ type: 'text', text: dynamicInstructions });
|
|
675
|
+
}
|
|
676
|
+
return new SystemMessage({ content } as BaseMessageFields);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (this.hasBedrockPromptCache() && stableInstructions) {
|
|
680
|
+
const content: AgentSystemContentBlock[] = [
|
|
681
|
+
{ type: 'text', text: stableInstructions },
|
|
682
|
+
{ cachePoint: { type: 'default' } },
|
|
683
|
+
];
|
|
684
|
+
if (dynamicInstructions) {
|
|
685
|
+
content.push({ type: 'text', text: dynamicInstructions });
|
|
686
|
+
}
|
|
687
|
+
return new SystemMessage({ content } as BaseMessageFields);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return new SystemMessage(
|
|
691
|
+
[stableInstructions, dynamicInstructions]
|
|
692
|
+
.filter((part) => part !== '')
|
|
693
|
+
.join('\n\n')
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
618
697
|
/**
|
|
619
698
|
* Reset context for a new run
|
|
620
699
|
*/
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/agents/__tests__/AgentContext.anthropic.live.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Live Anthropic prompt-cache verification.
|
|
4
|
+
*
|
|
5
|
+
* Run with:
|
|
6
|
+
* RUN_ANTHROPIC_PROMPT_CACHE_LIVE_TESTS=1 ANTHROPIC_API_KEY=... npm test -- AgentContext.anthropic.live.test.ts --runInBand
|
|
7
|
+
*/
|
|
8
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
9
|
+
dotenvConfig();
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from '@jest/globals';
|
|
12
|
+
import type * as t from '@/types';
|
|
13
|
+
import {
|
|
14
|
+
runLiveTurn,
|
|
15
|
+
assertSystemPayloadShape,
|
|
16
|
+
buildDynamicInstructions,
|
|
17
|
+
buildStableInstructions,
|
|
18
|
+
waitForCachePropagation,
|
|
19
|
+
} from './promptCacheLiveHelpers';
|
|
20
|
+
import { Providers } from '@/common';
|
|
21
|
+
|
|
22
|
+
const shouldRunLive =
|
|
23
|
+
process.env.RUN_ANTHROPIC_PROMPT_CACHE_LIVE_TESTS === '1' &&
|
|
24
|
+
process.env.ANTHROPIC_API_KEY != null &&
|
|
25
|
+
process.env.ANTHROPIC_API_KEY !== '';
|
|
26
|
+
|
|
27
|
+
const describeIfLive = shouldRunLive ? describe : describe.skip;
|
|
28
|
+
|
|
29
|
+
const modelName =
|
|
30
|
+
process.env.ANTHROPIC_PROMPT_CACHE_MODEL ?? 'claude-sonnet-4-5';
|
|
31
|
+
const providerLabel = 'Anthropic';
|
|
32
|
+
|
|
33
|
+
function createClientOptions(): t.AnthropicClientOptions {
|
|
34
|
+
return {
|
|
35
|
+
modelName,
|
|
36
|
+
temperature: 0,
|
|
37
|
+
maxTokens: 8,
|
|
38
|
+
streaming: true,
|
|
39
|
+
streamUsage: true,
|
|
40
|
+
promptCache: true,
|
|
41
|
+
clientOptions: {
|
|
42
|
+
defaultHeaders: {
|
|
43
|
+
'anthropic-beta': 'prompt-caching-2024-07-31',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describeIfLive('AgentContext Anthropic prompt cache live API', () => {
|
|
50
|
+
it('caches only the stable system prefix while dynamic tail changes', async () => {
|
|
51
|
+
const nonce = `agent-cache-live-${Date.now()}`;
|
|
52
|
+
const clientOptions = createClientOptions();
|
|
53
|
+
const stableInstructions = buildStableInstructions({
|
|
54
|
+
nonce,
|
|
55
|
+
providerLabel,
|
|
56
|
+
});
|
|
57
|
+
const firstDynamicInstructions = buildDynamicInstructions({
|
|
58
|
+
marker: 'alpha',
|
|
59
|
+
tailDescription:
|
|
60
|
+
'The Dynamic Marker line is runtime context and must remain outside the cached prefix.',
|
|
61
|
+
});
|
|
62
|
+
const secondDynamicInstructions = buildDynamicInstructions({
|
|
63
|
+
marker: 'bravo',
|
|
64
|
+
tailDescription:
|
|
65
|
+
'The Dynamic Marker line is runtime context and must remain outside the cached prefix.',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await assertSystemPayloadShape({
|
|
69
|
+
agentId: 'live-cache-shape-check',
|
|
70
|
+
provider: Providers.ANTHROPIC,
|
|
71
|
+
clientOptions,
|
|
72
|
+
stableInstructions,
|
|
73
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
74
|
+
expectedContent: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: stableInstructions,
|
|
78
|
+
cache_control: { type: 'ephemeral' },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: firstDynamicInstructions,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const first = await runLiveTurn({
|
|
88
|
+
provider: Providers.ANTHROPIC,
|
|
89
|
+
providerLabel,
|
|
90
|
+
clientOptions,
|
|
91
|
+
runId: `${nonce}-first`,
|
|
92
|
+
threadId: `${nonce}-thread`,
|
|
93
|
+
stableInstructions,
|
|
94
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(first.text.toLowerCase()).toContain('alpha');
|
|
98
|
+
expect(first.usage.input_token_details?.cache_creation).toBeGreaterThan(0);
|
|
99
|
+
expect(first.usage.input_token_details?.cache_read ?? 0).toBe(0);
|
|
100
|
+
|
|
101
|
+
await waitForCachePropagation();
|
|
102
|
+
|
|
103
|
+
const second = await runLiveTurn({
|
|
104
|
+
provider: Providers.ANTHROPIC,
|
|
105
|
+
providerLabel,
|
|
106
|
+
clientOptions,
|
|
107
|
+
runId: `${nonce}-second`,
|
|
108
|
+
threadId: `${nonce}-thread`,
|
|
109
|
+
stableInstructions,
|
|
110
|
+
dynamicInstructions: secondDynamicInstructions,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(second.text.toLowerCase()).toContain('bravo');
|
|
114
|
+
expect(second.usage.input_token_details?.cache_read).toBeGreaterThan(0);
|
|
115
|
+
}, 120_000);
|
|
116
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/agents/__tests__/AgentContext.bedrock.live.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Live Bedrock prompt-cache verification.
|
|
4
|
+
*
|
|
5
|
+
* Run with:
|
|
6
|
+
* RUN_BEDROCK_PROMPT_CACHE_LIVE_TESTS=1 BEDROCK_AWS_REGION=... BEDROCK_AWS_ACCESS_KEY_ID=... BEDROCK_AWS_SECRET_ACCESS_KEY=... npm test -- AgentContext.bedrock.live.test.ts --runInBand
|
|
7
|
+
*
|
|
8
|
+
* Standard AWS credential env vars or AWS_PROFILE can also be used.
|
|
9
|
+
*/
|
|
10
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
11
|
+
dotenvConfig();
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from '@jest/globals';
|
|
14
|
+
import type * as t from '@/types';
|
|
15
|
+
import {
|
|
16
|
+
runLiveTurn,
|
|
17
|
+
assertSystemPayloadShape,
|
|
18
|
+
buildDynamicInstructions,
|
|
19
|
+
buildStableInstructions,
|
|
20
|
+
waitForCachePropagation,
|
|
21
|
+
} from './promptCacheLiveHelpers';
|
|
22
|
+
import { Providers } from '@/common';
|
|
23
|
+
|
|
24
|
+
const accessKeyId =
|
|
25
|
+
process.env.BEDROCK_AWS_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY_ID;
|
|
26
|
+
const secretAccessKey =
|
|
27
|
+
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY ??
|
|
28
|
+
process.env.AWS_SECRET_ACCESS_KEY;
|
|
29
|
+
const sessionToken =
|
|
30
|
+
process.env.BEDROCK_AWS_SESSION_TOKEN ?? process.env.AWS_SESSION_TOKEN;
|
|
31
|
+
const hasCredentialPair =
|
|
32
|
+
accessKeyId != null &&
|
|
33
|
+
accessKeyId !== '' &&
|
|
34
|
+
secretAccessKey != null &&
|
|
35
|
+
secretAccessKey !== '';
|
|
36
|
+
const hasAmbientCredentials =
|
|
37
|
+
process.env.AWS_PROFILE != null ||
|
|
38
|
+
process.env.AWS_WEB_IDENTITY_TOKEN_FILE != null;
|
|
39
|
+
|
|
40
|
+
const shouldRunLive =
|
|
41
|
+
process.env.RUN_BEDROCK_PROMPT_CACHE_LIVE_TESTS === '1' &&
|
|
42
|
+
(hasCredentialPair || hasAmbientCredentials);
|
|
43
|
+
|
|
44
|
+
const describeIfLive = shouldRunLive ? describe : describe.skip;
|
|
45
|
+
|
|
46
|
+
const model =
|
|
47
|
+
process.env.BEDROCK_PROMPT_CACHE_MODEL ??
|
|
48
|
+
'us.anthropic.claude-sonnet-4-5-20250929-v1:0';
|
|
49
|
+
const region =
|
|
50
|
+
process.env.BEDROCK_AWS_REGION ?? process.env.AWS_REGION ?? 'us-east-1';
|
|
51
|
+
const providerLabel = 'Bedrock';
|
|
52
|
+
|
|
53
|
+
function getCredentials():
|
|
54
|
+
| t.BedrockAnthropicClientOptions['credentials']
|
|
55
|
+
| undefined {
|
|
56
|
+
if (!hasCredentialPair) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
accessKeyId,
|
|
62
|
+
secretAccessKey,
|
|
63
|
+
...(sessionToken != null && sessionToken !== '' ? { sessionToken } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createClientOptions(): t.BedrockAnthropicClientOptions {
|
|
68
|
+
const credentials = getCredentials();
|
|
69
|
+
return {
|
|
70
|
+
model,
|
|
71
|
+
region,
|
|
72
|
+
maxTokens: 8,
|
|
73
|
+
streaming: true,
|
|
74
|
+
streamUsage: true,
|
|
75
|
+
promptCache: true,
|
|
76
|
+
...(credentials != null ? { credentials } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describeIfLive('AgentContext Bedrock prompt cache live API', () => {
|
|
81
|
+
it('caches only the stable system prefix while dynamic tail changes', async () => {
|
|
82
|
+
const nonce = `agent-bedrock-cache-live-${Date.now()}`;
|
|
83
|
+
const clientOptions = createClientOptions();
|
|
84
|
+
const stableInstructions = buildStableInstructions({
|
|
85
|
+
nonce,
|
|
86
|
+
providerLabel,
|
|
87
|
+
});
|
|
88
|
+
const firstDynamicInstructions = buildDynamicInstructions({
|
|
89
|
+
marker: 'alpha',
|
|
90
|
+
tailDescription:
|
|
91
|
+
'The Dynamic Marker line is runtime context and must remain after the Bedrock cache point.',
|
|
92
|
+
});
|
|
93
|
+
const secondDynamicInstructions = buildDynamicInstructions({
|
|
94
|
+
marker: 'bravo',
|
|
95
|
+
tailDescription:
|
|
96
|
+
'The Dynamic Marker line is runtime context and must remain after the Bedrock cache point.',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await assertSystemPayloadShape({
|
|
100
|
+
agentId: 'live-bedrock-cache-shape-check',
|
|
101
|
+
provider: Providers.BEDROCK,
|
|
102
|
+
clientOptions,
|
|
103
|
+
stableInstructions,
|
|
104
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
105
|
+
expectedContent: [
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: stableInstructions,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
cachePoint: { type: 'default' },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: firstDynamicInstructions,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const first = await runLiveTurn({
|
|
121
|
+
provider: Providers.BEDROCK,
|
|
122
|
+
providerLabel,
|
|
123
|
+
clientOptions,
|
|
124
|
+
runId: `${nonce}-first`,
|
|
125
|
+
threadId: `${nonce}-thread`,
|
|
126
|
+
stableInstructions,
|
|
127
|
+
dynamicInstructions: firstDynamicInstructions,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(first.text.toLowerCase()).toContain('alpha');
|
|
131
|
+
expect(first.usage.input_token_details?.cache_creation).toBeGreaterThan(0);
|
|
132
|
+
expect(first.usage.input_token_details?.cache_read ?? 0).toBe(0);
|
|
133
|
+
|
|
134
|
+
await waitForCachePropagation();
|
|
135
|
+
|
|
136
|
+
const second = await runLiveTurn({
|
|
137
|
+
provider: Providers.BEDROCK,
|
|
138
|
+
providerLabel,
|
|
139
|
+
clientOptions,
|
|
140
|
+
runId: `${nonce}-second`,
|
|
141
|
+
threadId: `${nonce}-thread`,
|
|
142
|
+
stableInstructions,
|
|
143
|
+
dynamicInstructions: secondDynamicInstructions,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(second.text.toLowerCase()).toContain('bravo');
|
|
147
|
+
expect(second.usage.input_token_details?.cache_read).toBeGreaterThan(0);
|
|
148
|
+
}, 180_000);
|
|
149
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// src/agents/__tests__/AgentContext.test.ts
|
|
2
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
2
3
|
import { AgentContext } from '../AgentContext';
|
|
3
4
|
import { Providers } from '@/common';
|
|
5
|
+
import { addBedrockCacheControl } from '@/messages/cache';
|
|
4
6
|
import type * as t from '@/types';
|
|
5
7
|
|
|
6
8
|
describe('AgentContext', () => {
|
|
9
|
+
type TestSystemContentBlock =
|
|
10
|
+
| { type: 'text'; text: string; cache_control?: { type: 'ephemeral' } }
|
|
11
|
+
| { cachePoint: { type: 'default' } };
|
|
12
|
+
|
|
7
13
|
type ContextOptions = {
|
|
8
14
|
agentConfig?: Partial<t.AgentInputs>;
|
|
9
15
|
tokenCounter?: t.TokenCounter;
|
|
@@ -59,14 +65,161 @@ describe('AgentContext', () => {
|
|
|
59
65
|
expect(ctx.systemRunnable).toBeUndefined();
|
|
60
66
|
});
|
|
61
67
|
|
|
62
|
-
it('
|
|
68
|
+
it('keeps additional_instructions after stable instructions', async () => {
|
|
63
69
|
const ctx = createBasicContext({
|
|
64
70
|
agentConfig: {
|
|
65
71
|
instructions: 'Base instructions',
|
|
66
72
|
additional_instructions: 'Additional instructions',
|
|
67
73
|
},
|
|
68
74
|
});
|
|
69
|
-
|
|
75
|
+
|
|
76
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
77
|
+
expect(result[0].content).toBe(
|
|
78
|
+
'Base instructions\n\nAdditional instructions'
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('marks only stable system text for Anthropic prompt caching', async () => {
|
|
83
|
+
const ctx = createBasicContext({
|
|
84
|
+
agentConfig: {
|
|
85
|
+
provider: Providers.ANTHROPIC,
|
|
86
|
+
clientOptions: { model: 'claude-3-5-sonnet', promptCache: true },
|
|
87
|
+
instructions: 'Stable instructions',
|
|
88
|
+
additional_instructions: 'Dynamic instructions',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
93
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
94
|
+
expect(content).toHaveLength(2);
|
|
95
|
+
expect(content[0]).toMatchObject({
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: 'Stable instructions',
|
|
98
|
+
cache_control: { type: 'ephemeral' },
|
|
99
|
+
});
|
|
100
|
+
expect(content[1]).toEqual({
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: 'Dynamic instructions',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('omits Anthropic cache control when only dynamic system text exists', async () => {
|
|
107
|
+
const ctx = createBasicContext({
|
|
108
|
+
agentConfig: {
|
|
109
|
+
provider: Providers.ANTHROPIC,
|
|
110
|
+
clientOptions: { model: 'claude-3-5-sonnet', promptCache: true },
|
|
111
|
+
instructions: undefined,
|
|
112
|
+
additional_instructions: 'Dynamic only',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
117
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
118
|
+
expect(content).toEqual([{ type: 'text', text: 'Dynamic only' }]);
|
|
119
|
+
expect(content[0]).not.toHaveProperty('cache_control');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('keeps cross-run summaries in the dynamic Anthropic system tail', async () => {
|
|
123
|
+
const ctx = createBasicContext({
|
|
124
|
+
agentConfig: {
|
|
125
|
+
provider: Providers.ANTHROPIC,
|
|
126
|
+
clientOptions: { model: 'claude-3-5-sonnet', promptCache: true },
|
|
127
|
+
instructions: 'Stable instructions',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
ctx.setInitialSummary('Prior summary', 13);
|
|
131
|
+
|
|
132
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
133
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
134
|
+
expect(content).toHaveLength(2);
|
|
135
|
+
expect(content[0]).toHaveProperty('cache_control');
|
|
136
|
+
expect(content[1]).toEqual({
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: '## Conversation Summary\n\nPrior summary',
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('places the Bedrock cache point before dynamic system text', async () => {
|
|
143
|
+
const ctx = createBasicContext({
|
|
144
|
+
agentConfig: {
|
|
145
|
+
provider: Providers.BEDROCK,
|
|
146
|
+
clientOptions: {
|
|
147
|
+
model: 'anthropic.claude-3-5-sonnet',
|
|
148
|
+
promptCache: true,
|
|
149
|
+
},
|
|
150
|
+
instructions: 'Stable instructions',
|
|
151
|
+
additional_instructions: 'Dynamic instructions',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
156
|
+
const content = result[0].content as TestSystemContentBlock[];
|
|
157
|
+
expect(content).toEqual([
|
|
158
|
+
{ type: 'text', text: 'Stable instructions' },
|
|
159
|
+
{ cachePoint: { type: 'default' } },
|
|
160
|
+
{ type: 'text', text: 'Dynamic instructions' },
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses plain Bedrock system text when only dynamic system text exists', async () => {
|
|
165
|
+
const ctx = createBasicContext({
|
|
166
|
+
agentConfig: {
|
|
167
|
+
provider: Providers.BEDROCK,
|
|
168
|
+
clientOptions: {
|
|
169
|
+
model: 'anthropic.claude-3-5-sonnet',
|
|
170
|
+
promptCache: true,
|
|
171
|
+
},
|
|
172
|
+
instructions: undefined,
|
|
173
|
+
additional_instructions: 'Dynamic only',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
178
|
+
expect(result[0].content).toBe('Dynamic only');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('keeps non-cache providers as plain system text with promptCache-like options', async () => {
|
|
182
|
+
const clientOptions: t.OpenAIClientOptions & { promptCache: true } = {
|
|
183
|
+
modelName: 'gpt-4o-mini',
|
|
184
|
+
promptCache: true,
|
|
185
|
+
};
|
|
186
|
+
const ctx = createBasicContext({
|
|
187
|
+
agentConfig: {
|
|
188
|
+
provider: Providers.OPENAI,
|
|
189
|
+
clientOptions,
|
|
190
|
+
instructions: 'Stable instructions',
|
|
191
|
+
additional_instructions: 'Dynamic instructions',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await ctx.systemRunnable!.invoke([]);
|
|
196
|
+
expect(result[0].content).toBe(
|
|
197
|
+
'Stable instructions\n\nDynamic instructions'
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('preserves the Bedrock system cache point through message cache-control pass', async () => {
|
|
202
|
+
const ctx = createBasicContext({
|
|
203
|
+
agentConfig: {
|
|
204
|
+
provider: Providers.BEDROCK,
|
|
205
|
+
clientOptions: {
|
|
206
|
+
model: 'anthropic.claude-3-5-sonnet',
|
|
207
|
+
promptCache: true,
|
|
208
|
+
},
|
|
209
|
+
instructions: 'Stable instructions',
|
|
210
|
+
additional_instructions: 'Dynamic instructions',
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await ctx.systemRunnable!.invoke([
|
|
215
|
+
new HumanMessage('Hello'),
|
|
216
|
+
]);
|
|
217
|
+
const finalMessages = addBedrockCacheControl(result);
|
|
218
|
+
expect(finalMessages[0].content).toEqual([
|
|
219
|
+
{ type: 'text', text: 'Stable instructions' },
|
|
220
|
+
{ cachePoint: { type: 'default' } },
|
|
221
|
+
{ type: 'text', text: 'Dynamic instructions' },
|
|
222
|
+
]);
|
|
70
223
|
});
|
|
71
224
|
});
|
|
72
225
|
|