@gajae-code/agent-core 0.15.3 → 0.15.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.4] - 2026-08-29
6
+
7
+ ### Added
8
+
9
+ - Added opt-in adaptive compaction thresholding based on context fullness and recent call rate. The default remains disabled, fixed token thresholds keep precedence, and the bounded tracker resets after successful compaction to avoid repeated immediate compactions.
10
+
5
11
  ## [0.15.3] - 2026-08-27
6
12
 
7
13
  ### Changed
@@ -0,0 +1,31 @@
1
+ export interface AdaptiveCompactionState {
2
+ turnsSinceCompact: number;
3
+ callsInWindow: number;
4
+ windowStart: number;
5
+ lastContextTokens: number;
6
+ lastCompactContextTokens: number | null;
7
+ lastCompactTs: number | null;
8
+ }
9
+ export interface AdaptiveCompactionDecisionState {
10
+ turnsSinceCompact: number;
11
+ callsInWindow: number;
12
+ lastContextTokens?: number;
13
+ }
14
+ export interface AdaptiveCompactionOptions {
15
+ enabled: boolean;
16
+ turnWindow: number;
17
+ baseThresholdPercent: number;
18
+ aggression: number;
19
+ minThresholdPercent?: number;
20
+ }
21
+ export declare class AdaptiveCompactionTracker {
22
+ #private;
23
+ windowMs: number;
24
+ constructor(windowMs?: number, now?: number);
25
+ setWindowMs(windowMs: number, now?: number): void;
26
+ reset(now?: number): void;
27
+ recordCall(contextTokens: number, now?: number): void;
28
+ recordCompact(contextTokens: number, now?: number): void;
29
+ snapshot(): AdaptiveCompactionState;
30
+ decisionState(): AdaptiveCompactionDecisionState;
31
+ }
@@ -7,6 +7,7 @@
7
7
  import { type MessageAttribution, type Model, type ProviderSessionState, type Usage } from "@gajae-code/ai";
8
8
  import { type AgentTelemetry } from "../telemetry";
9
9
  import type { AgentMessage, AgentTool } from "../types";
10
+ import type { AdaptiveCompactionDecisionState, AdaptiveCompactionOptions } from "./adaptive";
10
11
  import type { SessionEntry } from "./entries";
11
12
  import { type ConvertToLlm } from "./messages";
12
13
  import { type FileOperations } from "./utils";
@@ -32,6 +33,8 @@ export interface CompactionSettings {
32
33
  strategy?: "context-full" | "handoff" | "off";
33
34
  thresholdPercent?: number;
34
35
  thresholdTokens?: number;
36
+ adaptive?: AdaptiveCompactionOptions;
37
+ adaptiveState?: AdaptiveCompactionDecisionState;
35
38
  reserveTokens: number;
36
39
  keepRecentTokens: number;
37
40
  autoContinue?: boolean;
@@ -52,6 +55,7 @@ export interface RemoteCompactionFallbackHealthHooks {
52
55
  recordRemoteCompactionFallback(event: RemoteCompactionFallbackHealthEvent): void;
53
56
  }
54
57
  export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
58
+ export declare function computeAdaptiveThresholdPercent(basePercent: number, contextTokens: number, contextWindow: number, state: AdaptiveCompactionDecisionState | undefined, options: AdaptiveCompactionOptions | undefined): number;
55
59
  /**
56
60
  * Calculate total context tokens from usage.
57
61
  * Uses the native totalTokens field when available, falls back to computing from components.
@@ -127,7 +131,7 @@ export declare const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLim
127
131
  * normal pair-safe `compact()` cut logic so a tool_use/tool_result pair is never split.
128
132
  */
129
133
  export declare function emergencyCompactionReason(sample: EmergencyCompactionSample, limits?: EmergencyCompactionLimits): CompactionTriggerReason | null;
130
- export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): number;
134
+ export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number, contextTokens?: number): number;
131
135
  /**
132
136
  * Image content has no tokenizer representation; charge a fixed estimate
133
137
  * matching what providers typically bill for inline images.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Compaction and summarization utilities.
3
3
  */
