@juspay/neurolink 10.10.3 → 10.10.4

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.
@@ -46,6 +46,22 @@ const TOKENS_PER_TOOL_DEFINITION = 200;
46
46
  const MAX_CALIBRATION_RATIO = 3;
47
47
  /** Messages at the end of the conversation the guard never modifies. */
48
48
  const PROTECTED_TAIL_MESSAGES = 4;
49
+ /**
50
+ * Fraction of the context window the guard reclaims DOWN TO once it fires.
51
+ *
52
+ * The high-water mark (`thresholdRatio`) decides *when* to act; this low-water
53
+ * mark decides *how far*. Reclaiming only back to the threshold meant the very
54
+ * next step — which appends an assistant turn plus its tool results — crossed
55
+ * it again, so the guard mutated the message prefix on every single step of a
56
+ * long agentic run. Each of those mutations invalidates the Anthropic
57
+ * `cache_control` prefix from the edit point onward (see
58
+ * anthropicCacheBreakpoints), turning a ~0.1x cached read into full-price
59
+ * input every step.
60
+ *
61
+ * One deeper reclaim every N steps saves the same tokens and leaves the prefix
62
+ * stable in between, which is what makes the cache worth having.
63
+ */
64
+ const CONTEXT_GUARD_LOW_WATER_RATIO = 0.6;
49
65
  /** Stage-1 preview budget for an old tool output (bytes). */
50
66
  const OLD_TOOL_OUTPUT_PREVIEW_BYTES = 2_048;
51
67
  /** Stage-1 preview budget for an old tool output (lines). */
@@ -295,14 +311,22 @@ export function createStepBudgetGuard(config) {
295
311
  lastRawEstimate = rawEstimate;
296
312
  return undefined;
297
313
  }
314
+ // Reclaim down to the LOW-WATER mark, not merely back under the threshold.
315
+ // See CONTEXT_GUARD_LOW_WATER_RATIO: stopping at the threshold guaranteed
316
+ // the next step crossed it again, mutating the cached prefix every step.
317
+ const lowWaterTokens = Math.floor((availableInput * CONTEXT_GUARD_LOW_WATER_RATIO) / calibration);
298
318
  // Stage 1: shrink old tool outputs to previews.
299
319
  const stage1 = truncateOldToolOutputs([...messages]);
300
320
  let compacted = stage1.messages;
301
321
  let newEstimate = overheadTokens + estimateStepMessagesTokens(compacted, provider);
302
- // Stage 2: drop oldest complete tool exchanges if still over.
322
+ // Stage 2: drop oldest complete tool exchanges until under the low-water
323
+ // mark. Stage order is deliberately unchanged — truncating first preserves
324
+ // a preview of each output, and since BOTH stages edit the oldest messages
325
+ // the cache prefix is invalidated at roughly the same point either way.
326
+ // Frequency, not stage order, is what governs cache retention here.
303
327
  let droppedExchanges = 0;
