@illuma-ai/agents 1.0.96 → 1.1.0

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 (76) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +6 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +78 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +191 -165
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +22 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/dedup.cjs +95 -0
  10. package/dist/cjs/messages/dedup.cjs.map +1 -0
  11. package/dist/cjs/tools/CodeExecutor.cjs +22 -3
  12. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  13. package/dist/cjs/types/graph.cjs.map +1 -1
  14. package/dist/cjs/utils/contextPressure.cjs +154 -0
  15. package/dist/cjs/utils/contextPressure.cjs.map +1 -0
  16. package/dist/cjs/utils/pruneCalibration.cjs +78 -0
  17. package/dist/cjs/utils/pruneCalibration.cjs.map +1 -0
  18. package/dist/cjs/utils/run.cjs.map +1 -1
  19. package/dist/cjs/utils/tokens.cjs.map +1 -1
  20. package/dist/cjs/utils/toolDiscoveryCache.cjs +127 -0
  21. package/dist/cjs/utils/toolDiscoveryCache.cjs.map +1 -0
  22. package/dist/esm/agents/AgentContext.mjs +6 -2
  23. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  24. package/dist/esm/common/constants.mjs +71 -1
  25. package/dist/esm/common/constants.mjs.map +1 -1
  26. package/dist/esm/graphs/Graph.mjs +192 -166
  27. package/dist/esm/graphs/Graph.mjs.map +1 -1
  28. package/dist/esm/main.mjs +5 -1
  29. package/dist/esm/main.mjs.map +1 -1
  30. package/dist/esm/messages/dedup.mjs +93 -0
  31. package/dist/esm/messages/dedup.mjs.map +1 -0
  32. package/dist/esm/tools/CodeExecutor.mjs +22 -3
  33. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  34. package/dist/esm/types/graph.mjs.map +1 -1
  35. package/dist/esm/utils/contextPressure.mjs +148 -0
  36. package/dist/esm/utils/contextPressure.mjs.map +1 -0
  37. package/dist/esm/utils/pruneCalibration.mjs +74 -0
  38. package/dist/esm/utils/pruneCalibration.mjs.map +1 -0
  39. package/dist/esm/utils/run.mjs.map +1 -1
  40. package/dist/esm/utils/tokens.mjs.map +1 -1
  41. package/dist/esm/utils/toolDiscoveryCache.mjs +125 -0
  42. package/dist/esm/utils/toolDiscoveryCache.mjs.map +1 -0
  43. package/dist/types/agents/AgentContext.d.ts +4 -1
  44. package/dist/types/common/constants.d.ts +49 -0
  45. package/dist/types/graphs/Graph.d.ts +25 -0
  46. package/dist/types/messages/dedup.d.ts +25 -0
  47. package/dist/types/messages/index.d.ts +1 -0
  48. package/dist/types/types/graph.d.ts +63 -0
  49. package/dist/types/utils/contextPressure.d.ts +72 -0
  50. package/dist/types/utils/index.d.ts +3 -0
  51. package/dist/types/utils/pruneCalibration.d.ts +43 -0
  52. package/dist/types/utils/toolDiscoveryCache.d.ts +77 -0
  53. package/package.json +1 -1
  54. package/src/agents/AgentContext.ts +7 -0
  55. package/src/common/constants.ts +82 -0
  56. package/src/graphs/Graph.ts +254 -208
  57. package/src/graphs/contextManagement.e2e.test.ts +28 -20
  58. package/src/graphs/gapFeatures.test.ts +520 -0
  59. package/src/graphs/nonBlockingSummarization.test.ts +307 -0
  60. package/src/messages/__tests__/dedup.test.ts +166 -0
  61. package/src/messages/dedup.ts +104 -0
  62. package/src/messages/index.ts +1 -0
  63. package/src/specs/agent-handoffs-bedrock.integration.test.ts +7 -7
  64. package/src/specs/agent-handoffs.test.ts +36 -36
  65. package/src/specs/thinking-handoff.test.ts +10 -10
  66. package/src/tools/CodeExecutor.ts +22 -3
  67. package/src/types/graph.ts +73 -0
  68. package/src/utils/__tests__/pruneCalibration.test.ts +148 -0
  69. package/src/utils/__tests__/toolDiscoveryCache.test.ts +214 -0
  70. package/src/utils/contextPressure.test.ts +262 -0
  71. package/src/utils/contextPressure.ts +188 -0
  72. package/src/utils/index.ts +3 -0
  73. package/src/utils/pruneCalibration.ts +92 -0
  74. package/src/utils/run.ts +108 -108
  75. package/src/utils/tokens.ts +118 -118
  76. package/src/utils/toolDiscoveryCache.ts +150 -0
