@librechat/agents 3.2.68 → 3.3.1

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.
Files changed (159) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +117 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +4 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +243 -49
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +103 -28
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  10. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  12. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  13. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  14. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  15. package/dist/cjs/llm/google/index.cjs +1 -1
  16. package/dist/cjs/llm/invoke.cjs +60 -3
  17. package/dist/cjs/llm/invoke.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +1 -1
  19. package/dist/cjs/main.cjs +21 -7
  20. package/dist/cjs/messages/format.cjs +136 -4
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +16 -5
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/messages/recency.cjs +2 -0
  25. package/dist/cjs/messages/recency.cjs.map +1 -1
  26. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  27. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +177 -3
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/stream.cjs +205 -49
  31. package/dist/cjs/stream.cjs.map +1 -1
  32. package/dist/cjs/summarization/node.cjs +55 -7
  33. package/dist/cjs/summarization/node.cjs.map +1 -1
  34. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  35. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  36. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  37. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  38. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  39. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  40. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  41. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  42. package/dist/cjs/tools/ToolNode.cjs +32 -3
  43. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  45. package/dist/cjs/utils/errors.cjs +317 -52
  46. package/dist/cjs/utils/errors.cjs.map +1 -1
  47. package/dist/esm/agents/AgentContext.mjs +118 -4
  48. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +4 -0
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +242 -48
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +103 -28
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  56. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  57. package/dist/esm/llm/bedrock/index.mjs +11 -2
  58. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  59. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  60. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  61. package/dist/esm/llm/google/index.mjs +1 -1
  62. package/dist/esm/llm/invoke.mjs +59 -4
  63. package/dist/esm/llm/invoke.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +1 -1
  65. package/dist/esm/main.mjs +10 -10
  66. package/dist/esm/messages/format.mjs +136 -5
  67. package/dist/esm/messages/format.mjs.map +1 -1
  68. package/dist/esm/messages/prune.mjs +14 -6
  69. package/dist/esm/messages/prune.mjs.map +1 -1
  70. package/dist/esm/messages/recency.mjs +2 -1
  71. package/dist/esm/messages/recency.mjs.map +1 -1
  72. package/dist/esm/prompts/activityLabel.mjs +100 -0
  73. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  74. package/dist/esm/run.mjs +178 -4
  75. package/dist/esm/run.mjs.map +1 -1
  76. package/dist/esm/stream.mjs +205 -49
  77. package/dist/esm/stream.mjs.map +1 -1
  78. package/dist/esm/summarization/node.mjs +55 -7
  79. package/dist/esm/summarization/node.mjs.map +1 -1
  80. package/dist/esm/tools/BashExecutor.mjs +3 -3
  81. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  82. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  83. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  84. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  85. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  86. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  87. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +32 -3
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  91. package/dist/esm/utils/errors.mjs +317 -53
  92. package/dist/esm/utils/errors.mjs.map +1 -1
  93. package/dist/types/agents/AgentContext.d.ts +62 -3
  94. package/dist/types/common/enum.d.ts +5 -1
  95. package/dist/types/graphs/Graph.d.ts +16 -2
  96. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  97. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  98. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  99. package/dist/types/llm/invoke.d.ts +35 -6
  100. package/dist/types/messages/format.d.ts +22 -0
  101. package/dist/types/messages/prune.d.ts +10 -2
  102. package/dist/types/messages/recency.d.ts +1 -0
  103. package/dist/types/prompts/activityLabel.d.ts +31 -0
  104. package/dist/types/run.d.ts +16 -0
  105. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  106. package/dist/types/types/activityLabel.d.ts +53 -0
  107. package/dist/types/types/index.d.ts +1 -0
  108. package/dist/types/types/llm.d.ts +7 -4
  109. package/dist/types/types/stream.d.ts +7 -4
  110. package/dist/types/types/summarize.d.ts +22 -0
  111. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  112. package/dist/types/utils/errors.d.ts +65 -16
  113. package/dist/types/utils/redactSecrets.d.ts +3 -0
  114. package/package.json +7 -8
  115. package/src/agents/AgentContext.ts +188 -7
  116. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  117. package/src/common/enum.ts +4 -0
  118. package/src/graphs/Graph.ts +409 -58
  119. package/src/graphs/MultiAgentGraph.ts +184 -46
  120. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  121. package/src/langfuseToolOutputTracing.ts +4 -1
  122. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  123. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  124. package/src/llm/bedrock/index.ts +25 -12
  125. package/src/llm/contextOverflowRecovery.ts +292 -0
  126. package/src/llm/invoke.ts +119 -4
  127. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  128. package/src/messages/format.ts +233 -5
  129. package/src/messages/prune.ts +24 -11
  130. package/src/messages/recency.ts +3 -1
  131. package/src/prompts/activityLabel.ts +177 -0
  132. package/src/run.ts +322 -3
  133. package/src/scripts/context-overflow-probe.ts +997 -0
  134. package/src/specs/activity-label-prompt.test.ts +128 -0
  135. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  136. package/src/specs/agent-handoffs.test.ts +903 -1
  137. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  138. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  139. package/src/splitStream.test.ts +882 -0
  140. package/src/stream.ts +315 -51
  141. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  142. package/src/summarization/__tests__/node.test.ts +139 -0
  143. package/src/summarization/node.ts +99 -14
  144. package/src/tools/BashExecutor.ts +4 -2
  145. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  146. package/src/tools/CodeExecutor.ts +119 -8
  147. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  148. package/src/tools/ToolNode.ts +50 -8
  149. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  150. package/src/types/activityLabel.ts +55 -0
  151. package/src/types/index.ts +1 -0
  152. package/src/types/llm.ts +8 -1
  153. package/src/types/stream.ts +7 -4
  154. package/src/types/summarize.ts +22 -0
  155. package/src/utils/__tests__/errors.test.ts +270 -0
  156. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  157. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  158. package/src/utils/errors.ts +484 -66
  159. package/src/utils/redactSecrets.ts +61 -0
