@librechat/agents 3.3.0 → 3.3.2
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 +116 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +229 -48
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +11 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
- package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +60 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +20 -7
- package/dist/cjs/messages/prune.cjs +16 -5
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +2 -0
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/run.cjs +15 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +205 -49
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -7
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +69 -8
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +317 -52
- package/dist/cjs/utils/errors.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +117 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +227 -46
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +11 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
- package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +59 -4
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +9 -9
- package/dist/esm/messages/prune.mjs +14 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +2 -1
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/run.mjs +15 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +205 -49
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +62 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +317 -53
- package/dist/esm/utils/errors.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +62 -3
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +16 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
- package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
- package/dist/types/llm/invoke.d.ts +35 -6
- package/dist/types/messages/prune.d.ts +10 -2
- package/dist/types/messages/recency.d.ts +1 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -1
- package/dist/types/types/llm.d.ts +7 -4
- package/dist/types/types/stream.d.ts +5 -4
- package/dist/types/types/summarize.d.ts +22 -0
- package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
- package/dist/types/utils/errors.d.ts +65 -16
- package/dist/types/utils/redactSecrets.d.ts +3 -0
- package/package.json +10 -11
- package/src/agents/AgentContext.ts +188 -7
- package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +389 -58
- package/src/graphs/MultiAgentGraph.ts +184 -46
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
- package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
- package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
- package/src/llm/anthropic/llm.spec.ts +8 -0
- package/src/llm/bedrock/index.ts +25 -12
- package/src/llm/contextOverflowRecovery.ts +292 -0
- package/src/llm/invoke.ts +119 -4
- package/src/messages/prune.ts +24 -11
- package/src/messages/recency.ts +3 -1
- package/src/run.ts +24 -1
- package/src/scripts/context-overflow-probe.ts +997 -0
- package/src/specs/agent-handoffs.test.ts +903 -1
- package/src/specs/context-overflow-recovery.live.test.ts +213 -0
- package/src/splitStream.test.ts +882 -0
- package/src/stream.ts +315 -51
- package/src/summarization/__tests__/aggregator.test.ts +83 -0
- package/src/summarization/__tests__/node.test.ts +139 -0
- package/src/summarization/node.ts +99 -14
- package/src/tools/BashExecutor.ts +4 -2
- package/src/tools/BashProgrammaticToolCalling.ts +4 -7
- package/src/tools/CodeExecutor.ts +119 -8
- package/src/tools/ProgrammaticToolCalling.ts +29 -27
- package/src/tools/ToolNode.ts +50 -8
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
- package/src/types/llm.ts +8 -1
- package/src/types/stream.ts +5 -4
- package/src/types/summarize.ts +22 -0
- package/src/utils/__tests__/errors.test.ts +270 -0
- package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
- package/src/utils/__tests__/redactSecrets.test.ts +56 -0
- package/src/utils/errors.ts +484 -66
- package/src/utils/redactSecrets.ts +61 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live end-to-end verification that a real provider rejection for an
|
|
3
|
+
* over-limit prompt is absorbed by forced compaction instead of surfacing.
|
|
4
|
+
*
|
|
5
|
+
* Each case deliberately configures `maxContextTokens` far above the model's
|
|
6
|
+
* real window, so the proactive pruner does not prevent the overflow and the
|
|
7
|
+
* reactive recovery path is what gets exercised.
|
|
8
|
+
*
|
|
9
|
+
* Run with:
|
|
10
|
+
* RUN_CONTEXT_OVERFLOW_LIVE_TESTS=1 OPENROUTER_API_KEY=... npm test -- context-overflow-recovery.live.test.ts --runInBand
|
|
11
|
+
*
|
|
12
|
+
* Cases self-skip when their provider credentials are absent. Models are the
|
|
13
|
+
* smallest-window, cheapest option per provider — the prompt has to exceed
|
|
14
|
+
* the window for the test to mean anything, so a large window is a large bill.
|
|
15
|
+
*
|
|
16
|
+
* The Bedrock case needs `NODE_OPTIONS='--experimental-vm-modules'`; the AWS
|
|
17
|
+
* SDK uses a dynamic import that jest otherwise refuses.
|
|
18
|
+
*/
|
|
19
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
20
|
+
dotenvConfig();
|
|
21
|
+
|
|
22
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
23
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
24
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
25
|
+
import type * as t from '@/types';
|
|
26
|
+
import { createTokenCounter } from '@/utils/tokens';
|
|
27
|
+
import { hasAnyEnv, hasEnv } from '@/specs/spec.utils';
|
|
28
|
+
import { GraphEvents, Providers } from '@/common';
|
|
29
|
+
import { Run } from '@/run';
|
|
30
|
+
|
|
31
|
+
jest.setTimeout(300_000);
|
|
32
|
+
|
|
33
|
+
const liveEnabled = process.env.RUN_CONTEXT_OVERFLOW_LIVE_TESTS === '1';
|
|
34
|
+
|
|
35
|
+
interface LiveCase {
|
|
36
|
+
label: string;
|
|
37
|
+
provider: Providers;
|
|
38
|
+
model: string;
|
|
39
|
+
/** The model's real input window. */
|
|
40
|
+
contextWindow: number;
|
|
41
|
+
envKeys: readonly string[];
|
|
42
|
+
clientOptions: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const LIVE_CASES: readonly LiveCase[] = [
|
|
46
|
+
{
|
|
47
|
+
label: 'openrouter / qwen-2.5-7b (32k window)',
|
|
48
|
+
provider: Providers.OPENROUTER,
|
|
49
|
+
model: 'qwen/qwen-2.5-7b-instruct',
|
|
50
|
+
contextWindow: 32_768,
|
|
51
|
+
envKeys: ['OPENROUTER_API_KEY'],
|
|
52
|
+
clientOptions: {
|
|
53
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
54
|
+
configuration: {
|
|
55
|
+
baseURL:
|
|
56
|
+
process.env.OPENROUTER_BASE_URL ?? 'https://openrouter.ai/api/v1',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
/**
|
|
62
|
+
* Google's text models all carry a ~1M window, which would make this the
|
|
63
|
+
* most expensive case in the file by an order of magnitude. This model
|
|
64
|
+
* accepts ordinary text and caps its input at 64k, so it exercises the
|
|
65
|
+
* same Gemini API rejection for a fraction of the tokens.
|
|
66
|
+
*/
|
|
67
|
+
label: 'google / gemini-3.1-flash-image (64k window)',
|
|
68
|
+
provider: Providers.GOOGLE,
|
|
69
|
+
model: 'gemini-3.1-flash-image',
|
|
70
|
+
contextWindow: 65_536,
|
|
71
|
+
envKeys: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
72
|
+
clientOptions: {
|
|
73
|
+
apiKey: process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'anthropic / haiku (200k window)',
|
|
78
|
+
provider: Providers.ANTHROPIC,
|
|
79
|
+
model: 'claude-haiku-4-5-20251001',
|
|
80
|
+
contextWindow: 200_000,
|
|
81
|
+
envKeys: ['ANTHROPIC_API_KEY'],
|
|
82
|
+
clientOptions: { apiKey: process.env.ANTHROPIC_API_KEY },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: 'bedrock / haiku (200k window)',
|
|
86
|
+
provider: Providers.BEDROCK,
|
|
87
|
+
model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
|
|
88
|
+
contextWindow: 200_000,
|
|
89
|
+
envKeys: ['BEDROCK_AWS_ACCESS_KEY_ID', 'AWS_ACCESS_KEY_ID'],
|
|
90
|
+
clientOptions: {
|
|
91
|
+
region:
|
|
92
|
+
process.env.BEDROCK_AWS_REGION ?? process.env.AWS_REGION ?? 'us-east-1',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Single-token words, so an N-word message is at least N tokens under every
|
|
99
|
+
* provider's tokenizer — the prompt must clear the window with certainty,
|
|
100
|
+
* since a request that squeaks under it is billed in full and proves nothing.
|
|
101
|
+
*/
|
|
102
|
+
const FILLER =
|
|
103
|
+
'the quick brown fox jumps over lazy dog and then runs past'.split(' ');
|
|
104
|
+
|
|
105
|
+
function buildOversizedHistory(totalWords: number): BaseMessage[] {
|
|
106
|
+
const perMessage = Math.ceil(totalWords / 4);
|
|
107
|
+
const messages: BaseMessage[] = [];
|
|
108
|
+
for (let turn = 0; turn < 4; turn++) {
|
|
109
|
+
const words: string[] = new Array(perMessage);
|
|
110
|
+
for (let i = 0; i < perMessage; i++) {
|
|
111
|
+
words[i] = FILLER[(i + turn) % FILLER.length];
|
|
112
|
+
}
|
|
113
|
+
messages.push(new HumanMessage(`Notes part ${turn}: ${words.join(' ')}`));
|
|
114
|
+
}
|
|
115
|
+
messages.push(
|
|
116
|
+
new HumanMessage('In one short sentence, what were those notes about?')
|
|
117
|
+
);
|
|
118
|
+
return messages;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const describeIfLive = liveEnabled ? describe : describe.skip;
|
|
122
|
+
|
|
123
|
+
describeIfLive('context overflow recovery (live)', () => {
|
|
124
|
+
for (const testCase of LIVE_CASES) {
|
|
125
|
+
const runnable = hasAnyEnv(testCase.envKeys);
|
|
126
|
+
const maybeIt = runnable ? it : it.skip;
|
|
127
|
+
|
|
128
|
+
maybeIt(
|
|
129
|
+
`recovers without surfacing an error — ${testCase.label}`,
|
|
130
|
+
async () => {
|
|
131
|
+
const summarizeEvents: t.SummarizeCompleteEvent[] = [];
|
|
132
|
+
const tokenCounter = await createTokenCounter();
|
|
133
|
+
|
|
134
|
+
const run = await Run.create<t.IState>({
|
|
135
|
+
runId: `overflow-live-${testCase.provider}-${Date.now()}`,
|
|
136
|
+
graphConfig: {
|
|
137
|
+
type: 'standard',
|
|
138
|
+
llmConfig: {
|
|
139
|
+
provider: testCase.provider,
|
|
140
|
+
...testCase.clientOptions,
|
|
141
|
+
model: testCase.model,
|
|
142
|
+
} as t.LLMConfig,
|
|
143
|
+
/**
|
|
144
|
+
* Deliberately wrong, by a wide margin: the pruner will happily
|
|
145
|
+
* build a prompt the provider cannot accept, which is exactly the
|
|
146
|
+
* situation the recovery path exists for.
|
|
147
|
+
*/
|
|
148
|
+
maxContextTokens: testCase.contextWindow * 2,
|
|
149
|
+
summarizationEnabled: true,
|
|
150
|
+
summarizationConfig: {
|
|
151
|
+
provider: testCase.provider,
|
|
152
|
+
model: testCase.model,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
returnContent: true,
|
|
156
|
+
/**
|
|
157
|
+
* The assertions below read post-run `AgentContext` state, and the
|
|
158
|
+
* default cleanup path calls `clearHeavyState()` → `reset()`, which
|
|
159
|
+
* deliberately undoes the overflow correction for the next turn.
|
|
160
|
+
*/
|
|
161
|
+
skipCleanup: true,
|
|
162
|
+
tokenCounter,
|
|
163
|
+
customHandlers: {
|
|
164
|
+
[GraphEvents.ON_SUMMARIZE_COMPLETE]: {
|
|
165
|
+
handle: (_event: string, data: t.StreamEventData): void => {
|
|
166
|
+
summarizeEvents.push(
|
|
167
|
+
data as unknown as t.SummarizeCompleteEvent
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const messages = buildOversizedHistory(
|
|
175
|
+
Math.ceil(testCase.contextWindow * 1.3)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const content = await run.processStream({ messages }, {
|
|
179
|
+
configurable: { thread_id: `overflow-live-${testCase.provider}` },
|
|
180
|
+
streamMode: 'values',
|
|
181
|
+
version: 'v2',
|
|
182
|
+
} as never);
|
|
183
|
+
|
|
184
|
+
expect(content).toBeDefined();
|
|
185
|
+
expect(Array.isArray(content)).toBe(true);
|
|
186
|
+
expect((content ?? []).length).toBeGreaterThan(0);
|
|
187
|
+
|
|
188
|
+
const agentContext = run.Graph?.agentContexts.get('default');
|
|
189
|
+
expect(agentContext?.overflowRecoveryAttempts).toBeGreaterThan(0);
|
|
190
|
+
/** The corrected budget must be below the fiction we started with. */
|
|
191
|
+
expect(agentContext?.maxContextTokens).toBeLessThan(
|
|
192
|
+
testCase.contextWindow * 2
|
|
193
|
+
);
|
|
194
|
+
/**
|
|
195
|
+
* No summarization assertion: the first recovery compacts by
|
|
196
|
+
* compressing tool output, so a run that fits after compression alone
|
|
197
|
+
* never spends a summarization call. Any summary event that does
|
|
198
|
+
* arrive must at least carry no error.
|
|
199
|
+
*/
|
|
200
|
+
for (const event of summarizeEvents) {
|
|
201
|
+
expect(event.error).toBeUndefined();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
it('has at least one runnable provider configured', () => {
|
|
208
|
+
const anyRunnable = LIVE_CASES.some((testCase) =>
|
|
209
|
+
testCase.envKeys.some(hasEnv)
|
|
210
|
+
);
|
|
211
|
+
expect(anyRunnable).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|