@librechat/agents 3.3.3 → 3.3.4
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/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +342 -109
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +64 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +19 -2
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +218 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +54 -41
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +349 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +65 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +7 -7
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +218 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +54 -41
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +27 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/graphs/Graph.ts +698 -200
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
- package/src/llm/bedrock/utils/message_inputs.ts +86 -13
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +196 -2
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +363 -91
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/prune.ts +996 -183
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.ts +23 -15
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +147 -68
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/stream.ts +1 -1
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
package/src/specs/prune.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isBaseMessage,
|
|
10
10
|
SystemMessage,
|
|
11
11
|
AIMessageChunk,
|
|
12
|
+
ChatMessage,
|
|
12
13
|
} from '@langchain/core/messages';
|
|
13
14
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
14
15
|
import type { UsageMetadata } from '@langchain/core/messages';
|
|
@@ -20,7 +21,10 @@ import {
|
|
|
20
21
|
sanitizeOrphanToolBlocks,
|
|
21
22
|
enforceOriginalContentCap,
|
|
22
23
|
ORIGINAL_CONTENT_MAX_CHARS,
|
|
24
|
+
calculateMaxToolCallInputChars,
|
|
23
25
|
createPruneMessages,
|
|
26
|
+
projectToolCallInputs,
|
|
27
|
+
serializeToolCallInput,
|
|
24
28
|
} from '@/messages/prune';
|
|
25
29
|
import { getLLMConfig } from '@/utils/llmConfig';
|
|
26
30
|
import { ensureThinkingBlockInMessages } from '@/messages/format';
|
|
@@ -430,7 +434,269 @@ describe('Prune Messages Tests', () => {
|
|
|
430
434
|
expect(result.context.length).toBe(3);
|
|
431
435
|
expect(result.context).toEqual(messages);
|
|
432
436
|
expect(result.messagesToRefine).toEqual([]);
|
|
433
|
-
expect(result.remainingContextTokens).
|
|
437
|
+
expect(result.remainingContextTokens).toBe(
|
|
438
|
+
95 -
|
|
439
|
+
Object.values(indexTokenCountMap).reduce(
|
|
440
|
+
(total, count) => total + count,
|
|
441
|
+
0
|
|
442
|
+
) -
|
|
443
|
+
3
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('recounts and compacts omitted structured tool results before sending', () => {
|
|
448
|
+
const tokenCounter: t.TokenCounter = (message) => {
|
|
449
|
+
const content =
|
|
450
|
+
typeof message.content === 'string'
|
|
451
|
+
? message.content
|
|
452
|
+
: JSON.stringify(message.content);
|
|
453
|
+
return content.length;
|
|
454
|
+
};
|
|
455
|
+
const toolCallId = 'tc-structured';
|
|
456
|
+
const messages: BaseMessage[] = [
|
|
457
|
+
new HumanMessage('query the table'),
|
|
458
|
+
new AIMessage({
|
|
459
|
+
content: '',
|
|
460
|
+
tool_calls: [
|
|
461
|
+
{
|
|
462
|
+
id: toolCallId,
|
|
463
|
+
name: 'run_select_query',
|
|
464
|
+
args: {},
|
|
465
|
+
type: 'tool_call',
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
}),
|
|
469
|
+
new ToolMessage({
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: ContentTypes.TEXT,
|
|
473
|
+
text: JSON.stringify(
|
|
474
|
+
Array.from({ length: 20 }, (_, index) => ({
|
|
475
|
+
id: index,
|
|
476
|
+
value: `${'x'.repeat(100)}-${index}`,
|
|
477
|
+
}))
|
|
478
|
+
),
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
tool_call_id: toolCallId,
|
|
482
|
+
name: 'run_select_query',
|
|
483
|
+
}),
|
|
484
|
+
new AIMessage('The query returned 20 rows.'),
|
|
485
|
+
new HumanMessage('compact context'),
|
|
486
|
+
];
|
|
487
|
+
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
488
|
+
0: tokenCounter(messages[0]),
|
|
489
|
+
1: tokenCounter(messages[1]),
|
|
490
|
+
3: tokenCounter(messages[3]),
|
|
491
|
+
4: tokenCounter(messages[4]),
|
|
492
|
+
};
|
|
493
|
+
const pruneMessages = createPruneMessages({
|
|
494
|
+
maxTokens: 2_000,
|
|
495
|
+
startIndex: messages.length,
|
|
496
|
+
tokenCounter,
|
|
497
|
+
indexTokenCountMap,
|
|
498
|
+
reserveRatio: 0,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const result = pruneMessages({ messages });
|
|
502
|
+
|
|
503
|
+
const structuredResult = result.context.find(
|
|
504
|
+
(message) => message.getType() === 'tool'
|
|
505
|
+
);
|
|
506
|
+
expect(structuredResult).toBeDefined();
|
|
507
|
+
expect(typeof structuredResult?.content).toBe('string');
|
|
508
|
+
expect(structuredResult?.content).toContain('truncated');
|
|
509
|
+
expect(result.indexTokenCountMap[2]).toBeGreaterThan(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('reconciles a stale cached legacy function-call count', () => {
|
|
513
|
+
const tokenCounter: t.TokenCounter = (message) => {
|
|
514
|
+
const functionCall = message.additional_kwargs.function_call;
|
|
515
|
+
return (
|
|
516
|
+
(typeof message.content === 'string' ? message.content.length : 0) +
|
|
517
|
+
(functionCall != null ? JSON.stringify(functionCall).length : 0) +
|
|
518
|
+
1
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
const message = new AIMessage({
|
|
522
|
+
content: '',
|
|
523
|
+
additional_kwargs: {
|
|
524
|
+
function_call: {
|
|
525
|
+
name: 'legacy_lookup',
|
|
526
|
+
arguments: `{"query":"${'x'.repeat(10_000)}"}`,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
const messages: BaseMessage[] = [message];
|
|
531
|
+
const pruneMessages = createPruneMessages({
|
|
532
|
+
maxTokens: 2_000,
|
|
533
|
+
startIndex: messages.length,
|
|
534
|
+
tokenCounter,
|
|
535
|
+
indexTokenCountMap: { 0: 1 },
|
|
536
|
+
reserveRatio: 0,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const result = pruneMessages({ messages });
|
|
540
|
+
const projectedFunctionCall = result.context[0].additional_kwargs
|
|
541
|
+
.function_call as { arguments: string };
|
|
542
|
+
|
|
543
|
+
expect(result.context[0]).not.toBe(message);
|
|
544
|
+
expect(projectedFunctionCall.arguments.length).toBeLessThanOrEqual(
|
|
545
|
+
calculateMaxToolCallInputChars(2_000)
|
|
546
|
+
);
|
|
547
|
+
expect(result.indexTokenCountMap[0]).toBe(
|
|
548
|
+
tokenCounter(result.context[0])
|
|
549
|
+
);
|
|
550
|
+
expect(result.indexTokenCountMap[0]).toBeGreaterThan(1);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('preserves provider output usage for an uncounted current legacy call', () => {
|
|
554
|
+
const message = new AIMessage({
|
|
555
|
+
content: '',
|
|
556
|
+
additional_kwargs: {
|
|
557
|
+
function_call: {
|
|
558
|
+
name: 'legacy_lookup',
|
|
559
|
+
arguments: '{}',
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
const pruneMessages = createPruneMessages({
|
|
564
|
+
maxTokens: 2_000,
|
|
565
|
+
startIndex: 0,
|
|
566
|
+
tokenCounter: () => 123,
|
|
567
|
+
indexTokenCountMap: {},
|
|
568
|
+
reserveRatio: 0,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const result = pruneMessages({
|
|
572
|
+
messages: [message],
|
|
573
|
+
usageMetadata: {
|
|
574
|
+
input_tokens: 10,
|
|
575
|
+
output_tokens: 7,
|
|
576
|
+
total_tokens: 17,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(result.indexTokenCountMap[0]).toBe(7);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('excludes a corrected tiny new tool count from calibration input', () => {
|
|
584
|
+
const tokenCounter = createTestTokenCounter();
|
|
585
|
+
const history: BaseMessage[] = [
|
|
586
|
+
new HumanMessage('h'.repeat(50)),
|
|
587
|
+
new AIMessage('a'.repeat(50)),
|
|
588
|
+
];
|
|
589
|
+
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
590
|
+
0: 50,
|
|
591
|
+
1: 50,
|
|
592
|
+
};
|
|
593
|
+
const pruneMessages = createPruneMessages({
|
|
594
|
+
maxTokens: 10_000,
|
|
595
|
+
startIndex: history.length,
|
|
596
|
+
tokenCounter,
|
|
597
|
+
indexTokenCountMap,
|
|
598
|
+
reserveRatio: 0,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const first = pruneMessages({
|
|
602
|
+
messages: history,
|
|
603
|
+
usageMetadata: {
|
|
604
|
+
input_tokens: 100,
|
|
605
|
+
output_tokens: 10,
|
|
606
|
+
total_tokens: 110,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
expect(first.calibrationRatio).toBe(1);
|
|
610
|
+
|
|
611
|
+
const toolCallId = 'new-output-with-tiny-count';
|
|
612
|
+
const secondMessages: BaseMessage[] = [
|
|
613
|
+
...history,
|
|
614
|
+
new HumanMessage('q'.repeat(50)),
|
|
615
|
+
new AIMessage({
|
|
616
|
+
content: '',
|
|
617
|
+
tool_calls: [
|
|
618
|
+
{
|
|
619
|
+
id: toolCallId,
|
|
620
|
+
name: 'run_select_query',
|
|
621
|
+
args: {},
|
|
622
|
+
type: 'tool_call',
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
}),
|
|
626
|
+
new ToolMessage({
|
|
627
|
+
content: 'r'.repeat(50),
|
|
628
|
+
tool_call_id: toolCallId,
|
|
629
|
+
}),
|
|
630
|
+
];
|
|
631
|
+
indexTokenCountMap[2] = 50;
|
|
632
|
+
indexTokenCountMap[4] = 1;
|
|
633
|
+
|
|
634
|
+
const second = pruneMessages({
|
|
635
|
+
messages: secondMessages,
|
|
636
|
+
usageMetadata: {
|
|
637
|
+
input_tokens: 100,
|
|
638
|
+
output_tokens: 10,
|
|
639
|
+
total_tokens: 110,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(second.indexTokenCountMap[4]).toBe(50);
|
|
644
|
+
expect(second.calibrationRatio).toBe(1);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('preserves full structured output for summarization before masking', () => {
|
|
648
|
+
const tokenCounter: t.TokenCounter = (message) => {
|
|
649
|
+
const content =
|
|
650
|
+
typeof message.content === 'string'
|
|
651
|
+
? message.content
|
|
652
|
+
: JSON.stringify(message.content);
|
|
653
|
+
return content.length;
|
|
654
|
+
};
|
|
655
|
+
const fullResult = JSON.stringify(
|
|
656
|
+
Array.from({ length: 50 }, (_, index) => ({
|
|
657
|
+
id: index,
|
|
658
|
+
value: `${'x'.repeat(100)}-${index}`,
|
|
659
|
+
}))
|
|
660
|
+
);
|
|
661
|
+
const messages: BaseMessage[] = [
|
|
662
|
+
new HumanMessage('query the table'),
|
|
663
|
+
new AIMessage({
|
|
664
|
+
content: '',
|
|
665
|
+
tool_calls: [
|
|
666
|
+
{
|
|
667
|
+
id: 'tc-summary-structured',
|
|
668
|
+
name: 'run_select_query',
|
|
669
|
+
args: {},
|
|
670
|
+
type: 'tool_call',
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
}),
|
|
674
|
+
new ToolMessage({
|
|
675
|
+
content: [{ type: ContentTypes.TEXT, text: fullResult }],
|
|
676
|
+
tool_call_id: 'tc-summary-structured',
|
|
677
|
+
}),
|
|
678
|
+
new AIMessage('The query returned 50 rows.'),
|
|
679
|
+
new HumanMessage('summarize the context'),
|
|
680
|
+
];
|
|
681
|
+
const indexTokenCountMap: Record<string, number | undefined> = {};
|
|
682
|
+
for (let i = 0; i < messages.length; i++) {
|
|
683
|
+
indexTokenCountMap[i] = tokenCounter(messages[i]);
|
|
684
|
+
}
|
|
685
|
+
const pruneMessages = createPruneMessages({
|
|
686
|
+
maxTokens: 500,
|
|
687
|
+
startIndex: messages.length,
|
|
688
|
+
tokenCounter,
|
|
689
|
+
indexTokenCountMap,
|
|
690
|
+
reserveRatio: 0,
|
|
691
|
+
summarizationEnabled: true,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const result = pruneMessages({ messages });
|
|
695
|
+
|
|
696
|
+
expect(result.newOriginalToolContent?.get(2)).toBe(fullResult);
|
|
697
|
+
expect(messages[2].content).not.toEqual([
|
|
698
|
+
{ type: ContentTypes.TEXT, text: fullResult },
|
|
699
|
+
]);
|
|
434
700
|
});
|
|
435
701
|
|
|
436
702
|
it('should prune messages when over token limit', () => {
|
|
@@ -718,6 +984,179 @@ describe('Prune Messages Tests', () => {
|
|
|
718
984
|
expect(repaired.reclaimedTokens).toBeGreaterThan(0);
|
|
719
985
|
});
|
|
720
986
|
|
|
987
|
+
it('strips orphan raw Responses computer calls with parsed tool calls', () => {
|
|
988
|
+
const tokenCounter = createTestTokenCounter();
|
|
989
|
+
const computerCall = new AIMessage({
|
|
990
|
+
content: 'Taking a screenshot.',
|
|
991
|
+
tool_calls: [
|
|
992
|
+
{
|
|
993
|
+
id: 'computer-orphan',
|
|
994
|
+
name: 'computer_use',
|
|
995
|
+
args: { action: { type: 'screenshot' } },
|
|
996
|
+
type: 'tool_call',
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
response_metadata: {
|
|
1000
|
+
output: [
|
|
1001
|
+
{
|
|
1002
|
+
type: 'computer_call',
|
|
1003
|
+
id: 'computer-item',
|
|
1004
|
+
call_id: 'computer-orphan',
|
|
1005
|
+
action: { type: 'screenshot' },
|
|
1006
|
+
},
|
|
1007
|
+
],
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
const context: BaseMessage[] = [
|
|
1011
|
+
new HumanMessage('take a screenshot'),
|
|
1012
|
+
computerCall,
|
|
1013
|
+
];
|
|
1014
|
+
|
|
1015
|
+
const repaired = repairOrphanedToolMessages({
|
|
1016
|
+
context,
|
|
1017
|
+
allMessages: context,
|
|
1018
|
+
tokenCounter,
|
|
1019
|
+
indexTokenCountMap: {
|
|
1020
|
+
0: tokenCounter(context[0]),
|
|
1021
|
+
1: tokenCounter(context[1]),
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
const repairedAI = repaired.context[1] as AIMessage;
|
|
1025
|
+
|
|
1026
|
+
expect(repairedAI.tool_calls ?? []).toHaveLength(0);
|
|
1027
|
+
expect(repairedAI.response_metadata.output).toEqual([]);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('strips orphan computer calls from the LangChain tool_outputs fallback', () => {
|
|
1031
|
+
const tokenCounter = createTestTokenCounter();
|
|
1032
|
+
const computerCall = new AIMessage({
|
|
1033
|
+
content: 'Taking a screenshot.',
|
|
1034
|
+
additional_kwargs: {
|
|
1035
|
+
tool_outputs: [
|
|
1036
|
+
{
|
|
1037
|
+
type: 'computer_call',
|
|
1038
|
+
call_id: 'computer-fallback-orphan',
|
|
1039
|
+
action: { type: 'screenshot' },
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
const context: BaseMessage[] = [
|
|
1045
|
+
new HumanMessage('take a screenshot'),
|
|
1046
|
+
computerCall,
|
|
1047
|
+
];
|
|
1048
|
+
|
|
1049
|
+
const repaired = repairOrphanedToolMessages({
|
|
1050
|
+
context,
|
|
1051
|
+
allMessages: context,
|
|
1052
|
+
tokenCounter,
|
|
1053
|
+
indexTokenCountMap: {
|
|
1054
|
+
0: tokenCounter(context[0]),
|
|
1055
|
+
1: tokenCounter(context[1]),
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
const repairedAI = repaired.context[1] as AIMessage;
|
|
1059
|
+
|
|
1060
|
+
expect(repairedAI.additional_kwargs.tool_outputs).toEqual([]);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('retains an empty-content AI parent when one raw call remains paired', () => {
|
|
1064
|
+
const tokenCounter = createTestTokenCounter();
|
|
1065
|
+
const computerCall = new AIMessage({
|
|
1066
|
+
content: [],
|
|
1067
|
+
tool_calls: [
|
|
1068
|
+
{
|
|
1069
|
+
id: 'computer-keep',
|
|
1070
|
+
name: 'computer_use',
|
|
1071
|
+
args: { action: { type: 'screenshot' } },
|
|
1072
|
+
type: 'tool_call',
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
id: 'computer-drop',
|
|
1076
|
+
name: 'computer_use',
|
|
1077
|
+
args: { action: { type: 'screenshot' } },
|
|
1078
|
+
type: 'tool_call',
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
response_metadata: {
|
|
1082
|
+
output: [
|
|
1083
|
+
{
|
|
1084
|
+
type: 'computer_call',
|
|
1085
|
+
call_id: 'computer-keep',
|
|
1086
|
+
action: { type: 'screenshot' },
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
type: 'computer_call',
|
|
1090
|
+
call_id: 'computer-drop',
|
|
1091
|
+
action: { type: 'screenshot' },
|
|
1092
|
+
},
|
|
1093
|
+
],
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
const computerOutput = new ToolMessage({
|
|
1097
|
+
content: 'data:image/png;base64,AA==',
|
|
1098
|
+
tool_call_id: 'computer-keep',
|
|
1099
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
1100
|
+
});
|
|
1101
|
+
const context: BaseMessage[] = [computerCall, computerOutput];
|
|
1102
|
+
|
|
1103
|
+
const repaired = repairOrphanedToolMessages({
|
|
1104
|
+
context,
|
|
1105
|
+
allMessages: context,
|
|
1106
|
+
tokenCounter,
|
|
1107
|
+
indexTokenCountMap: {
|
|
1108
|
+
0: tokenCounter(computerCall),
|
|
1109
|
+
1: tokenCounter(computerOutput),
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
expect(repaired.context).toHaveLength(2);
|
|
1114
|
+
const repairedAI = repaired.context[0] as AIMessage;
|
|
1115
|
+
expect(repairedAI.tool_calls?.map((call) => call.id)).toEqual([
|
|
1116
|
+
'computer-keep',
|
|
1117
|
+
]);
|
|
1118
|
+
expect(repairedAI.response_metadata.output).toEqual([
|
|
1119
|
+
expect.objectContaining({ call_id: 'computer-keep' }),
|
|
1120
|
+
]);
|
|
1121
|
+
expect(repaired.context[1]).toBe(computerOutput);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it('recognizes raw computer calls on generic assistant messages', () => {
|
|
1125
|
+
const tokenCounter = createTestTokenCounter();
|
|
1126
|
+
const computerCall = new ChatMessage({
|
|
1127
|
+
role: 'assistant',
|
|
1128
|
+
content: '',
|
|
1129
|
+
response_metadata: {
|
|
1130
|
+
output: [
|
|
1131
|
+
{
|
|
1132
|
+
type: 'computer_call',
|
|
1133
|
+
call_id: 'generic-computer',
|
|
1134
|
+
action: { type: 'screenshot' },
|
|
1135
|
+
},
|
|
1136
|
+
],
|
|
1137
|
+
},
|
|
1138
|
+
});
|
|
1139
|
+
const computerOutput = new ToolMessage({
|
|
1140
|
+
content: 'data:image/png;base64,AA==',
|
|
1141
|
+
tool_call_id: 'generic-computer',
|
|
1142
|
+
additional_kwargs: { type: 'computer_call_output' },
|
|
1143
|
+
});
|
|
1144
|
+
const context: BaseMessage[] = [computerCall, computerOutput];
|
|
1145
|
+
|
|
1146
|
+
const repaired = repairOrphanedToolMessages({
|
|
1147
|
+
context,
|
|
1148
|
+
allMessages: context,
|
|
1149
|
+
tokenCounter,
|
|
1150
|
+
indexTokenCountMap: {
|
|
1151
|
+
0: tokenCounter(computerCall),
|
|
1152
|
+
1: tokenCounter(computerOutput),
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
expect(repaired.context).toEqual(context);
|
|
1157
|
+
expect(repaired.droppedOrphanCount).toBe(0);
|
|
1158
|
+
});
|
|
1159
|
+
|
|
721
1160
|
it('should drop AI message entirely when it has only tool_use blocks with no text', () => {
|
|
722
1161
|
const tokenCounter = createTestTokenCounter();
|
|
723
1162
|
const context = [
|
|
@@ -999,6 +1438,505 @@ describe('Prune Messages Tests', () => {
|
|
|
999
1438
|
});
|
|
1000
1439
|
});
|
|
1001
1440
|
|
|
1441
|
+
describe('projectToolCallInputs', () => {
|
|
1442
|
+
it('uses the shared 15%-of-context cap with a 200K ceiling', () => {
|
|
1443
|
+
expect(calculateMaxToolCallInputChars()).toBe(200_000);
|
|
1444
|
+
expect(calculateMaxToolCallInputChars(0)).toBe(200_000);
|
|
1445
|
+
expect(calculateMaxToolCallInputChars(1_000)).toBe(600);
|
|
1446
|
+
expect(calculateMaxToolCallInputChars(1_000_000)).toBe(200_000);
|
|
1447
|
+
expect(serializeToolCallInput(undefined, 4)).toBe('null');
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it('returns the original array when every input is already safe and bounded', () => {
|
|
1451
|
+
const messages: BaseMessage[] = [
|
|
1452
|
+
new HumanMessage('Run it'),
|
|
1453
|
+
new AIMessage({
|
|
1454
|
+
content: '',
|
|
1455
|
+
tool_calls: [
|
|
1456
|
+
{ id: 'safe-call', name: 'lookup', args: { query: 'safe' } },
|
|
1457
|
+
],
|
|
1458
|
+
}),
|
|
1459
|
+
];
|
|
1460
|
+
|
|
1461
|
+
const projected = projectToolCallInputs(messages, 1_000);
|
|
1462
|
+
|
|
1463
|
+
expect(projected).toBe(messages);
|
|
1464
|
+
expect(projected[1]).toBe(messages[1]);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it('copy-on-write projects inline input and tool_calls args on AIMessageChunk', () => {
|
|
1468
|
+
let toJSONCalls = 0;
|
|
1469
|
+
const chunk = new AIMessageChunk({
|
|
1470
|
+
content: [
|
|
1471
|
+
{ type: 'text', text: 'Running.' },
|
|
1472
|
+
{
|
|
1473
|
+
type: 'tool_use',
|
|
1474
|
+
id: 'chunk-call',
|
|
1475
|
+
name: 'evaluate_script',
|
|
1476
|
+
input: { code: 'x'.repeat(2_000) },
|
|
1477
|
+
},
|
|
1478
|
+
],
|
|
1479
|
+
id: 'chunk-id',
|
|
1480
|
+
additional_kwargs: { marker: 'keep' },
|
|
1481
|
+
response_metadata: { model: 'chunk-model' },
|
|
1482
|
+
tool_calls: [
|
|
1483
|
+
{
|
|
1484
|
+
id: 'chunk-call',
|
|
1485
|
+
name: 'evaluate_script',
|
|
1486
|
+
args: {
|
|
1487
|
+
query: 'safe',
|
|
1488
|
+
toJSON() {
|
|
1489
|
+
toJSONCalls++;
|
|
1490
|
+
return { code: 'y'.repeat(2_000_000) };
|
|
1491
|
+
},
|
|
1492
|
+
},
|
|
1493
|
+
},
|
|
1494
|
+
],
|
|
1495
|
+
});
|
|
1496
|
+
chunk.tool_call_chunks = [
|
|
1497
|
+
{
|
|
1498
|
+
id: 'chunk-call',
|
|
1499
|
+
index: 0,
|
|
1500
|
+
name: 'evaluate_script',
|
|
1501
|
+
args: '',
|
|
1502
|
+
type: 'tool_call_chunk',
|
|
1503
|
+
},
|
|
1504
|
+
];
|
|
1505
|
+
const messages: BaseMessage[] = [chunk];
|
|
1506
|
+
|
|
1507
|
+
const projected = projectToolCallInputs(messages, 200);
|
|
1508
|
+
|
|
1509
|
+
expect(projected).not.toBe(messages);
|
|
1510
|
+
expect(projected[0]).not.toBe(chunk);
|
|
1511
|
+
expect(projected[0]).toBeInstanceOf(AIMessageChunk);
|
|
1512
|
+
expect(toJSONCalls).toBe(0);
|
|
1513
|
+
const projectedChunk = projected[0] as AIMessageChunk;
|
|
1514
|
+
expect(projectedChunk.id).toBe('chunk-id');
|
|
1515
|
+
expect(projectedChunk.additional_kwargs).toEqual({ marker: 'keep' });
|
|
1516
|
+
expect(projectedChunk.response_metadata).toEqual({
|
|
1517
|
+
model: 'chunk-model',
|
|
1518
|
+
});
|
|
1519
|
+
expect(projectedChunk.tool_call_chunks).toEqual(chunk.tool_call_chunks);
|
|
1520
|
+
const inlineToolUse = (
|
|
1521
|
+
projectedChunk.content as Array<Record<string, unknown>>
|
|
1522
|
+
).find((block) => block.type === 'tool_use');
|
|
1523
|
+
expect(inlineToolUse?.input).toMatchObject({
|
|
1524
|
+
_truncated: expect.stringContaining('truncated'),
|
|
1525
|
+
_originalChars: expect.any(Number),
|
|
1526
|
+
});
|
|
1527
|
+
expect(projectedChunk.tool_calls?.[0].args).toEqual({ query: 'safe' });
|
|
1528
|
+
expect(
|
|
1529
|
+
(
|
|
1530
|
+
(chunk.content as Array<Record<string, unknown>>)[1].input as {
|
|
1531
|
+
code: string;
|
|
1532
|
+
}
|
|
1533
|
+
).code
|
|
1534
|
+
).toHaveLength(2_000);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it('normalizes cyclic and bigint args into provider-safe JSON values', () => {
|
|
1538
|
+
const cyclic: Record<string, unknown> = { query: 'safe' };
|
|
1539
|
+
cyclic.self = cyclic;
|
|
1540
|
+
const messages: BaseMessage[] = [
|
|
1541
|
+
new AIMessage({
|
|
1542
|
+
content: '',
|
|
1543
|
+
tool_calls: [
|
|
1544
|
+
{
|
|
1545
|
+
id: 'non-json-call',
|
|
1546
|
+
name: 'lookup',
|
|
1547
|
+
args: { cyclic, count: 7n },
|
|
1548
|
+
},
|
|
1549
|
+
],
|
|
1550
|
+
}),
|
|
1551
|
+
];
|
|
1552
|
+
|
|
1553
|
+
const projected = projectToolCallInputs(messages, 1_000);
|
|
1554
|
+
|
|
1555
|
+
expect(projected).not.toBe(messages);
|
|
1556
|
+
const args = (projected[0] as AIMessage).tool_calls?.[0].args as {
|
|
1557
|
+
cyclic: { self: string };
|
|
1558
|
+
count: string;
|
|
1559
|
+
};
|
|
1560
|
+
expect(args.cyclic.self).toBe('[Circular]');
|
|
1561
|
+
expect(args.count).toBe('7');
|
|
1562
|
+
expect(() => JSON.stringify(args)).not.toThrow();
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
it('bounds legacy additional_kwargs.function_call arguments', () => {
|
|
1566
|
+
const message = new AIMessage({
|
|
1567
|
+
content: '',
|
|
1568
|
+
additional_kwargs: {
|
|
1569
|
+
marker: 'preserved',
|
|
1570
|
+
function_call: {
|
|
1571
|
+
name: 'legacy_lookup',
|
|
1572
|
+
arguments: `{"query":"${'x'.repeat(2_000)}"}`,
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
});
|
|
1576
|
+
const messages: BaseMessage[] = [message];
|
|
1577
|
+
|
|
1578
|
+
const projected = projectToolCallInputs(messages, 200);
|
|
1579
|
+
|
|
1580
|
+
expect(projected).not.toBe(messages);
|
|
1581
|
+
expect(projected[0]).not.toBe(message);
|
|
1582
|
+
expect(projected[0].additional_kwargs.marker).toBe('preserved');
|
|
1583
|
+
const projectedFunctionCall = projected[0].additional_kwargs
|
|
1584
|
+
.function_call as {
|
|
1585
|
+
name: string;
|
|
1586
|
+
arguments: string;
|
|
1587
|
+
};
|
|
1588
|
+
expect(projectedFunctionCall.name).toBe('legacy_lookup');
|
|
1589
|
+
expect(projectedFunctionCall.arguments.length).toBeLessThanOrEqual(200);
|
|
1590
|
+
expect(() => JSON.parse(projectedFunctionCall.arguments)).not.toThrow();
|
|
1591
|
+
expect(
|
|
1592
|
+
(
|
|
1593
|
+
message.additional_kwargs.function_call as {
|
|
1594
|
+
arguments: string;
|
|
1595
|
+
}
|
|
1596
|
+
).arguments.length
|
|
1597
|
+
).toBeGreaterThan(2_000);
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
it('normalizes legacy function calls without invoking serialization hooks', () => {
|
|
1601
|
+
const toJSON = jest.fn(() => ({
|
|
1602
|
+
name: 'legacy_lookup',
|
|
1603
|
+
arguments: 'x'.repeat(500_000),
|
|
1604
|
+
}));
|
|
1605
|
+
const originalFunctionCall = {
|
|
1606
|
+
name: 'legacy_lookup',
|
|
1607
|
+
arguments: '{}',
|
|
1608
|
+
toJSON,
|
|
1609
|
+
};
|
|
1610
|
+
const message = new AIMessage({
|
|
1611
|
+
content: '',
|
|
1612
|
+
additional_kwargs: {
|
|
1613
|
+
function_call: originalFunctionCall,
|
|
1614
|
+
},
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
const [projected] = projectToolCallInputs([message], 200);
|
|
1618
|
+
const projectedFunctionCall = projected.additional_kwargs
|
|
1619
|
+
.function_call as {
|
|
1620
|
+
name: string;
|
|
1621
|
+
arguments: string;
|
|
1622
|
+
};
|
|
1623
|
+
const serialized = JSON.stringify(projected.additional_kwargs);
|
|
1624
|
+
|
|
1625
|
+
expect(projected).not.toBe(message);
|
|
1626
|
+
expect(projectedFunctionCall).toEqual({
|
|
1627
|
+
name: 'legacy_lookup',
|
|
1628
|
+
arguments: '{}',
|
|
1629
|
+
});
|
|
1630
|
+
expect(Object.getPrototypeOf(projectedFunctionCall)).toBe(
|
|
1631
|
+
Object.prototype
|
|
1632
|
+
);
|
|
1633
|
+
expect(serialized.length).toBeLessThanOrEqual(300);
|
|
1634
|
+
expect(toJSON).not.toHaveBeenCalled();
|
|
1635
|
+
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
it('omits a proxied legacy function call without propagating descriptor traps', () => {
|
|
1639
|
+
let descriptorCalls = 0;
|
|
1640
|
+
const legacyProxy = new Proxy(
|
|
1641
|
+
{},
|
|
1642
|
+
{
|
|
1643
|
+
getOwnPropertyDescriptor() {
|
|
1644
|
+
descriptorCalls++;
|
|
1645
|
+
throw new Error('descriptor trap');
|
|
1646
|
+
},
|
|
1647
|
+
}
|
|
1648
|
+
);
|
|
1649
|
+
const message = new AIMessage({
|
|
1650
|
+
content: '',
|
|
1651
|
+
additional_kwargs: {},
|
|
1652
|
+
});
|
|
1653
|
+
(message.additional_kwargs as Record<string, unknown>).function_call =
|
|
1654
|
+
legacyProxy;
|
|
1655
|
+
|
|
1656
|
+
expect(() => projectToolCallInputs([message], 200)).not.toThrow();
|
|
1657
|
+
const [projected] = projectToolCallInputs([message], 200);
|
|
1658
|
+
|
|
1659
|
+
expect(projected.additional_kwargs.function_call).toBeUndefined();
|
|
1660
|
+
expect(descriptorCalls).toBe(0);
|
|
1661
|
+
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it('shadows proxy-backed legacy metadata without invoking its get trap', () => {
|
|
1665
|
+
let getCalls = 0;
|
|
1666
|
+
const hugeFunctionCall = {
|
|
1667
|
+
name: 'legacy_lookup',
|
|
1668
|
+
arguments: 'x'.repeat(500_000),
|
|
1669
|
+
};
|
|
1670
|
+
const message = new AIMessage({
|
|
1671
|
+
content: '',
|
|
1672
|
+
additional_kwargs: {},
|
|
1673
|
+
});
|
|
1674
|
+
message.additional_kwargs = new Proxy(
|
|
1675
|
+
{},
|
|
1676
|
+
{
|
|
1677
|
+
get(_target, property) {
|
|
1678
|
+
getCalls++;
|
|
1679
|
+
return property === 'function_call' ? hugeFunctionCall : undefined;
|
|
1680
|
+
},
|
|
1681
|
+
getOwnPropertyDescriptor() {
|
|
1682
|
+
return undefined;
|
|
1683
|
+
},
|
|
1684
|
+
getPrototypeOf() {
|
|
1685
|
+
return null;
|
|
1686
|
+
},
|
|
1687
|
+
}
|
|
1688
|
+
);
|
|
1689
|
+
|
|
1690
|
+
const [projected] = projectToolCallInputs([message], 200);
|
|
1691
|
+
|
|
1692
|
+
expect(getCalls).toBe(0);
|
|
1693
|
+
expect(projected).not.toBe(message);
|
|
1694
|
+
expect(projected.additional_kwargs.function_call).toBeUndefined();
|
|
1695
|
+
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
it('shadows legacy calls beyond the bounded prototype walk', () => {
|
|
1699
|
+
const inherited = {
|
|
1700
|
+
function_call: {
|
|
1701
|
+
name: 'legacy_lookup',
|
|
1702
|
+
arguments: 'x'.repeat(500_000),
|
|
1703
|
+
},
|
|
1704
|
+
};
|
|
1705
|
+
let additionalKwargs = inherited;
|
|
1706
|
+
for (let i = 0; i < 101; i++) {
|
|
1707
|
+
additionalKwargs = Object.create(additionalKwargs) as typeof inherited;
|
|
1708
|
+
}
|
|
1709
|
+
const message = new AIMessage({
|
|
1710
|
+
content: '',
|
|
1711
|
+
additional_kwargs: {},
|
|
1712
|
+
});
|
|
1713
|
+
message.additional_kwargs = additionalKwargs;
|
|
1714
|
+
|
|
1715
|
+
const [projected] = projectToolCallInputs([message], 200);
|
|
1716
|
+
|
|
1717
|
+
expect(projected).not.toBe(message);
|
|
1718
|
+
expect(
|
|
1719
|
+
Object.prototype.hasOwnProperty.call(
|
|
1720
|
+
projected.additional_kwargs,
|
|
1721
|
+
'function_call'
|
|
1722
|
+
)
|
|
1723
|
+
).toBe(true);
|
|
1724
|
+
expect(projected.additional_kwargs.function_call).toBeUndefined();
|
|
1725
|
+
expect(projectToolCallInputs([projected], 200)[0]).toBe(projected);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
it('stabilizes an omitted invalid legacy function call', () => {
|
|
1729
|
+
const message = new AIMessage({
|
|
1730
|
+
content: '',
|
|
1731
|
+
additional_kwargs: {},
|
|
1732
|
+
});
|
|
1733
|
+
(message.additional_kwargs as Record<string, unknown>).function_call =
|
|
1734
|
+
null;
|
|
1735
|
+
|
|
1736
|
+
const [first] = projectToolCallInputs([message], 200);
|
|
1737
|
+
const [second] = projectToolCallInputs([first], 200);
|
|
1738
|
+
|
|
1739
|
+
expect(first).not.toBe(message);
|
|
1740
|
+
expect(first.additional_kwargs.function_call).toBeUndefined();
|
|
1741
|
+
expect(second).toBe(first);
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
it('projects every provider-consumed tool-call representation to the same bounded wire args', () => {
|
|
1745
|
+
let getterCalls = 0;
|
|
1746
|
+
let toJSONCalls = 0;
|
|
1747
|
+
const adversarialArgs: Record<string, unknown> = {
|
|
1748
|
+
query: 'safe',
|
|
1749
|
+
payload: 'x'.repeat(2_000),
|
|
1750
|
+
};
|
|
1751
|
+
Object.defineProperty(adversarialArgs, 'expanded', {
|
|
1752
|
+
enumerable: true,
|
|
1753
|
+
get() {
|
|
1754
|
+
getterCalls++;
|
|
1755
|
+
return 'y'.repeat(2_000_000);
|
|
1756
|
+
},
|
|
1757
|
+
});
|
|
1758
|
+
adversarialArgs.self = adversarialArgs;
|
|
1759
|
+
adversarialArgs.toJSON = (): Record<string, string> => {
|
|
1760
|
+
toJSONCalls++;
|
|
1761
|
+
return { payload: 'z'.repeat(2_000_000) };
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
const message = new AIMessageChunk({
|
|
1765
|
+
content: [
|
|
1766
|
+
{
|
|
1767
|
+
type: 'tool_use',
|
|
1768
|
+
id: 'inline-use',
|
|
1769
|
+
name: 'evaluate',
|
|
1770
|
+
input: { code: 'i'.repeat(2_000) },
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
type: 'tool_call',
|
|
1774
|
+
id: 'inline-call',
|
|
1775
|
+
name: 'evaluate',
|
|
1776
|
+
args: { code: 'a'.repeat(2_000) },
|
|
1777
|
+
tool_call: {
|
|
1778
|
+
type: 'tool_call',
|
|
1779
|
+
id: 'nested-call',
|
|
1780
|
+
name: 'evaluate',
|
|
1781
|
+
args: { code: 'n'.repeat(2_000) },
|
|
1782
|
+
},
|
|
1783
|
+
},
|
|
1784
|
+
],
|
|
1785
|
+
tool_calls: [
|
|
1786
|
+
{
|
|
1787
|
+
id: 'shared-call',
|
|
1788
|
+
name: 'evaluate',
|
|
1789
|
+
args: adversarialArgs,
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
id: 'custom-call',
|
|
1793
|
+
name: 'shell',
|
|
1794
|
+
args: { input: 'c'.repeat(2_000) },
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
id: 'computer-call',
|
|
1798
|
+
name: 'computer_use',
|
|
1799
|
+
args: { action: { type: 'type', text: 't'.repeat(2_000) } },
|
|
1800
|
+
},
|
|
1801
|
+
],
|
|
1802
|
+
additional_kwargs: {
|
|
1803
|
+
tool_calls: [
|
|
1804
|
+
{
|
|
1805
|
+
id: 'shared-call',
|
|
1806
|
+
type: 'function',
|
|
1807
|
+
function: {
|
|
1808
|
+
name: 'evaluate',
|
|
1809
|
+
arguments: `{"code":"${'r'.repeat(2_000)}"}`,
|
|
1810
|
+
},
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
id: 'raw-only',
|
|
1814
|
+
type: 'function',
|
|
1815
|
+
function: {
|
|
1816
|
+
name: 'evaluate',
|
|
1817
|
+
arguments: `{"code":"${'o'.repeat(2_000)}"}`,
|
|
1818
|
+
},
|
|
1819
|
+
},
|
|
1820
|
+
],
|
|
1821
|
+
},
|
|
1822
|
+
response_metadata: {
|
|
1823
|
+
output: [
|
|
1824
|
+
{
|
|
1825
|
+
type: 'function_call',
|
|
1826
|
+
id: 'fc_shared',
|
|
1827
|
+
call_id: 'shared-call',
|
|
1828
|
+
name: 'evaluate',
|
|
1829
|
+
arguments: `{"code":"${'w'.repeat(2_000)}"}`,
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
type: 'function_call',
|
|
1833
|
+
id: 'fc_raw',
|
|
1834
|
+
call_id: 'response-only',
|
|
1835
|
+
name: 'evaluate',
|
|
1836
|
+
arguments: `{"code":"${'q'.repeat(2_000)}"}`,
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
type: 'custom_tool_call',
|
|
1840
|
+
id: 'ctc_shared',
|
|
1841
|
+
call_id: 'custom-call',
|
|
1842
|
+
name: 'shell',
|
|
1843
|
+
input: 'c'.repeat(2_000),
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
type: 'computer_call',
|
|
1847
|
+
id: 'cc_shared',
|
|
1848
|
+
call_id: 'computer-call',
|
|
1849
|
+
action: { type: 'type', text: 't'.repeat(2_000) },
|
|
1850
|
+
},
|
|
1851
|
+
],
|
|
1852
|
+
},
|
|
1853
|
+
});
|
|
1854
|
+
Object.defineProperty(message, 'projectionMarker', {
|
|
1855
|
+
configurable: false,
|
|
1856
|
+
enumerable: false,
|
|
1857
|
+
value: 'preserved',
|
|
1858
|
+
writable: false,
|
|
1859
|
+
});
|
|
1860
|
+
const messages: BaseMessage[] = [message];
|
|
1861
|
+
|
|
1862
|
+
const projected = projectToolCallInputs(messages, 200);
|
|
1863
|
+
|
|
1864
|
+
expect(projected).not.toBe(messages);
|
|
1865
|
+
expect(projected[0]).toBeInstanceOf(AIMessageChunk);
|
|
1866
|
+
expect(getterCalls).toBe(0);
|
|
1867
|
+
expect(toJSONCalls).toBe(0);
|
|
1868
|
+
expect(
|
|
1869
|
+
Object.getOwnPropertyDescriptor(projected[0], 'projectionMarker')
|
|
1870
|
+
).toEqual(Object.getOwnPropertyDescriptor(message, 'projectionMarker'));
|
|
1871
|
+
|
|
1872
|
+
const projectedMessage = projected[0] as AIMessageChunk;
|
|
1873
|
+
const content = projectedMessage.content as Array<
|
|
1874
|
+
Record<string, unknown>
|
|
1875
|
+
>;
|
|
1876
|
+
const inlineUse = content[0];
|
|
1877
|
+
const inlineCall = content[1];
|
|
1878
|
+
const nestedCall = inlineCall.tool_call as Record<string, unknown>;
|
|
1879
|
+
for (const input of [
|
|
1880
|
+
inlineUse.input,
|
|
1881
|
+
inlineCall.args,
|
|
1882
|
+
nestedCall.args,
|
|
1883
|
+
projectedMessage.tool_calls?.[0].args,
|
|
1884
|
+
]) {
|
|
1885
|
+
const serialized = serializeToolCallInput(input, 200);
|
|
1886
|
+
expect(serialized.length).toBeLessThanOrEqual(200);
|
|
1887
|
+
expect(() => JSON.parse(serialized)).not.toThrow();
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
const canonicalArguments = serializeToolCallInput(
|
|
1891
|
+
projectedMessage.tool_calls?.[0].args,
|
|
1892
|
+
200
|
|
1893
|
+
);
|
|
1894
|
+
const rawToolCalls = projectedMessage.additional_kwargs
|
|
1895
|
+
.tool_calls as Array<{
|
|
1896
|
+
id: string;
|
|
1897
|
+
function: { arguments: string };
|
|
1898
|
+
}>;
|
|
1899
|
+
expect(rawToolCalls[0].function.arguments).toBe(canonicalArguments);
|
|
1900
|
+
expect(rawToolCalls[1].function.arguments.length).toBeLessThanOrEqual(
|
|
1901
|
+
200
|
|
1902
|
+
);
|
|
1903
|
+
expect(() =>
|
|
1904
|
+
JSON.parse(rawToolCalls[1].function.arguments)
|
|
1905
|
+
).not.toThrow();
|
|
1906
|
+
|
|
1907
|
+
const responseOutput = projectedMessage.response_metadata
|
|
1908
|
+
.output as Array<{
|
|
1909
|
+
type: string;
|
|
1910
|
+
call_id: string;
|
|
1911
|
+
arguments?: string;
|
|
1912
|
+
input?: string;
|
|
1913
|
+
action?: unknown;
|
|
1914
|
+
}>;
|
|
1915
|
+
expect(responseOutput[0].arguments).toBe(canonicalArguments);
|
|
1916
|
+
expect(responseOutput[1].arguments?.length).toBeLessThanOrEqual(200);
|
|
1917
|
+
expect(() => JSON.parse(responseOutput[1].arguments ?? '')).not.toThrow();
|
|
1918
|
+
expect(responseOutput[2].input?.length).toBeLessThanOrEqual(200);
|
|
1919
|
+
expect(
|
|
1920
|
+
serializeToolCallInput(responseOutput[3].action, 200).length
|
|
1921
|
+
).toBeLessThanOrEqual(200);
|
|
1922
|
+
|
|
1923
|
+
expect(
|
|
1924
|
+
(
|
|
1925
|
+
message.additional_kwargs.tool_calls as Array<{
|
|
1926
|
+
function: { arguments: string };
|
|
1927
|
+
}>
|
|
1928
|
+
)[0].function.arguments.length
|
|
1929
|
+
).toBeGreaterThan(2_000);
|
|
1930
|
+
expect(
|
|
1931
|
+
(
|
|
1932
|
+
message.response_metadata.output as Array<{
|
|
1933
|
+
arguments: string;
|
|
1934
|
+
}>
|
|
1935
|
+
)[0].arguments.length
|
|
1936
|
+
).toBeGreaterThan(2_000);
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1002
1940
|
describe('preFlightTruncateToolCallInputs', () => {
|
|
1003
1941
|
it('should truncate oversized tool_use input fields in AI messages', () => {
|
|
1004
1942
|
const tokenCounter = createTestTokenCounter();
|
|
@@ -1066,6 +2004,88 @@ describe('Prune Messages Tests', () => {
|
|
|
1066
2004
|
expect(tc.args).toHaveProperty('_truncated');
|
|
1067
2005
|
});
|
|
1068
2006
|
|
|
2007
|
+
it('truncates tool_calls-only args and preserves AI message metadata', () => {
|
|
2008
|
+
let recounts = 0;
|
|
2009
|
+
const tokenCounter: t.TokenCounter = () => {
|
|
2010
|
+
recounts++;
|
|
2011
|
+
return 17;
|
|
2012
|
+
};
|
|
2013
|
+
const messages: BaseMessage[] = [
|
|
2014
|
+
new AIMessage({
|
|
2015
|
+
content: '',
|
|
2016
|
+
id: 'ai-tool-call-only',
|
|
2017
|
+
name: 'assistant',
|
|
2018
|
+
additional_kwargs: { trace_marker: 'keep' },
|
|
2019
|
+
response_metadata: { model: 'test-model' },
|
|
2020
|
+
tool_calls: [
|
|
2021
|
+
{
|
|
2022
|
+
id: 'tool-exec',
|
|
2023
|
+
name: 'evaluate_script',
|
|
2024
|
+
args: { code: 'x'.repeat(5_000) },
|
|
2025
|
+
},
|
|
2026
|
+
],
|
|
2027
|
+
}),
|
|
2028
|
+
];
|
|
2029
|
+
const indexTokenCountMap: Record<string, number | undefined> = { 0: 1 };
|
|
2030
|
+
|
|
2031
|
+
const truncated = preFlightTruncateToolCallInputs({
|
|
2032
|
+
messages,
|
|
2033
|
+
maxContextTokens: 1_000,
|
|
2034
|
+
indexTokenCountMap,
|
|
2035
|
+
tokenCounter,
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
expect(truncated).toBe(1);
|
|
2039
|
+
expect(recounts).toBe(1);
|
|
2040
|
+
expect(indexTokenCountMap[0]).toBe(17);
|
|
2041
|
+
const projected = messages[0] as AIMessage;
|
|
2042
|
+
expect(projected.content).toBe('');
|
|
2043
|
+
expect(projected.id).toBe('ai-tool-call-only');
|
|
2044
|
+
expect(projected.name).toBe('assistant');
|
|
2045
|
+
expect(projected.additional_kwargs).toEqual({ trace_marker: 'keep' });
|
|
2046
|
+
expect(projected.response_metadata).toEqual({ model: 'test-model' });
|
|
2047
|
+
expect(projected.tool_calls?.[0].args).toMatchObject({
|
|
2048
|
+
_truncated: expect.stringContaining('truncated'),
|
|
2049
|
+
_originalChars: expect.any(Number),
|
|
2050
|
+
});
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
it('neutralizes adversarial tool-call args without invoking toJSON', () => {
|
|
2054
|
+
let toJSONCalls = 0;
|
|
2055
|
+
const messages: BaseMessage[] = [
|
|
2056
|
+
new AIMessage({
|
|
2057
|
+
content: '',
|
|
2058
|
+
tool_calls: [
|
|
2059
|
+
{
|
|
2060
|
+
id: 'tool-adversarial',
|
|
2061
|
+
name: 'lookup',
|
|
2062
|
+
args: {
|
|
2063
|
+
query: 'safe',
|
|
2064
|
+
toJSON() {
|
|
2065
|
+
toJSONCalls++;
|
|
2066
|
+
return { query: 'x'.repeat(2_000_000) };
|
|
2067
|
+
},
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
],
|
|
2071
|
+
}),
|
|
2072
|
+
];
|
|
2073
|
+
const indexTokenCountMap: Record<string, number | undefined> = { 0: 1 };
|
|
2074
|
+
|
|
2075
|
+
const changed = preFlightTruncateToolCallInputs({
|
|
2076
|
+
messages,
|
|
2077
|
+
maxContextTokens: 1_000,
|
|
2078
|
+
indexTokenCountMap,
|
|
2079
|
+
tokenCounter: () => 5,
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
expect(changed).toBe(1);
|
|
2083
|
+
expect(toJSONCalls).toBe(0);
|
|
2084
|
+
expect((messages[0] as AIMessage).tool_calls?.[0].args).toEqual({
|
|
2085
|
+
query: 'safe',
|
|
2086
|
+
});
|
|
2087
|
+
});
|
|
2088
|
+
|
|
1069
2089
|
it('should not truncate inputs that fit within the budget', () => {
|
|
1070
2090
|
const tokenCounter = createTestTokenCounter();
|
|
1071
2091
|
const messages: BaseMessage[] = [
|
|
@@ -1147,9 +2167,9 @@ describe('Prune Messages Tests', () => {
|
|
|
1147
2167
|
|
|
1148
2168
|
// Total message tokens: 11 + 2 + 12 + 4 = 29
|
|
1149
2169
|
// Instruction tokens: 20 (simulating system prompt overhead)
|
|
1150
|
-
// Effective budget
|
|
2170
|
+
// Effective budget: 52 - 20 - 3 reply primer = 29 → exact fit
|
|
1151
2171
|
const pruneMessages = createPruneMessages({
|
|
1152
|
-
maxTokens:
|
|
2172
|
+
maxTokens: 52,
|
|
1153
2173
|
startIndex: 0,
|
|
1154
2174
|
tokenCounter,
|
|
1155
2175
|
indexTokenCountMap,
|
|
@@ -1159,7 +2179,7 @@ describe('Prune Messages Tests', () => {
|
|
|
1159
2179
|
|
|
1160
2180
|
const result = pruneMessages({ messages });
|
|
1161
2181
|
|
|
1162
|
-
// All messages
|
|
2182
|
+
// All messages fit exactly with instructions and reply framing.
|
|
1163
2183
|
expect(result.context.length).toBe(4);
|
|
1164
2184
|
expect(result.context).toEqual(messages);
|
|
1165
2185
|
expect(result.messagesToRefine).toEqual([]);
|
|
@@ -1418,6 +2438,63 @@ describe('Prune Messages Tests', () => {
|
|
|
1418
2438
|
const types = result.context.map((m) => m.getType());
|
|
1419
2439
|
expect(types).toContain('human');
|
|
1420
2440
|
});
|
|
2441
|
+
|
|
2442
|
+
it('emergency-truncates tool_calls-only args when content is empty', () => {
|
|
2443
|
+
const tokenCounter: t.TokenCounter = (message) => {
|
|
2444
|
+
let chars =
|
|
2445
|
+
typeof message.content === 'string' ? message.content.length : 0;
|
|
2446
|
+
if (message.getType() === 'ai') {
|
|
2447
|
+
for (const toolCall of (message as AIMessage).tool_calls ?? []) {
|
|
2448
|
+
chars +=
|
|
2449
|
+
toolCall.name.length + JSON.stringify(toolCall.args).length;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return Math.max(1, Math.ceil(chars / 4));
|
|
2453
|
+
};
|
|
2454
|
+
const messages: BaseMessage[] = [
|
|
2455
|
+
new AIMessage({
|
|
2456
|
+
content: '',
|
|
2457
|
+
tool_calls: [
|
|
2458
|
+
{
|
|
2459
|
+
id: 'tool-call-only',
|
|
2460
|
+
name: 'evaluate_script',
|
|
2461
|
+
args: { code: 'x'.repeat(4_000) },
|
|
2462
|
+
},
|
|
2463
|
+
],
|
|
2464
|
+
}),
|
|
2465
|
+
new ToolMessage({
|
|
2466
|
+
content: 'OK',
|
|
2467
|
+
tool_call_id: 'tool-call-only',
|
|
2468
|
+
name: 'evaluate_script',
|
|
2469
|
+
}),
|
|
2470
|
+
];
|
|
2471
|
+
const indexTokenCountMap: Record<string, number | undefined> = {
|
|
2472
|
+
0: tokenCounter(messages[0]),
|
|
2473
|
+
1: tokenCounter(messages[1]),
|
|
2474
|
+
};
|
|
2475
|
+
const pruneMessages = createPruneMessages({
|
|
2476
|
+
maxTokens: 1_000,
|
|
2477
|
+
startIndex: 0,
|
|
2478
|
+
tokenCounter,
|
|
2479
|
+
indexTokenCountMap,
|
|
2480
|
+
getInstructionTokens: () => 900,
|
|
2481
|
+
reserveRatio: 0,
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
const result = pruneMessages({ messages });
|
|
2485
|
+
|
|
2486
|
+
const projectedAI = result.context.find(
|
|
2487
|
+
(message) => message.getType() === 'ai'
|
|
2488
|
+
) as AIMessage | undefined;
|
|
2489
|
+
expect(projectedAI).toBeDefined();
|
|
2490
|
+
expect(projectedAI?.content).toBe('');
|
|
2491
|
+
const projectedArgs = projectedAI?.tool_calls?.[0].args as
|
|
2492
|
+
| { _truncated?: string; _originalChars?: number }
|
|
2493
|
+
| undefined;
|
|
2494
|
+
expect(projectedArgs?._truncated).toContain('truncated');
|
|
2495
|
+
expect(projectedArgs?._truncated?.length).toBeLessThanOrEqual(200);
|
|
2496
|
+
expect(projectedArgs?._originalChars).toBeGreaterThan(200);
|
|
2497
|
+
});
|
|
1421
2498
|
});
|
|
1422
2499
|
|
|
1423
2500
|
describe('Empty messages guard', () => {
|
|
@@ -1444,10 +2521,10 @@ describe('Prune Messages Tests', () => {
|
|
|
1444
2521
|
expect(result.context).toEqual([]);
|
|
1445
2522
|
expect(result.messagesToRefine).toEqual([]);
|
|
1446
2523
|
expect(result.prePruneContextTokens).toBe(0);
|
|
1447
|
-
/** Reserve-adjusted budget
|
|
2524
|
+
/** Reserve-adjusted budget minus instructions and reply primer */
|
|
1448
2525
|
expect(result.contextBudget).toBe(7600);
|
|
1449
2526
|
expect(result.effectiveInstructionTokens).toBe(4000);
|
|
1450
|
-
expect(result.remainingContextTokens).toBe(
|
|
2527
|
+
expect(result.remainingContextTokens).toBe(3597);
|
|
1451
2528
|
});
|
|
1452
2529
|
});
|
|
1453
2530
|
|