@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
@@ -1,28 +1,77 @@
1
+ import type { Providers } from '@/common';
1
2
  /**
2
- * Context overflow error detection utilities.
3
- *
4
- * Identifies provider-specific error messages that indicate the request
5
- * exceeded the model's context window. Used by the overflow recovery loop
6
- * to decide whether to retry with truncation/compaction vs. propagating
7
- * the error.
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
- * Returns true if the error message definitively indicates a context
15
- * overflow / prompt-too-large error from the provider.
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
- * This is the strict check: only matches known, unambiguous phrases.
18
- * Use this when you want high confidence before taking recovery action.
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(errorMessage?: string): boolean;
69
+ export declare function isContextOverflowError(error?: unknown, context?: ContextOverflowContext): boolean;
21
70
  /**
22
- * Returns true if the error message likely indicates a context overflow.
23
- * Uses broader heuristic matching (regex) in addition to exact phrases.
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 for unusual error messages. Use this when
26
- * the cost of a false positive (one extra retry) is acceptable.
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(errorMessage?: string): boolean;
77
+ export declare function isLikelyContextOverflowError(error?: unknown, context?: ContextOverflowContext): boolean;
@@ -0,0 +1,3 @@
1
+ export declare function isSecretKey(key: string): boolean;
2
+ /** Recursively removes credentials from structured diagnostic payloads. */
3
+ export declare function redactSecrets(value: unknown, seen?: WeakSet<object>): unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.68",
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.2.0"
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.16.0",
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.54"
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.54",
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.1",
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?: Map<number, string>;
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
+ });
@@ -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.) */