@sayknow-cli/agent-core 0.3.6 → 0.3.8
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 +9 -0
- package/dist/types/agent-loop.d.ts +56 -0
- package/dist/types/agent.d.ts +385 -0
- package/dist/types/append-only-context.d.ts +137 -0
- package/dist/types/compaction/branch-summarization.d.ts +103 -0
- package/dist/types/compaction/compaction.d.ts +284 -0
- package/dist/types/compaction/entries.d.ts +109 -0
- package/dist/types/compaction/errors.d.ts +26 -0
- package/dist/types/compaction/index.d.ts +11 -0
- package/dist/types/compaction/messages.d.ts +61 -0
- package/dist/types/compaction/openai.d.ts +63 -0
- package/dist/types/compaction/pruning.d.ts +69 -0
- package/dist/types/compaction/utils.d.ts +32 -0
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/harmony-leak.d.ts +100 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/proxy.d.ts +84 -0
- package/dist/types/run-collector.d.ts +196 -0
- package/dist/types/telemetry.d.ts +596 -0
- package/dist/types/thinking.d.ts +18 -0
- package/dist/types/types.d.ts +430 -0
- package/package.json +12 -11
- package/src/agent-loop.ts +132 -5
- package/src/agent.ts +28 -0
- package/src/append-only-context.ts +28 -6
- package/src/compaction/compaction.ts +87 -14
- package/src/compaction/pruning.ts +61 -4
package/src/agent.ts
CHANGED
|
@@ -954,6 +954,15 @@ export class Agent {
|
|
|
954
954
|
popLastSteer(): AgentMessage | undefined {
|
|
955
955
|
return this.#steeringQueue.pop();
|
|
956
956
|
}
|
|
957
|
+
removeSteerAt(index: number): AgentMessage | undefined {
|
|
958
|
+
if (index < 0 || index >= this.#steeringQueue.length) return undefined;
|
|
959
|
+
const [removed] = this.#steeringQueue.splice(index, 1);
|
|
960
|
+
return removed;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
moveSteer(fromIndex: number, toIndex: number): boolean {
|
|
964
|
+
return this.#moveQueuedMessage(this.#steeringQueue, fromIndex, toIndex);
|
|
965
|
+
}
|
|
957
966
|
|
|
958
967
|
/**
|
|
959
968
|
* Remove and return the last follow-up message from the queue (LIFO).
|
|
@@ -962,6 +971,25 @@ export class Agent {
|
|
|
962
971
|
popLastFollowUp(): AgentMessage | undefined {
|
|
963
972
|
return this.#followUpQueue.pop();
|
|
964
973
|
}
|
|
974
|
+
removeFollowUpAt(index: number): AgentMessage | undefined {
|
|
975
|
+
if (index < 0 || index >= this.#followUpQueue.length) return undefined;
|
|
976
|
+
const [removed] = this.#followUpQueue.splice(index, 1);
|
|
977
|
+
return removed;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
moveFollowUp(fromIndex: number, toIndex: number): boolean {
|
|
981
|
+
return this.#moveQueuedMessage(this.#followUpQueue, fromIndex, toIndex);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#moveQueuedMessage<T>(queue: T[], fromIndex: number, toIndex: number): boolean {
|
|
985
|
+
if (fromIndex < 0 || fromIndex >= queue.length) return false;
|
|
986
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
987
|
+
if (fromIndex === toIndex) return true;
|
|
988
|
+
const [item] = queue.splice(fromIndex, 1);
|
|
989
|
+
if (item === undefined) return false;
|
|
990
|
+
queue.splice(toIndex, 0, item);
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
965
993
|
|
|
966
994
|
/** Remove queued steering+follow-up messages matching `predicate`, preserving order of the rest. */
|
|
967
995
|
removeQueuedMessages(predicate: (message: AgentMessage) => boolean): {
|
|
@@ -194,6 +194,15 @@ export class AppendOnlyLog {
|
|
|
194
194
|
* ctx = mgr.build(context); // subsequent calls use cache
|
|
195
195
|
* ```
|
|
196
196
|
*/
|
|
197
|
+
export interface AppendOnlyContextManagerOptions {
|
|
198
|
+
/**
|
|
199
|
+
* Invoked whenever the stable prefix fingerprint changes on `build()` (a
|
|
200
|
+
* provider prompt-cache prefix reset). Used for per-session diagnostics; must
|
|
201
|
+
* not throw. `from` is `<unbuilt>` on the first build.
|
|
202
|
+
*/
|
|
203
|
+
readonly onPrefixChange?: (info: { from: string; to: string; version: number }) => void;
|
|
204
|
+
}
|
|
205
|
+
|
|
197
206
|
export class AppendOnlyContextManager {
|
|
198
207
|
readonly prefix = new StablePrefix();
|
|
199
208
|
readonly log = new AppendOnlyLog();
|
|
@@ -203,6 +212,11 @@ export class AppendOnlyContextManager {
|
|
|
203
212
|
#syncedHashes: (number | bigint)[] = [];
|
|
204
213
|
/** Number of provider-normalized messages that were seeded before child-local messages. */
|
|
205
214
|
#seededPrefixCount = 0;
|
|
215
|
+
readonly #onPrefixChange: AppendOnlyContextManagerOptions["onPrefixChange"];
|
|
216
|
+
|
|
217
|
+
constructor(options: AppendOnlyContextManagerOptions = {}) {
|
|
218
|
+
this.#onPrefixChange = options.onPrefixChange;
|
|
219
|
+
}
|
|
206
220
|
|
|
207
221
|
static forkFromSeed(args: {
|
|
208
222
|
prefixSnapshot?: StablePrefixSnapshot;
|
|
@@ -220,7 +234,15 @@ export class AppendOnlyContextManager {
|
|
|
220
234
|
}
|
|
221
235
|
|
|
222
236
|
build(context: AgentContext, options: BuildOptions): Context {
|
|
223
|
-
this.prefix.
|
|
237
|
+
const previousFingerprint = this.prefix.fingerprint;
|
|
238
|
+
const changed = this.prefix.build(context, options);
|
|
239
|
+
if (changed && this.#onPrefixChange) {
|
|
240
|
+
this.#onPrefixChange({
|
|
241
|
+
from: previousFingerprint,
|
|
242
|
+
to: this.prefix.fingerprint,
|
|
243
|
+
version: this.prefix.version,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
224
246
|
const { systemPrompt, tools } = this.prefix.toContext();
|
|
225
247
|
return { systemPrompt, messages: this.log.toMessages(), tools };
|
|
226
248
|
}
|
|
@@ -252,7 +274,7 @@ export class AppendOnlyContextManager {
|
|
|
252
274
|
if (this.#seededPrefixCount > 0) {
|
|
253
275
|
// F9: a seeded fork whose inherited prefix changed (e.g. after compaction)
|
|
254
276
|
// rebases onto the new provider context instead of throwing.
|
|
255
|
-
this.#rebaseToBaseline(
|
|
277
|
+
this.#rebaseToBaseline(messagesToSync, seededPrefixLength);
|
|
256
278
|
return;
|
|
257
279
|
}
|
|
258
280
|
this.log.clear();
|
|
@@ -265,7 +287,7 @@ export class AppendOnlyContextManager {
|
|
|
265
287
|
// while a seed prefix is active; a genuine seeded compaction rebases (F9).
|
|
266
288
|
if (messagesToSync.length < this.#lastSyncCount) {
|
|
267
289
|
if (this.#seededPrefixCount > 0) {
|
|
268
|
-
this.#rebaseToBaseline(
|
|
290
|
+
this.#rebaseToBaseline(messagesToSync, seededPrefixLength);
|
|
269
291
|
return;
|
|
270
292
|
}
|
|
271
293
|
this.log.clear();
|
|
@@ -359,12 +381,12 @@ export class AppendOnlyContextManager {
|
|
|
359
381
|
return false;
|
|
360
382
|
}
|
|
361
383
|
|
|
362
|
-
/** F9: reset the
|
|
363
|
-
#rebaseToBaseline(messages: readonly unknown[]): void {
|
|
384
|
+
/** F9: reset the log to a new provider-visible baseline after seeded compaction/rebase. */
|
|
385
|
+
#rebaseToBaseline(messages: readonly unknown[], seededPrefixCount = 0): void {
|
|
364
386
|
this.log.clear();
|
|
365
387
|
this.log.extend([...messages]);
|
|
366
388
|
this.#lastSyncCount = messages.length;
|
|
367
|
-
this.#seededPrefixCount =
|
|
389
|
+
this.#seededPrefixCount = seededPrefixCount;
|
|
368
390
|
this.#syncedHashes = this.#hashRange(messages, 0, messages.length);
|
|
369
391
|
}
|
|
370
392
|
}
|
|
@@ -315,7 +315,7 @@ export function resolveThresholdTokens(
|
|
|
315
315
|
* Image content has no tokenizer representation; charge a fixed estimate
|
|
316
316
|
* matching what providers typically bill for inline images.
|
|
317
317
|
*/
|
|
318
|
-
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
318
|
+
export const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
319
319
|
/**
|
|
320
320
|
* Estimate tokens for collected message fragments using the native-free
|
|
321
321
|
* heuristic. Provider usage is the authoritative anchor for context-changing
|
|
@@ -679,6 +679,49 @@ export interface SummaryOptions {
|
|
|
679
679
|
preferWebsockets?: boolean;
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Cap the serialized conversation fed to a summarization request so the request
|
|
684
|
+
* itself fits inside the model's context window.
|
|
685
|
+
*
|
|
686
|
+
* Without this, summarizing a near-full context serializes (nearly) the entire
|
|
687
|
+
* history back into a single summary request; on strict backends (e.g.
|
|
688
|
+
* OpenAI-code/Codex `context_length_exceeded`) that request itself overflows and
|
|
689
|
+
* throws, so context-overflow recovery cannot produce a summary and the agent
|
|
690
|
+
* fails to compact-and-continue — a non-interactive `skc -p` run then terminates
|
|
691
|
+
* on the very overflow the recovery was meant to absorb.
|
|
692
|
+
*
|
|
693
|
+
* The budget reserves the summary's own output tokens plus prompt/system/template
|
|
694
|
+
* overhead, and applies a conservative safety factor because the chars/4 heuristic
|
|
695
|
+
* undercounts dense or CJK text (the reason the original overflow was missed).
|
|
696
|
+
* Truncation keeps the head (origin/goals) and the tail (most recent state) and
|
|
697
|
+
* elides the middle; it is a last resort that only triggers when the input would
|
|
698
|
+
* otherwise not fit.
|
|
699
|
+
*/
|
|
700
|
+
export function boundConversationTextForSummary(
|
|
701
|
+
conversationText: string,
|
|
702
|
+
model: Model,
|
|
703
|
+
outputMaxTokens: number,
|
|
704
|
+
): string {
|
|
705
|
+
const contextWindow = model.contextWindow;
|
|
706
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return conversationText;
|
|
707
|
+
|
|
708
|
+
const OVERHEAD_TOKENS = 4096;
|
|
709
|
+
const SAFETY_FACTOR = 0.6;
|
|
710
|
+
const inputBudgetTokens = Math.floor(
|
|
711
|
+
(contextWindow - Math.max(0, outputMaxTokens) - OVERHEAD_TOKENS) * SAFETY_FACTOR,
|
|
712
|
+
);
|
|
713
|
+
if (inputBudgetTokens <= 0) return conversationText;
|
|
714
|
+
if (estimateTextTokensHeuristic(conversationText) <= inputBudgetTokens) return conversationText;
|
|
715
|
+
|
|
716
|
+
const budgetChars = inputBudgetTokens * HEURISTIC_BYTES_PER_TOKEN;
|
|
717
|
+
const headChars = Math.floor(budgetChars * 0.35);
|
|
718
|
+
const tailChars = Math.max(0, budgetChars - headChars);
|
|
719
|
+
const head = conversationText.slice(0, headChars);
|
|
720
|
+
const tail = tailChars > 0 ? conversationText.slice(conversationText.length - tailChars) : "";
|
|
721
|
+
const elided = conversationText.length - head.length - tail.length;
|
|
722
|
+
return `${head}\n\n[... ${elided} characters of older conversation elided so this summarization request fits within the model context window ...]\n\n${tail}`;
|
|
723
|
+
}
|
|
724
|
+
|
|
682
725
|
export async function generateSummary(
|
|
683
726
|
currentMessages: AgentMessage[],
|
|
684
727
|
model: Model,
|
|
@@ -703,7 +746,7 @@ export async function generateSummary(
|
|
|
703
746
|
// Serialize conversation to text so model doesn't try to continue it
|
|
704
747
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
705
748
|
const llmMessages = (options?.convertToLlm ?? convertToLlm)(currentMessages);
|
|
706
|
-
const conversationText = serializeConversation(llmMessages);
|
|
749
|
+
const conversationText = boundConversationTextForSummary(serializeConversation(llmMessages), model, maxTokens);
|
|
707
750
|
|
|
708
751
|
// Build the prompt with conversation wrapped in tags
|
|
709
752
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
@@ -859,7 +902,7 @@ async function generateShortSummary(
|
|
|
859
902
|
): Promise<string> {
|
|
860
903
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
861
904
|
const llmMessages = (options?.convertToLlm ?? convertToLlm)(recentMessages);
|
|
862
|
-
const conversationText = serializeConversation(llmMessages);
|
|
905
|
+
const conversationText = boundConversationTextForSummary(serializeConversation(llmMessages), model, maxTokens);
|
|
863
906
|
|
|
864
907
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
865
908
|
if (historySummary) {
|
|
@@ -934,11 +977,35 @@ export interface CompactionPreparation {
|
|
|
934
977
|
fileOps: FileOperations;
|
|
935
978
|
/** Compaction settions from settings.jsonl */
|
|
936
979
|
settings: CompactionSettings;
|
|
980
|
+
/**
|
|
981
|
+
* Diagnostics for the keep-window token correction (Finding 7). `ratio` is the
|
|
982
|
+
* clamped heuristic→actual correction that was applied (1 when none supplied);
|
|
983
|
+
* `keepRecentTokensCorrected` is the heuristic budget findCutPoint actually used.
|
|
984
|
+
*/
|
|
985
|
+
tokenCorrection: { ratio: number; keepRecentTokensCorrected: number };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Bounds for the keep-window token correction (Finding 7): never trust a ratio
|
|
989
|
+
* beyond 2x in either direction so a bad estimate cannot balloon or collapse the
|
|
990
|
+
* kept window. */
|
|
991
|
+
export const TOKEN_CORRECTION_MIN_RATIO = 0.5;
|
|
992
|
+
export const TOKEN_CORRECTION_MAX_RATIO = 2;
|
|
993
|
+
|
|
994
|
+
export interface PrepareCompactionOptions {
|
|
995
|
+
/**
|
|
996
|
+
* Observed heuristic→actual token correction for the post-boundary keep window
|
|
997
|
+
* (actualTokens / chars-4-heuristicTokens), supplied by the caller from per-turn
|
|
998
|
+
* Usage deltas or a stable-prefix-subtracted comparison. Clamped to
|
|
999
|
+
* [0.5, 2] and applied bidirectionally. When omitted, no correction is applied
|
|
1000
|
+
* (the confounded raw promptTokens/estimatedTokens quotient is never used).
|
|
1001
|
+
*/
|
|
1002
|
+
tokenCorrectionRatio?: number;
|
|
937
1003
|
}
|
|
938
1004
|
|
|
939
1005
|
export function prepareCompaction(
|
|
940
1006
|
pathEntries: SessionEntry[],
|
|
941
1007
|
settings: CompactionSettings,
|
|
1008
|
+
options: PrepareCompactionOptions = {},
|
|
942
1009
|
): CompactionPreparation | undefined {
|
|
943
1010
|
if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
|
|
944
1011
|
return undefined;
|
|
@@ -956,17 +1023,22 @@ export function prepareCompaction(
|
|
|
956
1023
|
|
|
957
1024
|
const lastUsage = getLastAssistantUsage(pathEntries);
|
|
958
1025
|
const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
|
|
959
|
-
let keepRecentTokens = settings.keepRecentTokens;
|
|
960
|
-
if (lastUsage) {
|
|
961
|
-
const estimatedTokens = estimateEntriesTokens(pathEntries, boundaryStart, boundaryEnd);
|
|
962
|
-
const promptTokens = calculatePromptTokens(lastUsage);
|
|
963
|
-
const ratio = estimatedTokens > 0 ? promptTokens / estimatedTokens : 0;
|
|
964
|
-
if (Number.isFinite(ratio) && ratio > 1) {
|
|
965
|
-
keepRecentTokens = Math.max(1, Math.floor(keepRecentTokens / ratio));
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
1026
|
|
|
969
|
-
|
|
1027
|
+
// Correct the keep-window budget for the chars/4 heuristic error using the
|
|
1028
|
+
// caller-supplied observed ratio (actual/heuristic). The legacy raw
|
|
1029
|
+
// promptTokens/estimatedTokens quotient is intentionally NOT used: promptTokens
|
|
1030
|
+
// counts system+tools+full history while estimatedTokens counted only the
|
|
1031
|
+
// post-boundary slice, so it was confounded and only ever shrank the window.
|
|
1032
|
+
// Here the correction is bidirectional and clamped to [0.5, 2].
|
|
1033
|
+
const keepRecentTokens = settings.keepRecentTokens;
|
|
1034
|
+
const rawRatio = options.tokenCorrectionRatio;
|
|
1035
|
+
const appliedRatio =
|
|
1036
|
+
rawRatio !== undefined && Number.isFinite(rawRatio) && rawRatio > 0
|
|
1037
|
+
? Math.min(TOKEN_CORRECTION_MAX_RATIO, Math.max(TOKEN_CORRECTION_MIN_RATIO, rawRatio))
|
|
1038
|
+
: 1;
|
|
1039
|
+
const keepRecentTokensCorrected = Math.max(1, Math.round(keepRecentTokens / appliedRatio));
|
|
1040
|
+
|
|
1041
|
+
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, keepRecentTokensCorrected);
|
|
970
1042
|
|
|
971
1043
|
// Get ID of first kept entry
|
|
972
1044
|
const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
|
|
@@ -1034,6 +1106,7 @@ export function prepareCompaction(
|
|
|
1034
1106
|
previousPreserveData,
|
|
1035
1107
|
fileOps,
|
|
1036
1108
|
settings,
|
|
1109
|
+
tokenCorrection: { ratio: appliedRatio, keepRecentTokensCorrected },
|
|
1037
1110
|
};
|
|
1038
1111
|
}
|
|
1039
1112
|
|
|
@@ -1235,7 +1308,7 @@ async function generateTurnPrefixSummary(
|
|
|
1235
1308
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1236
1309
|
|
|
1237
1310
|
const llmMessages = (options?.convertToLlm ?? convertToLlm)(messages);
|
|
1238
|
-
const conversationText = serializeConversation(llmMessages);
|
|
1311
|
+
const conversationText = boundConversationTextForSummary(serializeConversation(llmMessages), model, maxTokens);
|
|
1239
1312
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
1240
1313
|
const summarizationMessages = [
|
|
1241
1314
|
{
|
|
@@ -527,14 +527,29 @@ export function pruneAssistantToolArguments(
|
|
|
527
527
|
return { argumentPrunedCount: candidates.length, argumentTokensSaved, prunedEntries };
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
|
|
530
|
+
interface ToolOutputPruneCandidate {
|
|
531
|
+
entry: SessionMessageEntry;
|
|
532
|
+
tokens: number;
|
|
533
|
+
notice: string;
|
|
534
|
+
savings: number;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Read-only pass that collects the tool-result entries that {@link pruneToolOutputs}
|
|
539
|
+
* would prune, plus the total estimated token savings. Shared by the mutating
|
|
540
|
+
* prune and the non-mutating {@link estimateToolOutputPruneSavings} so the
|
|
541
|
+
* maintenance gate (Finding 13) can decide whether pruning is worth a cache-epoch
|
|
542
|
+
* reset without rewriting history.
|
|
543
|
+
*/
|
|
544
|
+
function collectToolOutputPruneCandidates(
|
|
545
|
+
entries: SessionEntry[],
|
|
546
|
+
config: PruneConfig,
|
|
547
|
+
): { candidates: ToolOutputPruneCandidate[]; tokensSaved: number } {
|
|
531
548
|
let accumulatedTokens = 0;
|
|
532
|
-
let tokensSaved = 0;
|
|
533
|
-
let prunedCount = 0;
|
|
534
549
|
|
|
535
550
|
const { staleResultIndices } = buildStalenessIndex(entries);
|
|
536
551
|
const staleOverridable = new Set(config.staleOverridableTools ?? []);
|
|
537
|
-
const candidates:
|
|
552
|
+
const candidates: ToolOutputPruneCandidate[] = [];
|
|
538
553
|
|
|
539
554
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
540
555
|
const entry = entries[i];
|
|
@@ -574,14 +589,56 @@ export function pruneToolOutputs(entries: SessionEntry[], config: PruneConfig =
|
|
|
574
589
|
accumulatedTokens += tokens;
|
|
575
590
|
}
|
|
576
591
|
|
|
592
|
+
let tokensSaved = 0;
|
|
577
593
|
for (const candidate of candidates) {
|
|
578
594
|
tokensSaved += candidate.savings;
|
|
579
595
|
}
|
|
596
|
+
return { candidates, tokensSaved };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Estimate the token savings {@link pruneToolOutputs} would achieve, without
|
|
601
|
+
* mutating any entry. Returns 0 savings when below the configured minimum so the
|
|
602
|
+
* caller sees the same gate the real prune enforces.
|
|
603
|
+
*/
|
|
604
|
+
export function estimateToolOutputPruneSavings(
|
|
605
|
+
entries: SessionEntry[],
|
|
606
|
+
config: PruneConfig = DEFAULT_PRUNE_CONFIG,
|
|
607
|
+
): { prunableCount: number; tokensSaved: number } {
|
|
608
|
+
const { candidates, tokensSaved } = collectToolOutputPruneCandidates(entries, config);
|
|
609
|
+
if (tokensSaved < config.minimumSavings || candidates.length === 0) {
|
|
610
|
+
return { prunableCount: 0, tokensSaved: 0 };
|
|
611
|
+
}
|
|
612
|
+
return { prunableCount: candidates.length, tokensSaved };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Evidence gate for below-threshold maintenance pruning (Finding 13). Pruning
|
|
617
|
+
* forces a prompt-cache-epoch reset, so it only runs when opted in AND the
|
|
618
|
+
* estimated stale savings clear a high minimum AND exceed the one-time reset
|
|
619
|
+
* cost (so the reclaim pays the reset back). Default-off/blocked until live
|
|
620
|
+
* evidence justifies enabling.
|
|
621
|
+
*/
|
|
622
|
+
export function shouldRunMaintenancePrune(args: {
|
|
623
|
+
enabled: boolean;
|
|
624
|
+
estimatedSavings: number;
|
|
625
|
+
minSavings: number;
|
|
626
|
+
cacheEpochResetCost: number;
|
|
627
|
+
}): boolean {
|
|
628
|
+
if (!args.enabled) return false;
|
|
629
|
+
if (args.estimatedSavings < args.minSavings) return false;
|
|
630
|
+
return args.estimatedSavings > args.cacheEpochResetCost;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function pruneToolOutputs(entries: SessionEntry[], config: PruneConfig = DEFAULT_PRUNE_CONFIG): PruneResult {
|
|
634
|
+
const { candidates, tokensSaved } = collectToolOutputPruneCandidates(entries, config);
|
|
580
635
|
|
|
581
636
|
if (tokensSaved < config.minimumSavings || candidates.length === 0) {
|
|
582
637
|
return { prunedCount: 0, tokensSaved: 0, prunedEntries: [] };
|
|
583
638
|
}
|
|
584
639
|
|
|
640
|
+
let prunedCount = 0;
|
|
641
|
+
|
|
585
642
|
const prunedAt = Date.now();
|
|
586
643
|
const prunedEntries: SessionMessageEntry[] = [];
|
|
587
644
|
for (const candidate of candidates) {
|