4
+ export * from "./adaptive";
4
5
  export * from "./branch-summarization";
5
6
  export * from "./compaction";
6
7
  export * from "./entries";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.15.3",
4
+ "version": "0.15.4",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -32,9 +32,9 @@
32
32
  "fmt": "biome format --write ."
33
33
  },
34
34
  "dependencies": {
35
- "@gajae-code/ai": "0.15.3",
36
- "@gajae-code/natives": "0.15.3",
37
- "@gajae-code/utils": "0.15.3",
35
+ "@gajae-code/ai": "0.15.4",
36
+ "@gajae-code/natives": "0.15.4",
37
+ "@gajae-code/utils": "0.15.4",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {
@@ -0,0 +1,92 @@
1
+ export interface AdaptiveCompactionState {
2
+ turnsSinceCompact: number;
3
+ callsInWindow: number;
4
+ windowStart: number;
5
+ lastContextTokens: number;
6
+ lastCompactContextTokens: number | null;
7
+ lastCompactTs: number | null;
8
+ }
9
+
10
+ export interface AdaptiveCompactionDecisionState {
11
+ turnsSinceCompact: number;
12
+ callsInWindow: number;
13
+ lastContextTokens?: number;
14
+ }
15
+
16
+ export interface AdaptiveCompactionOptions {
17
+ enabled: boolean;
18
+ turnWindow: number;
19
+ baseThresholdPercent: number;
20
+ aggression: number;
21
+ minThresholdPercent?: number;
22
+ }
23
+
24
+ export class AdaptiveCompactionTracker {
25
+ #state: AdaptiveCompactionState;
26
+ windowMs: number;
27
+
28
+ constructor(windowMs = 60_000, now = Date.now()) {
29
+ this.windowMs = Number.isFinite(windowMs) && windowMs > 0 ? windowMs : 60_000;
30
+ this.#state = {
31
+ turnsSinceCompact: 0,
32
+ callsInWindow: 0,
33
+ windowStart: now,
34
+ lastContextTokens: 0,
35
+ lastCompactContextTokens: null,
36
+ lastCompactTs: null,
37
+ };
38
+ }
39
+
40
+ setWindowMs(windowMs: number, now = Date.now()): void {
41
+ if (!Number.isFinite(windowMs)) return;
42
+ const nextWindowMs = Math.max(1, windowMs);
43
+ if (nextWindowMs === this.windowMs) return;
44
+ this.windowMs = nextWindowMs;
45
+ this.#state.windowStart = now;
46
+ this.#state.callsInWindow = 0;
47
+ }
48
+
49
+ reset(now = Date.now()): void {
50
+ this.#state = {
51
+ turnsSinceCompact: 0,
52
+ callsInWindow: 0,
53
+ windowStart: now,
54
+ lastContextTokens: 0,
55
+ lastCompactContextTokens: null,
56
+ lastCompactTs: null,
57
+ };
58
+ }
59
+
60
+ recordCall(contextTokens: number, now = Date.now()): void {
61
+ const timestamp = Number.isFinite(now) ? now : Date.now();
62
+ this.#state.turnsSinceCompact += 1;
63
+ if (timestamp - this.#state.windowStart >= this.windowMs) {
64
+ this.#state.windowStart = timestamp;
65
+ this.#state.callsInWindow = 0;
66
+ }
67
+ this.#state.callsInWindow += 1;
68
+ this.#state.lastContextTokens = contextTokens;
69
+ }
70
+
71
+ recordCompact(contextTokens: number, now = Date.now()): void {
72
+ const timestamp = Number.isFinite(now) ? now : Date.now();
73
+ this.#state.turnsSinceCompact = 0;
74
+ this.#state.callsInWindow = 0;
75
+ this.#state.windowStart = timestamp;
76
+ this.#state.lastContextTokens = contextTokens;
77
+ this.#state.lastCompactContextTokens = contextTokens;
78
+ this.#state.lastCompactTs = timestamp;
79
+ }
80
+
81
+ snapshot(): AdaptiveCompactionState {
82
+ return { ...this.#state };
83
+ }
84
+
85
+ decisionState(): AdaptiveCompactionDecisionState {
86
+ return {
87
+ turnsSinceCompact: this.#state.turnsSinceCompact,
88
+ callsInWindow: this.#state.callsInWindow,
89
+ lastContextTokens: this.#state.lastContextTokens,
90
+ };
91
+ }
92
+ }
@@ -18,6 +18,7 @@ import {
18
18
  import { logger, prompt } from "@gajae-code/utils";
19
19
  import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
20
20
  import type { AgentMessage, AgentTool } from "../types";
21
+ import type { AdaptiveCompactionDecisionState, AdaptiveCompactionOptions } from "./adaptive";
21
22
  import type { CompactionEntry, SessionEntry } from "./entries";
22
23
  import { type ConvertToLlm, convertToLlm, createBranchSummaryMessage, createCustomMessage } from "./messages";
23
24
  import {
@@ -33,7 +34,6 @@ import compactionSummaryPrompt from "./prompts/compaction-summary.md" with { typ
33
34
  import compactionTurnPrefixPrompt from "./prompts/compaction-turn-prefix.md" with { type: "text" };
34
35
  import compactionUpdateSummaryPrompt from "./prompts/compaction-update-summary.md" with { type: "text" };
35
36
  import handoffDocumentPrompt from "./prompts/handoff-document.md" with { type: "text" };
36
-
37
37
  import {
38
38
  computeFileLists,
39
39
  createFileOps,
@@ -136,6 +136,8 @@ export interface CompactionSettings {
136
136
  strategy?: "context-full" | "handoff" | "off";
137
137
  thresholdPercent?: number;
138
138
  thresholdTokens?: number;
139
+ adaptive?: AdaptiveCompactionOptions;
140
+ adaptiveState?: AdaptiveCompactionDecisionState;
139
141
  reserveTokens: number;
140
142
  keepRecentTokens: number;
141
143
  autoContinue?: boolean;
@@ -166,6 +168,40 @@ export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
166
168
  remoteEnabled: true,
167
169
  };
168
170
 
171
+ export function computeAdaptiveThresholdPercent(
172
+ basePercent: number,
173
+ contextTokens: number,
174
+ contextWindow: number,
175
+ state: AdaptiveCompactionDecisionState | undefined,
176
+ options: AdaptiveCompactionOptions | undefined,
177
+ ): number {
178
+ const clampedBasePercent = Number.isFinite(basePercent) ? Math.min(99, Math.max(1, basePercent)) : 85;
179
+ if (!options?.enabled) return basePercent;
180
+ if (!state || !Number.isFinite(contextWindow) || contextWindow <= 0) return clampedBasePercent;
181
+ if (!Number.isFinite(options.turnWindow) || options.turnWindow <= 0) return clampedBasePercent;
182
+
183
+ const safeContextTokens = Number.isFinite(contextTokens) ? Math.max(0, contextTokens) : 0;
184
+ const fillRatio = safeContextTokens / contextWindow;
185
+ const baseRatio = clampedBasePercent / 100;
186
+ if (fillRatio < baseRatio * 0.7) return clampedBasePercent;
187
+
188
+ const turnsSinceCompact = Number.isFinite(state.turnsSinceCompact) ? Math.max(0, state.turnsSinceCompact) : 0;
189
+ const callsInWindow = Number.isFinite(state.callsInWindow) ? Math.max(0, state.callsInWindow) : 0;
190
+ if (turnsSinceCompact <= 3) return clampedBasePercent;
191
+ const windowTurns = Math.max(1, options.turnWindow * 4);
192
+ const intensity = Math.min(1, callsInWindow / windowTurns);
193
+ const aggression = Number.isFinite(options.aggression) ? Math.min(1, Math.max(0, options.aggression)) : 0;
194
+ const configuredMinThresholdPercent = options.minThresholdPercent;
195
+ const minThresholdPercent = Math.min(
196
+ clampedBasePercent,
197
+ typeof configuredMinThresholdPercent === "number" && Number.isFinite(configuredMinThresholdPercent)
198
+ ? Math.max(1, configuredMinThresholdPercent)
199
+ : clampedBasePercent * 0.5,
200
+ );
201
+ const loweredPercent = clampedBasePercent - (clampedBasePercent - minThresholdPercent) * aggression * intensity;
202
+ return Math.max(1, Math.min(99, Math.round(loweredPercent)));
203
+ }
204
+
169
205
  // ============================================================================
170
206
  // Token calculation
171
207
  // ============================================================================
@@ -244,7 +280,7 @@ export function shouldCompact(
244
280
  maxOutputTokens = 0,
245
281
  ): boolean {
246
282
  if (!settings.enabled || settings.strategy === "off" || contextWindow <= 0) return false;
247
- const thresholdTokens = resolveThresholdTokens(contextWindow, settings, maxOutputTokens);
283
+ const thresholdTokens = resolveThresholdTokens(contextWindow, settings, maxOutputTokens, contextTokens);
248
284
  return contextTokens > thresholdTokens;
249
285
  }
250
286
 
@@ -381,6 +417,7 @@ export function resolveThresholdTokens(
381
417
  contextWindow: number,
382
418
  settings: CompactionSettings,
383
419
  maxOutputTokens = 0,
420
+ contextTokens?: number,
384
421
  ): number {
385
422
  // Fixed token limit takes priority over percentage
386
423
  const thresholdTokens = settings.thresholdTokens;
@@ -392,10 +429,38 @@ export function resolveThresholdTokens(
392
429
  // Percentage-based threshold
393
430
  const thresholdPercent = settings.thresholdPercent;
394
431
  if (typeof thresholdPercent !== "number" || !Number.isFinite(thresholdPercent) || thresholdPercent <= 0) {
395
- return contextWindow - effectiveReserveTokens(contextWindow, settings, maxOutputTokens);
432
+ if (!settings.adaptive?.enabled) {
433
+ return contextWindow - effectiveReserveTokens(contextWindow, settings, maxOutputTokens);
434
+ }
435
+ const adaptiveBasePercent = Number.isFinite(settings.adaptive.baseThresholdPercent)
436
+ ? Math.min(99, Math.max(1, settings.adaptive.baseThresholdPercent))
437
+ : 85;
438
+ const adaptiveThresholdPercent = computeAdaptiveThresholdPercent(
439
+ adaptiveBasePercent,
440
+ adaptiveContextTokens(contextTokens, settings.adaptiveState?.lastContextTokens),
441
+ contextWindow,
442
+ settings.adaptiveState,
443
+ settings.adaptive,
444
+ );
445
+ return Math.floor(contextWindow * (adaptiveThresholdPercent / 100));
396
446
  }
397
447
  const clampedThresholdPercent = Math.min(99, Math.max(1, thresholdPercent));
398
- return Math.floor(contextWindow * (clampedThresholdPercent / 100));
448
+ const adaptiveThresholdPercent = computeAdaptiveThresholdPercent(
449
+ settings.adaptive?.baseThresholdPercent ?? clampedThresholdPercent,
450
+ adaptiveContextTokens(contextTokens, settings.adaptiveState?.lastContextTokens),
451
+ contextWindow,
452
+ settings.adaptiveState,
453
+ settings.adaptive,
454
+ );
455
+ const effectiveThresholdPercent = settings.adaptive?.enabled ? adaptiveThresholdPercent : clampedThresholdPercent;
456
+ return Math.floor(contextWindow * (effectiveThresholdPercent / 100));
457
+ }
458
+
459
+ function adaptiveContextTokens(contextTokens: number | undefined, lastContextTokens: number | undefined): number {
460
+ if (contextTokens !== undefined && Number.isFinite(contextTokens)) return Math.max(0, contextTokens);
461
+ if (typeof lastContextTokens === "number" && Number.isFinite(lastContextTokens))
462
+ return Math.max(0, lastContextTokens);
463
+ return 0;
399
464
  }
400
465
 
401
466
  // ============================================================================
@@ -2,6 +2,7 @@
2
2
  * Compaction and summarization utilities.
3
3
  */
4
4
 
5
+ export * from "./adaptive";
5
6
  export * from "./branch-summarization";
6
7
  export * from "./compaction";
7
8
  export * from "./entries";