@librechat/agents 3.1.97 → 3.1.99
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/dist/cjs/graphs/Graph.cjs +6 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +16 -5
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +10 -0
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/toolCache.cjs +125 -0
- package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -0
- package/dist/cjs/messages/cache.cjs +17 -9
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +45 -8
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +6 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +6 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +16 -5
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +10 -0
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/toolCache.mjs +122 -0
- package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -0
- package/dist/esm/messages/cache.mjs +17 -9
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +45 -8
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +6 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/llm/bedrock/index.d.ts +16 -0
- package/dist/types/llm/bedrock/toolCache.d.ts +4 -0
- package/dist/types/messages/cache.d.ts +2 -2
- package/dist/types/types/llm.d.ts +2 -2
- package/package.json +1 -1
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +332 -0
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +504 -0
- package/src/graphs/Graph.ts +14 -0
- package/src/langfuseToolOutputTracing.ts +26 -7
- package/src/llm/bedrock/index.ts +32 -1
- package/src/llm/bedrock/llm.spec.ts +154 -1
- package/src/llm/bedrock/toolCache.test.ts +131 -0
- package/src/llm/bedrock/toolCache.ts +191 -0
- package/src/messages/cache.test.ts +97 -38
- package/src/messages/cache.ts +18 -10
- package/src/messages/prune.ts +55 -17
- package/src/specs/langfuse-tool-output-tracing.test.ts +28 -0
- package/src/specs/prune.test.ts +193 -0
- package/src/tools/ToolNode.ts +7 -1
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +6 -0
- package/src/types/llm.ts +2 -2
|
@@ -287,23 +287,28 @@ type TestMsg = {
|
|
|
287
287
|
};
|
|
288
288
|
|
|
289
289
|
describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
290
|
-
it('returns input
|
|
290
|
+
it('returns empty input unchanged and caches a single user message', () => {
|
|
291
291
|
const empty: TestMsg[] = [];
|
|
292
292
|
expect(addBedrockCacheControl(empty)).toEqual(empty);
|
|
293
293
|
const single: TestMsg[] = [{ role: 'user', content: 'only' }];
|
|
294
|
-
expect(addBedrockCacheControl(single)).toEqual(
|
|
294
|
+
expect(addBedrockCacheControl(single)[0].content).toEqual([
|
|
295
|
+
{ type: ContentTypes.TEXT, text: 'only' },
|
|
296
|
+
{ cachePoint: { type: 'default' } },
|
|
297
|
+
]);
|
|
295
298
|
});
|
|
296
299
|
|
|
297
|
-
it('wraps string content and appends separate cachePoint block', () => {
|
|
300
|
+
it('wraps latest user string content and appends separate cachePoint block', () => {
|
|
298
301
|
const messages: TestMsg[] = [
|
|
299
302
|
{ role: 'user', content: 'Hello' },
|
|
300
303
|
{ role: 'assistant', content: [{ type: ContentTypes.TEXT, text: 'Hi' }] },
|
|
301
304
|
];
|
|
302
305
|
const result = addBedrockCacheControl(messages);
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
expect(
|
|
306
|
-
expect(
|
|
306
|
+
const user = result[0].content as MessageContentComplex[];
|
|
307
|
+
const assistant = result[1].content as MessageContentComplex[];
|
|
308
|
+
expect(Array.isArray(user)).toBe(true);
|
|
309
|
+
expect(user[0]).toEqual({ type: ContentTypes.TEXT, text: 'Hello' });
|
|
310
|
+
expect(user[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
311
|
+
expect(assistant).toEqual([{ type: ContentTypes.TEXT, text: 'Hi' }]);
|
|
307
312
|
});
|
|
308
313
|
|
|
309
314
|
it('inserts cachePoint after the last text when multiple text blocks exist', () => {
|
|
@@ -345,20 +350,21 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
345
350
|
|
|
346
351
|
expect(second[0]).toEqual({ type: ContentTypes.TEXT, text: 'Reply A' });
|
|
347
352
|
expect(second[1]).toEqual({ type: ContentTypes.TEXT, text: 'Reply B' });
|
|
348
|
-
expect(second
|
|
353
|
+
expect(second).toHaveLength(2);
|
|
349
354
|
});
|
|
350
355
|
|
|
351
|
-
it('skips
|
|
356
|
+
it('skips empty arrays and caches the latest non-empty user message', () => {
|
|
352
357
|
const messages: TestMsg[] = [
|
|
353
358
|
{ role: 'user', content: [] },
|
|
354
359
|
{ role: 'assistant', content: [] },
|
|
355
|
-
{ role: 'user', content: '
|
|
360
|
+
{ role: 'user', content: 'latest cacheable user message' },
|
|
356
361
|
];
|
|
357
362
|
|
|
358
363
|
const result = addBedrockCacheControl(messages);
|
|
359
364
|
|
|
360
365
|
const first = result[0].content as MessageContentComplex[];
|
|
361
366
|
const second = result[1].content as MessageContentComplex[];
|
|
367
|
+
const third = result[2].content as MessageContentComplex[];
|
|
362
368
|
|
|
363
369
|
expect(Array.isArray(first)).toBe(true);
|
|
364
370
|
expect(first.length).toBe(0);
|
|
@@ -366,39 +372,51 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
366
372
|
expect(Array.isArray(second)).toBe(true);
|
|
367
373
|
expect(second.length).toBe(0);
|
|
368
374
|
expect(second[0]).not.toEqual({ cachePoint: { type: 'default' } });
|
|
375
|
+
|
|
376
|
+
expect(third).toEqual([
|
|
377
|
+
{ type: ContentTypes.TEXT, text: 'latest cacheable user message' },
|
|
378
|
+
{ cachePoint: { type: 'default' } },
|
|
379
|
+
]);
|
|
369
380
|
});
|
|
370
381
|
|
|
371
|
-
it('skips
|
|
382
|
+
it('skips empty strings and caches the latest non-empty user message', () => {
|
|
372
383
|
const messages: TestMsg[] = [
|
|
373
384
|
{ role: 'user', content: '' },
|
|
374
385
|
{ role: 'assistant', content: '' },
|
|
375
|
-
{ role: 'user', content: '
|
|
386
|
+
{ role: 'user', content: 'latest cacheable user message' },
|
|
376
387
|
];
|
|
377
388
|
|
|
378
389
|
const result = addBedrockCacheControl(messages);
|
|
379
390
|
|
|
380
391
|
expect(result[0].content).toBe('');
|
|
381
392
|
expect(result[1].content).toBe('');
|
|
393
|
+
expect(result[2].content).toEqual([
|
|
394
|
+
{ type: ContentTypes.TEXT, text: 'latest cacheable user message' },
|
|
395
|
+
{ cachePoint: { type: 'default' } },
|
|
396
|
+
]);
|
|
382
397
|
});
|
|
383
398
|
|
|
384
399
|
/** (I don't think this will ever occur in actual use, but its the only branch left uncovered so I'm covering it */
|
|
385
|
-
it('skips messages with non-string, non-array content
|
|
400
|
+
it('skips messages with non-string, non-array content', () => {
|
|
386
401
|
const messages: TestMsg[] = [
|
|
387
402
|
{
|
|
388
403
|
role: 'user',
|
|
389
|
-
content: [{ type: ContentTypes.TEXT, text: '
|
|
404
|
+
content: [{ type: ContentTypes.TEXT, text: 'Older user message' }],
|
|
390
405
|
},
|
|
391
406
|
{ role: 'assistant', content: undefined },
|
|
392
407
|
{
|
|
393
408
|
role: 'user',
|
|
394
|
-
content: [{ type: ContentTypes.TEXT, text: '
|
|
409
|
+
content: [{ type: ContentTypes.TEXT, text: 'Latest user message' }],
|
|
395
410
|
},
|
|
396
411
|
];
|
|
397
412
|
|
|
398
413
|
const result = addBedrockCacheControl(messages);
|
|
399
414
|
|
|
400
415
|
const last = result[2].content as MessageContentComplex[];
|
|
401
|
-
expect(last[0]).toEqual({
|
|
416
|
+
expect(last[0]).toEqual({
|
|
417
|
+
type: ContentTypes.TEXT,
|
|
418
|
+
text: 'Latest user message',
|
|
419
|
+
});
|
|
402
420
|
expect(last[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
403
421
|
|
|
404
422
|
expect(result[1].content).toBeUndefined();
|
|
@@ -406,7 +424,7 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
406
424
|
const first = result[0].content as MessageContentComplex[];
|
|
407
425
|
expect(first[0]).toEqual({
|
|
408
426
|
type: ContentTypes.TEXT,
|
|
409
|
-
text: '
|
|
427
|
+
text: 'Older user message',
|
|
410
428
|
});
|
|
411
429
|
expect(first[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
412
430
|
});
|
|
@@ -511,7 +529,7 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
511
529
|
expect(systemContent[2]).toHaveProperty('cache_control');
|
|
512
530
|
});
|
|
513
531
|
|
|
514
|
-
it('skips serialized system messages while adding cache
|
|
532
|
+
it('skips serialized system messages while adding a cache point to the latest user turn', () => {
|
|
515
533
|
const messages: TestMsg[] = [
|
|
516
534
|
{
|
|
517
535
|
role: 'system',
|
|
@@ -575,7 +593,7 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
575
593
|
type: ContentTypes.TEXT,
|
|
576
594
|
text: 'Sure! The capital of France is Paris.',
|
|
577
595
|
});
|
|
578
|
-
expect(assistant
|
|
596
|
+
expect(assistant).toHaveLength(1);
|
|
579
597
|
});
|
|
580
598
|
|
|
581
599
|
it('is idempotent - calling multiple times does not add duplicate cache points', () => {
|
|
@@ -601,12 +619,11 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
601
619
|
});
|
|
602
620
|
expect(firstContent[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
603
621
|
|
|
604
|
-
expect(secondContent.length).toBe(
|
|
622
|
+
expect(secondContent.length).toBe(1);
|
|
605
623
|
expect(secondContent[0]).toEqual({
|
|
606
624
|
type: ContentTypes.TEXT,
|
|
607
625
|
text: 'First response',
|
|
608
626
|
});
|
|
609
|
-
expect(secondContent[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
610
627
|
|
|
611
628
|
const result2 = addBedrockCacheControl(result1);
|
|
612
629
|
const firstContentAfter = result2[0].content as MessageContentComplex[];
|
|
@@ -619,15 +636,14 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
619
636
|
});
|
|
620
637
|
expect(firstContentAfter[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
621
638
|
|
|
622
|
-
expect(secondContentAfter.length).toBe(
|
|
639
|
+
expect(secondContentAfter.length).toBe(1);
|
|
623
640
|
expect(secondContentAfter[0]).toEqual({
|
|
624
641
|
type: ContentTypes.TEXT,
|
|
625
642
|
text: 'First response',
|
|
626
643
|
});
|
|
627
|
-
expect(secondContentAfter[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
628
644
|
});
|
|
629
645
|
|
|
630
|
-
it('
|
|
646
|
+
it('strips stale cache points and caches the latest user messages in multi-agent scenarios', () => {
|
|
631
647
|
const messages: TestMsg[] = [
|
|
632
648
|
{
|
|
633
649
|
role: 'user',
|
|
@@ -647,9 +663,17 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
647
663
|
];
|
|
648
664
|
|
|
649
665
|
const result = addBedrockCacheControl(messages);
|
|
666
|
+
const firstContent = result[0].content as MessageContentComplex[];
|
|
650
667
|
const lastContent = result[2].content as MessageContentComplex[];
|
|
651
668
|
const secondLastContent = result[1].content as MessageContentComplex[];
|
|
652
669
|
|
|
670
|
+
expect(firstContent.length).toBe(2);
|
|
671
|
+
expect(firstContent[0]).toEqual({
|
|
672
|
+
type: ContentTypes.TEXT,
|
|
673
|
+
text: 'Hello',
|
|
674
|
+
});
|
|
675
|
+
expect(firstContent[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
676
|
+
|
|
653
677
|
expect(lastContent.length).toBe(2);
|
|
654
678
|
expect(lastContent[0]).toEqual({
|
|
655
679
|
type: ContentTypes.TEXT,
|
|
@@ -657,12 +681,11 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
657
681
|
});
|
|
658
682
|
expect(lastContent[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
659
683
|
|
|
660
|
-
expect(secondLastContent.length).toBe(
|
|
684
|
+
expect(secondLastContent.length).toBe(1);
|
|
661
685
|
expect(secondLastContent[0]).toEqual({
|
|
662
686
|
type: ContentTypes.TEXT,
|
|
663
687
|
text: 'Response from agent 1',
|
|
664
688
|
});
|
|
665
|
-
expect(secondLastContent[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
666
689
|
});
|
|
667
690
|
|
|
668
691
|
it('skips cachePoint on AI messages with only whitespace text and reasoning (tool-call scenario)', () => {
|
|
@@ -705,6 +728,42 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
705
728
|
cachePoint: { type: 'default' },
|
|
706
729
|
});
|
|
707
730
|
});
|
|
731
|
+
|
|
732
|
+
it('keeps cachePoint on the stable user boundary through tool loops', () => {
|
|
733
|
+
const messages: BaseMessage[] = [
|
|
734
|
+
new HumanMessage('Use the stable prompt context.'),
|
|
735
|
+
new AIMessage({
|
|
736
|
+
content: 'I will call the first tool.',
|
|
737
|
+
tool_calls: [{ id: 'call_1', name: 'lookup', args: { step: 1 } }],
|
|
738
|
+
}),
|
|
739
|
+
new ToolMessage({
|
|
740
|
+
content: 'volatile tool result 1',
|
|
741
|
+
tool_call_id: 'call_1',
|
|
742
|
+
}),
|
|
743
|
+
new AIMessage({
|
|
744
|
+
content: 'I will call the second tool.',
|
|
745
|
+
tool_calls: [{ id: 'call_2', name: 'lookup', args: { step: 2 } }],
|
|
746
|
+
}),
|
|
747
|
+
new ToolMessage({
|
|
748
|
+
content: 'volatile tool result 2',
|
|
749
|
+
tool_call_id: 'call_2',
|
|
750
|
+
}),
|
|
751
|
+
];
|
|
752
|
+
|
|
753
|
+
const result = addBedrockCacheControl(messages);
|
|
754
|
+
|
|
755
|
+
const userContent = result[0].content as MessageContentComplex[];
|
|
756
|
+
expect(userContent[userContent.length - 1]).toEqual({
|
|
757
|
+
cachePoint: { type: 'default' },
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
for (const message of result.slice(1)) {
|
|
761
|
+
const content = message.content;
|
|
762
|
+
expect(
|
|
763
|
+
Array.isArray(content) && content.some((block) => 'cachePoint' in block)
|
|
764
|
+
).toBe(false);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
708
767
|
});
|
|
709
768
|
|
|
710
769
|
describe('stripAnthropicCacheControl', () => {
|
|
@@ -947,7 +1006,7 @@ describe('Multi-agent provider interoperability', () => {
|
|
|
947
1006
|
expect('cache_control' in secondContent[0]).toBe(false);
|
|
948
1007
|
|
|
949
1008
|
expect(firstContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
950
|
-
expect(secondContent.some((b) => 'cachePoint' in b)).toBe(
|
|
1009
|
+
expect(secondContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
951
1010
|
});
|
|
952
1011
|
|
|
953
1012
|
it('strips Bedrock cache using separate function (backwards compat)', () => {
|
|
@@ -1142,7 +1201,7 @@ describe('Immutability - addBedrockCacheControl does not mutate original message
|
|
|
1142
1201
|
expect(typeof originalMessages[1].content).toBe('string');
|
|
1143
1202
|
|
|
1144
1203
|
expect(Array.isArray(result[0].content)).toBe(true);
|
|
1145
|
-
expect(
|
|
1204
|
+
expect(result[1].content).toBe('Hi there');
|
|
1146
1205
|
});
|
|
1147
1206
|
|
|
1148
1207
|
it('should not mutate original messages when adding cache points to array content', () => {
|
|
@@ -1178,9 +1237,9 @@ describe('Immutability - addBedrockCacheControl does not mutate original message
|
|
|
1178
1237
|
const resultFirstContent = result[0].content as MessageContentComplex[];
|
|
1179
1238
|
const resultSecondContent = result[1].content as MessageContentComplex[];
|
|
1180
1239
|
expect(resultFirstContent.length).toBe(originalFirstContentLength + 1);
|
|
1181
|
-
expect(resultSecondContent.length).toBe(originalSecondContentLength
|
|
1240
|
+
expect(resultSecondContent.length).toBe(originalSecondContentLength);
|
|
1182
1241
|
expect(resultFirstContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
1183
|
-
expect(resultSecondContent.some((b) => 'cachePoint' in b)).toBe(
|
|
1242
|
+
expect(resultSecondContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
1184
1243
|
});
|
|
1185
1244
|
|
|
1186
1245
|
it('should not mutate original messages when stripping existing cache control', () => {
|
|
@@ -1339,13 +1398,13 @@ describe('Multi-turn cache cleanup', () => {
|
|
|
1339
1398
|
const lastContent = result[3].content as MessageContentComplex[];
|
|
1340
1399
|
const secondLastContent = result[2].content as MessageContentComplex[];
|
|
1341
1400
|
|
|
1342
|
-
expect(lastContent.some((b) => 'cachePoint' in b)).toBe(
|
|
1401
|
+
expect(lastContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
1343
1402
|
expect(secondLastContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
1344
1403
|
|
|
1345
1404
|
const firstContent = result[0].content as MessageContentComplex[];
|
|
1346
1405
|
const secondContent = result[1].content as MessageContentComplex[];
|
|
1347
1406
|
|
|
1348
|
-
expect(firstContent.some((b) => 'cachePoint' in b)).toBe(
|
|
1407
|
+
expect(firstContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
1349
1408
|
expect(secondContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
1350
1409
|
});
|
|
1351
1410
|
|
|
@@ -1561,14 +1620,14 @@ describe('OpenRouter prompt caching (reuses addCacheControl)', () => {
|
|
|
1561
1620
|
const lastUser = converted[2];
|
|
1562
1621
|
|
|
1563
1622
|
expect(Array.isArray(firstUser.content)).toBe(true);
|
|
1564
|
-
expect(
|
|
1565
|
-
|
|
1566
|
-
)
|
|
1623
|
+
expect((firstUser.content as CacheControlBlock[])[0]).toHaveProperty(
|
|
1624
|
+
'cache_control'
|
|
1625
|
+
);
|
|
1567
1626
|
|
|
1568
1627
|
expect(Array.isArray(lastUser.content)).toBe(true);
|
|
1569
|
-
expect(
|
|
1570
|
-
|
|
1571
|
-
)
|
|
1628
|
+
expect((lastUser.content as CacheControlBlock[])[0]).toHaveProperty(
|
|
1629
|
+
'cache_control'
|
|
1630
|
+
);
|
|
1572
1631
|
});
|
|
1573
1632
|
|
|
1574
1633
|
it('strips Bedrock cache before applying OpenRouter/Anthropic cache', () => {
|
package/src/messages/cache.ts
CHANGED
|
@@ -470,11 +470,11 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
/**
|
|
473
|
-
* Adds Bedrock Converse API cache points to the
|
|
473
|
+
* Adds Bedrock Converse API cache points to the latest two user messages.
|
|
474
474
|
* Inserts `{ cachePoint: { type: 'default' } }` as a separate content block
|
|
475
475
|
* immediately after the last text block in each targeted message.
|
|
476
476
|
* Strips ALL existing cache control (both Bedrock and Anthropic formats) from all messages,
|
|
477
|
-
* then adds fresh cache points to the
|
|
477
|
+
* then adds fresh cache points to the latest two non-tool user messages in a single backward pass.
|
|
478
478
|
* This ensures we don't accumulate stale cache points across multiple turns.
|
|
479
479
|
* Returns a new array - only clones messages that require modification.
|
|
480
480
|
* @param messages - The array of message objects.
|
|
@@ -483,12 +483,12 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
|
|
|
483
483
|
export function addBedrockCacheControl<
|
|
484
484
|
T extends MessageWithContent & { getType?: () => string; role?: string },
|
|
485
485
|
>(messages: T[]): T[] {
|
|
486
|
-
if (!Array.isArray(messages) || messages.length
|
|
486
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
487
487
|
return messages;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
const updatedMessages: T[] = [...messages];
|
|
491
|
-
let
|
|
491
|
+
let cachePointsAdded = 0;
|
|
492
492
|
|
|
493
493
|
for (let i = updatedMessages.length - 1; i >= 0; i--) {
|
|
494
494
|
const originalMessage = updatedMessages[i];
|
|
@@ -510,21 +510,27 @@ export function addBedrockCacheControl<
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
const isToolMessage = messageType === 'tool' || messageRole === 'tool';
|
|
513
|
+
const isUserMessage = messageType === 'human' || messageRole === 'user';
|
|
513
514
|
const content = originalMessage.content;
|
|
515
|
+
const hasSerializationProps =
|
|
516
|
+
'lc_kwargs' in originalMessage ||
|
|
517
|
+
'lc_serializable' in originalMessage ||
|
|
518
|
+
'lc_namespace' in originalMessage;
|
|
514
519
|
const hasArrayContent = Array.isArray(content);
|
|
515
520
|
const isEmptyString = typeof content === 'string' && content === '';
|
|
516
521
|
const needsCacheAdd =
|
|
517
|
-
|
|
522
|
+
cachePointsAdded < 2 &&
|
|
523
|
+
isUserMessage &&
|
|
518
524
|
!isToolMessage &&
|
|
519
525
|
!isEmptyString &&
|
|
520
526
|
(typeof content === 'string' || hasArrayContent);
|
|
521
527
|
|
|
522
|
-
if (!needsCacheAdd && !hasArrayContent) {
|
|
528
|
+
if (!needsCacheAdd && !hasArrayContent && !hasSerializationProps) {
|
|
523
529
|
continue;
|
|
524
530
|
}
|
|
525
531
|
|
|
526
|
-
let workingContent: MessageContentComplex[];
|
|
527
|
-
let modified =
|
|
532
|
+
let workingContent: string | MessageContentComplex[];
|
|
533
|
+
let modified = hasSerializationProps;
|
|
528
534
|
|
|
529
535
|
if (hasArrayContent) {
|
|
530
536
|
// Single pass: clone blocks, strip cache markers, find last
|
|
@@ -563,14 +569,16 @@ export function addBedrockCacheControl<
|
|
|
563
569
|
workingContent.splice(lastNonEmptyTextIndex + 1, 0, {
|
|
564
570
|
cachePoint: { type: 'default' },
|
|
565
571
|
} as MessageContentComplex);
|
|
566
|
-
|
|
572
|
+
cachePointsAdded++;
|
|
567
573
|
}
|
|
568
574
|
} else if (typeof content === 'string' && needsCacheAdd) {
|
|
569
575
|
workingContent = [
|
|
570
576
|
{ type: ContentTypes.TEXT, text: content },
|
|
571
577
|
{ cachePoint: { type: 'default' } } as MessageContentComplex,
|
|
572
578
|
];
|
|
573
|
-
|
|
579
|
+
cachePointsAdded++;
|
|
580
|
+
} else if (typeof content === 'string' && hasSerializationProps) {
|
|
581
|
+
workingContent = content;
|
|
574
582
|
} else {
|
|
575
583
|
continue;
|
|
576
584
|
}
|
package/src/messages/prune.ts
CHANGED
|
@@ -563,7 +563,7 @@ function addThinkingBlock(
|
|
|
563
563
|
},
|
|
564
564
|
];
|
|
565
565
|
/** Edge case, the message already has the thinking block */
|
|
566
|
-
if (content[0]
|
|
566
|
+
if (content[0]?.type === thinkingBlock.type) {
|
|
567
567
|
return message;
|
|
568
568
|
}
|
|
569
569
|
content.unshift(thinkingBlock);
|
|
@@ -608,6 +608,33 @@ export type PruningResult = {
|
|
|
608
608
|
thinkingStartIndex?: number;
|
|
609
609
|
};
|
|
610
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Locates a reasoning block in assistant content. Reasoning blocks carry
|
|
613
|
+
* provider-specific `type` tags: Anthropic emits `thinking`, while Bedrock and
|
|
614
|
+
* OpenAI-compatible reasoning providers (DeepSeek-R1, DashScope/Qwen-thinking)
|
|
615
|
+
* emit `reasoning_content`. DeepSeek/Qwen route through the `THINKING` default
|
|
616
|
+
* even though their blocks are `reasoning_content` and aren't normalized
|
|
617
|
+
* upstream, so for the `THINKING` case we also accept `reasoning_content` — this
|
|
618
|
+
* is what fixes issue #191.
|
|
619
|
+
*
|
|
620
|
+
* The broadening is intentionally one-directional. A Bedrock run
|
|
621
|
+
* (`REASONING_CONTENT`) must NOT match an Anthropic `thinking` block: the
|
|
622
|
+
* Bedrock input converter rejects `thinking` blocks outright
|
|
623
|
+
* (`src/llm/bedrock/utils/message_inputs.ts`), so reattaching one to a
|
|
624
|
+
* surviving message would make the request fail before it is sent.
|
|
625
|
+
*/
|
|
626
|
+
function findReasoningBlock(
|
|
627
|
+
content: MessageContentComplex[],
|
|
628
|
+
reasoningType: ContentTypes
|
|
629
|
+
): ThinkingContentText | ReasoningContentText | undefined {
|
|
630
|
+
return content.find(
|
|
631
|
+
(part) =>
|
|
632
|
+
part.type === reasoningType ||
|
|
633
|
+
(reasoningType === ContentTypes.THINKING &&
|
|
634
|
+
part.type === ContentTypes.REASONING_CONTENT)
|
|
635
|
+
) as ThinkingContentText | ReasoningContentText | undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
611
638
|
/**
|
|
612
639
|
* Processes an array of messages and returns a context of messages that fit within a specified token limit.
|
|
613
640
|
* It iterates over the messages from newest to oldest, adding them to the context until the token limit is reached.
|
|
@@ -670,9 +697,7 @@ export function getMessagesWithinTokenLimit({
|
|
|
670
697
|
if (_thinkingStartIndex > -1) {
|
|
671
698
|
const thinkingMessageContent = messages[_thinkingStartIndex]?.content;
|
|
672
699
|
if (Array.isArray(thinkingMessageContent)) {
|
|
673
|
-
thinkingBlock = thinkingMessageContent
|
|
674
|
-
(content) => content.type === reasoningType
|
|
675
|
-
) as ThinkingContentText | undefined;
|
|
700
|
+
thinkingBlock = findReasoningBlock(thinkingMessageContent, reasoningType);
|
|
676
701
|
}
|
|
677
702
|
}
|
|
678
703
|
|
|
@@ -705,9 +730,10 @@ export function getMessagesWithinTokenLimit({
|
|
|
705
730
|
messageType === 'ai' &&
|
|
706
731
|
Array.isArray(poppedMessage.content)
|
|
707
732
|
) {
|
|
708
|
-
thinkingBlock =
|
|
709
|
-
|
|
710
|
-
|
|
733
|
+
thinkingBlock = findReasoningBlock(
|
|
734
|
+
poppedMessage.content,
|
|
735
|
+
reasoningType
|
|
736
|
+
);
|
|
711
737
|
thinkingStartIndex = thinkingBlock != null ? currentIndex : -1;
|
|
712
738
|
}
|
|
713
739
|
/**
|
|
@@ -811,16 +837,28 @@ export function getMessagesWithinTokenLimit({
|
|
|
811
837
|
return result;
|
|
812
838
|
}
|
|
813
839
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
840
|
+
/**
|
|
841
|
+
* A trailing reasoning sequence was detected but its block could not be
|
|
842
|
+
* located in the surviving context. Rather than throw — which permanently
|
|
843
|
+
* bricks the conversation, re-firing on every retry of the same thread (see
|
|
844
|
+
* issue #191) — return the partially-pruned context and let the provider
|
|
845
|
+
* surface a real, recoverable error if the payload is genuinely malformed.
|
|
846
|
+
* Strict providers (Anthropic) reject it cleanly; lenient ones (DeepSeek,
|
|
847
|
+
* Qwen) proceed. The pruner cannot know which applies, so it must not be the
|
|
848
|
+
* one to make the failure fatal.
|
|
849
|
+
*/
|
|
850
|
+
if ((thinkingEndIndex > -1 && thinkingStartIndex < 0) || !thinkingBlock) {
|
|
851
|
+
/**
|
|
852
|
+
* No block was located, so any `thinkingStartIndex` set above came from a
|
|
853
|
+
* stale carried-over index pointing at a block-less message. Drop it:
|
|
854
|
+
* `createPruneMessages` persists the returned index as
|
|
855
|
+
* `runThinkingStartIndex`, and a stale value would suppress the trailing
|
|
856
|
+
* scan (`thinkingStartIndex < 0`) on later turns, causing a real reasoning
|
|
857
|
+
* block to be missed and never reattached.
|
|
858
|
+
*/
|
|
859
|
+
delete result.thinkingStartIndex;
|
|
860
|
+
result.context = context.reverse() as BaseMessage[];
|
|
861
|
+
return result;
|
|
824
862
|
}
|
|
825
863
|
|
|
826
864
|
let assistantIndex = -1;
|
|
@@ -180,6 +180,34 @@ describe('Langfuse tool output tracing redaction', () => {
|
|
|
180
180
|
).toBe(false);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
it('classifies LangGraph tool-node spans as Langfuse tool observations', () => {
|
|
184
|
+
const span = createSpan('tool_batch', {
|
|
185
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'span',
|
|
186
|
+
[`${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`]:
|
|
187
|
+
'tools=agent_1',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
redactLangfuseSpanToolOutputs(span, createConfig());
|
|
191
|
+
|
|
192
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]).toBe(
|
|
193
|
+
'tool'
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('does not reclassify non-tool LangGraph spans', () => {
|
|
198
|
+
const span = createSpan('agent=agent_1', {
|
|
199
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'span',
|
|
200
|
+
[`${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`]:
|
|
201
|
+
'agent=agent_1',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
redactLangfuseSpanToolOutputs(span, createConfig());
|
|
205
|
+
|
|
206
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]).toBe(
|
|
207
|
+
'span'
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
183
211
|
it('redacts raw tool observation output when tool output tracing is disabled', () => {
|
|
184
212
|
const span = createSpan('execute_sql', {
|
|
185
213
|
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
|