304
- if (newEstimate > effectiveThreshold) {
305
- const stage2 = dropOldestToolExchanges(compacted, effectiveThreshold, overheadTokens, provider);
328
+ if (newEstimate > lowWaterTokens) {
329
+ const stage2 = dropOldestToolExchanges(compacted, lowWaterTokens, overheadTokens, provider);
306
330
  compacted = stage2.messages;
307
331
  droppedExchanges = stage2.droppedExchanges;
308
332
  newEstimate =
@@ -317,8 +341,13 @@ export function createStepBudgetGuard(config) {
317
341
  model,
318
342
  estimatedTokens: rawEstimate,
319
343
  thresholdTokens: effectiveThreshold,
344
+ lowWaterTokens,
320
345
  calibration,
321
346
  afterTokens: newEstimate,
347
+ // Headroom reclaimed below the firing threshold. Roughly how many further
348
+ // steps can run before the guard mutates the prefix again — a value near
349
+ // zero means the cache is being invalidated every step.
350
+ headroomTokens: effectiveThreshold - newEstimate,
322
351
  toolOutputsTruncated: stage1.truncated,
323
352
  exchangesDropped: droppedExchanges,
324
353
  });
@@ -46,6 +46,22 @@ const TOKENS_PER_TOOL_DEFINITION = 200;
46
46
  const MAX_CALIBRATION_RATIO = 3;
47
47
  /** Messages at the end of the conversation the guard never modifies. */
48
48
  const PROTECTED_TAIL_MESSAGES = 4;
49
+ /**
50
+ * Fraction of the context window the guard reclaims DOWN TO once it fires.
51
+ *
52
+ * The high-water mark (`thresholdRatio`) decides *when* to act; this low-water
53
+ * mark decides *how far*. Reclaiming only back to the threshold meant the very
54
+ * next step — which appends an assistant turn plus its tool results — crossed
55
+ * it again, so the guard mutated the message prefix on every single step of a
56
+ * long agentic run. Each of those mutations invalidates the Anthropic
57
+ * `cache_control` prefix from the edit point onward (see
58
+ * anthropicCacheBreakpoints), turning a ~0.1x cached read into full-price
59
+ * input every step.
60
+ *
61
+ * One deeper reclaim every N steps saves the same tokens and leaves the prefix
62
+ * stable in between, which is what makes the cache worth having.
63
+ */
64
+ const CONTEXT_GUARD_LOW_WATER_RATIO = 0.6;
49
65
  /** Stage-1 preview budget for an old tool output (bytes). */
50
66
  const OLD_TOOL_OUTPUT_PREVIEW_BYTES = 2_048;
51
67
  /** Stage-1 preview budget for an old tool output (lines). */
@@ -295,14 +311,22 @@ export function createStepBudgetGuard(config) {
295
311
  lastRawEstimate = rawEstimate;
296
312
  return undefined;
297
313
  }
314
+ // Reclaim down to the LOW-WATER mark, not merely back under the threshold.
315
+ // See CONTEXT_GUARD_LOW_WATER_RATIO: stopping at the threshold guaranteed
316
+ // the next step crossed it again, mutating the cached prefix every step.
317
+ const lowWaterTokens = Math.floor((availableInput * CONTEXT_GUARD_LOW_WATER_RATIO) / calibration);
298
318
  // Stage 1: shrink old tool outputs to previews.
299
319
  const stage1 = truncateOldToolOutputs([...messages]);
300
320
  let compacted = stage1.messages;
301
321
  let newEstimate = overheadTokens + estimateStepMessagesTokens(compacted, provider);
302
- // Stage 2: drop oldest complete tool exchanges if still over.
322
+ // Stage 2: drop oldest complete tool exchanges until under the low-water
323
+ // mark. Stage order is deliberately unchanged — truncating first preserves
324
+ // a preview of each output, and since BOTH stages edit the oldest messages
325
+ // the cache prefix is invalidated at roughly the same point either way.
326
+ // Frequency, not stage order, is what governs cache retention here.
303
327
  let droppedExchanges = 0;
304
- if (newEstimate > effectiveThreshold) {
305
- const stage2 = dropOldestToolExchanges(compacted, effectiveThreshold, overheadTokens, provider);
328
+ if (newEstimate > lowWaterTokens) {
329
+ const stage2 = dropOldestToolExchanges(compacted, lowWaterTokens, overheadTokens, provider);
306
330
  compacted = stage2.messages;
307
331
  droppedExchanges = stage2.droppedExchanges;
308
332
  newEstimate =
@@ -317,8 +341,13 @@ export function createStepBudgetGuard(config) {
317
341
  model,
318
342
  estimatedTokens: rawEstimate,
319
343
  thresholdTokens: effectiveThreshold,
344
+ lowWaterTokens,
320
345
  calibration,
321
346
  afterTokens: newEstimate,
347
+ // Headroom reclaimed below the firing threshold. Roughly how many further
348
+ // steps can run before the guard mutates the prefix again — a value near
349
+ // zero means the cache is being invalidated every step.
350
+ headroomTokens: effectiveThreshold - newEstimate,
322
351
  toolOutputsTruncated: stage1.truncated,
323
352
  exchangesDropped: droppedExchanges,
324
353
  });
@@ -52,7 +52,7 @@ export declare function estimateTokens(text: string, provider?: string, isCode?:
52
52
  * Includes message framing overhead.
53
53
  *
54
54
  * Counts `content` AND `args`. A `tool_call` is persisted with an EMPTY
55
- * `content` and its entire payload in `args` (see flushPendingToolExecutions),
55
+ * `content` and its entire payload in `args` (see flushPendingToolData),
56
56
  * so a content-only estimate scored a 39 KB Write call at ~28 tokens against a
57
57
  * real cost near 9,750 — the budget checker, the compaction trigger and the
58
58
  * summarization threshold were all blind to the single largest source of
@@ -109,7 +109,7 @@ function serializeForEstimate(value) {
109
109
  * Includes message framing overhead.
110
110
  *
111
111
  * Counts `content` AND `args`. A `tool_call` is persisted with an EMPTY
112
- * `content` and its entire payload in `args` (see flushPendingToolExecutions),
112
+ * `content` and its entire payload in `args` (see flushPendingToolData),
113
113
  * so a content-only estimate scored a 39 KB Write call at ~28 tokens against a
114
114
  * real cost near 9,750 — the budget checker, the compaction trigger and the
115
115
  * summarization threshold were all blind to the single largest source of
@@ -52,7 +52,7 @@ export declare function estimateTokens(text: string, provider?: string, isCode?:
52
52
  * Includes message framing overhead.
53
53
  *
54
54
  * Counts `content` AND `args`. A `tool_call` is persisted with an EMPTY
55
- * `content` and its entire payload in `args` (see flushPendingToolExecutions),
55
+ * `content` and its entire payload in `args` (see flushPendingToolData),
56
56
  * so a content-only estimate scored a 39 KB Write call at ~28 tokens against a
57
57
  * real cost near 9,750 — the budget checker, the compaction trigger and the
58
58
  * summarization threshold were all blind to the single largest source of
@@ -109,7 +109,7 @@ function serializeForEstimate(value) {
109
109
  * Includes message framing overhead.
110
110
  *
111
111
  * Counts `content` AND `args`. A `tool_call` is persisted with an EMPTY
112
- * `content` and its entire payload in `args` (see flushPendingToolExecutions),
112
+ * `content` and its entire payload in `args` (see flushPendingToolData),
113
113
  * so a content-only estimate scored a 39 KB Write call at ~28 tokens against a
114
114
  * real cost near 9,750 — the budget checker, the compaction trigger and the
115
115
  * summarization threshold were all blind to the single largest source of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "10.10.3",
3
+ "version": "10.10.4",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -84,6 +84,7 @@
84
84
  "test:memory": "npx tsx test/continuous-test-suite-memory.ts",
85
85
  "test:tool-pairing": "npx tsx test/continuous-test-suite-tool-pairing.ts",
86
86
  "test:token-accounting": "npx tsx test/continuous-test-suite-token-accounting.ts",
87
+ "test:step-guard": "npx tsx test/continuous-test-suite-step-guard.ts",
87
88
  "test:middleware": "npx tsx test/continuous-test-suite-middleware.ts",
88
89
  "test:observability": "npx tsx test/continuous-test-suite-observability.ts",
89
90
  "test:ppt": "npx tsx test/continuous-test-suite-ppt.ts",