@@ -0,0 +1,74 @@
1
+ import { PRUNING_EMA_ALPHA, PRUNING_INITIAL_CALIBRATION } from '../common/constants.mjs';
2
+
3
+ /**
4
+ * Creates an initial pruning calibration state.
5
+ *
6
+ * @param initialRatio - Starting calibration ratio (default: 1.0)
7
+ * @returns Fresh calibration state
8
+ */
9
+ function createPruneCalibration(initialRatio) {
10
+ return {
11
+ ratio: initialRatio ?? PRUNING_INITIAL_CALIBRATION,
12
+ iterations: 0,
13
+ };
14
+ }
15
+ /**
16
+ * Updates the pruning calibration using Exponential Moving Average (EMA).
17
+ *
18
+ * Problem: Without calibration, the pruner's token estimates can diverge from
19
+ * reality across iterations, causing either:
20
+ * - Over-pruning (context cliff): Too many messages removed at once, losing critical tool results
21
+ * - Under-pruning: Not enough messages removed, hitting hard token limits
22
+ *
23
+ * Solution: Track the ratio between actual token usage (from API response) and
24
+ * estimated token usage (from our token counter). Apply EMA smoothing so the
25
+ * calibration adjusts gradually, preventing oscillation.
26
+ *
27
+ * The calibration ratio is applied to maxTokens in the pruner:
28
+ * effectiveMaxTokens = maxTokens * calibrationRatio
29
+ *
30
+ * If actual > estimated → ratio decreases → prune more aggressively
31
+ * If actual < estimated → ratio increases → prune less aggressively
32
+ *
33
+ * @param state - Current calibration state
34
+ * @param actualTokens - Actual token count from API response (UsageMetadata)
35
+ * @param estimatedTokens - Estimated token count from token counter
36
+ * @param alpha - EMA smoothing factor (default: PRUNING_EMA_ALPHA)
37
+ * @returns Updated calibration state (new object, does not mutate input)
38
+ */
39
+ function updatePruneCalibration(state, actualTokens, estimatedTokens, alpha = PRUNING_EMA_ALPHA) {
40
+ // Guard against division by zero or invalid inputs
41
+ if (estimatedTokens <= 0 || actualTokens <= 0) {
42
+ return state;
43
+ }
44
+ // Raw ratio: how much our estimate differs from reality
45
+ const observedRatio = estimatedTokens / actualTokens;
46
+ // Clamp to prevent extreme adjustments from outlier readings
47
+ // Range [0.5, 2.0] means we never more than double or halve the budget
48
+ const clampedRatio = Math.max(0.5, Math.min(2.0, observedRatio));
49
+ // Apply EMA: new_ratio = α * observed + (1 - α) * previous
50
+ const newRatio = alpha * clampedRatio + (1 - alpha) * state.ratio;
51
+ return {
52
+ ratio: newRatio,
53
+ iterations: state.iterations + 1,
54
+ };
55
+ }
56
+ /**
57
+ * Applies the calibration ratio to a max token budget.
58
+ * The ratio adjusts the effective budget so pruning is more or less aggressive
59
+ * based on observed vs. estimated token divergence.
60
+ *
61
+ * @param maxTokens - Raw max token budget
62
+ * @param state - Current calibration state
63
+ * @returns Adjusted max token budget
64
+ */
65
+ function applyCalibration(maxTokens, state) {
66
+ if (state.iterations === 0) {
67
+ // No calibration data yet — use raw budget
68
+ return maxTokens;
69
+ }
70
+ return Math.floor(maxTokens * state.ratio);
71
+ }
72
+
73
+ export { applyCalibration, createPruneCalibration, updatePruneCalibration };
74
+ //# sourceMappingURL=pruneCalibration.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pruneCalibration.mjs","sources":["../../../src/utils/pruneCalibration.ts"],"sourcesContent":["// src/utils/pruneCalibration.ts\nimport type { PruneCalibrationState } from '@/types/graph';\nimport {\n PRUNING_EMA_ALPHA,\n PRUNING_INITIAL_CALIBRATION,\n} from '@/common/constants';\n\n/**\n * Creates an initial pruning calibration state.\n *\n * @param initialRatio - Starting calibration ratio (default: 1.0)\n * @returns Fresh calibration state\n */\nexport function createPruneCalibration(\n initialRatio?: number\n): PruneCalibrationState {\n return {\n ratio: initialRatio ?? PRUNING_INITIAL_CALIBRATION,\n iterations: 0,\n };\n}\n\n/**\n * Updates the pruning calibration using Exponential Moving Average (EMA).\n *\n * Problem: Without calibration, the pruner's token estimates can diverge from\n * reality across iterations, causing either:\n * - Over-pruning (context cliff): Too many messages removed at once, losing critical tool results\n * - Under-pruning: Not enough messages removed, hitting hard token limits\n *\n * Solution: Track the ratio between actual token usage (from API response) and\n * estimated token usage (from our token counter). Apply EMA smoothing so the\n * calibration adjusts gradually, preventing oscillation.\n *\n * The calibration ratio is applied to maxTokens in the pruner:\n * effectiveMaxTokens = maxTokens * calibrationRatio\n *\n * If actual > estimated → ratio decreases → prune more aggressively\n * If actual < estimated → ratio increases → prune less aggressively\n *\n * @param state - Current calibration state\n * @param actualTokens - Actual token count from API response (UsageMetadata)\n * @param estimatedTokens - Estimated token count from token counter\n * @param alpha - EMA smoothing factor (default: PRUNING_EMA_ALPHA)\n * @returns Updated calibration state (new object, does not mutate input)\n */\nexport function updatePruneCalibration(\n state: PruneCalibrationState,\n actualTokens: number,\n estimatedTokens: number,\n alpha: number = PRUNING_EMA_ALPHA\n): PruneCalibrationState {\n // Guard against division by zero or invalid inputs\n if (estimatedTokens <= 0 || actualTokens <= 0) {\n return state;\n }\n\n // Raw ratio: how much our estimate differs from reality\n const observedRatio = estimatedTokens / actualTokens;\n\n // Clamp to prevent extreme adjustments from outlier readings\n // Range [0.5, 2.0] means we never more than double or halve the budget\n const clampedRatio = Math.max(0.5, Math.min(2.0, observedRatio));\n\n // Apply EMA: new_ratio = α * observed + (1 - α) * previous\n const newRatio = alpha * clampedRatio + (1 - alpha) * state.ratio;\n\n return {\n ratio: newRatio,\n iterations: state.iterations + 1,\n };\n}\n\n/**\n * Applies the calibration ratio to a max token budget.\n * The ratio adjusts the effective budget so pruning is more or less aggressive\n * based on observed vs. estimated token divergence.\n *\n * @param maxTokens - Raw max token budget\n * @param state - Current calibration state\n * @returns Adjusted max token budget\n */\nexport function applyCalibration(\n maxTokens: number,\n state: PruneCalibrationState\n): number {\n if (state.iterations === 0) {\n // No calibration data yet — use raw budget\n return maxTokens;\n }\n return Math.floor(maxTokens * state.ratio);\n}\n"],"names":[],"mappings":";;AAOA;;;;;AAKG;AACG,SAAU,sBAAsB,CACpC,YAAqB,EAAA;IAErB,OAAO;QACL,KAAK,EAAE,YAAY,IAAI,2BAA2B;AAClD,QAAA,UAAU,EAAE,CAAC;KACd;AACH;AAEA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,sBAAsB,CACpC,KAA4B,EAC5B,YAAoB,EACpB,eAAuB,EACvB,KAAA,GAAgB,iBAAiB,EAAA;;IAGjC,IAAI,eAAe,IAAI,CAAC,IAAI,YAAY,IAAI,CAAC,EAAE;AAC7C,QAAA,OAAO,KAAK;IACd;;AAGA,IAAA,MAAM,aAAa,GAAG,eAAe,GAAG,YAAY;;;AAIpD,IAAA,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;;AAGhE,IAAA,MAAM,QAAQ,GAAG,KAAK,GAAG,YAAY,GAAG,CAAC,CAAC,GAAG,KAAK,IAAI,KAAK,CAAC,KAAK;IAEjE,OAAO;AACL,QAAA,KAAK,EAAE,QAAQ;AACf,QAAA,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,CAAC;KACjC;AACH;AAEA;;;;;;;;AAQG;AACG,SAAU,gBAAgB,CAC9B,SAAiB,EACjB,KAA4B,EAAA;AAE5B,IAAA,IAAI,KAAK,CAAC,UAAU,KAAK,CAAC,EAAE;;AAE1B,QAAA,OAAO,SAAS;IAClB;IACA,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;AAC5C;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"run.mjs","sources":["../../../src/utils/run.ts"],"sourcesContent":["import { CallbackManagerForChainRun } from '@langchain/core/callbacks/manager';\r\nimport {\r\n mergeConfigs,\r\n patchConfig,\r\n Runnable,\r\n RunnableConfig,\r\n} from '@langchain/core/runnables';\r\nimport { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';\r\n\r\n/**\r\n * Delays the execution for a specified number of milliseconds.\r\n *\r\n * @param {number} ms - The number of milliseconds to delay.\r\n * @return {Promise<void>} A promise that resolves after the specified delay.\r\n */\r\nexport function sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nexport interface RunnableCallableArgs extends Partial<any> {\r\n name?: string;\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n func: (...args: any[]) => any;\r\n tags?: string[];\r\n trace?: boolean;\r\n recurse?: boolean;\r\n}\r\n\r\nexport class RunnableCallable<I = unknown, O = unknown> extends Runnable<I, O> {\r\n lc_namespace: string[] = ['langgraph'];\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n func: (...args: any[]) => any;\r\n\r\n tags?: string[];\r\n\r\n config?: RunnableConfig;\r\n\r\n trace: boolean = true;\r\n\r\n recurse: boolean = true;\r\n\r\n constructor(fields: RunnableCallableArgs) {\r\n super();\r\n this.name = fields.name ?? fields.func.name;\r\n this.func = fields.func;\r\n this.config = fields.tags ? { tags: fields.tags } : undefined;\r\n this.trace = fields.trace ?? this.trace;\r\n this.recurse = fields.recurse ?? this.recurse;\r\n }\r\n\r\n protected async _tracedInvoke(\r\n input: I,\r\n config?: Partial<RunnableConfig>,\r\n runManager?: CallbackManagerForChainRun\r\n ): Promise<O> {\r\n return new Promise<O>((resolve, reject) => {\r\n // Defensive check: ensure runManager has getChild method before calling\r\n const childCallbacks =\r\n typeof runManager?.getChild === 'function'\r\n ? runManager.getChild()\r\n : undefined;\r\n let childConfig: Partial<RunnableConfig> | null = patchConfig(config, {\r\n callbacks: childCallbacks,\r\n });\r\n void AsyncLocalStorageProviderSingleton.runWithConfig(\r\n childConfig,\r\n async () => {\r\n try {\r\n const output = await this.func(input, childConfig);\r\n childConfig = null;\r\n resolve(output);\r\n } catch (e) {\r\n childConfig = null;\r\n reject(e);\r\n }\r\n }\r\n );\r\n });\r\n }\r\n\r\n async invoke(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n input: any,\r\n options?: Partial<RunnableConfig> | undefined\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n ): Promise<any> {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n let returnValue: any;\r\n\r\n if (this.trace) {\r\n returnValue = await this._callWithConfig(\r\n this._tracedInvoke,\r\n input,\r\n mergeConfigs(this.config, options)\r\n );\r\n } else {\r\n returnValue = await this.func(input, mergeConfigs(this.config, options));\r\n }\r\n\r\n if (Runnable.isRunnable(returnValue) && this.recurse) {\r\n return await returnValue.invoke(input, options);\r\n }\r\n\r\n return returnValue;\r\n }\r\n}\r\n"],"names":[],"mappings":";;;AASA;;;;;AAKG;AACG,SAAU,KAAK,CAAC,EAAU,EAAA;AAC9B,IAAA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC1D;AAYM,MAAO,gBAA2C,SAAQ,QAAc,CAAA;AAC5E,IAAA,YAAY,GAAa,CAAC,WAAW,CAAC;;AAGtC,IAAA,IAAI;AAEJ,IAAA,IAAI;AAEJ,IAAA,MAAM;IAEN,KAAK,GAAY,IAAI;IAErB,OAAO,GAAY,IAAI;AAEvB,IAAA,WAAA,CAAY,MAA4B,EAAA;AACtC,QAAA,KAAK,EAAE;AACP,QAAA,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI;AAC3C,QAAA,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,SAAS;QAC7D,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK;QACvC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO;IAC/C;AAEU,IAAA,MAAM,aAAa,CAC3B,KAAQ,EACR,MAAgC,EAChC,UAAuC,EAAA;QAEvC,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,KAAI;;AAExC,YAAA,MAAM,cAAc,GAClB,OAAO,UAAU,EAAE,QAAQ,KAAK;AAC9B,kBAAE,UAAU,CAAC,QAAQ;kBACnB,SAAS;AACf,YAAA,IAAI,WAAW,GAAmC,WAAW,CAAC,MAAM,EAAE;AACpE,gBAAA,SAAS,EAAE,cAAc;AAC1B,aAAA,CAAC;YACF,KAAK,kCAAkC,CAAC,aAAa,CACnD,WAAW,EACX,YAAW;AACT,gBAAA,IAAI;oBACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC;oBAClD,WAAW,GAAG,IAAI;oBAClB,OAAO,CAAC,MAAM,CAAC;gBACjB;gBAAE,OAAO,CAAC,EAAE;oBACV,WAAW,GAAG,IAAI;oBAClB,MAAM,CAAC,CAAC,CAAC;gBACX;AACF,YAAA,CAAC,CACF;AACH,QAAA,CAAC,CAAC;IACJ;AAEA,IAAA,MAAM,MAAM;;AAEV,IAAA,KAAU,EACV;;;;AAIA,QAAA,IAAI,WAAgB;AAEpB,QAAA,IAAI,IAAI,CAAC,KAAK,EAAE;YACd,WAAW,GAAG,MAAM,IAAI,CAAC,eAAe,CACtC,IAAI,CAAC,aAAa,EAClB,KAAK,EACL,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CACnC;QACH;aAAO;AACL,YAAA,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1E;QAEA,IAAI,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE;YACpD,OAAO,MAAM,WAAW,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC;QACjD;AAEA,QAAA,OAAO,WAAW;IACpB;AACD;;;;"}
1
+ {"version":3,"file":"run.mjs","sources":["../../../src/utils/run.ts"],"sourcesContent":["import { CallbackManagerForChainRun } from '@langchain/core/callbacks/manager';\nimport {\n mergeConfigs,\n patchConfig,\n Runnable,\n RunnableConfig,\n} from '@langchain/core/runnables';\nimport { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';\n\n/**\n * Delays the execution for a specified number of milliseconds.\n *\n * @param {number} ms - The number of milliseconds to delay.\n * @return {Promise<void>} A promise that resolves after the specified delay.\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface RunnableCallableArgs extends Partial<any> {\n name?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n func: (...args: any[]) => any;\n tags?: string[];\n trace?: boolean;\n recurse?: boolean;\n}\n\nexport class RunnableCallable<I = unknown, O = unknown> extends Runnable<I, O> {\n lc_namespace: string[] = ['langgraph'];\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n func: (...args: any[]) => any;\n\n tags?: string[];\n\n config?: RunnableConfig;\n\n trace: boolean = true;\n\n recurse: boolean = true;\n\n constructor(fields: RunnableCallableArgs) {\n super();\n this.name = fields.name ?? fields.func.name;\n this.func = fields.func;\n this.config = fields.tags ? { tags: fields.tags } : undefined;\n this.trace = fields.trace ?? this.trace;\n this.recurse = fields.recurse ?? this.recurse;\n }\n\n protected async _tracedInvoke(\n input: I,\n config?: Partial<RunnableConfig>,\n runManager?: CallbackManagerForChainRun\n ): Promise<O> {\n return new Promise<O>((resolve, reject) => {\n // Defensive check: ensure runManager has getChild method before calling\n const childCallbacks =\n typeof runManager?.getChild === 'function'\n ? runManager.getChild()\n : undefined;\n let childConfig: Partial<RunnableConfig> | null = patchConfig(config, {\n callbacks: childCallbacks,\n });\n void AsyncLocalStorageProviderSingleton.runWithConfig(\n childConfig,\n async () => {\n try {\n const output = await this.func(input, childConfig);\n childConfig = null;\n resolve(output);\n } catch (e) {\n childConfig = null;\n reject(e);\n }\n }\n );\n });\n }\n\n async invoke(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n input: any,\n options?: Partial<RunnableConfig> | undefined\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ): Promise<any> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let returnValue: any;\n\n if (this.trace) {\n returnValue = await this._callWithConfig(\n this._tracedInvoke,\n input,\n mergeConfigs(this.config, options)\n );\n } else {\n returnValue = await this.func(input, mergeConfigs(this.config, options));\n }\n\n if (Runnable.isRunnable(returnValue) && this.recurse) {\n return await returnValue.invoke(input, options);\n }\n\n return returnValue;\n }\n}\n"],"names":[],"mappings":";;;AASA;;;;;AAKG;AACG,SAAU,KAAK,CAAC,EAAU,EAAA;AAC9B,IAAA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC1D;AAYM,MAAO,gBAA2C,SAAQ,QAAc,CAAA;AAC5E,IAAA,YAAY,GAAa,CAAC,WAAW,CAAC;;AAGtC,IAAA,IAAI;AAEJ,IAAA,IAAI;AAEJ,IAAA,MAAM;IAEN,KAAK,GAAY,IAAI;IAErB,OAAO,GAAY,IAAI;AAEvB,IAAA,WAAA,CAAY,MAA4B,EAAA;AACtC,QAAA,KAAK,EAAE;AACP,QAAA,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI;AAC3C,QAAA,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,SAAS;QAC7D,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK;QACvC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO;IAC/C;AAEU,IAAA,MAAM,aAAa,CAC3B,KAAQ,EACR,MAAgC,EAChC,UAAuC,EAAA;QAEvC,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,KAAI;;AAExC,YAAA,MAAM,cAAc,GAClB,OAAO,UAAU,EAAE,QAAQ,KAAK;AAC9B,kBAAE,UAAU,CAAC,QAAQ;kBACnB,SAAS;AACf,YAAA,IAAI,WAAW,GAAmC,WAAW,CAAC,MAAM,EAAE;AACpE,gBAAA,SAAS,EAAE,cAAc;AAC1B,aAAA,CAAC;YACF,KAAK,kCAAkC,CAAC,aAAa,CACnD,WAAW,EACX,YAAW;AACT,gBAAA,IAAI;oBACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC;oBAClD,WAAW,GAAG,IAAI;oBAClB,OAAO,CAAC,MAAM,CAAC;gBACjB;gBAAE,OAAO,CAAC,EAAE;oBACV,WAAW,GAAG,IAAI;oBAClB,MAAM,CAAC,CAAC,CAAC;gBACX;AACF,YAAA,CAAC,CACF;AACH,QAAA,CAAC,CAAC;IACJ;AAEA,IAAA,MAAM,MAAM;;AAEV,IAAA,KAAU,EACV;;;;AAIA,QAAA,IAAI,WAAgB;AAEpB,QAAA,IAAI,IAAI,CAAC,KAAK,EAAE;YACd,WAAW,GAAG,MAAM,IAAI,CAAC,eAAe,CACtC,IAAI,CAAC,aAAa,EAClB,KAAK,EACL,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CACnC;QACH;aAAO;AACL,YAAA,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1E;QAEA,IAAI,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE;YACpD,OAAO,MAAM,WAAW,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC;QACjD;AAEA,QAAA,OAAO,WAAW;IACpB;AACD;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.mjs","sources":["../../../src/utils/tokens.ts"],"sourcesContent":["import { Tokenizer } from 'ai-tokenizer';\r\nimport type { BaseMessage } from '@langchain/core/messages';\r\nimport { ContentTypes } from '@/common/enum';\r\n\r\nexport type EncodingName = 'o200k_base' | 'claude';\r\n\r\nconst tokenizers: Partial<Record<EncodingName, Tokenizer>> = {};\r\n\r\nasync function getTokenizer(\r\n encoding: EncodingName = 'o200k_base'\r\n): Promise<Tokenizer> {\r\n const cached = tokenizers[encoding];\r\n if (cached) {\r\n return cached;\r\n }\r\n const data =\r\n encoding === 'claude'\r\n ? await import('ai-tokenizer/encoding/claude')\r\n : await import('ai-tokenizer/encoding/o200k_base');\r\n const instance = new Tokenizer(data);\r\n tokenizers[encoding] = instance;\r\n return instance;\r\n}\r\n\r\nexport function encodingForModel(model: string): EncodingName {\r\n if (model.toLowerCase().includes('claude')) {\r\n return 'claude';\r\n }\r\n return 'o200k_base';\r\n}\r\n\r\nexport function getTokenCountForMessage(\r\n message: BaseMessage,\r\n getTokenCount: (text: string) => number\r\n): number {\r\n const tokensPerMessage = 3;\r\n\r\n const processValue = (value: unknown): void => {\r\n if (Array.isArray(value)) {\r\n for (const item of value) {\r\n if (\r\n !item ||\r\n !item.type ||\r\n item.type === ContentTypes.ERROR ||\r\n item.type === ContentTypes.IMAGE_URL\r\n ) {\r\n continue;\r\n }\r\n\r\n if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) {\r\n const toolName = item.tool_call?.name || '';\r\n if (toolName != null && toolName && typeof toolName === 'string') {\r\n numTokens += getTokenCount(toolName);\r\n }\r\n\r\n const args = item.tool_call?.args || '';\r\n if (args != null && args && typeof args === 'string') {\r\n numTokens += getTokenCount(args);\r\n }\r\n\r\n const output = item.tool_call?.output || '';\r\n if (output != null && output && typeof output === 'string') {\r\n numTokens += getTokenCount(output);\r\n }\r\n continue;\r\n }\r\n\r\n const nestedValue = item[item.type];\r\n\r\n if (!nestedValue) {\r\n continue;\r\n }\r\n\r\n processValue(nestedValue);\r\n }\r\n } else if (typeof value === 'string') {\r\n numTokens += getTokenCount(value);\r\n } else if (typeof value === 'number') {\r\n numTokens += getTokenCount(value.toString());\r\n } else if (typeof value === 'boolean') {\r\n numTokens += getTokenCount(value.toString());\r\n }\r\n };\r\n\r\n let numTokens = tokensPerMessage;\r\n processValue(message.content);\r\n return numTokens;\r\n}\r\n\r\n/**\r\n * Creates a token counter function using the specified encoding.\r\n * Lazily loads the encoding data on first use via dynamic import.\r\n */\r\nexport const createTokenCounter = async (\r\n encoding: EncodingName = 'o200k_base'\r\n): Promise<(message: BaseMessage) => number> => {\r\n const tok = await getTokenizer(encoding);\r\n const countTokens = (text: string): number => tok.count(text);\r\n return (message: BaseMessage): number =>\r\n getTokenCountForMessage(message, countTokens);\r\n};\r\n\r\n/** Utility to manage the token encoder lifecycle explicitly. */\r\nexport const TokenEncoderManager = {\r\n async initialize(): Promise<void> {\r\n // No-op: ai-tokenizer is synchronously initialized from bundled data.\r\n },\r\n\r\n reset(): void {\r\n for (const key of Object.keys(tokenizers)) {\r\n delete tokenizers[key as EncodingName];\r\n }\r\n },\r\n\r\n isInitialized(): boolean {\r\n return Object.keys(tokenizers).length > 0;\r\n },\r\n};\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokens.mjs","sources":["../../../src/utils/tokens.ts"],"sourcesContent":["import { Tokenizer } from 'ai-tokenizer';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport { ContentTypes } from '@/common/enum';\n\nexport type EncodingName = 'o200k_base' | 'claude';\n\nconst tokenizers: Partial<Record<EncodingName, Tokenizer>> = {};\n\nasync function getTokenizer(\n encoding: EncodingName = 'o200k_base'\n): Promise<Tokenizer> {\n const cached = tokenizers[encoding];\n if (cached) {\n return cached;\n }\n const data =\n encoding === 'claude'\n ? await import('ai-tokenizer/encoding/claude')\n : await import('ai-tokenizer/encoding/o200k_base');\n const instance = new Tokenizer(data);\n tokenizers[encoding] = instance;\n return instance;\n}\n\nexport function encodingForModel(model: string): EncodingName {\n if (model.toLowerCase().includes('claude')) {\n return 'claude';\n }\n return 'o200k_base';\n}\n\nexport function getTokenCountForMessage(\n message: BaseMessage,\n getTokenCount: (text: string) => number\n): number {\n const tokensPerMessage = 3;\n\n const processValue = (value: unknown): void => {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (\n !item ||\n !item.type ||\n item.type === ContentTypes.ERROR ||\n item.type === ContentTypes.IMAGE_URL\n ) {\n continue;\n }\n\n if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) {\n const toolName = item.tool_call?.name || '';\n if (toolName != null && toolName && typeof toolName === 'string') {\n numTokens += getTokenCount(toolName);\n }\n\n const args = item.tool_call?.args || '';\n if (args != null && args && typeof args === 'string') {\n numTokens += getTokenCount(args);\n }\n\n const output = item.tool_call?.output || '';\n if (output != null && output && typeof output === 'string') {\n numTokens += getTokenCount(output);\n }\n continue;\n }\n\n const nestedValue = item[item.type];\n\n if (!nestedValue) {\n continue;\n }\n\n processValue(nestedValue);\n }\n } else if (typeof value === 'string') {\n numTokens += getTokenCount(value);\n } else if (typeof value === 'number') {\n numTokens += getTokenCount(value.toString());\n } else if (typeof value === 'boolean') {\n numTokens += getTokenCount(value.toString());\n }\n };\n\n let numTokens = tokensPerMessage;\n processValue(message.content);\n return numTokens;\n}\n\n/**\n * Creates a token counter function using the specified encoding.\n * Lazily loads the encoding data on first use via dynamic import.\n */\nexport const createTokenCounter = async (\n encoding: EncodingName = 'o200k_base'\n): Promise<(message: BaseMessage) => number> => {\n const tok = await getTokenizer(encoding);\n const countTokens = (text: string): number => tok.count(text);\n return (message: BaseMessage): number =>\n getTokenCountForMessage(message, countTokens);\n};\n\n/** Utility to manage the token encoder lifecycle explicitly. */\nexport const TokenEncoderManager = {\n async initialize(): Promise<void> {\n // No-op: ai-tokenizer is synchronously initialized from bundled data.\n },\n\n reset(): void {\n for (const key of Object.keys(tokenizers)) {\n delete tokenizers[key as EncodingName];\n }\n },\n\n isInitialized(): boolean {\n return Object.keys(tokenizers).length > 0;\n },\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ import { MessageTypes, Constants } from '../common/enum.mjs';
2
+ import { TOOL_DISCOVERY_CACHE_MAX_SIZE } from '../common/constants.mjs';
3
+
4
+ /**
5
+ * ToolDiscoveryCache provides a run-scoped cache of tool search results.
6
+ *
7
+ * Problem: Without caching, every LLM iteration re-parses the full message
8
+ * history via extractToolDiscoveries() to find tool_search results. In long
9
+ * conversations with many tool iterations, this is redundant work.
10
+ *
11
+ * Solution: Cache discovered tool names by message index. On each iteration,
12
+ * only scan messages AFTER the last scanned index. Already-seen discoveries
13
+ * are returned from cache instantly.
14
+ *
15
+ * This mirrors the pattern used by VS Code Copilot Chat where tool search
16
+ * results from prior turns are cached to avoid re-discovery.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const cache = new ToolDiscoveryCache();
21
+ *
22
+ * // First call: scans all messages
23
+ * const newTools = cache.getNewDiscoveries(messages);
24
+ * // Returns: ['web_search', 'file_read']
25
+ *
26
+ * // Second call (3 new messages added): only scans new messages
27
+ * const moreTools = cache.getNewDiscoveries(messages);
28
+ * // Returns: ['code_exec'] (only newly discovered)
29
+ * ```
30
+ */
31
+ class ToolDiscoveryCache {
32
+ /** Set of all discovered tool names (deduped) */
33
+ _discoveredTools = new Set();
34
+ /** Last message index that was scanned */
35
+ _lastScannedIndex = -1;
36
+ /**
37
+ * Scan messages for new tool_search results since the last scan.
38
+ * Only processes messages after `_lastScannedIndex` to avoid redundant work.
39
+ *
40
+ * @param messages - Full conversation message array
41
+ * @returns Array of newly discovered tool names (not previously cached)
42
+ */
43
+ getNewDiscoveries(messages) {
44
+ if (messages.length === 0) {
45
+ return [];
46
+ }
47
+ const startIndex = this._lastScannedIndex + 1;
48
+ if (startIndex >= messages.length) {
49
+ return [];
50
+ }
51
+ const newDiscoveries = [];
52
+ for (let i = startIndex; i < messages.length; i++) {
53
+ const msg = messages[i];
54
+ if (msg.getType() !== MessageTypes.TOOL) {
55
+ continue;
56
+ }
57
+ // Check if this is a tool_search result
58
+ if (msg.name !== Constants.TOOL_SEARCH) {
59
+ continue;
60
+ }
61
+ // Extract tool references from artifact
62
+ const artifact = msg.artifact;
63
+ if (typeof artifact === 'object' && artifact != null) {
64
+ const refs = artifact.tool_references;
65
+ if (refs && refs.length > 0) {
66
+ for (const ref of refs) {
67
+ if (!this._discoveredTools.has(ref.tool_name)) {
68
+ // Enforce cache size limit
69
+ if (this._discoveredTools.size >= TOOL_DISCOVERY_CACHE_MAX_SIZE) {
70
+ break;
71
+ }
72
+ this._discoveredTools.add(ref.tool_name);
73
+ newDiscoveries.push(ref.tool_name);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ this._lastScannedIndex = messages.length - 1;
80
+ return newDiscoveries;
81
+ }
82
+ /**
83
+ * Returns all tool names discovered so far (across all scans).
84
+ */
85
+ getAllDiscoveredTools() {
86
+ return [...this._discoveredTools];
87
+ }
88
+ /**
89
+ * Check if a specific tool has been discovered.
90
+ */
91
+ has(toolName) {
92
+ return this._discoveredTools.has(toolName);
93
+ }
94
+ /**
95
+ * Number of unique tools discovered.
96
+ */
97
+ get size() {
98
+ return this._discoveredTools.size;
99
+ }
100
+ /**
101
+ * Reset the cache (e.g., on graph reset).
102
+ */
103
+ reset() {
104
+ this._discoveredTools.clear();
105
+ this._lastScannedIndex = -1;
106
+ }
107
+ /**
108
+ * Seed the cache with previously known tool names (e.g., from prior conversation turns).
109
+ * Does not affect _lastScannedIndex — the next getNewDiscoveries call will still
110
+ * scan all messages from the beginning.
111
+ *
112
+ * @param toolNames - Tool names to pre-seed into the cache
113
+ */
114
+ seed(toolNames) {
115
+ for (const name of toolNames) {
116
+ if (this._discoveredTools.size >= TOOL_DISCOVERY_CACHE_MAX_SIZE) {
117
+ break;
118
+ }
119
+ this._discoveredTools.add(name);
120
+ }
121
+ }
122
+ }
123
+
124
+ export { ToolDiscoveryCache };
125
+ //# sourceMappingURL=toolDiscoveryCache.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolDiscoveryCache.mjs","sources":["../../../src/utils/toolDiscoveryCache.ts"],"sourcesContent":["// src/utils/toolDiscoveryCache.ts\nimport type { BaseMessage } from '@langchain/core/messages';\nimport { Constants, MessageTypes } from '@/common';\nimport { TOOL_DISCOVERY_CACHE_MAX_SIZE } from '@/common/constants';\n\n/**\n * Cached tool discovery entry.\n * Stores the tool name and the message index where it was discovered,\n * enabling efficient lookups without re-parsing conversation history.\n */\nexport interface ToolDiscoveryEntry {\n /** The tool name that was discovered */\n toolName: string;\n /** Message index in conversation history where discovery occurred */\n discoveredAtIndex: number;\n}\n\n/**\n * ToolDiscoveryCache provides a run-scoped cache of tool search results.\n *\n * Problem: Without caching, every LLM iteration re-parses the full message\n * history via extractToolDiscoveries() to find tool_search results. In long\n * conversations with many tool iterations, this is redundant work.\n *\n * Solution: Cache discovered tool names by message index. On each iteration,\n * only scan messages AFTER the last scanned index. Already-seen discoveries\n * are returned from cache instantly.\n *\n * This mirrors the pattern used by VS Code Copilot Chat where tool search\n * results from prior turns are cached to avoid re-discovery.\n *\n * @example\n * ```ts\n * const cache = new ToolDiscoveryCache();\n *\n * // First call: scans all messages\n * const newTools = cache.getNewDiscoveries(messages);\n * // Returns: ['web_search', 'file_read']\n *\n * // Second call (3 new messages added): only scans new messages\n * const moreTools = cache.getNewDiscoveries(messages);\n * // Returns: ['code_exec'] (only newly discovered)\n * ```\n */\nexport class ToolDiscoveryCache {\n /** Set of all discovered tool names (deduped) */\n private _discoveredTools: Set<string> = new Set();\n /** Last message index that was scanned */\n private _lastScannedIndex: number = -1;\n\n /**\n * Scan messages for new tool_search results since the last scan.\n * Only processes messages after `_lastScannedIndex` to avoid redundant work.\n *\n * @param messages - Full conversation message array\n * @returns Array of newly discovered tool names (not previously cached)\n */\n getNewDiscoveries(messages: BaseMessage[]): string[] {\n if (messages.length === 0) {\n return [];\n }\n\n const startIndex = this._lastScannedIndex + 1;\n if (startIndex >= messages.length) {\n return [];\n }\n\n const newDiscoveries: string[] = [];\n\n for (let i = startIndex; i < messages.length; i++) {\n const msg = messages[i];\n if (msg.getType() !== MessageTypes.TOOL) {\n continue;\n }\n\n // Check if this is a tool_search result\n if ((msg as { name?: string }).name !== Constants.TOOL_SEARCH) {\n continue;\n }\n\n // Extract tool references from artifact\n const artifact = (msg as { artifact?: unknown }).artifact;\n if (typeof artifact === 'object' && artifact != null) {\n const refs = (\n artifact as { tool_references?: Array<{ tool_name: string }> }\n ).tool_references;\n if (refs && refs.length > 0) {\n for (const ref of refs) {\n if (!this._discoveredTools.has(ref.tool_name)) {\n // Enforce cache size limit\n if (this._discoveredTools.size >= TOOL_DISCOVERY_CACHE_MAX_SIZE) {\n break;\n }\n this._discoveredTools.add(ref.tool_name);\n newDiscoveries.push(ref.tool_name);\n }\n }\n }\n }\n }\n\n this._lastScannedIndex = messages.length - 1;\n return newDiscoveries;\n }\n\n /**\n * Returns all tool names discovered so far (across all scans).\n */\n getAllDiscoveredTools(): string[] {\n return [...this._discoveredTools];\n }\n\n /**\n * Check if a specific tool has been discovered.\n */\n has(toolName: string): boolean {\n return this._discoveredTools.has(toolName);\n }\n\n /**\n * Number of unique tools discovered.\n */\n get size(): number {\n return this._discoveredTools.size;\n }\n\n /**\n * Reset the cache (e.g., on graph reset).\n */\n reset(): void {\n this._discoveredTools.clear();\n this._lastScannedIndex = -1;\n }\n\n /**\n * Seed the cache with previously known tool names (e.g., from prior conversation turns).\n * Does not affect _lastScannedIndex — the next getNewDiscoveries call will still\n * scan all messages from the beginning.\n *\n * @param toolNames - Tool names to pre-seed into the cache\n */\n seed(toolNames: string[]): void {\n for (const name of toolNames) {\n if (this._discoveredTools.size >= TOOL_DISCOVERY_CACHE_MAX_SIZE) {\n break;\n }\n this._discoveredTools.add(name);\n }\n }\n}\n"],"names":[],"mappings":";;;AAiBA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BG;MACU,kBAAkB,CAAA;;AAErB,IAAA,gBAAgB,GAAgB,IAAI,GAAG,EAAE;;IAEzC,iBAAiB,GAAW,EAAE;AAEtC;;;;;;AAMG;AACH,IAAA,iBAAiB,CAAC,QAAuB,EAAA;AACvC,QAAA,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE;AACzB,YAAA,OAAO,EAAE;QACX;AAEA,QAAA,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB,GAAG,CAAC;AAC7C,QAAA,IAAI,UAAU,IAAI,QAAQ,CAAC,MAAM,EAAE;AACjC,YAAA,OAAO,EAAE;QACX;QAEA,MAAM,cAAc,GAAa,EAAE;AAEnC,QAAA,KAAK,IAAI,CAAC,GAAG,UAAU,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACjD,YAAA,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC;YACvB,IAAI,GAAG,CAAC,OAAO,EAAE,KAAK,YAAY,CAAC,IAAI,EAAE;gBACvC;YACF;;YAGA,IAAK,GAAyB,CAAC,IAAI,KAAK,SAAS,CAAC,WAAW,EAAE;gBAC7D;YACF;;AAGA,YAAA,MAAM,QAAQ,GAAI,GAA8B,CAAC,QAAQ;YACzD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,IAAI,IAAI,EAAE;AACpD,gBAAA,MAAM,IAAI,GACR,QACD,CAAC,eAAe;gBACjB,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3B,oBAAA,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE;AACtB,wBAAA,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;;4BAE7C,IAAI,IAAI,CAAC,gBAAgB,CAAC,IAAI,IAAI,6BAA6B,EAAE;gCAC/D;4BACF;4BACA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;AACxC,4BAAA,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;wBACpC;oBACF;gBACF;YACF;QACF;QAEA,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC;AAC5C,QAAA,OAAO,cAAc;IACvB;AAEA;;AAEG;IACH,qBAAqB,GAAA;AACnB,QAAA,OAAO,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC;IACnC;AAEA;;AAEG;AACH,IAAA,GAAG,CAAC,QAAgB,EAAA;QAClB,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC5C;AAEA;;AAEG;AACH,IAAA,IAAI,IAAI,GAAA;AACN,QAAA,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI;IACnC;AAEA;;AAEG;IACH,KAAK,GAAA;AACH,QAAA,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE;AAC7B,QAAA,IAAI,CAAC,iBAAiB,GAAG,EAAE;IAC7B;AAEA;;;;;;AAMG;AACH,IAAA,IAAI,CAAC,SAAmB,EAAA;AACtB,QAAA,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE;YAC5B,IAAI,IAAI,CAAC,gBAAgB,CAAC,IAAI,IAAI,6BAA6B,EAAE;gBAC/D;YACF;AACA,YAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;QACjC;IACF;AACD;;;;"}
@@ -116,7 +116,9 @@ export declare class AgentContext {
116
116
  summarizeCallback?: (messages: BaseMessage[]) => Promise<string | undefined>;
117
117
  /** Pre-existing summary loaded from persistent storage, injected into context on new turns */
118
118
  persistedSummary?: string;
119
- constructor({ agentId, name, description, provider, clientOptions, maxContextTokens, streamBuffer, tokenCounter, tools, toolMap, toolRegistry, toolDefinitions, instructions, additionalInstructions, dynamicContext, reasoningKey, toolEnd, instructionTokens, useLegacyContent, structuredOutput, discoveredTools, summarizeCallback, persistedSummary, }: {
119
+ /** Summarization configuration controlling trigger strategy, reserve ratio, and EMA calibration */
120
+ summarizationConfig?: t.SummarizationConfig;
121
+ constructor({ agentId, name, description, provider, clientOptions, maxContextTokens, streamBuffer, tokenCounter, tools, toolMap, toolRegistry, toolDefinitions, instructions, additionalInstructions, dynamicContext, reasoningKey, toolEnd, instructionTokens, useLegacyContent, structuredOutput, discoveredTools, summarizeCallback, persistedSummary, summarizationConfig, }: {
120
122
  agentId: string;
121
123
  name?: string;
122
124
  description?: string;
@@ -140,6 +142,7 @@ export declare class AgentContext {
140
142
  discoveredTools?: string[];
141
143
  summarizeCallback?: (messages: BaseMessage[]) => Promise<string | undefined>;
142
144
  persistedSummary?: string;
145
+ summarizationConfig?: t.SummarizationConfig;
143
146
  });
144
147
  /**
145
148
  * Checks if structured output mode is enabled for this agent.
@@ -16,3 +16,52 @@ export declare const MIN_THINKING_BUDGET = 1024;
16
16
  * compounding across multi-tool conversations (e.g., 10 tool calls).
17
17
  */
18
18
  export declare const TOOL_TURN_THINKING_BUDGET = 1024;
19
+ /**
20
+ * Minimum number of attached documents before the multi-document delegation
21
+ * hint is injected. Below this threshold, the agent processes documents
22
+ * directly within its own context.
23
+ */
24
+ export declare const MULTI_DOCUMENT_THRESHOLD = 3;
25
+ /**
26
+ * Context utilization safety buffer multiplier (0-1).
27
+ * Applied as: effectiveMax = (maxContextTokens - maxOutputTokens) * CONTEXT_SAFETY_BUFFER
28
+ *
29
+ * Reserves headroom so the LLM doesn't hit hard token limits mid-generation.
30
+ * 0.9 = 10% reserved for safety.
31
+ */
32
+ export declare const CONTEXT_SAFETY_BUFFER = 0.9;
33
+ /**
34
+ * Default context utilization percentage (0-100) at which summarization triggers.
35
+ * When the context window is ≥80% full, pruning + summarization activates.
36
+ */
37
+ export declare const SUMMARIZATION_CONTEXT_THRESHOLD = 80;
38
+ /**
39
+ * Default reserve ratio (0-1) — fraction of context window to preserve as recent messages.
40
+ * 0.3 means 30% of the context budget is reserved for the most recent messages,
41
+ * ensuring the model always has immediate conversation history even after aggressive pruning.
42
+ */
43
+ export declare const SUMMARIZATION_RESERVE_RATIO = 0.3;
44
+ /**
45
+ * Default EMA (Exponential Moving Average) alpha for pruning calibration.
46
+ * Controls how quickly the calibration adapts to new token counts.
47
+ * Higher α = faster adaptation (more responsive to recent changes).
48
+ * Lower α = smoother adaptation (more stable across iterations).
49
+ * 0.3 provides a balance between responsiveness and stability.
50
+ */
51
+ export declare const PRUNING_EMA_ALPHA = 0.3;
52
+ /**
53
+ * Default initial calibration ratio for EMA pruning.
54
+ * 1.0 means no adjustment on the first iteration (trust the raw token counts).
55
+ * Subsequent iterations will adjust based on actual vs. estimated token usage.
56
+ */
57
+ export declare const PRUNING_INITIAL_CALIBRATION = 1;
58
+ /**
59
+ * Maximum number of tool discovery entries to cache per conversation.
60
+ * Prevents unbounded memory growth in very long conversations.
61
+ */
62
+ export declare const TOOL_DISCOVERY_CACHE_MAX_SIZE = 200;
63
+ /**
64
+ * Maximum length of system message content to hash for deduplication.
65
+ * Messages longer than this are always considered unique (hashing would be expensive).
66
+ */
67
+ export declare const DEDUP_MAX_CONTENT_LENGTH = 10000;
@@ -73,6 +73,13 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
73
73
  runId: string | undefined;
74
74
  startIndex: number;
75
75
  signal?: AbortSignal;
76
+ /** Cached summary from the first prune in this run.
77
+ * Reused for subsequent prunes to avoid blocking LLM calls on every tool iteration. */
78
+ private _cachedRunSummary;
79
+ /** EMA-based pruning calibration state — smooths token budget adjustments across iterations */
80
+ private _pruneCalibration;
81
+ /** Run-scoped tool discovery cache — avoids re-parsing conversation history on every iteration */
82
+ private _toolDiscoveryCache;
76
83
  /** Map of agent contexts by agent ID */
77
84
  agentContexts: Map<string, AgentContext>;
78
85
  /** Default agent ID to use */
@@ -105,6 +112,24 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
105
112
  * @returns Shallow-cloned clientOptions with reduced thinking budget, or the original if no reduction needed
106
113
  */
107
114
  getAdaptiveClientOptions(clientOptions: t.ClientOptions, provider: Providers): t.ClientOptions;
115
+ /**
116
+ * Determines whether summarization should trigger based on SummarizationConfig.
117
+ *
118
+ * Supports three trigger strategies:
119
+ * - contextPercentage (default): Trigger when context utilization >= threshold%
120
+ * - messageCount: Trigger when pruned message count >= threshold
121
+ * - tokenThreshold: Trigger when total estimated tokens >= threshold
122
+ *
123
+ * When no config is provided, always triggers (preserves backward compatibility).
124
+ *
125
+ * @param prunedMessageCount - Number of messages that were pruned
126
+ * @param maxContextTokens - Maximum context token budget
127
+ * @param indexTokenCountMap - Token count map by message index
128
+ * @param instructionTokens - Token count for instructions/system message
129
+ * @param config - Optional SummarizationConfig
130
+ * @returns Whether summarization should be triggered
131
+ */
132
+ private shouldTriggerSummarization;
108
133
  /**
109
134
  * Returns the normalized finish/stop reason from the last LLM invocation.
110
135
  * Used by callers to detect when the response was truncated due to max_tokens.
@@ -0,0 +1,25 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ /**
3
+ * Deduplicates consecutive identical system messages in the context window.
4
+ *
5
+ * Problem: In long tool-use chains, the same system messages (e.g., post-prune notes,
6
+ * conversation summaries) can accumulate when the context is rebuilt on each iteration.
7
+ * These duplicates waste tokens without adding information.
8
+ *
9
+ * Strategy: Only deduplicate system messages that appear consecutively or are exact
10
+ * duplicates of an earlier system message. The FIRST occurrence is always kept.
11
+ * Non-system messages (human, ai, tool) are never touched.
12
+ *
13
+ * Important constraints:
14
+ * - The first system message (index 0) is ALWAYS preserved (it's the main system prompt)
15
+ * - Only system messages are candidates for deduplication
16
+ * - Messages with content longer than DEDUP_MAX_CONTENT_LENGTH are skipped (too expensive to compare)
17
+ * - Content comparison is by string equality (fast and deterministic)
18
+ *
19
+ * @param messages - The message array to deduplicate (not mutated)
20
+ * @returns A new array with duplicate system messages removed, and the count of removed messages
21
+ */
22
+ export declare function deduplicateSystemMessages(messages: BaseMessage[]): {
23
+ messages: BaseMessage[];
24
+ removedCount: number;
25
+ };
@@ -6,3 +6,4 @@ export * from './cache';
6
6
  export * from './content';
7
7
  export * from './tools';
8
8
  export * from './summarize';
9
+ export * from './dedup';
@@ -343,6 +343,63 @@ export interface StructuredOutputInput {
343
343
  /** Whether to enforce strict schema validation */
344
344
  strict?: boolean;
345
345
  }
346
+ /**
347
+ * Trigger strategy for when summarization should activate.
348
+ * - 'contextPercentage': Trigger when context utilization exceeds a threshold percentage
349
+ * - 'messageCount': Trigger when pruned message count exceeds a threshold
350
+ * - 'tokenThreshold': Trigger when total token count exceeds a raw threshold
351
+ */
352
+ export type SummarizationTriggerType = 'contextPercentage' | 'messageCount' | 'tokenThreshold';
353
+ /**
354
+ * Configuration for summarization behavior within the agent pipeline.
355
+ * All fields are optional — sensible defaults are provided via constants.
356
+ *
357
+ * @see SUMMARIZATION_CONTEXT_THRESHOLD, SUMMARIZATION_RESERVE_RATIO, PRUNING_EMA_ALPHA
358
+ */
359
+ export interface SummarizationConfig {
360
+ /**
361
+ * Strategy for when summarization triggers.
362
+ * @default 'contextPercentage'
363
+ */
364
+ triggerType?: SummarizationTriggerType;
365
+ /**
366
+ * Threshold value interpreted based on triggerType:
367
+ * - contextPercentage: 0-100 (percentage of context window)
368
+ * - messageCount: absolute count of messages pruned
369
+ * - tokenThreshold: absolute token count
370
+ * @default 80 (for contextPercentage)
371
+ */
372
+ triggerThreshold?: number;
373
+ /**
374
+ * Fraction of context window (0-1) reserved for recent messages.
375
+ * Prevents over-pruning by ensuring at least this fraction of the
376
+ * context budget is preserved as recent conversation history.
377
+ * @default 0.3
378
+ */
379
+ reserveRatio?: number;
380
+ /**
381
+ * Whether context pruning is enabled (can be disabled for debugging).
382
+ * @default true
383
+ */
384
+ contextPruning?: boolean;
385
+ /**
386
+ * Initial summary text to seed across runs.
387
+ * Different from persistedSummary: this is provided by the caller as a
388
+ * cross-conversation seed (e.g., agent personality or recurring context),
389
+ * while persistedSummary is loaded from the conversation's own history.
390
+ */
391
+ initialSummary?: string;
392
+ }
393
+ /**
394
+ * Runtime state for EMA-based pruning calibration.
395
+ * Maintained across iterations within a single run to smooth pruning decisions.
396
+ */
397
+ export interface PruneCalibrationState {
398
+ /** Current EMA calibration ratio */
399
+ ratio: number;
400
+ /** Number of calibration updates applied */
401
+ iterations: number;
402
+ }
346
403
  export interface AgentInputs {
347
404
  agentId: string;
348
405
  /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
@@ -412,4 +469,10 @@ export interface AgentInputs {
412
469
  * Set by Ranger's SummaryStore when resuming a conversation.
413
470
  */
414
471
  persistedSummary?: string;
472
+ /**
473
+ * Summarization configuration controlling trigger strategy, reserve ratio,
474
+ * and EMA calibration for pruning. When omitted, sensible defaults apply.
475
+ * @see SummarizationConfig
476
+ */
477
+ summarizationConfig?: SummarizationConfig;
415
478
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Context Pressure Utilities
3
+ *
4
+ * Pure functions for context overflow management. These handle:
5
+ * 1. Multi-document detection — counting attached documents in messages
6
+ * 2. Multi-document delegation hint — injected when 3+ documents detected
7
+ * 3. Post-prune context note — injected after pruning/summarization
8
+ *
9
+ * DESIGN PRINCIPLE: The LLM never sees raw token numbers. Context overflow
10
+ * is handled mechanically by pruning (Graph) + auto-continuation (client.js).
11
+ * Only task-driven hints (multi-document) are injected — never budget-based.
12
+ *
13
+ * @see docs/context-overflow-architecture.md
14
+ */
15
+ import type { BaseMessage } from '@langchain/core/messages';
16
+ /** Result of scanning messages for attached documents */
17
+ export interface DocumentDetectionResult {
18
+ /** Total unique documents detected */
19
+ count: number;
20
+ /** Names of detected documents */
21
+ names: string[];
22
+ }
23
+ /**
24
+ * Scan messages for attached documents using known content patterns.
25
+ *
26
+ * Detects documents from:
27
+ * 1. `# "filename"` headers in "Attached document(s):" blocks (text content)
28
+ * 2. `**filename1, filename2**` in "The user has attached:" blocks (embedded files)
29
+ *
30
+ * @param messages - Conversation messages to scan
31
+ * @returns Document count and names (deduplicated)
32
+ */
33
+ export declare function detectDocuments(messages: BaseMessage[]): DocumentDetectionResult;
34
+ /**
35
+ * Determine whether the multi-document delegation hint should be injected.
36
+ *
37
+ * Only fires on the first iteration (before any AI response) when the
38
+ * document count meets the threshold. This ensures the agent delegates
39
+ * upfront rather than trying to process all documents itself.
40
+ *
41
+ * @param documentCount - Number of detected documents
42
+ * @param hasAiResponse - Whether the agent has already responded in this chain
43
+ * @returns Whether to inject the delegation hint
44
+ */
45
+ export declare function shouldInjectMultiDocHint(documentCount: number, hasAiResponse: boolean): boolean;
46
+ /**
47
+ * Build the multi-document delegation hint message content.
48
+ *
49
+ * @param documentCount - Number of detected documents
50
+ * @param documentNames - Names of detected documents
51
+ * @returns Message content string for injection as HumanMessage
52
+ */
53
+ export declare function buildMultiDocHintContent(documentCount: number, documentNames: string[]): string;
54
+ /**
55
+ * Build the post-prune context note injected after messages are pruned
56
+ * and summarized. No token numbers — just a contextual signal that
57
+ * earlier conversation was compressed.
58
+ *
59
+ * @param discardedCount - Number of messages that were pruned
60
+ * @param hasSummary - Whether a summary was successfully generated
61
+ * @returns Message content string for injection as SystemMessage, or null if no note needed
62
+ */
63
+ export declare function buildPostPruneNote(discardedCount: number, hasSummary: boolean): string | null;
64
+ /**
65
+ * Check whether a tool named "task" exists in the agent's tool set.
66
+ *
67
+ * @param tools - Array of tool objects or structured tools
68
+ * @returns Whether the task tool is available
69
+ */
70
+ export declare function hasTaskTool(tools: Array<{
71
+ name?: string;
72
+ } | unknown> | undefined): boolean;
@@ -8,3 +8,6 @@ export * from './toonFormat';
8
8
  export * from './contextAnalytics';
9
9
  export * from './schema';
10
10
  export * from './toolCallContinuation';
11
+ export * from './contextPressure';
12
+ export * from './toolDiscoveryCache';
13
+ export * from './pruneCalibration';
@@ -0,0 +1,43 @@
1
+ import type { PruneCalibrationState } from '@/types/graph';
2
+ /**
3
+ * Creates an initial pruning calibration state.
4
+ *
5
+ * @param initialRatio - Starting calibration ratio (default: 1.0)
6
+ * @returns Fresh calibration state
7
+ */
8
+ export declare function createPruneCalibration(initialRatio?: number): PruneCalibrationState;
9
+ /**
10
+ * Updates the pruning calibration using Exponential Moving Average (EMA).
11
+ *
12
+ * Problem: Without calibration, the pruner's token estimates can diverge from
13
+ * reality across iterations, causing either:
14
+ * - Over-pruning (context cliff): Too many messages removed at once, losing critical tool results
15
+ * - Under-pruning: Not enough messages removed, hitting hard token limits
16
+ *
17
+ * Solution: Track the ratio between actual token usage (from API response) and
18
+ * estimated token usage (from our token counter). Apply EMA smoothing so the
19
+ * calibration adjusts gradually, preventing oscillation.
20
+ *
21
+ * The calibration ratio is applied to maxTokens in the pruner:
22
+ * effectiveMaxTokens = maxTokens * calibrationRatio
23
+ *
24
+ * If actual > estimated → ratio decreases → prune more aggressively
25
+ * If actual < estimated → ratio increases → prune less aggressively
26
+ *
27
+ * @param state - Current calibration state
28
+ * @param actualTokens - Actual token count from API response (UsageMetadata)
29
+ * @param estimatedTokens - Estimated token count from token counter
30
+ * @param alpha - EMA smoothing factor (default: PRUNING_EMA_ALPHA)
31
+ * @returns Updated calibration state (new object, does not mutate input)
32
+ */
33
+ export declare function updatePruneCalibration(state: PruneCalibrationState, actualTokens: number, estimatedTokens: number, alpha?: number): PruneCalibrationState;
34
+ /**
35
+ * Applies the calibration ratio to a max token budget.
36
+ * The ratio adjusts the effective budget so pruning is more or less aggressive
37
+ * based on observed vs. estimated token divergence.
38
+ *
39
+ * @param maxTokens - Raw max token budget
40
+ * @param state - Current calibration state
41
+ * @returns Adjusted max token budget
42
+ */
43
+ export declare function applyCalibration(maxTokens: number, state: PruneCalibrationState): number;