@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 +6 -0
- package/dist/types/compaction/adaptive.d.ts +31 -0
- package/dist/types/compaction/compaction.d.ts +5 -1
- package/dist/types/compaction/index.d.ts +1 -0
- package/package.json +4 -4
- package/src/compaction/adaptive.ts +92 -0
- package/src/compaction/compaction.ts +69 -4
- package/src/compaction/index.ts +1 -0
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.
|
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.
|
|
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.
|
|
36
|
-
"@gajae-code/natives": "0.15.
|
|
37
|
-
"@gajae-code/utils": "0.15.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ============================================================================
|