@librechat/agents 3.3.3 → 3.3.5
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/common/constants.cjs +21 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/events.cjs +10 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +793 -111
- 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/hooks/HookRegistry.cjs +25 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +8 -0
- package/dist/cjs/langfuse.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 +38 -13
- 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 +331 -5
- 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/llm/preempt.cjs +132 -0
- package/dist/cjs/llm/preempt.cjs.map +1 -0
- package/dist/cjs/main.cjs +36 -5
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- 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 +290 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +63 -0
- package/dist/cjs/messages/handoffCue.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +4 -1
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +80 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +18 -9
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +35 -10
- 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 +56 -63
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +9 -1
- package/dist/cjs/tools/search/tool.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/common/constants.mjs +19 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/events.mjs +10 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +798 -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/hooks/HookRegistry.mjs +25 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +9 -1
- package/dist/esm/langfuse.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 +38 -13
- 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 +332 -8
- 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/llm/preempt.mjs +131 -0
- package/dist/esm/llm/preempt.mjs.map +1 -0
- package/dist/esm/main.mjs +14 -11
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- 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 +290 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +61 -0
- package/dist/esm/messages/handoffCue.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +4 -1
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +80 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +19 -10
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +34 -11
- 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 +56 -63
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +9 -1
- package/dist/esm/tools/search/tool.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/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +139 -2
- package/dist/types/hooks/HookRegistry.d.ts +15 -0
- package/dist/types/hooks/index.d.ts +12 -1
- package/dist/types/hooks/types.d.ts +45 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +34 -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/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -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/handoffCue.d.ts +40 -0
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/injected.d.ts +3 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/run.d.ts +7 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +0 -7
- package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
- package/dist/types/tools/search/types.d.ts +31 -2
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/run.d.ts +65 -0
- package/dist/types/types/stream.d.ts +1 -26
- 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/{splitStream.test.ts → aggregator.test.ts} +59 -666
- package/src/common/constants.ts +21 -0
- package/src/events.ts +15 -1
- package/src/graphs/Graph.ts +1261 -198
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
- package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
- package/src/hooks/HookRegistry.ts +40 -0
- package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
- package/src/hooks/index.ts +16 -2
- package/src/hooks/types.ts +47 -3
- package/src/index.ts +1 -1
- package/src/langfuse.ts +26 -1
- 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 +131 -1
- package/src/llm/bedrock/utils/message_inputs.ts +100 -24
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +611 -8
- 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/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- 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 +454 -91
- package/src/messages/formatAgentMessages.steer.test.ts +267 -0
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/handoffCue.test.ts +96 -0
- package/src/messages/handoffCue.ts +78 -0
- package/src/messages/index.ts +3 -0
- package/src/messages/injected.test.ts +90 -0
- package/src/messages/injected.ts +74 -0
- package/src/messages/prune.ts +996 -183
- package/src/run.ts +91 -6
- package/src/scripts/preempt-probe.ts +330 -0
- package/src/scripts/preempt-scenarios.ts +388 -0
- package/src/session/handlers.ts +32 -12
- package/src/specs/handoffCue.test.ts +165 -0
- package/src/specs/langfuse-callbacks.test.ts +352 -2
- package/src/specs/preemptSeal.test.ts +309 -0
- 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.dispatch.test.ts +63 -0
- package/src/stream.ts +59 -23
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +150 -99
- 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/search/keenable-scraper.test.ts +153 -0
- package/src/tools/search/keenable-scraper.ts +137 -0
- package/src/tools/search/tool.ts +13 -2
- package/src/tools/search/types.ts +50 -3
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +1 -42
- 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/dist/cjs/splitStream.cjs +0 -151
- package/dist/cjs/splitStream.cjs.map +0 -1
- package/dist/esm/splitStream.mjs +0 -150
- package/dist/esm/splitStream.mjs.map +0 -1
- package/dist/types/mockStream.d.ts +0 -32
- package/dist/types/splitStream.d.ts +0 -37
- package/src/mockStream.ts +0 -99
- package/src/splitStream.ts +0 -234
|
@@ -47,6 +47,10 @@ describe('convertToConverseMessages — tool-result cachePoint hoisting', () =>
|
|
|
47
47
|
|
|
48
48
|
const { converseMessages } = convertToConverseMessages([
|
|
49
49
|
new HumanMessage('go'),
|
|
50
|
+
new AIMessage({
|
|
51
|
+
content: '',
|
|
52
|
+
tool_calls: [{ id: 't1', name: 'calc', args: {}, type: 'tool_call' }],
|
|
53
|
+
}),
|
|
50
54
|
toolMsg,
|
|
51
55
|
]);
|
|
52
56
|
|
|
@@ -66,6 +70,17 @@ describe('convertToConverseMessages — tool-result cachePoint hoisting', () =>
|
|
|
66
70
|
it('leaves tool results without a cachePoint untouched', () => {
|
|
67
71
|
const { converseMessages } = convertToConverseMessages([
|
|
68
72
|
new HumanMessage('go'),
|
|
73
|
+
/**
|
|
74
|
+
* The calling assistant turn matters: without it the human turn and
|
|
75
|
+
* the tool result are adjacent user-role messages, which the converter
|
|
76
|
+
* now merges (Converse rejects consecutive user messages), and this
|
|
77
|
+
* test would assert against the merged artifact instead of the
|
|
78
|
+
* tool-result body it is about.
|
|
79
|
+
*/
|
|
80
|
+
new AIMessage({
|
|
81
|
+
content: '',
|
|
82
|
+
tool_calls: [{ id: 't1', name: 'calc', args: {}, type: 'tool_call' }],
|
|
83
|
+
}),
|
|
69
84
|
new ToolMessage({ tool_call_id: 't1', content: 'plain result' }),
|
|
70
85
|
]);
|
|
71
86
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { expect, test, describe } from '@jest/globals';
|
|
2
|
-
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
-
import type {
|
|
2
|
+
import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import type {
|
|
4
|
+
Content,
|
|
5
|
+
EnhancedGenerateContentResponse,
|
|
6
|
+
} from '@google/generative-ai';
|
|
4
7
|
import {
|
|
5
8
|
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
6
9
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
@@ -8,9 +11,53 @@ import {
|
|
|
8
11
|
} from '@/tools/streamedToolCallSeals';
|
|
9
12
|
import {
|
|
10
13
|
convertResponseContentToChatGenerationChunk,
|
|
14
|
+
convertMessageContentToParts,
|
|
11
15
|
dropUnsupportedModelTurnPrefill,
|
|
12
16
|
rejectsModelTurnPrefill,
|
|
13
17
|
} from './common';
|
|
18
|
+
import { toLangChainContent } from '@/messages/langchain';
|
|
19
|
+
|
|
20
|
+
describe('convertMessageContentToParts cross-provider tool calls', () => {
|
|
21
|
+
test('deduplicates Anthropic tool_use content mirrored in parsed tool_calls', () => {
|
|
22
|
+
const message = new AIMessage({
|
|
23
|
+
content: toLangChainContent([
|
|
24
|
+
{ type: 'text', text: 'Checking.' },
|
|
25
|
+
{
|
|
26
|
+
type: 'tool_use',
|
|
27
|
+
id: 'call_weather',
|
|
28
|
+
name: 'weather',
|
|
29
|
+
input: '',
|
|
30
|
+
},
|
|
31
|
+
{ type: 'text', index: 0, input: '{"city":"NYC"}' },
|
|
32
|
+
]),
|
|
33
|
+
tool_calls: [
|
|
34
|
+
{
|
|
35
|
+
id: 'call_weather',
|
|
36
|
+
name: 'weather',
|
|
37
|
+
args: { city: 'NYC' },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const parts = convertMessageContentToParts(
|
|
43
|
+
message,
|
|
44
|
+
false,
|
|
45
|
+
[],
|
|
46
|
+
'gemini-3.5-flash'
|
|
47
|
+
);
|
|
48
|
+
const functionCalls = parts.filter((part) => 'functionCall' in part);
|
|
49
|
+
|
|
50
|
+
expect(functionCalls).toHaveLength(1);
|
|
51
|
+
expect(parts).not.toContainEqual({});
|
|
52
|
+
expect(functionCalls[0]).toMatchObject({
|
|
53
|
+
functionCall: {
|
|
54
|
+
id: 'call_weather',
|
|
55
|
+
name: 'weather',
|
|
56
|
+
args: { city: 'NYC' },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
14
61
|
|
|
15
62
|
function buildResponse(
|
|
16
63
|
parts: Array<Record<string, unknown>>
|
|
@@ -72,7 +119,9 @@ describe('rejectsModelTurnPrefill', () => {
|
|
|
72
119
|
expect(rejectsModelTurnPrefill('gemini-3.6-flash')).toBe(true);
|
|
73
120
|
expect(rejectsModelTurnPrefill('gemini-3.5-flash-lite')).toBe(true);
|
|
74
121
|
expect(rejectsModelTurnPrefill('models/gemini-3.6-flash')).toBe(true);
|
|
75
|
-
expect(rejectsModelTurnPrefill('google/gemini-3.5-flash-lite-latest')).toBe(
|
|
122
|
+
expect(rejectsModelTurnPrefill('google/gemini-3.5-flash-lite-latest')).toBe(
|
|
123
|
+
true
|
|
124
|
+
);
|
|
76
125
|
});
|
|
77
126
|
|
|
78
127
|
test('is false for models that still accept prefill and for empty input', () => {
|
|
@@ -86,34 +135,51 @@ describe('rejectsModelTurnPrefill', () => {
|
|
|
86
135
|
|
|
87
136
|
describe('dropUnsupportedModelTurnPrefill', () => {
|
|
88
137
|
const userTurn: Content = { role: 'user', parts: [{ text: 'Hi' }] };
|
|
89
|
-
const modelTurn: Content = {
|
|
138
|
+
const modelTurn: Content = {
|
|
139
|
+
role: 'model',
|
|
140
|
+
parts: [{ text: 'Hello, I am' }],
|
|
141
|
+
};
|
|
90
142
|
|
|
91
143
|
test('drops a trailing model turn for no-prefill models', () => {
|
|
92
144
|
const contents: Content[] = [userTurn, modelTurn];
|
|
93
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
145
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
146
|
+
contents,
|
|
147
|
+
'gemini-3.6-flash'
|
|
148
|
+
);
|
|
94
149
|
expect(result).toEqual([userTurn]);
|
|
95
150
|
});
|
|
96
151
|
|
|
97
152
|
test('drops multiple consecutive trailing model turns but keeps one turn', () => {
|
|
98
153
|
const contents: Content[] = [userTurn, modelTurn, modelTurn];
|
|
99
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
154
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
155
|
+
contents,
|
|
156
|
+
'gemini-3.5-flash-lite'
|
|
157
|
+
);
|
|
100
158
|
expect(result).toEqual([userTurn]);
|
|
101
159
|
});
|
|
102
160
|
|
|
103
161
|
test('leaves a trailing model turn for models that accept prefill', () => {
|
|
104
162
|
const contents: Content[] = [userTurn, modelTurn];
|
|
105
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
163
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
164
|
+
contents,
|
|
165
|
+
'gemini-3.5-flash'
|
|
166
|
+
);
|
|
106
167
|
expect(result).toBe(contents);
|
|
107
168
|
});
|
|
108
169
|
|
|
109
170
|
test('is a no-op when the request already ends with a user turn', () => {
|
|
110
171
|
const contents: Content[] = [modelTurn, userTurn];
|
|
111
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
172
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
173
|
+
contents,
|
|
174
|
+
'gemini-3.6-flash'
|
|
175
|
+
);
|
|
112
176
|
expect(result).toBe(contents);
|
|
113
177
|
});
|
|
114
178
|
|
|
115
179
|
test('is a no-op for empty or undefined contents', () => {
|
|
116
180
|
expect(dropUnsupportedModelTurnPrefill([], 'gemini-3.6-flash')).toEqual([]);
|
|
117
|
-
expect(
|
|
181
|
+
expect(
|
|
182
|
+
dropUnsupportedModelTurnPrefill(undefined, 'gemini-3.6-flash')
|
|
183
|
+
).toBeUndefined();
|
|
118
184
|
});
|
|
119
185
|
});
|
|
@@ -390,7 +390,9 @@ function _convertLangChainContentToPart(
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
if (content.type === 'text') {
|
|
393
|
-
return
|
|
393
|
+
return typeof content.text === 'string' && content.text !== ''
|
|
394
|
+
? { text: content.text }
|
|
395
|
+
: undefined;
|
|
394
396
|
} else if (content.type === 'executableCode') {
|
|
395
397
|
return { executableCode: content.executableCode };
|
|
396
398
|
} else if (content.type === 'codeExecutionResult') {
|
|
@@ -429,10 +431,14 @@ function _convertLangChainContentToPart(
|
|
|
429
431
|
} else if (content.type === 'media') {
|
|
430
432
|
return messageContentMedia(content);
|
|
431
433
|
} else if (content.type === 'tool_use') {
|
|
434
|
+
const functionId = getGoogleFunctionId(
|
|
435
|
+
typeof content.id === 'string' ? content.id : undefined
|
|
436
|
+
);
|
|
432
437
|
return {
|
|
433
438
|
functionCall: {
|
|
434
439
|
name: content.name,
|
|
435
440
|
args: content.input,
|
|
441
|
+
...(functionId != null ? { id: functionId } : {}),
|
|
436
442
|
},
|
|
437
443
|
};
|
|
438
444
|
} else if (
|
|
@@ -553,7 +559,28 @@ export function convertMessageContentToParts(
|
|
|
553
559
|
});
|
|
554
560
|
}
|
|
555
561
|
|
|
556
|
-
|
|
562
|
+
const parsedFunctionCallIds = new Set(
|
|
563
|
+
functionCalls.flatMap((part) => {
|
|
564
|
+
const functionCall = part.functionCall as GoogleFunctionCallWithId;
|
|
565
|
+
return functionCall.id != null ? [functionCall.id] : [];
|
|
566
|
+
})
|
|
567
|
+
);
|
|
568
|
+
const parsedFunctionCallNames = new Set(
|
|
569
|
+
functionCalls.map((part) => part.functionCall.name)
|
|
570
|
+
);
|
|
571
|
+
const contentWithoutParsedMirrors = messageParts.filter((part) => {
|
|
572
|
+
if (!('functionCall' in part) || part.functionCall == null) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const functionCall = part.functionCall as GoogleFunctionCallWithId;
|
|
576
|
+
return !(
|
|
577
|
+
(functionCall.id != null && parsedFunctionCallIds.has(functionCall.id)) ||
|
|
578
|
+
(functionCall.id == null &&
|
|
579
|
+
parsedFunctionCallNames.has(functionCall.name))
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return [...contentWithoutParsedMirrors, ...functionCalls];
|
|
557
584
|
}
|
|
558
585
|
|
|
559
586
|
export function convertBaseMessagesToContent(
|
|
@@ -665,7 +692,11 @@ export function dropUnsupportedModelTurnPrefill(
|
|
|
665
692
|
contents: Content[] | undefined,
|
|
666
693
|
model?: string
|
|
667
694
|
): Content[] | undefined {
|
|
668
|
-
if (
|
|
695
|
+
if (
|
|
696
|
+
contents == null ||
|
|
697
|
+
contents.length === 0 ||
|
|
698
|
+
!rejectsModelTurnPrefill(model)
|
|
699
|
+
) {
|
|
669
700
|
return contents;
|
|
670
701
|
}
|
|
671
702
|
let end = contents.length;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/llm/invoke.alternation.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* `attemptInvoke` is the single funnel for primary, fallback and
|
|
4
|
+
* summarization model calls, and it keys the alternation pass on the provider
|
|
5
|
+
* ACTUALLY serving the call. This is the seam that protects a fallback: an
|
|
6
|
+
* OpenAI primary that failed after a boundary injected two human turns hands
|
|
7
|
+
* the same array to a Bedrock fallback, which rejects consecutive user turns.
|
|
8
|
+
*/
|
|
9
|
+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
10
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
11
|
+
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
12
|
+
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
13
|
+
import { Providers } from '@/common';
|
|
14
|
+
import { FakeChatModel } from '@/llm/fake';
|
|
15
|
+
import { attemptInvoke } from './invoke';
|
|
16
|
+
import type * as t from '@/types';
|
|
17
|
+
|
|
18
|
+
class CapturingChatModel extends FakeChatModel {
|
|
19
|
+
readonly invocations: BaseMessage[][] = [];
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super({ responses: ['ok'] });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override async *_streamResponseChunks(
|
|
26
|
+
messages: BaseMessage[],
|
|
27
|
+
options: this['ParsedCallOptions'],
|
|
28
|
+
runManager?: CallbackManagerForLLMRun
|
|
29
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
30
|
+
this.invocations.push(messages);
|
|
31
|
+
yield* super._streamResponseChunks(messages, options, runManager);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const adjacentUserPayload = (): BaseMessage[] => [
|
|
36
|
+
new HumanMessage({ content: 'question' }),
|
|
37
|
+
new AIMessage({ content: 'partial answer' }),
|
|
38
|
+
new HumanMessage({ content: 'hook context' }),
|
|
39
|
+
new HumanMessage({ content: 'steer' }),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
async function invokeAs(provider: Providers): Promise<BaseMessage[]> {
|
|
43
|
+
const model = new CapturingChatModel();
|
|
44
|
+
await attemptInvoke({
|
|
45
|
+
model: model as unknown as t.ChatModel,
|
|
46
|
+
messages: adjacentUserPayload(),
|
|
47
|
+
provider,
|
|
48
|
+
onChunk: async () => undefined,
|
|
49
|
+
});
|
|
50
|
+
expect(model.invocations).toHaveLength(1);
|
|
51
|
+
return model.invocations[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('attemptInvoke alternation funnel', () => {
|
|
55
|
+
it('coalesces adjacent user turns when the serving provider is strict', async () => {
|
|
56
|
+
const sent = await invokeAs(Providers.BEDROCK);
|
|
57
|
+
expect(sent.map((m) => m.getType())).toEqual(['human', 'ai', 'human']);
|
|
58
|
+
expect(sent[2].content).toBe('hook context\n\nsteer');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('leaves the payload alone for a tolerant provider', async () => {
|
|
62
|
+
const sent = await invokeAs(Providers.OPENAI);
|
|
63
|
+
expect(sent.map((m) => m.getType())).toEqual([
|
|
64
|
+
'human',
|
|
65
|
+
'ai',
|
|
66
|
+
'human',
|
|
67
|
+
'human',
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('is idempotent when the graph already coalesced for the primary', async () => {
|
|
72
|
+
const model = new CapturingChatModel();
|
|
73
|
+
const once = await invokeAs(Providers.BEDROCK);
|
|
74
|
+
await attemptInvoke({
|
|
75
|
+
model: model as unknown as t.ChatModel,
|
|
76
|
+
messages: once,
|
|
77
|
+
provider: Providers.BEDROCK,
|
|
78
|
+
onChunk: async () => undefined,
|
|
79
|
+
});
|
|
80
|
+
expect(model.invocations[0].map((m) => m.getType())).toEqual([
|
|
81
|
+
'human',
|
|
82
|
+
'ai',
|
|
83
|
+
'human',
|
|
84
|
+
]);
|
|
85
|
+
expect(model.invocations[0][2].content).toBe('hook context\n\nsteer');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/llm/invoke.handoffCue.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* The handoff cue is re-keyed on the provider ACTUALLY serving the call at
|
|
4
|
+
* the `attemptInvoke` funnel — the seam every fallback send passes through.
|
|
5
|
+
* A tolerant primary falling back to a Claude surface must gain the cue; an
|
|
6
|
+
* Anthropic primary falling back to a tolerant provider must have the baked
|
|
7
|
+
* cue stripped; and the serving model id must be read through the wrapper
|
|
8
|
+
* stack, or a wrapped Bedrock-Nova model would default to Claude.
|
|
9
|
+
*/
|
|
10
|
+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
11
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
12
|
+
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
13
|
+
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
14
|
+
import { RunnableBinding } from '@langchain/core/runnables';
|
|
15
|
+
import { Providers } from '@/common';
|
|
16
|
+
import { PREDECESSOR_HANDOFF_CUE } from '@/messages/handoffCue';
|
|
17
|
+
import { FakeChatModel } from '@/llm/fake';
|
|
18
|
+
import { attemptInvoke, type InvokeContext } from './invoke';
|
|
19
|
+
|
|
20
|
+
class CapturingChatModel extends FakeChatModel {
|
|
21
|
+
readonly invocations: BaseMessage[][] = [];
|
|
22
|
+
/** Serving model id, as a real provider client would expose it. */
|
|
23
|
+
model?: string;
|
|
24
|
+
|
|
25
|
+
constructor(modelId?: string) {
|
|
26
|
+
super({ responses: ['ok'] });
|
|
27
|
+
this.model = modelId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override async *_streamResponseChunks(
|
|
31
|
+
messages: BaseMessage[],
|
|
32
|
+
options: this['ParsedCallOptions'],
|
|
33
|
+
runManager?: CallbackManagerForLLMRun
|
|
34
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
35
|
+
this.invocations.push(messages);
|
|
36
|
+
yield* super._streamResponseChunks(messages, options, runManager);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const runTail = new AIMessage({ content: 'predecessor output', id: 'run-1' });
|
|
41
|
+
const context = {
|
|
42
|
+
isRunProducedMessage: (message: BaseMessage): boolean =>
|
|
43
|
+
message.id === 'run-1',
|
|
44
|
+
getOrCreateToolOutputRegistry: (): undefined => undefined,
|
|
45
|
+
} as unknown as InvokeContext;
|
|
46
|
+
|
|
47
|
+
const bakedCue = (): HumanMessage =>
|
|
48
|
+
new HumanMessage({
|
|
49
|
+
content: PREDECESSOR_HANDOFF_CUE,
|
|
50
|
+
additional_kwargs: { role: 'user', isMeta: true, source: 'handoff' },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
async function sentBy(
|
|
54
|
+
provider: Providers,
|
|
55
|
+
options?: { modelId?: string; wrap?: boolean; messages?: BaseMessage[] }
|
|
56
|
+
): Promise<BaseMessage[]> {
|
|
57
|
+
const model = new CapturingChatModel(options?.modelId);
|
|
58
|
+
const served =
|
|
59
|
+
options?.wrap === true
|
|
60
|
+
? new RunnableBinding({ bound: model, kwargs: {}, config: {} })
|
|
61
|
+
: model;
|
|
62
|
+
await attemptInvoke({
|
|
63
|
+
model: served as never,
|
|
64
|
+
messages: options?.messages ?? [new HumanMessage('go'), runTail],
|
|
65
|
+
provider,
|
|
66
|
+
context,
|
|
67
|
+
onChunk: async () => undefined,
|
|
68
|
+
});
|
|
69
|
+
return model.invocations[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('attemptInvoke handoff-cue funnel', () => {
|
|
73
|
+
it('applies the cue when the serving provider is Anthropic', async () => {
|
|
74
|
+
const sent = await sentBy(Providers.ANTHROPIC);
|
|
75
|
+
expect(sent.at(-1)?.content).toBe(PREDECESSOR_HANDOFF_CUE);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('applies the cue for Bedrock serving a WRAPPED Claude model', async () => {
|
|
79
|
+
const sent = await sentBy(Providers.BEDROCK, {
|
|
80
|
+
modelId: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
|
81
|
+
wrap: true,
|
|
82
|
+
});
|
|
83
|
+
expect(sent.at(-1)?.content).toBe(PREDECESSOR_HANDOFF_CUE);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not apply the cue for Bedrock serving a WRAPPED Nova model', async () => {
|
|
87
|
+
const sent = await sentBy(Providers.BEDROCK, {
|
|
88
|
+
modelId: 'us.amazon.nova-pro-v1:0',
|
|
89
|
+
wrap: true,
|
|
90
|
+
});
|
|
91
|
+
expect(sent.at(-1)?.getType()).toBe('ai');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('strips a baked cue for a tolerant serving provider', async () => {
|
|
95
|
+
const sent = await sentBy(Providers.OPENAI, {
|
|
96
|
+
messages: [new HumanMessage('go'), runTail, bakedCue()],
|
|
97
|
+
});
|
|
98
|
+
expect(sent.at(-1)?.getType()).toBe('ai');
|
|
99
|
+
expect(sent.some((m) => m.content === PREDECESSOR_HANDOFF_CUE)).toBe(
|
|
100
|
+
false
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('is idempotent when the primary already baked the cue', async () => {
|
|
105
|
+
const sent = await sentBy(Providers.ANTHROPIC, {
|
|
106
|
+
messages: [new HumanMessage('go'), runTail, bakedCue()],
|
|
107
|
+
});
|
|
108
|
+
expect(
|
|
109
|
+
sent.filter((m) => m.content === PREDECESSOR_HANDOFF_CUE)
|
|
110
|
+
).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
});
|