@gajae-code/agent-core 0.7.3 → 0.7.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/pruning.d.ts +10 -0
- package/package.json +4 -4
- package/src/compaction/pruning.ts +165 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.4] - 2026-06-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `pruneAssistantToolArguments`: an isolated pre-compaction pruning pass that redacts stale `edit`/`write`/`apply_patch`/`ast_edit` tool-call argument payloads only when every touched path group has a later successful mutation, preserving tool-call identity (id/name/customWireName/signatures/intent/path hints), protecting latest/failed/ambiguous calls, and reporting separate stats from tool-result pruning. Reduces pre-compaction context pressure and the resident footprint of superseded large edit arguments.
|
|
10
|
+
|
|
5
11
|
## [0.7.3] - 2026-06-25
|
|
6
12
|
### Added
|
|
7
13
|
|
|
@@ -34,4 +34,14 @@ export interface PruneResult {
|
|
|
34
34
|
*/
|
|
35
35
|
prunedEntries: SessionMessageEntry[];
|
|
36
36
|
}
|
|
37
|
+
export interface AssistantArgumentPruneResult {
|
|
38
|
+
argumentPrunedCount: number;
|
|
39
|
+
argumentTokensSaved: number;
|
|
40
|
+
/**
|
|
41
|
+
* The mutated assistant message entries. Callers whose entry source returns
|
|
42
|
+
* materialized copies must write these back into their canonical store by id.
|
|
43
|
+
*/
|
|
44
|
+
prunedEntries: SessionMessageEntry[];
|
|
45
|
+
}
|
|
46
|
+
export declare function pruneAssistantToolArguments(entries: SessionEntry[], config?: PruneConfig): AssistantArgumentPruneResult;
|
|
37
47
|
export declare function pruneToolOutputs(entries: SessionEntry[], config?: PruneConfig): PruneResult;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/agent-core",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.4",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@gajae-code/ai": "0.7.
|
|
39
|
-
"@gajae-code/natives": "0.7.
|
|
40
|
-
"@gajae-code/utils": "0.7.
|
|
38
|
+
"@gajae-code/ai": "0.7.4",
|
|
39
|
+
"@gajae-code/natives": "0.7.4",
|
|
40
|
+
"@gajae-code/utils": "0.7.4",
|
|
41
41
|
"@opentelemetry/api": "^1.9.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
@@ -126,6 +126,24 @@ function estimatePrunedSavings(tokens: number, notice: string): number {
|
|
|
126
126
|
return Math.max(0, tokens - noticeTokens);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export interface AssistantArgumentPruneResult {
|
|
130
|
+
argumentPrunedCount: number;
|
|
131
|
+
argumentTokensSaved: number;
|
|
132
|
+
/**
|
|
133
|
+
* The mutated assistant message entries. Callers whose entry source returns
|
|
134
|
+
* materialized copies must write these back into their canonical store by id.
|
|
135
|
+
*/
|
|
136
|
+
prunedEntries: SessionMessageEntry[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface PrunedToolArgumentsSentinel {
|
|
140
|
+
pruned: true;
|
|
141
|
+
reason: "stale_tool_arguments";
|
|
142
|
+
pathHints: string[];
|
|
143
|
+
originalChars: number;
|
|
144
|
+
prunedAt: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
129
147
|
const EDIT_TOOL_NAMES = new Set(["edit", "write", "apply_patch", "ast_edit"]);
|
|
130
148
|
|
|
131
149
|
/** Extract the file-path argument from a tool call, when the tool has one. */
|
|
@@ -170,6 +188,68 @@ function editToolPathGroups(call: ToolCall): string[][] {
|
|
|
170
188
|
}
|
|
171
189
|
return groups;
|
|
172
190
|
}
|
|
191
|
+
function pathGroupKey(group: string[]): string {
|
|
192
|
+
return JSON.stringify([...group].sort());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function pathHintsForGroups(groups: string[][]): string[] {
|
|
196
|
+
return [...new Set(groups.flat())].sort();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isPrunedToolArgumentsSentinel(value: unknown): value is PrunedToolArgumentsSentinel {
|
|
200
|
+
return (
|
|
201
|
+
typeof value === "object" &&
|
|
202
|
+
value !== null &&
|
|
203
|
+
(value as { pruned?: unknown; reason?: unknown }).pruned === true &&
|
|
204
|
+
(value as { pruned?: unknown; reason?: unknown }).reason === "stale_tool_arguments"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isEditToolCall(call: ToolCall): boolean {
|
|
209
|
+
return EDIT_TOOL_NAMES.has(call.name) || call.customWireName === "apply_patch";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface AssistantArgumentStalenessIndex {
|
|
213
|
+
latestSuccessfulMutationByPathGroup: Map<string, { index: number; callId: string }>;
|
|
214
|
+
failedCallIds: Set<string>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildAssistantArgumentStalenessIndex(entries: SessionEntry[]): AssistantArgumentStalenessIndex {
|
|
218
|
+
const callsById = new Map<string, ToolCall>();
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (entry.type !== "message") continue;
|
|
221
|
+
const message = entry.message as AgentMessage;
|
|
222
|
+
if (message.role !== "assistant") continue;
|
|
223
|
+
for (const content of message.content) {
|
|
224
|
+
if (content.type === "toolCall") callsById.set(content.id, content);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const latestSuccessfulMutationByPathGroup = new Map<string, { index: number; callId: string }>();
|
|
229
|
+
const failedCallIds = new Set<string>();
|
|
230
|
+
for (let i = 0; i < entries.length; i++) {
|
|
231
|
+
const message = getToolResultMessage(entries[i]);
|
|
232
|
+
if (!message) continue;
|
|
233
|
+
const call = callsById.get(message.toolCallId);
|
|
234
|
+
if (!call || !isEditToolCall(call)) continue;
|
|
235
|
+
const detailFiles = call.name === "ast_edit" ? resultDetailFiles(message) : [];
|
|
236
|
+
const groups = detailFiles.length > 0 ? detailFiles.map(file => [file]) : editToolPathGroups(call);
|
|
237
|
+
if (groups.length === 0) continue;
|
|
238
|
+
if (message.isError) {
|
|
239
|
+
failedCallIds.add(call.id);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const failed = failedEditPaths(message);
|
|
243
|
+
let mutated = false;
|
|
244
|
+
for (const group of groups) {
|
|
245
|
+
if (group.some(groupPath => failed.has(groupPath))) continue;
|
|
246
|
+
latestSuccessfulMutationByPathGroup.set(pathGroupKey(group), { index: i, callId: call.id });
|
|
247
|
+
mutated = true;
|
|
248
|
+
}
|
|
249
|
+
if (!mutated) failedCallIds.add(call.id);
|
|
250
|
+
}
|
|
251
|
+
return { latestSuccessfulMutationByPathGroup, failedCallIds };
|
|
252
|
+
}
|
|
173
253
|
|
|
174
254
|
/**
|
|
175
255
|
* Trailing read selectors (`:50`, `:50-200`, `:50+150`, `:5-16,960-973`,
|
|
@@ -361,6 +441,91 @@ function buildStalenessIndex(entries: SessionEntry[]): StalenessIndex {
|
|
|
361
441
|
|
|
362
442
|
return { staleResultIndices };
|
|
363
443
|
}
|
|
444
|
+
export function pruneAssistantToolArguments(
|
|
445
|
+
entries: SessionEntry[],
|
|
446
|
+
config: PruneConfig = DEFAULT_PRUNE_CONFIG,
|
|
447
|
+
): AssistantArgumentPruneResult {
|
|
448
|
+
let accumulatedTokens = 0;
|
|
449
|
+
let argumentTokensSaved = 0;
|
|
450
|
+
const { latestSuccessfulMutationByPathGroup, failedCallIds } = buildAssistantArgumentStalenessIndex(entries);
|
|
451
|
+
const candidates: Array<{
|
|
452
|
+
entry: SessionMessageEntry;
|
|
453
|
+
call: ToolCall;
|
|
454
|
+
pathHints: string[];
|
|
455
|
+
originalChars: number;
|
|
456
|
+
savings: number;
|
|
457
|
+
}> = [];
|
|
458
|
+
|
|
459
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
460
|
+
const entry = entries[i];
|
|
461
|
+
if (entry.type !== "message") continue;
|
|
462
|
+
const message = entry.message as AgentMessage;
|
|
463
|
+
if (message.role !== "assistant") continue;
|
|
464
|
+
const entryTokens = estimateEntryTokens(entry);
|
|
465
|
+
const insideProtectWindow = accumulatedTokens < config.protectTokens;
|
|
466
|
+
accumulatedTokens += entryTokens;
|
|
467
|
+
for (const content of message.content) {
|
|
468
|
+
if (content.type !== "toolCall" || !isEditToolCall(content)) continue;
|
|
469
|
+
const argumentJson = JSON.stringify(content.arguments);
|
|
470
|
+
if (argumentJson === undefined) continue;
|
|
471
|
+
const originalChars = argumentJson.length;
|
|
472
|
+
if (isPrunedToolArgumentsSentinel(content.arguments)) continue;
|
|
473
|
+
if (insideProtectWindow || failedCallIds.has(content.id)) continue;
|
|
474
|
+
const groups = editToolPathGroups(content);
|
|
475
|
+
if (groups.length === 0) continue;
|
|
476
|
+
// Arguments are pruned as one indivisible payload, so require EVERY
|
|
477
|
+
// concrete path group to be stale from a later successful mutation.
|
|
478
|
+
// A group with no later success (failed/unknown/ambiguous) protects the
|
|
479
|
+
// whole call rather than dropping non-stale multi-file patch evidence.
|
|
480
|
+
const isStale =
|
|
481
|
+
groups.length > 0 &&
|
|
482
|
+
groups.every(group => {
|
|
483
|
+
const latest = latestSuccessfulMutationByPathGroup.get(pathGroupKey(group));
|
|
484
|
+
return latest !== undefined && latest.index > i && latest.callId !== content.id;
|
|
485
|
+
});
|
|
486
|
+
if (!isStale) continue;
|
|
487
|
+
const sentinelChars = JSON.stringify({
|
|
488
|
+
pruned: true,
|
|
489
|
+
reason: "stale_tool_arguments",
|
|
490
|
+
pathHints: pathHintsForGroups(groups),
|
|
491
|
+
originalChars,
|
|
492
|
+
prunedAt: 0,
|
|
493
|
+
} satisfies PrunedToolArgumentsSentinel).length;
|
|
494
|
+
candidates.push({
|
|
495
|
+
entry: entry as SessionMessageEntry,
|
|
496
|
+
call: content,
|
|
497
|
+
pathHints: pathHintsForGroups(groups),
|
|
498
|
+
originalChars,
|
|
499
|
+
savings: Math.max(0, Math.ceil((originalChars - sentinelChars) / 4)),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const candidate of candidates) {
|
|
505
|
+
argumentTokensSaved += candidate.savings;
|
|
506
|
+
}
|
|
507
|
+
if (argumentTokensSaved < config.minimumSavings || candidates.length === 0) {
|
|
508
|
+
return { argumentPrunedCount: 0, argumentTokensSaved: 0, prunedEntries: [] };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const prunedAt = Date.now();
|
|
512
|
+
const prunedEntries: SessionMessageEntry[] = [];
|
|
513
|
+
const prunedEntryIds = new Set<string>();
|
|
514
|
+
for (const candidate of candidates) {
|
|
515
|
+
candidate.call.arguments = {
|
|
516
|
+
pruned: true,
|
|
517
|
+
reason: "stale_tool_arguments",
|
|
518
|
+
pathHints: candidate.pathHints,
|
|
519
|
+
originalChars: candidate.originalChars,
|
|
520
|
+
prunedAt,
|
|
521
|
+
};
|
|
522
|
+
if (!prunedEntryIds.has(candidate.entry.id)) {
|
|
523
|
+
prunedEntries.push(candidate.entry);
|
|
524
|
+
prunedEntryIds.add(candidate.entry.id);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { argumentPrunedCount: candidates.length, argumentTokensSaved, prunedEntries };
|
|
528
|
+
}
|
|
364
529
|
|
|
365
530
|
export function pruneToolOutputs(entries: SessionEntry[], config: PruneConfig = DEFAULT_PRUNE_CONFIG): PruneResult {
|
|
366
531
|
let accumulatedTokens = 0;
|