@animalabs/context-manager 0.5.3 → 0.5.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/dist/src/adaptive/index.d.ts +6 -0
- package/dist/src/adaptive/index.d.ts.map +1 -1
- package/dist/src/adaptive/index.js +8 -0
- package/dist/src/adaptive/index.js.map +1 -1
- package/dist/src/adaptive/kv-cache-sim.d.ts +128 -0
- package/dist/src/adaptive/kv-cache-sim.d.ts.map +1 -0
- package/dist/src/adaptive/kv-cache-sim.js +0 -0
- package/dist/src/adaptive/kv-cache-sim.js.map +1 -0
- package/dist/src/adaptive/kv-control.d.ts +152 -0
- package/dist/src/adaptive/kv-control.d.ts.map +1 -0
- package/dist/src/adaptive/kv-control.js +368 -0
- package/dist/src/adaptive/kv-control.js.map +1 -0
- package/dist/src/adaptive/kv-replay.d.ts +76 -0
- package/dist/src/adaptive/kv-replay.d.ts.map +1 -0
- package/dist/src/adaptive/kv-replay.js +175 -0
- package/dist/src/adaptive/kv-replay.js.map +1 -0
- package/dist/src/adaptive/render-offsets.d.ts +61 -0
- package/dist/src/adaptive/render-offsets.d.ts.map +1 -0
- package/dist/src/adaptive/render-offsets.js +88 -0
- package/dist/src/adaptive/render-offsets.js.map +1 -0
- package/dist/src/adaptive/strategies/kv-stable.d.ts +48 -0
- package/dist/src/adaptive/strategies/kv-stable.d.ts.map +1 -0
- package/dist/src/adaptive/strategies/kv-stable.js +102 -0
- package/dist/src/adaptive/strategies/kv-stable.js.map +1 -0
- package/dist/src/adaptive/summary-tree.d.ts +98 -0
- package/dist/src/adaptive/summary-tree.d.ts.map +1 -0
- package/dist/src/adaptive/summary-tree.js +214 -0
- package/dist/src/adaptive/summary-tree.js.map +1 -0
- package/dist/src/context-log.d.ts.map +1 -1
- package/dist/src/context-log.js +7 -2
- package/dist/src/context-log.js.map +1 -1
- package/dist/src/message-store.js +1 -1
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/strategies/autobiographical.d.ts +68 -39
- package/dist/src/strategies/autobiographical.d.ts.map +1 -1
- package/dist/src/strategies/autobiographical.js +301 -213
- package/dist/src/strategies/autobiographical.js.map +1 -1
- package/dist/src/types/strategy.d.ts +40 -1
- package/dist/src/types/strategy.d.ts.map +1 -1
- package/dist/src/types/strategy.js +2 -0
- package/dist/src/types/strategy.js.map +1 -1
- package/dist/test/adaptive/kv-cache-sim.test.d.ts +16 -0
- package/dist/test/adaptive/kv-cache-sim.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-cache-sim.test.js +173 -0
- package/dist/test/adaptive/kv-cache-sim.test.js.map +1 -0
- package/dist/test/adaptive/kv-control.test.d.ts +14 -0
- package/dist/test/adaptive/kv-control.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-control.test.js +130 -0
- package/dist/test/adaptive/kv-control.test.js.map +1 -0
- package/dist/test/adaptive/kv-replay.test.d.ts +15 -0
- package/dist/test/adaptive/kv-replay.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-replay.test.js +112 -0
- package/dist/test/adaptive/kv-replay.test.js.map +1 -0
- package/dist/test/adaptive/kv-stable-strategy.test.d.ts +10 -0
- package/dist/test/adaptive/kv-stable-strategy.test.d.ts.map +1 -0
- package/dist/test/adaptive/kv-stable-strategy.test.js +89 -0
- package/dist/test/adaptive/kv-stable-strategy.test.js.map +1 -0
- package/dist/test/adaptive/render-offsets.test.d.ts +11 -0
- package/dist/test/adaptive/render-offsets.test.d.ts.map +1 -0
- package/dist/test/adaptive/render-offsets.test.js +85 -0
- package/dist/test/adaptive/render-offsets.test.js.map +1 -0
- package/dist/test/adaptive/summary-tree.test.d.ts +5 -0
- package/dist/test/adaptive/summary-tree.test.d.ts.map +1 -0
- package/dist/test/adaptive/summary-tree.test.js +72 -0
- package/dist/test/adaptive/summary-tree.test.js.map +1 -0
- package/dist/test/image-strip-render-stats.test.d.ts +2 -0
- package/dist/test/image-strip-render-stats.test.d.ts.map +1 -0
- package/dist/test/image-strip-render-stats.test.js +130 -0
- package/dist/test/image-strip-render-stats.test.js.map +1 -0
- package/dist/test/image-stripping.test.d.ts +13 -0
- package/dist/test/image-stripping.test.d.ts.map +1 -0
- package/dist/test/image-stripping.test.js +104 -0
- package/dist/test/image-stripping.test.js.map +1 -0
- package/dist/test/render-stats.test.d.ts +2 -0
- package/dist/test/render-stats.test.d.ts.map +1 -0
- package/dist/test/render-stats.test.js +91 -0
- package/dist/test/render-stats.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/adaptive/index.ts +48 -0
- package/src/adaptive/kv-cache-sim.ts +0 -0
- package/src/adaptive/kv-control.ts +497 -0
- package/src/adaptive/kv-replay.ts +238 -0
- package/src/adaptive/render-offsets.ts +116 -0
- package/src/adaptive/strategies/kv-stable.ts +123 -0
- package/src/adaptive/summary-tree.ts +252 -0
- package/src/context-log.ts +7 -2
- package/src/message-store.ts +1 -1
- package/src/strategies/autobiographical.ts +314 -263
- package/src/types/strategy.ts +40 -1
|
@@ -6,6 +6,7 @@ import { appendFileSync, mkdirSync } from 'node:fs';
|
|
|
6
6
|
import { dirname } from 'node:path';
|
|
7
7
|
import { Picker, OverBudgetError } from '../adaptive/picker.js';
|
|
8
8
|
import { FlatProfileStrategy } from '../adaptive/strategies/flat-profile.js';
|
|
9
|
+
import { KvStableStrategy } from '../adaptive/strategies/kv-stable.js';
|
|
9
10
|
import { OldestFirstStrategy } from '../adaptive/strategies/oldest-first.js';
|
|
10
11
|
import { chunkMessage, DEFAULT_CHUNKER_OPTIONS } from '../adaptive/chunker.js';
|
|
11
12
|
/**
|
|
@@ -923,9 +924,7 @@ export class AutobiographicalStrategy {
|
|
|
923
924
|
const chunk = this.chunks[chunkIndex];
|
|
924
925
|
if (!chunk || chunk.compressed)
|
|
925
926
|
return;
|
|
926
|
-
this.pendingCompression = this.
|
|
927
|
-
? this.compressChunkHierarchical(chunk, ctx)
|
|
928
|
-
: this.compressChunkLegacy(chunk, ctx);
|
|
927
|
+
this.pendingCompression = this.compressChunkHierarchical(chunk, ctx);
|
|
929
928
|
try {
|
|
930
929
|
await this.pendingCompression;
|
|
931
930
|
}
|
|
@@ -992,12 +991,13 @@ export class AutobiographicalStrategy {
|
|
|
992
991
|
}
|
|
993
992
|
select(store, log, budget) {
|
|
994
993
|
this.rebuildChunks(store);
|
|
994
|
+
// Image stripping runs inside each select path (before stats commit / cache
|
|
995
|
+
// markers), so the returned entries are already bounded — see
|
|
996
|
+
// applyImageStripping.
|
|
995
997
|
if (this.config.adaptiveResolution) {
|
|
996
998
|
return this.selectAdaptive(store, budget);
|
|
997
999
|
}
|
|
998
|
-
return this.
|
|
999
|
-
? this.selectHierarchical(store, budget)
|
|
1000
|
-
: this.selectLegacy(store, log, budget);
|
|
1000
|
+
return this.selectHierarchical(store, budget);
|
|
1001
1001
|
}
|
|
1002
1002
|
/**
|
|
1003
1003
|
* Get summary statistics for observability.
|
|
@@ -1022,7 +1022,85 @@ export class AutobiographicalStrategy {
|
|
|
1022
1022
|
* Useful for TUI / dashboards. The token sums use the strategy's own
|
|
1023
1023
|
* token estimates (which match what `select()` uses for budget math).
|
|
1024
1024
|
*/
|
|
1025
|
+
// ===========================================================================
|
|
1026
|
+
// Render-stats instrumentation — inspect, don't reconstruct.
|
|
1027
|
+
//
|
|
1028
|
+
// selectAdaptive/selectHierarchical tally each entry into `_rs` AS THEY EMIT
|
|
1029
|
+
// it (raw head/tail, raw middle the picker kept verbatim, and recall pairs
|
|
1030
|
+
// bucketed by the ancestor summary's level), using the same token numbers the
|
|
1031
|
+
// renderer uses for budget math. `getRenderStats()` returns that committed
|
|
1032
|
+
// snapshot, so it reflects what the last compile actually rendered rather than
|
|
1033
|
+
// re-deriving the full pyramid (which, under adaptive resolution, bears little
|
|
1034
|
+
// resemblance to the folded output).
|
|
1035
|
+
// ===========================================================================
|
|
1036
|
+
_rs = null;
|
|
1037
|
+
_lastRenderStats = null;
|
|
1038
|
+
/** Begin a render-stats accumulation for one select() pass. */
|
|
1039
|
+
rsBegin() {
|
|
1040
|
+
this._rs = {
|
|
1041
|
+
head: { messages: 0, tokens: 0 },
|
|
1042
|
+
tail: { messages: 0, tokens: 0 },
|
|
1043
|
+
middleRaw: { messages: 0, tokens: 0 },
|
|
1044
|
+
summaries: {
|
|
1045
|
+
l1: { count: 0, tokens: 0 },
|
|
1046
|
+
l2: { count: 0, tokens: 0 },
|
|
1047
|
+
l3: { count: 0, tokens: 0 },
|
|
1048
|
+
},
|
|
1049
|
+
pending: { chunks: 0, merges: 0 },
|
|
1050
|
+
total: { messages: 0, tokens: 0 },
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/** Tally one (or `count`) raw message(s) into a raw bucket. */
|
|
1054
|
+
rsRaw(bucket, tokens, count = 1) {
|
|
1055
|
+
const r = this._rs;
|
|
1056
|
+
if (!r)
|
|
1057
|
+
return;
|
|
1058
|
+
r[bucket].messages += count;
|
|
1059
|
+
r[bucket].tokens += tokens;
|
|
1060
|
+
}
|
|
1061
|
+
/** Tally one emitted recall pair under its ancestor's level (>=3 folds into l3). */
|
|
1062
|
+
rsSummary(level, tokens) {
|
|
1063
|
+
const r = this._rs;
|
|
1064
|
+
if (!r)
|
|
1065
|
+
return;
|
|
1066
|
+
const k = level <= 1 ? 'l1' : level === 2 ? 'l2' : 'l3';
|
|
1067
|
+
r.summaries[k].count += 1;
|
|
1068
|
+
r.summaries[k].tokens += tokens;
|
|
1069
|
+
}
|
|
1070
|
+
/** Commit the accumulated stats as the last-render snapshot. */
|
|
1071
|
+
rsEnd() {
|
|
1072
|
+
const r = this._rs;
|
|
1073
|
+
if (!r)
|
|
1074
|
+
return;
|
|
1075
|
+
r.pending = {
|
|
1076
|
+
chunks: this.chunks.filter(c => !c.compressed).length,
|
|
1077
|
+
merges: this.mergeQueue.length,
|
|
1078
|
+
};
|
|
1079
|
+
const s = r.summaries;
|
|
1080
|
+
const summaryMsgs = (s.l1.count + s.l2.count + s.l3.count) * 2; // Q/A pair each
|
|
1081
|
+
r.total = {
|
|
1082
|
+
messages: r.head.messages + r.tail.messages + r.middleRaw.messages + summaryMsgs,
|
|
1083
|
+
tokens: r.head.tokens + r.tail.tokens + r.middleRaw.tokens +
|
|
1084
|
+
s.l1.tokens + s.l2.tokens + s.l3.tokens,
|
|
1085
|
+
};
|
|
1086
|
+
this._lastRenderStats = r;
|
|
1087
|
+
this._rs = null;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Stats describing the LAST rendered context. Returns the inspected snapshot
|
|
1091
|
+
* captured during the most recent `select()`. Before any compile has run (no
|
|
1092
|
+
* snapshot yet), falls back to a reconstructed pyramid view so callers still
|
|
1093
|
+
* get a non-null shape.
|
|
1094
|
+
*/
|
|
1025
1095
|
getRenderStats(store) {
|
|
1096
|
+
return this._lastRenderStats ?? this.reconstructRenderStats(store);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Pre-render fallback: re-derive head/tail windows + the full live pyramid.
|
|
1100
|
+
* NOTE: this is the old "reconstruct" behavior and does NOT reflect adaptive
|
|
1101
|
+
* folding — used only until the first compile populates the inspected stats.
|
|
1102
|
+
*/
|
|
1103
|
+
reconstructRenderStats(store) {
|
|
1026
1104
|
const messages = store.getAll();
|
|
1027
1105
|
const headStart = this.getHeadWindowStartIndex(store);
|
|
1028
1106
|
const headEnd = this.getHeadWindowEnd(store);
|
|
@@ -1032,131 +1110,30 @@ export class AutobiographicalStrategy {
|
|
|
1032
1110
|
const tailMsgs = messages.slice(recentStart);
|
|
1033
1111
|
const live = (level) => this.summaries.filter(s => s.level === level && !s.mergedInto);
|
|
1034
1112
|
const sumLevelTokens = (level) => live(level).reduce((acc, s) => acc + s.tokens, 0);
|
|
1113
|
+
const head = { messages: headMsgs.length, tokens: sumTokens(headMsgs) };
|
|
1114
|
+
const tail = { messages: tailMsgs.length, tokens: sumTokens(tailMsgs) };
|
|
1115
|
+
const summaries = {
|
|
1116
|
+
l1: { count: live(1).length, tokens: sumLevelTokens(1) },
|
|
1117
|
+
l2: { count: live(2).length, tokens: sumLevelTokens(2) },
|
|
1118
|
+
l3: { count: live(3).length, tokens: sumLevelTokens(3) },
|
|
1119
|
+
};
|
|
1035
1120
|
return {
|
|
1036
|
-
head
|
|
1037
|
-
tail
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
l2: { count: live(2).length, tokens: sumLevelTokens(2) },
|
|
1041
|
-
l3: { count: live(3).length, tokens: sumLevelTokens(3) },
|
|
1042
|
-
},
|
|
1121
|
+
head,
|
|
1122
|
+
tail,
|
|
1123
|
+
middleRaw: { messages: 0, tokens: 0 },
|
|
1124
|
+
summaries,
|
|
1043
1125
|
pending: {
|
|
1044
1126
|
chunks: this.chunks.filter(c => !c.compressed).length,
|
|
1045
1127
|
merges: this.mergeQueue.length,
|
|
1046
1128
|
},
|
|
1129
|
+
total: {
|
|
1130
|
+
messages: head.messages + tail.messages
|
|
1131
|
+
+ (summaries.l1.count + summaries.l2.count + summaries.l3.count) * 2,
|
|
1132
|
+
tokens: head.tokens + tail.tokens
|
|
1133
|
+
+ summaries.l1.tokens + summaries.l2.tokens + summaries.l3.tokens,
|
|
1134
|
+
},
|
|
1047
1135
|
};
|
|
1048
1136
|
}
|
|
1049
|
-
// ============================================================================
|
|
1050
|
-
// Legacy (single-level) path
|
|
1051
|
-
// ============================================================================
|
|
1052
|
-
selectLegacy(store, _log, budget) {
|
|
1053
|
-
const entries = [];
|
|
1054
|
-
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
1055
|
-
let totalTokens = 0;
|
|
1056
|
-
const messages = store.getAll();
|
|
1057
|
-
const msgCap = this.config.maxMessageTokens;
|
|
1058
|
-
// 1. Head window: preserved verbatim as raw copies
|
|
1059
|
-
const headStart = this.getHeadWindowStartIndex(store);
|
|
1060
|
-
const headEnd = this.getHeadWindowEnd(store);
|
|
1061
|
-
for (let i = headStart; i < headEnd && i < messages.length; i++) {
|
|
1062
|
-
const msg = messages[i];
|
|
1063
|
-
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1064
|
-
const tokens = msgCap > 0 ? Math.min(store.estimateTokens(msg), msgCap + 50) : store.estimateTokens(msg);
|
|
1065
|
-
if (this.isOverBudget(totalTokens + tokens, maxTokens))
|
|
1066
|
-
break;
|
|
1067
|
-
entries.push({
|
|
1068
|
-
index: entries.length,
|
|
1069
|
-
sourceMessageId: msg.id,
|
|
1070
|
-
sourceRelation: 'copy',
|
|
1071
|
-
participant: msg.participant,
|
|
1072
|
-
content,
|
|
1073
|
-
});
|
|
1074
|
-
totalTokens += tokens;
|
|
1075
|
-
}
|
|
1076
|
-
// Mark the last head entry as a cache boundary (even if budget truncated the window)
|
|
1077
|
-
if (entries.length > 0) {
|
|
1078
|
-
entries[entries.length - 1].cacheMarker = true;
|
|
1079
|
-
}
|
|
1080
|
-
// 2. Middle zone: compressed chunks as diary pairs, uncompressed as raw messages.
|
|
1081
|
-
const rawRecentStart = this.getRecentWindowStart(store);
|
|
1082
|
-
// Track which message IDs are covered by chunks
|
|
1083
|
-
const coveredByChunks = new Set();
|
|
1084
|
-
for (const chunk of this.chunks) {
|
|
1085
|
-
for (const m of chunk.messages)
|
|
1086
|
-
coveredByChunks.add(m.id);
|
|
1087
|
-
if (chunk.compressed && chunk.diary) {
|
|
1088
|
-
const contextLabel = this.config.summaryContextLabel ?? 'Here is a summary of earlier conversation context:';
|
|
1089
|
-
const summaryParticipant = this.config.summaryParticipant ?? 'Summary';
|
|
1090
|
-
const questionEntry = {
|
|
1091
|
-
index: entries.length,
|
|
1092
|
-
participant: 'Context Manager',
|
|
1093
|
-
content: [{ type: 'text', text: contextLabel }],
|
|
1094
|
-
sourceRelation: 'derived',
|
|
1095
|
-
};
|
|
1096
|
-
// Synthesised summary turns must respect maxMessageTokens just like raw
|
|
1097
|
-
// copies do — otherwise a runaway diary can starve recent messages.
|
|
1098
|
-
const answerContent = [{ type: 'text', text: chunk.diary }];
|
|
1099
|
-
const answerEntry = {
|
|
1100
|
-
index: entries.length + 1,
|
|
1101
|
-
participant: summaryParticipant,
|
|
1102
|
-
content: msgCap > 0 ? this.truncateContent(answerContent, msgCap) : answerContent,
|
|
1103
|
-
sourceRelation: 'derived',
|
|
1104
|
-
};
|
|
1105
|
-
const pairTokens = this.estimateTokens(questionEntry.content) +
|
|
1106
|
-
this.estimateTokens(answerEntry.content);
|
|
1107
|
-
if (this.isOverBudget(totalTokens + pairTokens, maxTokens))
|
|
1108
|
-
break;
|
|
1109
|
-
entries.push(questionEntry);
|
|
1110
|
-
entries.push(answerEntry);
|
|
1111
|
-
totalTokens += pairTokens;
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
// Uncompressed: emit raw messages so they aren't lost
|
|
1115
|
-
for (const msg of chunk.messages) {
|
|
1116
|
-
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1117
|
-
const tokens = msgCap > 0 ? Math.min(store.estimateTokens(msg), msgCap + 50) : store.estimateTokens(msg);
|
|
1118
|
-
if (this.isOverBudget(totalTokens + tokens, maxTokens))
|
|
1119
|
-
break;
|
|
1120
|
-
entries.push({
|
|
1121
|
-
index: entries.length,
|
|
1122
|
-
sourceMessageId: msg.id,
|
|
1123
|
-
sourceRelation: 'copy',
|
|
1124
|
-
participant: msg.participant,
|
|
1125
|
-
content,
|
|
1126
|
-
});
|
|
1127
|
-
totalTokens += tokens;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
// Emit gap messages in the compressible zone not covered by any chunk.
|
|
1132
|
-
// Compressible zone: [0, headStart) ∪ [headEnd, rawRecentStart)
|
|
1133
|
-
for (let i = 0; i < rawRecentStart && i < messages.length; i++) {
|
|
1134
|
-
// Skip head window messages (already emitted verbatim above)
|
|
1135
|
-
if (i >= headStart && i < headEnd)
|
|
1136
|
-
continue;
|
|
1137
|
-
if (coveredByChunks.has(messages[i].id))
|
|
1138
|
-
continue;
|
|
1139
|
-
const msg = messages[i];
|
|
1140
|
-
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1141
|
-
const tokens = msgCap > 0 ? Math.min(store.estimateTokens(msg), msgCap + 50) : store.estimateTokens(msg);
|
|
1142
|
-
if (this.isOverBudget(totalTokens + tokens, maxTokens))
|
|
1143
|
-
break;
|
|
1144
|
-
entries.push({
|
|
1145
|
-
index: entries.length,
|
|
1146
|
-
sourceMessageId: msg.id,
|
|
1147
|
-
sourceRelation: 'copy',
|
|
1148
|
-
participant: msg.participant,
|
|
1149
|
-
content,
|
|
1150
|
-
});
|
|
1151
|
-
totalTokens += tokens;
|
|
1152
|
-
}
|
|
1153
|
-
// 3. Recent uncompressed messages (skip those already in head window)
|
|
1154
|
-
const recentStart = Math.max(this.getRecentWindowStart(store), headEnd);
|
|
1155
|
-
this.emitRecentNewestFirst(entries, store, messages, recentStart, msgCap, maxTokens, totalTokens);
|
|
1156
|
-
this.trimOrphanedToolUse(entries);
|
|
1157
|
-
this.pruneToolEntries(entries);
|
|
1158
|
-
return entries;
|
|
1159
|
-
}
|
|
1160
1137
|
/**
|
|
1161
1138
|
* Emit recent-window messages, evicting OLDEST-first when the budget is tight.
|
|
1162
1139
|
*
|
|
@@ -1170,7 +1147,7 @@ export class AutobiographicalStrategy {
|
|
|
1170
1147
|
*/
|
|
1171
1148
|
emitRecentNewestFirst(entries, store, messages, recentStart, msgCap, maxTokens, totalTokensBefore) {
|
|
1172
1149
|
if (recentStart >= messages.length)
|
|
1173
|
-
return;
|
|
1150
|
+
return { messages: 0, tokens: 0 };
|
|
1174
1151
|
const accepted = [];
|
|
1175
1152
|
let acceptedTokens = 0;
|
|
1176
1153
|
for (let i = messages.length - 1; i >= recentStart; i--) {
|
|
@@ -1190,9 +1167,13 @@ export class AutobiographicalStrategy {
|
|
|
1190
1167
|
!this.hasToolUse(messages[accepted[0]])) {
|
|
1191
1168
|
accepted.shift();
|
|
1192
1169
|
}
|
|
1170
|
+
let emittedTokens = 0;
|
|
1193
1171
|
for (const i of accepted) {
|
|
1194
1172
|
const msg = messages[i];
|
|
1195
1173
|
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1174
|
+
const tokens = msgCap > 0
|
|
1175
|
+
? Math.min(store.estimateTokens(msg), msgCap + 50)
|
|
1176
|
+
: store.estimateTokens(msg);
|
|
1196
1177
|
entries.push({
|
|
1197
1178
|
index: entries.length,
|
|
1198
1179
|
sourceMessageId: msg.id,
|
|
@@ -1200,86 +1181,9 @@ export class AutobiographicalStrategy {
|
|
|
1200
1181
|
participant: msg.participant,
|
|
1201
1182
|
content,
|
|
1202
1183
|
});
|
|
1184
|
+
emittedTokens += tokens;
|
|
1203
1185
|
}
|
|
1204
|
-
|
|
1205
|
-
async compressChunkLegacy(chunk, ctx) {
|
|
1206
|
-
if (!ctx.membrane) {
|
|
1207
|
-
throw new Error('No membrane instance for compression');
|
|
1208
|
-
}
|
|
1209
|
-
const priorContext = this.buildPriorContextLegacy(chunk, ctx);
|
|
1210
|
-
const chunkContent = this.formatChunkForCompression(chunk);
|
|
1211
|
-
const prompt = this.config.diaryUserPrompt ?? this.config.summaryUserPrompt;
|
|
1212
|
-
const systemPrompt = this.config.diarySystemPrompt ?? this.config.summarySystemPrompt;
|
|
1213
|
-
const messages = [
|
|
1214
|
-
...priorContext,
|
|
1215
|
-
{
|
|
1216
|
-
participant: 'Context Manager',
|
|
1217
|
-
content: [{ type: 'text', text: prompt.replace('{content}', chunkContent) }],
|
|
1218
|
-
},
|
|
1219
|
-
];
|
|
1220
|
-
const request = {
|
|
1221
|
-
messages: messages.map((m) => ({
|
|
1222
|
-
participant: m.participant,
|
|
1223
|
-
content: m.content,
|
|
1224
|
-
})),
|
|
1225
|
-
system: systemPrompt,
|
|
1226
|
-
config: {
|
|
1227
|
-
model: this.config.compressionModel ?? 'claude-sonnet-4-20250514',
|
|
1228
|
-
maxTokens: 2000,
|
|
1229
|
-
},
|
|
1230
|
-
};
|
|
1231
|
-
try {
|
|
1232
|
-
const response = await ctx.membrane.complete(request);
|
|
1233
|
-
const diaryText = response.content
|
|
1234
|
-
.filter((b) => b.type === 'text')
|
|
1235
|
-
.map((b) => b.text)
|
|
1236
|
-
.join('\n');
|
|
1237
|
-
chunk.compressed = true;
|
|
1238
|
-
chunk.diary = diaryText;
|
|
1239
|
-
this._compressionCount++;
|
|
1240
|
-
}
|
|
1241
|
-
catch (error) {
|
|
1242
|
-
console.error('Failed to compress chunk:', error);
|
|
1243
|
-
throw error;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
buildPriorContextLegacy(chunk, ctx) {
|
|
1247
|
-
const context = [];
|
|
1248
|
-
for (const prevChunk of this.chunks) {
|
|
1249
|
-
if (prevChunk.index >= chunk.index)
|
|
1250
|
-
break;
|
|
1251
|
-
if (!prevChunk.compressed || !prevChunk.diary)
|
|
1252
|
-
continue;
|
|
1253
|
-
context.push({
|
|
1254
|
-
participant: 'Context Manager',
|
|
1255
|
-
content: [{ type: 'text', text: this.config.summaryContextLabel ?? 'Summary of earlier context:' }],
|
|
1256
|
-
});
|
|
1257
|
-
context.push({
|
|
1258
|
-
participant: this.config.summaryParticipant ?? 'Summary',
|
|
1259
|
-
content: [{ type: 'text', text: prevChunk.diary }],
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
// Find the actual position of this chunk's first message in the full array
|
|
1263
|
-
const messages = ctx.messageStore.getAll();
|
|
1264
|
-
const firstMsgId = chunk.messages[0]?.id;
|
|
1265
|
-
const chunkAbsStart = firstMsgId
|
|
1266
|
-
? messages.findIndex(m => m.id === firstMsgId)
|
|
1267
|
-
: -1;
|
|
1268
|
-
if (chunkAbsStart > 0) {
|
|
1269
|
-
const precedingStart = Math.max(0, chunkAbsStart - 50);
|
|
1270
|
-
let tokens = 0;
|
|
1271
|
-
for (let i = chunkAbsStart - 1; i >= precedingStart && tokens < 15000; i--) {
|
|
1272
|
-
const msg = messages[i];
|
|
1273
|
-
if (!msg)
|
|
1274
|
-
break;
|
|
1275
|
-
tokens += ctx.messageStore.estimateTokens(msg);
|
|
1276
|
-
context.unshift({
|
|
1277
|
-
participant: msg.participant,
|
|
1278
|
-
content: msg.content,
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
return context;
|
|
1186
|
+
return { messages: accepted.length, tokens: emittedTokens };
|
|
1283
1187
|
}
|
|
1284
1188
|
// ============================================================================
|
|
1285
1189
|
// Hierarchical (L1/L2/L3) path
|
|
@@ -1493,6 +1397,10 @@ export class AutobiographicalStrategy {
|
|
|
1493
1397
|
let logSummaryId;
|
|
1494
1398
|
try {
|
|
1495
1399
|
const response = await ctx.membrane.complete(request, { formatter: this.nativeFormatter });
|
|
1400
|
+
// Text-only on purpose: this is the SUMMARIZER's one-shot response —
|
|
1401
|
+
// its thinking/redacted_thinking blocks are scratch work, not part of
|
|
1402
|
+
// the agent's history, and signed thinking is never valid inside a
|
|
1403
|
+
// rewritten summary anyway.
|
|
1496
1404
|
const summaryText = response.content
|
|
1497
1405
|
.filter((b) => b.type === 'text')
|
|
1498
1406
|
.map(b => b.text)
|
|
@@ -1913,6 +1821,7 @@ export class AutobiographicalStrategy {
|
|
|
1913
1821
|
let logNewSummaryId;
|
|
1914
1822
|
try {
|
|
1915
1823
|
const response = await ctx.membrane.complete(request, { formatter: this.nativeFormatter });
|
|
1824
|
+
// Text-only on purpose: summarizer scratch thinking is not agent history
|
|
1916
1825
|
const mergedText = response.content
|
|
1917
1826
|
.filter((b) => b.type === 'text')
|
|
1918
1827
|
.map(b => b.text)
|
|
@@ -1992,6 +1901,7 @@ export class AutobiographicalStrategy {
|
|
|
1992
1901
|
* See `docs/adaptive-resolution-design.md` §3, §5.
|
|
1993
1902
|
*/
|
|
1994
1903
|
selectAdaptive(store, budget) {
|
|
1904
|
+
this.rsBegin();
|
|
1995
1905
|
const entries = [];
|
|
1996
1906
|
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
1997
1907
|
const messages = store.getAll();
|
|
@@ -2022,10 +1932,11 @@ export class AutobiographicalStrategy {
|
|
|
2022
1932
|
totalTokens += tokens;
|
|
2023
1933
|
headMessageIds.add(msg.id);
|
|
2024
1934
|
headTokens += tokens;
|
|
1935
|
+
this.rsRaw('head', tokens);
|
|
2025
1936
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
1937
|
+
// (Cache breakpoints are placed in one pass over the FINAL ordered entries
|
|
1938
|
+
// below — see placeCacheMarkers — capturing the stable folded prefix, not
|
|
1939
|
+
// just the head boundary.)
|
|
2029
1940
|
// Compute tail message IDs (will be emitted at end)
|
|
2030
1941
|
const effectiveRecentStart = Math.max(recentStart, headEnd);
|
|
2031
1942
|
for (let i = effectiveRecentStart; i < messages.length; i++) {
|
|
@@ -2120,8 +2031,7 @@ export class AutobiographicalStrategy {
|
|
|
2120
2031
|
};
|
|
2121
2032
|
const headSetForPicker = new Set(headMessageIds);
|
|
2122
2033
|
const tailSetForPicker = new Set(tailMessageIds);
|
|
2123
|
-
const
|
|
2124
|
-
const result = picker.run({
|
|
2034
|
+
const pickerInputs = {
|
|
2125
2035
|
chunks: pickerChunks,
|
|
2126
2036
|
summaries: summariesMap,
|
|
2127
2037
|
recallPairTokens,
|
|
@@ -2129,7 +2039,9 @@ export class AutobiographicalStrategy {
|
|
|
2129
2039
|
tailChunkIds: tailSetForPicker,
|
|
2130
2040
|
headTokens,
|
|
2131
2041
|
tailTokens,
|
|
2132
|
-
}
|
|
2042
|
+
};
|
|
2043
|
+
const picker = this.buildPicker(pickerInputs);
|
|
2044
|
+
const result = picker.run(pickerInputs, foldingBudget);
|
|
2133
2045
|
// Commit the new resolutions back to strategy state for next compile.
|
|
2134
2046
|
// Persist to chronicle only if anything actually changed — avoids
|
|
2135
2047
|
// unnecessary state-slot writes on no-op compiles (which is the common
|
|
@@ -2236,6 +2148,7 @@ export class AutobiographicalStrategy {
|
|
|
2236
2148
|
content,
|
|
2237
2149
|
});
|
|
2238
2150
|
totalTokens += tokens;
|
|
2151
|
+
this.rsRaw('middleRaw', tokens);
|
|
2239
2152
|
}
|
|
2240
2153
|
else {
|
|
2241
2154
|
// summary run — emit Q+A pair, dedup at the strategy level
|
|
@@ -2263,6 +2176,7 @@ export class AutobiographicalStrategy {
|
|
|
2263
2176
|
entries.push(questionEntry);
|
|
2264
2177
|
entries.push(answerEntry);
|
|
2265
2178
|
totalTokens += pairTokens;
|
|
2179
|
+
this.rsSummary(ancestor.level, pairTokens);
|
|
2266
2180
|
}
|
|
2267
2181
|
}
|
|
2268
2182
|
currentRun = null;
|
|
@@ -2336,6 +2250,7 @@ export class AutobiographicalStrategy {
|
|
|
2336
2250
|
content,
|
|
2337
2251
|
});
|
|
2338
2252
|
totalTokens += tokens;
|
|
2253
|
+
this.rsRaw('middleRaw', tokens);
|
|
2339
2254
|
i++;
|
|
2340
2255
|
}
|
|
2341
2256
|
else {
|
|
@@ -2353,6 +2268,7 @@ export class AutobiographicalStrategy {
|
|
|
2353
2268
|
content,
|
|
2354
2269
|
});
|
|
2355
2270
|
totalTokens += tokens;
|
|
2271
|
+
this.rsRaw('middleRaw', tokens);
|
|
2356
2272
|
i++;
|
|
2357
2273
|
continue;
|
|
2358
2274
|
}
|
|
@@ -2380,11 +2296,13 @@ export class AutobiographicalStrategy {
|
|
|
2380
2296
|
entries.push(questionEntry);
|
|
2381
2297
|
entries.push(answerEntry);
|
|
2382
2298
|
totalTokens += pairTokens;
|
|
2299
|
+
this.rsSummary(ancestor.level, pairTokens);
|
|
2383
2300
|
i++;
|
|
2384
2301
|
}
|
|
2385
2302
|
}
|
|
2386
2303
|
// ----- 6. Emit tail entries newest-first eviction (matches existing behavior) -----
|
|
2387
|
-
this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
2304
|
+
const tailStats = this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
2305
|
+
this.rsRaw('tail', tailStats.tokens, tailStats.messages);
|
|
2388
2306
|
// ----- 7. Post-process: merge consecutive raw entries from the same bodyGroup -----
|
|
2389
2307
|
// Both head and tail emission paths emit shards as separate ContextEntries.
|
|
2390
2308
|
// The middle path already merges consecutive same-bodyGroup raw shards into
|
|
@@ -2392,9 +2310,62 @@ export class AutobiographicalStrategy {
|
|
|
2392
2310
|
// a sharded message renders as ONE API message regardless of which region
|
|
2393
2311
|
// it falls into (preserves KV cache through region transitions).
|
|
2394
2312
|
const merged = this.mergeAdjacentBodyGroupRaw(entries, store);
|
|
2313
|
+
this.pruneToolEntries(merged);
|
|
2395
2314
|
this.trimOrphanedToolUse(merged);
|
|
2315
|
+
// Strip stale images BEFORE placing markers and committing stats, so both
|
|
2316
|
+
// describe the post-strip context the agent actually receives.
|
|
2317
|
+
this.applyImageStripping(merged, store);
|
|
2318
|
+
// Place ≤4 cache breakpoints across the FINAL ordered entries so the
|
|
2319
|
+
// provider can reuse the stable folded prefix — not just the head. With a
|
|
2320
|
+
// single head marker the cache hit is ~2%; well-placed breakpoints take the
|
|
2321
|
+
// real strategy to ~50% (docs/kv-stable-context-control.md — marker
|
|
2322
|
+
// placement is the dominant KV lever).
|
|
2323
|
+
this.placeCacheMarkers(merged, headMessageIds, tailMessageIds);
|
|
2324
|
+
this.rsEnd();
|
|
2396
2325
|
return merged;
|
|
2397
2326
|
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Place up to four `cache_control` breakpoints across the final ordered
|
|
2329
|
+
* entries: the head/system boundary, the end of the folded history (the
|
|
2330
|
+
* stable prefix that persists turn-to-turn — the most valuable), a mid-history
|
|
2331
|
+
* seam, and the very end (for pure-append reuse). Mirrors `placeMarkers` in
|
|
2332
|
+
* the adaptive layer but operates on emitted entries. Idempotent; clears any
|
|
2333
|
+
* pre-existing markers first.
|
|
2334
|
+
*/
|
|
2335
|
+
placeCacheMarkers(entries, headMessageIds, tailMessageIds) {
|
|
2336
|
+
for (const e of entries)
|
|
2337
|
+
if (e.cacheMarker)
|
|
2338
|
+
e.cacheMarker = false;
|
|
2339
|
+
const n = entries.length;
|
|
2340
|
+
if (n === 0)
|
|
2341
|
+
return;
|
|
2342
|
+
let lastHead = -1;
|
|
2343
|
+
let firstTail = n;
|
|
2344
|
+
for (let i = 0; i < n; i++) {
|
|
2345
|
+
const sid = entries[i].sourceMessageId;
|
|
2346
|
+
if (sid && headMessageIds.has(sid))
|
|
2347
|
+
lastHead = i;
|
|
2348
|
+
}
|
|
2349
|
+
for (let i = 0; i < n; i++) {
|
|
2350
|
+
const sid = entries[i].sourceMessageId;
|
|
2351
|
+
if (sid && tailMessageIds.has(sid)) {
|
|
2352
|
+
firstTail = i;
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const historyEnd = firstTail - 1; // last middle (folded-history) entry
|
|
2357
|
+
const marks = new Set();
|
|
2358
|
+
if (lastHead >= 0)
|
|
2359
|
+
marks.add(lastHead); // system / head block
|
|
2360
|
+
if (historyEnd > lastHead)
|
|
2361
|
+
marks.add(historyEnd); // stable folded prefix (the big one)
|
|
2362
|
+
if (historyEnd - lastHead > 2)
|
|
2363
|
+
marks.add(lastHead + Math.floor((historyEnd - lastHead) / 2)); // mid-history
|
|
2364
|
+
marks.add(n - 1); // end → pure-append reuse
|
|
2365
|
+
for (const idx of marks)
|
|
2366
|
+
if (idx >= 0 && idx < n)
|
|
2367
|
+
entries[idx].cacheMarker = true;
|
|
2368
|
+
}
|
|
2398
2369
|
/**
|
|
2399
2370
|
* Walk an entries array; for every run of consecutive entries that
|
|
2400
2371
|
* (a) have sourceRelation: 'copy' (raw, not a synthesized recall pair)
|
|
@@ -2483,6 +2454,20 @@ export class AutobiographicalStrategy {
|
|
|
2483
2454
|
this._adaptivePicker = new Picker(strategy);
|
|
2484
2455
|
return this._adaptivePicker;
|
|
2485
2456
|
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Build the picker for this compile. Instance folding strategies (kv-stable)
|
|
2459
|
+
* need the per-compile `PickerInputs` at construction, so they're built fresh
|
|
2460
|
+
* here; stateless ones (flat-profile / oldest-first) reuse the memoized picker.
|
|
2461
|
+
*/
|
|
2462
|
+
buildPicker(inputs) {
|
|
2463
|
+
if (this.config.foldingStrategy === 'kv-stable') {
|
|
2464
|
+
return new Picker(new KvStableStrategy(inputs, {
|
|
2465
|
+
reachTokens: this.config.kvStableReachTokens,
|
|
2466
|
+
mergeThreshold: this.config.mergeThreshold,
|
|
2467
|
+
}));
|
|
2468
|
+
}
|
|
2469
|
+
return this.getAdaptivePicker();
|
|
2470
|
+
}
|
|
2486
2471
|
/**
|
|
2487
2472
|
* Walk the summary tree to find the L_k ancestor of a message.
|
|
2488
2473
|
* Returns null if no ancestor exists at that level (e.g., L_k not yet produced).
|
|
@@ -2556,6 +2541,7 @@ export class AutobiographicalStrategy {
|
|
|
2556
2541
|
* Matches moltbot's budget waterfall: L3 → L2 → L1 with unused budget flowing down.
|
|
2557
2542
|
*/
|
|
2558
2543
|
selectHierarchical(store, budget) {
|
|
2544
|
+
this.rsBegin();
|
|
2559
2545
|
const entries = [];
|
|
2560
2546
|
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
2561
2547
|
const messages = store.getAll();
|
|
@@ -2578,6 +2564,7 @@ export class AutobiographicalStrategy {
|
|
|
2578
2564
|
content,
|
|
2579
2565
|
});
|
|
2580
2566
|
totalTokens += tokens;
|
|
2567
|
+
this.rsRaw('head', tokens);
|
|
2581
2568
|
}
|
|
2582
2569
|
// Mark the last head entry as a cache boundary (even if budget truncated the window)
|
|
2583
2570
|
if (entries.length > 0) {
|
|
@@ -2727,6 +2714,7 @@ export class AutobiographicalStrategy {
|
|
|
2727
2714
|
entries.push(questionEntry);
|
|
2728
2715
|
entries.push(answerEntry);
|
|
2729
2716
|
totalTokens += pairTokens;
|
|
2717
|
+
this.rsSummary(summary.level, pairTokens);
|
|
2730
2718
|
}
|
|
2731
2719
|
else {
|
|
2732
2720
|
const msg = item.msg;
|
|
@@ -2744,6 +2732,7 @@ export class AutobiographicalStrategy {
|
|
|
2744
2732
|
content,
|
|
2745
2733
|
});
|
|
2746
2734
|
totalTokens += tokens;
|
|
2735
|
+
this.rsRaw('middleRaw', tokens);
|
|
2747
2736
|
}
|
|
2748
2737
|
}
|
|
2749
2738
|
}
|
|
@@ -2775,6 +2764,8 @@ export class AutobiographicalStrategy {
|
|
|
2775
2764
|
entries.push(questionEntry);
|
|
2776
2765
|
entries.push(answerEntry);
|
|
2777
2766
|
totalTokens += pairTokens;
|
|
2767
|
+
for (const s of selectedSummaries)
|
|
2768
|
+
this.rsSummary(s.level, s.tokens);
|
|
2778
2769
|
}
|
|
2779
2770
|
// Sort by position so uncompressed-middle messages and pins both
|
|
2780
2771
|
// appear in their chronological place after the combined recall pair.
|
|
@@ -2794,6 +2785,7 @@ export class AutobiographicalStrategy {
|
|
|
2794
2785
|
content,
|
|
2795
2786
|
});
|
|
2796
2787
|
totalTokens += tokens;
|
|
2788
|
+
this.rsRaw('middleRaw', tokens);
|
|
2797
2789
|
}
|
|
2798
2790
|
}
|
|
2799
2791
|
}
|
|
@@ -2802,9 +2794,14 @@ export class AutobiographicalStrategy {
|
|
|
2802
2794
|
// budget, the latest messages (the ones the agent actually needs to act
|
|
2803
2795
|
// on) are preserved and the oldest recent-window messages are dropped.
|
|
2804
2796
|
const effectiveRecentStart = Math.max(recentStart, headEnd);
|
|
2805
|
-
this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
2797
|
+
const tailStats = this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
2798
|
+
this.rsRaw('tail', tailStats.tokens, tailStats.messages);
|
|
2806
2799
|
this.trimOrphanedToolUse(entries);
|
|
2807
2800
|
this.pruneToolEntries(entries);
|
|
2801
|
+
// Strip stale images before committing stats so RenderStats.total reflects
|
|
2802
|
+
// the post-strip context (this path places no cache markers).
|
|
2803
|
+
this.applyImageStripping(entries, store);
|
|
2804
|
+
this.rsEnd();
|
|
2808
2805
|
return entries;
|
|
2809
2806
|
}
|
|
2810
2807
|
// ============================================================================
|
|
@@ -3025,6 +3022,7 @@ export class AutobiographicalStrategy {
|
|
|
3025
3022
|
},
|
|
3026
3023
|
};
|
|
3027
3024
|
const response = await ctx.membrane.complete(request, { formatter: this.nativeFormatter });
|
|
3025
|
+
// Text-only on purpose: summarizer scratch thinking is not agent history
|
|
3028
3026
|
return response.content
|
|
3029
3027
|
.filter((b) => b.type === 'text')
|
|
3030
3028
|
.map(b => b.text)
|
|
@@ -3156,7 +3154,6 @@ export class AutobiographicalStrategy {
|
|
|
3156
3154
|
const existing = existingCompressed.get(key);
|
|
3157
3155
|
if (existing) {
|
|
3158
3156
|
chunk.compressed = true;
|
|
3159
|
-
chunk.diary = existing.diary;
|
|
3160
3157
|
chunk.summaryId = existing.summaryId;
|
|
3161
3158
|
}
|
|
3162
3159
|
// In hierarchical mode, also check if a summary exists for this chunk
|
|
@@ -3172,6 +3169,97 @@ export class AutobiographicalStrategy {
|
|
|
3172
3169
|
chunkKey(chunk) {
|
|
3173
3170
|
return chunk.messages.map((m) => m.id).join(':');
|
|
3174
3171
|
}
|
|
3172
|
+
/** True if any content block is a live image. */
|
|
3173
|
+
hasImageBlock(content) {
|
|
3174
|
+
return content.some((b) => b.type === 'image');
|
|
3175
|
+
}
|
|
3176
|
+
/** Message index marking the image-strip depth boundary: walks newest→oldest
|
|
3177
|
+
* summing the same per-message estimate as getRecentWindowStart, and returns
|
|
3178
|
+
* the index of the first message still within `depthTokens`. Messages before
|
|
3179
|
+
* this index have their images stripped to placeholders. */
|
|
3180
|
+
getImageStripStart(store, depthTokens) {
|
|
3181
|
+
const messages = store.getAll();
|
|
3182
|
+
let tokens = 0;
|
|
3183
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3184
|
+
tokens += store.estimateTokens(messages[i]);
|
|
3185
|
+
if (tokens > depthTokens)
|
|
3186
|
+
return i + 1;
|
|
3187
|
+
}
|
|
3188
|
+
return 0;
|
|
3189
|
+
}
|
|
3190
|
+
/** Text substituted for an image block once it leaves the live-image window. */
|
|
3191
|
+
static IMAGE_PLACEHOLDER = '[image dropped from live context]';
|
|
3192
|
+
/** Post-pass over compiled entries: replace image blocks with a text
|
|
3193
|
+
* placeholder once they fall outside the live-image window — either deeper
|
|
3194
|
+
* than `imageStripDepthTokens` from the newest message, or beyond the
|
|
3195
|
+
* `maxLiveImages` most-recent images (counted newest-first). Summaries are
|
|
3196
|
+
* already text, so they're naturally unaffected. The adjacent
|
|
3197
|
+
* "[image attachment: <name>]" text added at ingest preserves the filename,
|
|
3198
|
+
* so the placeholder itself stays terse. Reduces tokens, so it never pushes
|
|
3199
|
+
* a compiled context back over budget.
|
|
3200
|
+
*
|
|
3201
|
+
* Runs INSIDE each select path, *before* `rsEnd()` and `placeCacheMarkers`,
|
|
3202
|
+
* so the committed render stats (and the cache breakpoints) describe the
|
|
3203
|
+
* post-strip context. As it strips, it decrements the matching raw bucket of
|
|
3204
|
+
* the in-progress render stats by the reclaimed tokens, keeping
|
|
3205
|
+
* `RenderStats.total` equal to the real rendered size. */
|
|
3206
|
+
applyImageStripping(entries, store) {
|
|
3207
|
+
const maxLive = this.config.maxLiveImages ?? 0; // 0 = unlimited count
|
|
3208
|
+
const depthTokens = this.config.imageStripDepthTokens ?? 0; // 0 = no depth strip
|
|
3209
|
+
if (maxLive === 0 && depthTokens === 0)
|
|
3210
|
+
return; // policy disabled
|
|
3211
|
+
const messages = store.getAll();
|
|
3212
|
+
const posById = new Map();
|
|
3213
|
+
for (let i = 0; i < messages.length; i++)
|
|
3214
|
+
posById.set(messages[i].id, i);
|
|
3215
|
+
const stripStart = depthTokens > 0 ? this.getImageStripStart(store, depthTokens) : 0;
|
|
3216
|
+
// Same region windows select() bucketed by, so a stripped image's reclaimed
|
|
3217
|
+
// tokens come back out of the bucket it was originally tallied into.
|
|
3218
|
+
const headStart = this.getHeadWindowStartIndex(store);
|
|
3219
|
+
const headEnd = this.getHeadWindowEnd(store);
|
|
3220
|
+
const recentStart = Math.max(this.getRecentWindowStart(store), headEnd);
|
|
3221
|
+
const bucketAt = (pos) => {
|
|
3222
|
+
if (pos < 0)
|
|
3223
|
+
return 'middleRaw'; // no resolvable region — keep total == Σbuckets
|
|
3224
|
+
if (pos >= headStart && pos < headEnd)
|
|
3225
|
+
return 'head';
|
|
3226
|
+
if (pos >= recentStart)
|
|
3227
|
+
return 'tail';
|
|
3228
|
+
return 'middleRaw';
|
|
3229
|
+
};
|
|
3230
|
+
const placeholderTokens = Math.ceil(AutobiographicalStrategy.IMAGE_PLACEHOLDER.length / 4);
|
|
3231
|
+
// Image-bearing entries, newest-first by source position. Entries with no
|
|
3232
|
+
// resolvable source position sort last (pos -1) and never count as "live".
|
|
3233
|
+
const ordered = entries
|
|
3234
|
+
.map((entry, idx) => ({
|
|
3235
|
+
idx,
|
|
3236
|
+
pos: entry.sourceMessageId !== undefined ? posById.get(entry.sourceMessageId) ?? -1 : -1,
|
|
3237
|
+
}))
|
|
3238
|
+
.filter(({ idx }) => this.hasImageBlock(entries[idx].content))
|
|
3239
|
+
.sort((a, b) => b.pos - a.pos);
|
|
3240
|
+
let keptImages = 0;
|
|
3241
|
+
for (const { idx, pos } of ordered) {
|
|
3242
|
+
const entry = entries[idx];
|
|
3243
|
+
const tooDeep = depthTokens > 0 && (pos < 0 || pos < stripStart);
|
|
3244
|
+
const bucket = bucketAt(pos);
|
|
3245
|
+
entry.content = entry.content.map((block) => {
|
|
3246
|
+
if (block.type !== 'image')
|
|
3247
|
+
return block;
|
|
3248
|
+
const overCount = maxLive > 0 && keptImages >= maxLive;
|
|
3249
|
+
if (tooDeep || overCount) {
|
|
3250
|
+
// The renderer estimates an image at `tokenEstimate ?? 1600` (see
|
|
3251
|
+
// message-store/context-log). Hand that back to the bucket, less the
|
|
3252
|
+
// placeholder text that replaces it, so the stats match the output.
|
|
3253
|
+
const reclaimed = (block.tokenEstimate ?? 1600) - placeholderTokens;
|
|
3254
|
+
if (this._rs && reclaimed > 0)
|
|
3255
|
+
this._rs[bucket].tokens -= reclaimed;
|
|
3256
|
+
return { type: 'text', text: AutobiographicalStrategy.IMAGE_PLACEHOLDER };
|
|
3257
|
+
}
|
|
3258
|
+
keptImages++;
|
|
3259
|
+
return block;
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3175
3263
|
getRecentWindowStart(store) {
|
|
3176
3264
|
const messages = store.getAll();
|
|
3177
3265
|
let tokens = 0;
|