@@ -0,0 +1,123 @@
1
+ // src/specs/bedrock-toolless.live.test.ts
2
+ /**
3
+ * Live Bedrock verification for the tool-less-destination toolConfig fix.
4
+ *
5
+ * A tool-less agent in a multi-agent graph inherits the prior agent's
6
+ * toolUse/toolResult history. Because it binds no tools, Bedrock's Converse API
7
+ * rejects the request ("The toolConfig field must be defined when using toolUse
8
+ * and toolResult content blocks"). `foldToolBlocksForToollessAgent` folds that
9
+ * history into text so the request becomes valid.
10
+ *
11
+ * Run with:
12
+ * RUN_BEDROCK_LIVE_TESTS=1 BEDROCK_AWS_ACCESS_KEY_ID=... BEDROCK_AWS_SECRET_ACCESS_KEY=... \
13
+ * BEDROCK_AWS_DEFAULT_REGION=us-west-2 npm test -- bedrock-toolless.live.test.ts --runInBand
14
+ */
15
+ import { config as dotenvConfig } from 'dotenv';
16
+ dotenvConfig();
17
+
18
+ import {
19
+ AIMessage,
20
+ HumanMessage,
21
+ ToolMessage,
22
+ } from '@langchain/core/messages';
23
+ import { beforeAll, describe, expect, it } from '@jest/globals';
24
+ import type { BaseMessage } from '@langchain/core/messages';
25
+ import type * as t from '@/types';
26
+ import { Providers } from '@/common';
27
+ import { initializeModel } from '@/llm/init';
28
+ import { foldToolBlocksForToollessAgent } from '@/messages';
29
+
30
+ const accessKeyId =
31
+ process.env.BEDROCK_AWS_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY_ID;
32
+ const secretAccessKey =
33
+ process.env.BEDROCK_AWS_SECRET_ACCESS_KEY ??
34
+ process.env.AWS_SECRET_ACCESS_KEY;
35
+
36
+ const shouldRunLive =
37
+ process.env.RUN_BEDROCK_LIVE_TESTS === '1' &&
38
+ accessKeyId != null &&
39
+ accessKeyId !== '' &&
40
+ secretAccessKey != null &&
41
+ secretAccessKey !== '';
42
+
43
+ const describeIfLive = shouldRunLive ? describe : describe.skip;
44
+
45
+ const MODEL =
46
+ process.env.LIVE_BEDROCK_MODEL ??
47
+ 'us.anthropic.claude-sonnet-4-5-20250929-v1:0';
48
+ const REGION =
49
+ process.env.BEDROCK_AWS_DEFAULT_REGION ??
50
+ process.env.AWS_REGION ??
51
+ 'us-west-2';
52
+
53
+ /** History a tool-less destination inherits: a completed tool call, then a
54
+ * follow-up user turn that itself invokes no tool. */
55
+ function toollessHistory(): BaseMessage[] {
56
+ return [
57
+ new HumanMessage('Search my files for the roadmap.'),
58
+ new AIMessage({
59
+ content: '',
60
+ tool_calls: [
61
+ {
62
+ id: 'tt_live_1',
63
+ name: 'file_search',
64
+ args: { query: 'roadmap' },
65
+ type: 'tool_call',
66
+ },
67
+ ],
68
+ }),
69
+ new ToolMessage({
70
+ content: 'Found: roadmap.md — Q3 goals and milestones.',
71
+ tool_call_id: 'tt_live_1',
72
+ name: 'file_search',
73
+ }),
74
+ new AIMessage('I found roadmap.md with your Q3 goals.'),
75
+ new HumanMessage('thanks!'),
76
+ ];
77
+ }
78
+
79
+ describeIfLive('Bedrock tool-less destination (live)', () => {
80
+ let clientOptions: t.BedrockConverseClientOptions;
81
+
82
+ beforeAll(() => {
83
+ // Force SigV4 with the explicit keys; the Bedrock API-key (bearer) auth
84
+ // scheme otherwise takes precedence in the AWS SDK.
85
+ delete process.env.AWS_BEARER_TOKEN_BEDROCK;
86
+ // `shouldRunLive` already guarantees both keys are set; `?? ''` only keeps
87
+ // the credential fields typed as strings.
88
+ clientOptions = {
89
+ region: REGION,
90
+ model: MODEL,
91
+ credentials: {
92
+ accessKeyId: accessKeyId ?? '',
93
+ secretAccessKey: secretAccessKey ?? '',
94
+ },
95
+ };
96
+ });
97
+
98
+ it('rejects inherited tool history when the agent binds no tools', async () => {
99
+ const model = initializeModel({
100
+ provider: Providers.BEDROCK,
101
+ clientOptions,
102
+ tools: undefined,
103
+ });
104
+ await expect(model.invoke(toollessHistory())).rejects.toThrow(
105
+ /toolConfig field must be defined/i
106
+ );
107
+ }, 30000);
108
+
109
+ it('succeeds once inherited tool blocks are folded to text', async () => {
110
+ const model = initializeModel({
111
+ provider: Providers.BEDROCK,
112
+ clientOptions,
113
+ tools: undefined,
114
+ });
115
+ const folded = foldToolBlocksForToollessAgent(toollessHistory());
116
+ const res = await model.invoke(folded);
117
+ const text =
118
+ typeof res.content === 'string'
119
+ ? res.content
120
+ : JSON.stringify(res.content);
121
+ expect(text.length).toBeGreaterThan(0);
122
+ }, 30000);
123
+ });
@@ -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
+ });