@librechat/agents 3.3.0 → 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.
- 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 +7 -8
- 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/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
|
@@ -18,6 +18,13 @@ import {
|
|
|
18
18
|
cloneMessage,
|
|
19
19
|
type PromptCacheTtl,
|
|
20
20
|
} from '@/messages/cache';
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_RESERVE_RATIO,
|
|
23
|
+
ORIGINAL_CONTENT_MAX_CHARS,
|
|
24
|
+
clampCalibrationRatio,
|
|
25
|
+
createPruneMessages,
|
|
26
|
+
syncBudgetDerivedFields,
|
|
27
|
+
} from '@/messages';
|
|
21
28
|
import {
|
|
22
29
|
ANTHROPIC_TOOL_TOKEN_MULTIPLIER,
|
|
23
30
|
DEFAULT_TOOL_TOKEN_MULTIPLIER,
|
|
@@ -25,11 +32,6 @@ import {
|
|
|
25
32
|
Constants,
|
|
26
33
|
Providers,
|
|
27
34
|
} from '@/common';
|
|
28
|
-
import {
|
|
29
|
-
DEFAULT_RESERVE_RATIO,
|
|
30
|
-
createPruneMessages,
|
|
31
|
-
syncBudgetDerivedFields,
|
|
32
|
-
} from '@/messages';
|
|
33
35
|
import { createSchemaOnlyTools } from '@/tools/schema';
|
|
34
36
|
import { apportionTokenCounts } from '@/utils/tokens';
|
|
35
37
|
import { isThinkingEnabled } from '@/llm/request';
|
|
@@ -220,8 +222,22 @@ export class AgentContext {
|
|
|
220
222
|
calibrationRatio: number = 1;
|
|
221
223
|
/** Provider-observed instruction overhead from the pruner's best-variance turn. */
|
|
222
224
|
resolvedInstructionOverhead?: number;
|
|
225
|
+
private _pendingOriginalToolContent?: Map<number, string>;
|
|
226
|
+
private pendingOriginalToolContentChars = 0;
|
|
223
227
|
/** Pre-masking tool content keyed by message index, consumed by the summarize node. */
|
|
224
|
-
pendingOriginalToolContent
|
|
228
|
+
get pendingOriginalToolContent(): Map<number, string> | undefined {
|
|
229
|
+
return this._pendingOriginalToolContent;
|
|
230
|
+
}
|
|
231
|
+
set pendingOriginalToolContent(value: Map<number, string> | undefined) {
|
|
232
|
+
this._pendingOriginalToolContent = value;
|
|
233
|
+
this.pendingOriginalToolContentChars = 0;
|
|
234
|
+
if (value != null) {
|
|
235
|
+
for (const content of value.values()) {
|
|
236
|
+
this.pendingOriginalToolContentChars += content.length;
|
|
237
|
+
}
|
|
238
|
+
this.enforcePendingOriginalContentCap();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
225
241
|
|
|
226
242
|
/** Total instruction overhead: system message + tool schemas + pending summary. */
|
|
227
243
|
get instructionTokens(): number {
|
|
@@ -322,6 +338,25 @@ export class AgentContext {
|
|
|
322
338
|
* Summarization is allowed to fire again only when new messages appear.
|
|
323
339
|
*/
|
|
324
340
|
private _lastSummarizationMsgCount: number = 0;
|
|
341
|
+
/**
|
|
342
|
+
* Forced compactions performed after a provider rejected a prompt as too
|
|
343
|
+
* large. Bounds the recovery loop so a model that keeps refusing cannot
|
|
344
|
+
* make the run compact indefinitely.
|
|
345
|
+
*/
|
|
346
|
+
private _overflowRecoveryAttempts: number = 0;
|
|
347
|
+
/**
|
|
348
|
+
* Budget in force before the first overflow correction of the current run.
|
|
349
|
+
* Recorded so `reset()` can undo the correction for the next run without
|
|
350
|
+
* disturbing a `maxContextTokens` that no correction ever touched.
|
|
351
|
+
*/
|
|
352
|
+
private _preOverflowMaxContextTokens?: number;
|
|
353
|
+
/**
|
|
354
|
+
* Prompt size, normalized into the local counter's uncalibrated units, at
|
|
355
|
+
* the last overflow correction. Keeping both measurements in the same units
|
|
356
|
+
* lets a later overflow prove whether compaction changed anything even when
|
|
357
|
+
* the provider observation updated calibration between attempts.
|
|
358
|
+
*/
|
|
359
|
+
private _lastOverflowPromptTokens?: number;
|
|
325
360
|
/**
|
|
326
361
|
* Handoff context when this agent receives control via handoff.
|
|
327
362
|
* Contains source and parallel execution info for system message context.
|
|
@@ -964,7 +999,7 @@ export class AgentContext {
|
|
|
964
999
|
/**
|
|
965
1000
|
* Reset context for a new run
|
|
966
1001
|
*/
|
|
967
|
-
reset(): void {
|
|
1002
|
+
reset(options?: { preserveOriginalToolContent?: boolean }): void {
|
|
968
1003
|
this.systemMessageTokens = 0;
|
|
969
1004
|
this.dynamicInstructionTokens = 0;
|
|
970
1005
|
this.toolSchemaTokens = 0;
|
|
@@ -982,12 +1017,16 @@ export class AgentContext {
|
|
|
982
1017
|
this.currentTokenType = ContentTypes.TEXT;
|
|
983
1018
|
this.discoveredToolNames.clear();
|
|
984
1019
|
this.handoffContext = undefined;
|
|
1020
|
+
if (options?.preserveOriginalToolContent !== true) {
|
|
1021
|
+
this.pendingOriginalToolContent = undefined;
|
|
1022
|
+
}
|
|
985
1023
|
|
|
986
1024
|
this.summaryText = this._durableSummaryText;
|
|
987
1025
|
this.summaryTokenCount = this._durableSummaryTokenCount;
|
|
988
1026
|
this._lastSummarizationMsgCount = 0;
|
|
989
1027
|
this.lastCallUsage = undefined;
|
|
990
1028
|
this.totalTokensFresh = false;
|
|
1029
|
+
this.restoreContextBudgetAfterOverflow();
|
|
991
1030
|
|
|
992
1031
|
if (this.tokenCounter) {
|
|
993
1032
|
this.initializeSystemRunnable();
|
|
@@ -1308,6 +1347,148 @@ export class AgentContext {
|
|
|
1308
1347
|
this._lastSummarizationMsgCount = msgCount;
|
|
1309
1348
|
}
|
|
1310
1349
|
|
|
1350
|
+
get overflowRecoveryAttempts(): number {
|
|
1351
|
+
return this._overflowRecoveryAttempts;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
shouldSummarizeOverflow(): boolean {
|
|
1355
|
+
return (
|
|
1356
|
+
this.summarizationEnabled === true &&
|
|
1357
|
+
(this.tokenCounter == null ||
|
|
1358
|
+
this.maxContextTokens == null ||
|
|
1359
|
+
this._overflowRecoveryAttempts > 0)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** Preserves the earliest full tool output recorded for each message index. */
|
|
1364
|
+
preserveOriginalToolContent(
|
|
1365
|
+
originalToolContent: Map<number, string> | undefined
|
|
1366
|
+
): void {
|
|
1367
|
+
if (originalToolContent == null || originalToolContent.size === 0) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (this.pendingOriginalToolContent == null) {
|
|
1371
|
+
this.pendingOriginalToolContent = new Map();
|
|
1372
|
+
}
|
|
1373
|
+
for (const [index, content] of originalToolContent) {
|
|
1374
|
+
if (!this.pendingOriginalToolContent.has(index)) {
|
|
1375
|
+
this.pendingOriginalToolContent.set(index, content);
|
|
1376
|
+
this.pendingOriginalToolContentChars += content.length;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
this.enforcePendingOriginalContentCap();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private enforcePendingOriginalContentCap(): void {
|
|
1383
|
+
const pending = this._pendingOriginalToolContent;
|
|
1384
|
+
if (pending == null) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
while (
|
|
1388
|
+
this.pendingOriginalToolContentChars > ORIGINAL_CONTENT_MAX_CHARS &&
|
|
1389
|
+
pending.size > 0
|
|
1390
|
+
) {
|
|
1391
|
+
const oldest = pending.keys().next();
|
|
1392
|
+
if (oldest.done === true) {
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
const removed = pending.get(oldest.value);
|
|
1396
|
+
if (removed != null) {
|
|
1397
|
+
this.pendingOriginalToolContentChars -= removed.length;
|
|
1398
|
+
}
|
|
1399
|
+
pending.delete(oldest.value);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Retargets the context budget after a provider rejected the prompt as too
|
|
1405
|
+
* large, and clears the memoized pruner so the next call is planned against
|
|
1406
|
+
* the corrected budget rather than the one that was evidently wrong.
|
|
1407
|
+
*
|
|
1408
|
+
* Also clears the "already summarized at this message count" guard: that
|
|
1409
|
+
* guard exists to stop redundant summarization of an unchanged history, but
|
|
1410
|
+
* here the history has not changed and compaction is exactly what is
|
|
1411
|
+
* needed.
|
|
1412
|
+
*/
|
|
1413
|
+
applyContextBudgetCorrection(
|
|
1414
|
+
budgetTokens: number | undefined,
|
|
1415
|
+
promptTokens?: number
|
|
1416
|
+
): void {
|
|
1417
|
+
if (this._overflowRecoveryAttempts === 0) {
|
|
1418
|
+
this._preOverflowMaxContextTokens = this.maxContextTokens;
|
|
1419
|
+
}
|
|
1420
|
+
if (budgetTokens != null) {
|
|
1421
|
+
this.maxContextTokens = budgetTokens;
|
|
1422
|
+
}
|
|
1423
|
+
this.pruneMessages = undefined;
|
|
1424
|
+
this._lastSummarizationMsgCount = 0;
|
|
1425
|
+
this._lastOverflowPromptTokens =
|
|
1426
|
+
promptTokens != null
|
|
1427
|
+
? this.normalizePromptTokens(promptTokens)
|
|
1428
|
+
: promptTokens;
|
|
1429
|
+
this._overflowRecoveryAttempts += 1;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** Applies token calibration only when the observation came from this provider. */
|
|
1433
|
+
applyObservedOverflowCalibration(
|
|
1434
|
+
provider: Providers | undefined,
|
|
1435
|
+
observedCalibrationRatio: number | undefined
|
|
1436
|
+
): void {
|
|
1437
|
+
if (
|
|
1438
|
+
provider !== this.provider ||
|
|
1439
|
+
observedCalibrationRatio == null ||
|
|
1440
|
+
observedCalibrationRatio <= 0
|
|
1441
|
+
) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
this.calibrationRatio = clampCalibrationRatio(observedCalibrationRatio);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* True when a previous correction failed to make the prompt any smaller —
|
|
1449
|
+
* the signature of a state nothing can compact further (an emptied message
|
|
1450
|
+
* list carrying its content in an injected summary, for example). Retrying
|
|
1451
|
+
* from there resends a byte-identical prompt, so the caller should stop.
|
|
1452
|
+
*/
|
|
1453
|
+
overflowRecoveryStalled(currentPromptTokens?: number): boolean {
|
|
1454
|
+
const previous = this._lastOverflowPromptTokens;
|
|
1455
|
+
if (
|
|
1456
|
+
previous == null ||
|
|
1457
|
+
currentPromptTokens == null ||
|
|
1458
|
+
!Number.isFinite(currentPromptTokens)
|
|
1459
|
+
) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
const rawCurrent = this.normalizePromptTokens(currentPromptTokens);
|
|
1463
|
+
return rawCurrent >= previous;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private normalizePromptTokens(promptTokens: number): number {
|
|
1467
|
+
if (this.calibrationRatio <= 0) {
|
|
1468
|
+
return promptTokens;
|
|
1469
|
+
}
|
|
1470
|
+
const messageTokens = Math.max(0, promptTokens - this.instructionTokens);
|
|
1471
|
+
return this.instructionTokens + messageTokens / this.calibrationRatio;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Undoes overflow corrections so a reused context starts the next run with
|
|
1476
|
+
* the budget it was configured with and a fresh recovery allowance.
|
|
1477
|
+
*
|
|
1478
|
+
* Without this, a single overflow would permanently shrink the budget for
|
|
1479
|
+
* every later turn, and two would exhaust the per-run allowance for the
|
|
1480
|
+
* lifetime of the context.
|
|
1481
|
+
*/
|
|
1482
|
+
private restoreContextBudgetAfterOverflow(): void {
|
|
1483
|
+
if (this._overflowRecoveryAttempts === 0) {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
this.maxContextTokens = this._preOverflowMaxContextTokens;
|
|
1487
|
+
this._preOverflowMaxContextTokens = undefined;
|
|
1488
|
+
this._lastOverflowPromptTokens = undefined;
|
|
1489
|
+
this._overflowRecoveryAttempts = 0;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1311
1492
|
clearSummary(): void {
|
|
1312
1493
|
if (this.summaryText != null) {
|
|
1313
1494
|
this.summaryText = undefined;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type * as t from '@/types';
|
|
2
|
+
import { AgentContext } from '@/agents/AgentContext';
|
|
3
|
+
import { Providers } from '@/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The overflow-recovery bookkeeping on AgentContext: the budget correction,
|
|
7
|
+
* its restoration between runs, and the stall detector that stops a recovery
|
|
8
|
+
* loop when a correction demonstrably changed nothing.
|
|
9
|
+
*/
|
|
10
|
+
describe('AgentContext overflow recovery state', () => {
|
|
11
|
+
const createContext = (maxContextTokens?: number): AgentContext =>
|
|
12
|
+
AgentContext.fromConfig({
|
|
13
|
+
agentId: 'overflow-agent',
|
|
14
|
+
provider: Providers.ANTHROPIC,
|
|
15
|
+
instructions: 'Test instructions',
|
|
16
|
+
maxContextTokens,
|
|
17
|
+
} as Partial<t.AgentInputs> as t.AgentInputs);
|
|
18
|
+
|
|
19
|
+
it('records the correction and counts the attempt', () => {
|
|
20
|
+
const context = createContext(1_000_000);
|
|
21
|
+
context.applyContextBudgetCorrection(190_000, 274_468);
|
|
22
|
+
|
|
23
|
+
expect(context.maxContextTokens).toBe(190_000);
|
|
24
|
+
expect(context.overflowRecoveryAttempts).toBe(1);
|
|
25
|
+
/** Forces the pruner to be rebuilt against the corrected budget. */
|
|
26
|
+
expect(context.pruneMessages).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('summarizes the first overflow when deterministic pruning is unavailable', () => {
|
|
30
|
+
const context = createContext(1_000_000);
|
|
31
|
+
context.summarizationEnabled = true;
|
|
32
|
+
|
|
33
|
+
expect(context.shouldSummarizeOverflow()).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('summarizes immediately when no pruning budget is configured', () => {
|
|
37
|
+
const context = AgentContext.fromConfig(
|
|
38
|
+
{
|
|
39
|
+
agentId: 'overflow-agent',
|
|
40
|
+
provider: Providers.ANTHROPIC,
|
|
41
|
+
instructions: 'Test instructions',
|
|
42
|
+
summarizationEnabled: true,
|
|
43
|
+
} as Partial<t.AgentInputs> as t.AgentInputs,
|
|
44
|
+
() => 1
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(context.shouldSummarizeOverflow()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('stages deterministic pruning before summarization when a counter exists', () => {
|
|
51
|
+
const context = AgentContext.fromConfig(
|
|
52
|
+
{
|
|
53
|
+
agentId: 'overflow-agent',
|
|
54
|
+
provider: Providers.ANTHROPIC,
|
|
55
|
+
instructions: 'Test instructions',
|
|
56
|
+
maxContextTokens: 1_000_000,
|
|
57
|
+
summarizationEnabled: true,
|
|
58
|
+
} as Partial<t.AgentInputs> as t.AgentInputs,
|
|
59
|
+
() => 1
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(context.shouldSummarizeOverflow()).toBe(false);
|
|
63
|
+
context.applyContextBudgetCorrection(190_000, 274_468);
|
|
64
|
+
expect(context.shouldSummarizeOverflow()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('preserves the earliest full tool output when masking records collide', () => {
|
|
68
|
+
const context = createContext(1_000_000);
|
|
69
|
+
context.preserveOriginalToolContent(
|
|
70
|
+
new Map([
|
|
71
|
+
[2, 'full output'],
|
|
72
|
+
[4, 'another output'],
|
|
73
|
+
])
|
|
74
|
+
);
|
|
75
|
+
context.preserveOriginalToolContent(
|
|
76
|
+
new Map([
|
|
77
|
+
[2, 'truncated placeholder'],
|
|
78
|
+
[6, 'new output'],
|
|
79
|
+
])
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(context.pendingOriginalToolContent).toEqual(
|
|
83
|
+
new Map([
|
|
84
|
+
[2, 'full output'],
|
|
85
|
+
[4, 'another output'],
|
|
86
|
+
[6, 'new output'],
|
|
87
|
+
])
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('releases index-keyed tool output snapshots on reset', () => {
|
|
92
|
+
const context = createContext(1_000_000);
|
|
93
|
+
context.preserveOriginalToolContent(new Map([[2, 'full output']]));
|
|
94
|
+
context.reset();
|
|
95
|
+
|
|
96
|
+
expect(context.pendingOriginalToolContent).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('preserves tool output snapshots when checkpointed messages survive reset', () => {
|
|
100
|
+
const context = createContext(1_000_000);
|
|
101
|
+
context.preserveOriginalToolContent(new Map([[2, 'full output']]));
|
|
102
|
+
context.reset({ preserveOriginalToolContent: true });
|
|
103
|
+
|
|
104
|
+
expect(context.pendingOriginalToolContent).toEqual(
|
|
105
|
+
new Map([[2, 'full output']])
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('restores the pre-correction budget on reset', () => {
|
|
110
|
+
const context = createContext(1_000_000);
|
|
111
|
+
context.applyContextBudgetCorrection(190_000, 274_468);
|
|
112
|
+
context.applyContextBudgetCorrection(133_000, 180_000);
|
|
113
|
+
context.reset();
|
|
114
|
+
|
|
115
|
+
expect(context.maxContextTokens).toBe(1_000_000);
|
|
116
|
+
expect(context.overflowRecoveryAttempts).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('leaves an untouched budget alone on reset', () => {
|
|
120
|
+
const context = createContext(1_000_000);
|
|
121
|
+
context.maxContextTokens = 500_000;
|
|
122
|
+
context.reset();
|
|
123
|
+
|
|
124
|
+
expect(context.maxContextTokens).toBe(500_000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('keeps fallback calibration out of the primary agent context', () => {
|
|
128
|
+
const context = createContext(1_000_000);
|
|
129
|
+
context.calibrationRatio = 1.5;
|
|
130
|
+
|
|
131
|
+
context.applyObservedOverflowCalibration(Providers.VERTEXAI, 2);
|
|
132
|
+
expect(context.calibrationRatio).toBe(1.5);
|
|
133
|
+
|
|
134
|
+
context.applyObservedOverflowCalibration(Providers.ANTHROPIC, 2);
|
|
135
|
+
expect(context.calibrationRatio).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('clamps provider-observed calibration to the shared safe range', () => {
|
|
139
|
+
const context = createContext(1_000_000);
|
|
140
|
+
|
|
141
|
+
context.applyObservedOverflowCalibration(Providers.ANTHROPIC, 10);
|
|
142
|
+
expect(context.calibrationRatio).toBe(5);
|
|
143
|
+
|
|
144
|
+
context.applyObservedOverflowCalibration(Providers.ANTHROPIC, 0.1);
|
|
145
|
+
expect(context.calibrationRatio).toBe(0.5);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('records a summary-only recovery without inventing a token budget', () => {
|
|
149
|
+
const context = createContext();
|
|
150
|
+
context.applyContextBudgetCorrection(undefined, undefined);
|
|
151
|
+
|
|
152
|
+
expect(context.maxContextTokens).toBeUndefined();
|
|
153
|
+
expect(context.overflowRecoveryAttempts).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports a stall when the prompt did not shrink', () => {
|
|
157
|
+
const context = createContext(1_000_000);
|
|
158
|
+
context.applyContextBudgetCorrection(190_000, 250_000);
|
|
159
|
+
|
|
160
|
+
expect(context.overflowRecoveryStalled(250_000)).toBe(true);
|
|
161
|
+
expect(context.overflowRecoveryStalled(260_000)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('reports no stall while the prompt is still shrinking', () => {
|
|
165
|
+
const context = createContext(1_000_000);
|
|
166
|
+
context.applyContextBudgetCorrection(190_000, 250_000);
|
|
167
|
+
|
|
168
|
+
expect(context.overflowRecoveryStalled(180_000)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('compares stall measurements in uncalibrated token units', () => {
|
|
172
|
+
const context = createContext(1_000_000);
|
|
173
|
+
context.applyContextBudgetCorrection(190_000, 250_000);
|
|
174
|
+
context.calibrationRatio = 2;
|
|
175
|
+
|
|
176
|
+
expect(context.overflowRecoveryStalled(360_000)).toBe(false);
|
|
177
|
+
expect(context.overflowRecoveryStalled(500_000)).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('leaves fixed instruction overhead out of calibration normalization', () => {
|
|
181
|
+
const context = createContext(1_000_000);
|
|
182
|
+
context.systemMessageTokens = 100_000;
|
|
183
|
+
context.applyContextBudgetCorrection(190_000, 250_000);
|
|
184
|
+
context.calibrationRatio = 0.5;
|
|
185
|
+
|
|
186
|
+
expect(context.overflowRecoveryStalled(160_000)).toBe(false);
|
|
187
|
+
expect(context.overflowRecoveryStalled(175_000)).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('reports no stall before any correction, or without a measurement', () => {
|
|
191
|
+
const context = createContext(1_000_000);
|
|
192
|
+
expect(context.overflowRecoveryStalled(250_000)).toBe(false);
|
|
193
|
+
|
|
194
|
+
context.applyContextBudgetCorrection(190_000, 250_000);
|
|
195
|
+
expect(context.overflowRecoveryStalled(undefined)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('clears the stall measurement on reset', () => {
|
|
199
|
+
const context = createContext(1_000_000);
|
|
200
|
+
context.applyContextBudgetCorrection(190_000, 250_000);
|
|
201
|
+
context.reset();
|
|
202
|
+
|
|
203
|
+
expect(context.overflowRecoveryStalled(250_000)).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
package/src/common/enum.ts
CHANGED
|
@@ -186,6 +186,8 @@ export enum Constants {
|
|
|
186
186
|
WEB_SEARCH = 'web_search',
|
|
187
187
|
CONTENT_AND_ARTIFACT = 'content_and_artifact',
|
|
188
188
|
LC_TRANSFER_TO_ = 'lc_transfer_to_',
|
|
189
|
+
HANDOFF_PARALLEL_BATCH = '__handoff_parallel_batch',
|
|
190
|
+
HANDOFF_GROUP_ID = '__handoff_group_id',
|
|
189
191
|
/** Delimiter for MCP tools: toolName_mcp_serverName */
|
|
190
192
|
MCP_DELIMITER = '_mcp_',
|
|
191
193
|
/** Anthropic server tool ID prefix (web_search, code_execution, etc.) */
|