@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.
- package/dist/cjs/agents/AgentContext.cjs +117 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +4 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +243 -49
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +103 -28
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
- package/dist/cjs/langfuseToolOutputTracing.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/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +21 -7
- package/dist/cjs/messages/format.cjs +136 -4
- package/dist/cjs/messages/format.cjs.map +1 -1
- 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/prompts/activityLabel.cjs +101 -0
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
- package/dist/cjs/run.cjs +177 -3
- 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/tools/subagent/SubagentExecutor.cjs +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 +118 -4
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +4 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +242 -48
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +103 -28
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
- package/dist/esm/langfuseToolOutputTracing.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/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +10 -10
- package/dist/esm/messages/format.mjs +136 -5
- package/dist/esm/messages/format.mjs.map +1 -1
- 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/prompts/activityLabel.mjs +100 -0
- package/dist/esm/prompts/activityLabel.mjs.map +1 -0
- package/dist/esm/run.mjs +178 -4
- 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/tools/subagent/SubagentExecutor.mjs +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 +5 -1
- package/dist/types/graphs/Graph.d.ts +16 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
- package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
- package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
- package/dist/types/llm/invoke.d.ts +35 -6
- package/dist/types/messages/format.d.ts +22 -0
- package/dist/types/messages/prune.d.ts +10 -2
- package/dist/types/messages/recency.d.ts +1 -0
- package/dist/types/prompts/activityLabel.d.ts +31 -0
- package/dist/types/run.d.ts +16 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -1
- package/dist/types/types/activityLabel.d.ts +53 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/llm.d.ts +7 -4
- package/dist/types/types/stream.d.ts +7 -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 +4 -0
- package/src/graphs/Graph.ts +409 -58
- package/src/graphs/MultiAgentGraph.ts +184 -46
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
- package/src/langfuseToolOutputTracing.ts +4 -1
- 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/foldToollessToolBlocks.test.ts +438 -0
- package/src/messages/format.ts +233 -5
- package/src/messages/prune.ts +24 -11
- package/src/messages/recency.ts +3 -1
- package/src/prompts/activityLabel.ts +177 -0
- package/src/run.ts +322 -3
- package/src/scripts/context-overflow-probe.ts +997 -0
- package/src/specs/activity-label-prompt.test.ts +128 -0
- package/src/specs/activity-label-trace-seed.test.ts +47 -0
- package/src/specs/agent-handoffs.test.ts +903 -1
- package/src/specs/bedrock-toolless.live.test.ts +123 -0
- 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/activityLabel.ts +55 -0
- package/src/types/index.ts +1 -0
- package/src/types/llm.ts +8 -1
- package/src/types/stream.ts +7 -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
|
@@ -1,28 +1,77 @@
|
|
|
1
|
+
import type { Providers } from '@/common';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
* Why the request was rejected. Both kinds are fixed by shrinking the
|
|
4
|
+
* prompt, which is what makes them recoverable; they are distinguished
|
|
5
|
+
* because only `context_window` tells us the model's true window.
|
|
6
|
+
*/
|
|
7
|
+
export type ContextOverflowKind =
|
|
8
|
+
/** Input exceeded the model's context window. */
|
|
9
|
+
'context_window'
|
|
10
|
+
/**
|
|
11
|
+
* A single request exceeded a per-minute token allowance. Waiting cannot
|
|
12
|
+
* help — the request can never fit the bucket — so this is a payload
|
|
13
|
+
* problem wearing a 429, not throttling.
|
|
8
14
|
*/
|
|
15
|
+
| 'request_too_large';
|
|
16
|
+
export interface ContextOverflowInfo {
|
|
17
|
+
kind: ContextOverflowKind;
|
|
18
|
+
/** Ceiling the provider reported, when it named one. */
|
|
19
|
+
limitTokens?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Token count the provider attributed to the whole request. Several
|
|
22
|
+
* providers fold the requested completion allowance into this number, so it
|
|
23
|
+
* is not interchangeable with the prompt size.
|
|
24
|
+
*/
|
|
25
|
+
requestedTokens?: number;
|
|
26
|
+
/**
|
|
27
|
+
* The prompt alone, counted by the provider — set only when the provider
|
|
28
|
+
* distinguished input from output, either by reporting an input-only figure
|
|
29
|
+
* or by breaking the total down. Callers comparing provider counts against
|
|
30
|
+
* their own prompt estimate must use this and not `requestedTokens`, whose
|
|
31
|
+
* completion component would inflate the comparison.
|
|
32
|
+
*/
|
|
33
|
+
promptTokens?: number;
|
|
34
|
+
/** Which layer produced the verdict. Surfaced in logs and asserted in tests. */
|
|
35
|
+
source: 'langchain' | 'pattern';
|
|
36
|
+
provider?: Providers;
|
|
37
|
+
}
|
|
38
|
+
export interface ContextOverflowContext {
|
|
39
|
+
provider?: Providers;
|
|
40
|
+
/** Our own estimate of the prompt size for the call that failed. */
|
|
41
|
+
estimatedPromptTokens?: number;
|
|
42
|
+
/** The budget we believed applied when we built that prompt. */
|
|
43
|
+
maxContextTokens?: number;
|
|
44
|
+
}
|
|
9
45
|
/**
|
|
10
46
|
* Extracts a human-readable error message from an unknown error value.
|
|
11
47
|
*/
|
|
12
48
|
export declare function extractErrorMessage(error: unknown): string;
|
|
13
49
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
50
|
+
* Classifies a provider error as a recoverable context overflow, returning
|
|
51
|
+
* whatever the provider disclosed about the limit and the request size.
|
|
52
|
+
*
|
|
53
|
+
* Returns `null` for anything that compaction cannot fix — genuine
|
|
54
|
+
* throttling, auth, quota, and output-token-cap errors all mention limits
|
|
55
|
+
* and must not be mistaken for overflow.
|
|
56
|
+
*
|
|
57
|
+
* The reported numbers are the point of the return value: they let the
|
|
58
|
+
* caller retarget the token budget to the provider's real ceiling instead of
|
|
59
|
+
* retrying blindly against a configured value that was evidently wrong.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getContextOverflowInfo(error: unknown, context?: ContextOverflowContext): ContextOverflowInfo | null;
|
|
62
|
+
/**
|
|
63
|
+
* Returns true if the error definitively indicates a context overflow.
|
|
16
64
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
65
|
+
* Accepts either a raw error or a pre-extracted message; passing the error
|
|
66
|
+
* itself is preferred, since several providers report the decisive detail in
|
|
67
|
+
* a nested body rather than in `message`.
|
|
19
68
|
*/
|
|
20
|
-
export declare function isContextOverflowError(
|
|
69
|
+
export declare function isContextOverflowError(error?: unknown, context?: ContextOverflowContext): boolean;
|
|
21
70
|
/**
|
|
22
|
-
* Returns true if the error
|
|
23
|
-
*
|
|
71
|
+
* Returns true if the error likely indicates a context overflow, adding
|
|
72
|
+
* body-size and token-count heuristics on top of the definitive patterns.
|
|
24
73
|
*
|
|
25
|
-
* May produce false positives
|
|
26
|
-
*
|
|
74
|
+
* May produce false positives on unusual messages. Use when the cost of
|
|
75
|
+
* being wrong is one extra compaction pass.
|
|
27
76
|
*/
|
|
28
|
-
export declare function isLikelyContextOverflowError(
|
|
77
|
+
export declare function isLikelyContextOverflowError(error?: unknown, context?: ContextOverflowContext): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"main": "./dist/cjs/main.cjs",
|
|
5
5
|
"module": "./dist/esm/main.mjs",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -140,6 +140,7 @@
|
|
|
140
140
|
"search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/search.ts --provider 'bedrock' --name 'Jo' --location 'New York, NY'",
|
|
141
141
|
"tool_search": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/tool_search.ts",
|
|
142
142
|
"bench:cache": "node --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/bench-prompt-cache.ts",
|
|
143
|
+
"probe:overflow": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/context-overflow-probe.ts",
|
|
143
144
|
"subagent": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/multi-agent-subagent.ts",
|
|
144
145
|
"subagent:events": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/subagent-event-driven-debug.ts",
|
|
145
146
|
"subagent:tools": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/scripts/subagent-tools-debug.ts",
|
|
@@ -205,11 +206,9 @@
|
|
|
205
206
|
"openai": "$openai"
|
|
206
207
|
},
|
|
207
208
|
"uuid": "$uuid",
|
|
208
|
-
"fast-xml-parser": "5.7.2",
|
|
209
209
|
"ajv": "6.14.0",
|
|
210
|
-
"minimatch": "3.1.4",
|
|
211
210
|
"@opentelemetry/core": "^2.9.0",
|
|
212
|
-
"js-yaml": "^4.
|
|
211
|
+
"js-yaml": "^4.3.0"
|
|
213
212
|
},
|
|
214
213
|
"dependencies": {
|
|
215
214
|
"@anthropic-ai/sdk": "^0.103.0",
|
|
@@ -235,7 +234,7 @@
|
|
|
235
234
|
"@scarf/scarf": "^1.4.0",
|
|
236
235
|
"@types/diff": "^7.0.2",
|
|
237
236
|
"ai-tokenizer": "^1.0.6",
|
|
238
|
-
"axios": "^1.
|
|
237
|
+
"axios": "^1.18.1",
|
|
239
238
|
"cheerio": "^1.0.0",
|
|
240
239
|
"diff": "^9.0.0",
|
|
241
240
|
"dotenv": "^16.4.7",
|
|
@@ -247,7 +246,7 @@
|
|
|
247
246
|
"uuid": "^11.1.1"
|
|
248
247
|
},
|
|
249
248
|
"peerDependencies": {
|
|
250
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.
|
|
249
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.67"
|
|
251
250
|
},
|
|
252
251
|
"peerDependenciesMeta": {
|
|
253
252
|
"@anthropic-ai/sandbox-runtime": {
|
|
@@ -259,7 +258,7 @@
|
|
|
259
258
|
"~/*": "./*"
|
|
260
259
|
},
|
|
261
260
|
"devDependencies": {
|
|
262
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.
|
|
261
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.67",
|
|
263
262
|
"@anthropic-ai/vertex-sdk": "^0.12.0",
|
|
264
263
|
"@eslint/compat": "^1.2.7",
|
|
265
264
|
"@langchain/langgraph-checkpoint-mongodb": "^1.4.0",
|
|
@@ -270,7 +269,7 @@
|
|
|
270
269
|
"@types/yargs-parser": "^21.0.3",
|
|
271
270
|
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
|
272
271
|
"@typescript-eslint/parser": "^8.24.0",
|
|
273
|
-
"eslint": "^9.39.
|
|
272
|
+
"eslint": "^9.39.5",
|
|
274
273
|
"eslint-import-resolver-typescript": "^3.7.0",
|
|
275
274
|
"eslint-plugin-import": "^2.31.0",
|
|
276
275
|
"husky": "^9.1.7",
|
|
@@ -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
|
@@ -142,6 +142,8 @@ export enum ContentTypes {
|
|
|
142
142
|
REASONING_CONTENT = 'reasoning_content',
|
|
143
143
|
/** Mid-run user steer persisted inline in an assistant message; replayed as a user turn */
|
|
144
144
|
STEER = 'steer',
|
|
145
|
+
/** Fast-model activity label for a tool/reasoning block; UI-only, never model input */
|
|
146
|
+
ACTIVITY_LABEL = 'activity_label',
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export enum ToolCallTypes {
|
|
@@ -184,6 +186,8 @@ export enum Constants {
|
|
|
184
186
|
WEB_SEARCH = 'web_search',
|
|
185
187
|
CONTENT_AND_ARTIFACT = 'content_and_artifact',
|
|
186
188
|
LC_TRANSFER_TO_ = 'lc_transfer_to_',
|
|
189
|
+
HANDOFF_PARALLEL_BATCH = '__handoff_parallel_batch',
|
|
190
|
+
HANDOFF_GROUP_ID = '__handoff_group_id',
|
|
187
191
|
/** Delimiter for MCP tools: toolName_mcp_serverName */
|
|
188
192
|
MCP_DELIMITER = '_mcp_',
|
|
189
193
|
/** Anthropic server tool ID prefix (web_search, code_execution, etc.) */
|