@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
|
@@ -17,14 +17,16 @@ import type {
|
|
|
17
17
|
ProtectedRange,
|
|
18
18
|
SearchQuery,
|
|
19
19
|
SearchResult,
|
|
20
|
+
RenderStats,
|
|
20
21
|
} from '../types/index.js';
|
|
21
22
|
import { DEFAULT_AUTOBIOGRAPHICAL_CONFIG } from '../types/index.js';
|
|
22
23
|
import { getSummaryParentId } from '../types/strategy.js';
|
|
23
24
|
import { splitMixedToolMessages, stripUnpairedToolBlocks } from '../normalize-tool-messages.js';
|
|
24
25
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
25
26
|
import { dirname } from 'node:path';
|
|
26
|
-
import { Picker, OverBudgetError, type PickerChunk } from '../adaptive/picker.js';
|
|
27
|
+
import { Picker, OverBudgetError, type PickerChunk, type PickerInputs } from '../adaptive/picker.js';
|
|
27
28
|
import { FlatProfileStrategy } from '../adaptive/strategies/flat-profile.js';
|
|
29
|
+
import { KvStableStrategy } from '../adaptive/strategies/kv-stable.js';
|
|
28
30
|
import { OldestFirstStrategy } from '../adaptive/strategies/oldest-first.js';
|
|
29
31
|
import type {
|
|
30
32
|
FoldingStrategy,
|
|
@@ -223,8 +225,6 @@ export interface Chunk {
|
|
|
223
225
|
tokens: number;
|
|
224
226
|
/** Whether this chunk has been compressed */
|
|
225
227
|
compressed: boolean;
|
|
226
|
-
/** The diary entry if compressed (legacy mode) */
|
|
227
|
-
diary?: string;
|
|
228
228
|
/** ID of the L1 SummaryEntry (hierarchical mode) */
|
|
229
229
|
summaryId?: string;
|
|
230
230
|
/** Phase type tag (set by KnowledgeStrategy for semantic chunking) */
|
|
@@ -1064,9 +1064,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1064
1064
|
|
|
1065
1065
|
if (!chunk || chunk.compressed) return;
|
|
1066
1066
|
|
|
1067
|
-
this.pendingCompression = this.
|
|
1068
|
-
? this.compressChunkHierarchical(chunk, ctx)
|
|
1069
|
-
: this.compressChunkLegacy(chunk, ctx);
|
|
1067
|
+
this.pendingCompression = this.compressChunkHierarchical(chunk, ctx);
|
|
1070
1068
|
|
|
1071
1069
|
try {
|
|
1072
1070
|
await this.pendingCompression;
|
|
@@ -1136,12 +1134,13 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1136
1134
|
): ContextEntry[] {
|
|
1137
1135
|
this.rebuildChunks(store);
|
|
1138
1136
|
|
|
1137
|
+
// Image stripping runs inside each select path (before stats commit / cache
|
|
1138
|
+
// markers), so the returned entries are already bounded — see
|
|
1139
|
+
// applyImageStripping.
|
|
1139
1140
|
if (this.config.adaptiveResolution) {
|
|
1140
1141
|
return this.selectAdaptive(store, budget);
|
|
1141
1142
|
}
|
|
1142
|
-
return this.
|
|
1143
|
-
? this.selectHierarchical(store, budget)
|
|
1144
|
-
: this.selectLegacy(store, log, budget);
|
|
1143
|
+
return this.selectHierarchical(store, budget);
|
|
1145
1144
|
}
|
|
1146
1145
|
|
|
1147
1146
|
/**
|
|
@@ -1171,16 +1170,89 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1171
1170
|
* Useful for TUI / dashboards. The token sums use the strategy's own
|
|
1172
1171
|
* token estimates (which match what `select()` uses for budget math).
|
|
1173
1172
|
*/
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1173
|
+
// ===========================================================================
|
|
1174
|
+
// Render-stats instrumentation — inspect, don't reconstruct.
|
|
1175
|
+
//
|
|
1176
|
+
// selectAdaptive/selectHierarchical tally each entry into `_rs` AS THEY EMIT
|
|
1177
|
+
// it (raw head/tail, raw middle the picker kept verbatim, and recall pairs
|
|
1178
|
+
// bucketed by the ancestor summary's level), using the same token numbers the
|
|
1179
|
+
// renderer uses for budget math. `getRenderStats()` returns that committed
|
|
1180
|
+
// snapshot, so it reflects what the last compile actually rendered rather than
|
|
1181
|
+
// re-deriving the full pyramid (which, under adaptive resolution, bears little
|
|
1182
|
+
// resemblance to the folded output).
|
|
1183
|
+
// ===========================================================================
|
|
1184
|
+
private _rs: RenderStats | null = null;
|
|
1185
|
+
private _lastRenderStats: RenderStats | null = null;
|
|
1186
|
+
|
|
1187
|
+
/** Begin a render-stats accumulation for one select() pass. */
|
|
1188
|
+
protected rsBegin(): void {
|
|
1189
|
+
this._rs = {
|
|
1190
|
+
head: { messages: 0, tokens: 0 },
|
|
1191
|
+
tail: { messages: 0, tokens: 0 },
|
|
1192
|
+
middleRaw: { messages: 0, tokens: 0 },
|
|
1193
|
+
summaries: {
|
|
1194
|
+
l1: { count: 0, tokens: 0 },
|
|
1195
|
+
l2: { count: 0, tokens: 0 },
|
|
1196
|
+
l3: { count: 0, tokens: 0 },
|
|
1197
|
+
},
|
|
1198
|
+
pending: { chunks: 0, merges: 0 },
|
|
1199
|
+
total: { messages: 0, tokens: 0 },
|
|
1181
1200
|
};
|
|
1182
|
-
|
|
1183
|
-
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** Tally one (or `count`) raw message(s) into a raw bucket. */
|
|
1204
|
+
protected rsRaw(bucket: 'head' | 'tail' | 'middleRaw', tokens: number, count = 1): void {
|
|
1205
|
+
const r = this._rs;
|
|
1206
|
+
if (!r) return;
|
|
1207
|
+
r[bucket].messages += count;
|
|
1208
|
+
r[bucket].tokens += tokens;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** Tally one emitted recall pair under its ancestor's level (>=3 folds into l3). */
|
|
1212
|
+
protected rsSummary(level: number, tokens: number): void {
|
|
1213
|
+
const r = this._rs;
|
|
1214
|
+
if (!r) return;
|
|
1215
|
+
const k: 'l1' | 'l2' | 'l3' = level <= 1 ? 'l1' : level === 2 ? 'l2' : 'l3';
|
|
1216
|
+
r.summaries[k].count += 1;
|
|
1217
|
+
r.summaries[k].tokens += tokens;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** Commit the accumulated stats as the last-render snapshot. */
|
|
1221
|
+
protected rsEnd(): void {
|
|
1222
|
+
const r = this._rs;
|
|
1223
|
+
if (!r) return;
|
|
1224
|
+
r.pending = {
|
|
1225
|
+
chunks: this.chunks.filter(c => !c.compressed).length,
|
|
1226
|
+
merges: this.mergeQueue.length,
|
|
1227
|
+
};
|
|
1228
|
+
const s = r.summaries;
|
|
1229
|
+
const summaryMsgs = (s.l1.count + s.l2.count + s.l3.count) * 2; // Q/A pair each
|
|
1230
|
+
r.total = {
|
|
1231
|
+
messages: r.head.messages + r.tail.messages + r.middleRaw.messages + summaryMsgs,
|
|
1232
|
+
tokens:
|
|
1233
|
+
r.head.tokens + r.tail.tokens + r.middleRaw.tokens +
|
|
1234
|
+
s.l1.tokens + s.l2.tokens + s.l3.tokens,
|
|
1235
|
+
};
|
|
1236
|
+
this._lastRenderStats = r;
|
|
1237
|
+
this._rs = null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Stats describing the LAST rendered context. Returns the inspected snapshot
|
|
1242
|
+
* captured during the most recent `select()`. Before any compile has run (no
|
|
1243
|
+
* snapshot yet), falls back to a reconstructed pyramid view so callers still
|
|
1244
|
+
* get a non-null shape.
|
|
1245
|
+
*/
|
|
1246
|
+
getRenderStats(store: MessageStoreView): RenderStats {
|
|
1247
|
+
return this._lastRenderStats ?? this.reconstructRenderStats(store);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Pre-render fallback: re-derive head/tail windows + the full live pyramid.
|
|
1252
|
+
* NOTE: this is the old "reconstruct" behavior and does NOT reflect adaptive
|
|
1253
|
+
* folding — used only until the first compile populates the inspected stats.
|
|
1254
|
+
*/
|
|
1255
|
+
protected reconstructRenderStats(store: MessageStoreView): RenderStats {
|
|
1184
1256
|
const messages = store.getAll();
|
|
1185
1257
|
const headStart = this.getHeadWindowStartIndex(store);
|
|
1186
1258
|
const headEnd = this.getHeadWindowEnd(store);
|
|
@@ -1197,145 +1269,31 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1197
1269
|
const sumLevelTokens = (level: SummaryLevel): number =>
|
|
1198
1270
|
live(level).reduce((acc, s) => acc + s.tokens, 0);
|
|
1199
1271
|
|
|
1272
|
+
const head = { messages: headMsgs.length, tokens: sumTokens(headMsgs) };
|
|
1273
|
+
const tail = { messages: tailMsgs.length, tokens: sumTokens(tailMsgs) };
|
|
1274
|
+
const summaries = {
|
|
1275
|
+
l1: { count: live(1).length, tokens: sumLevelTokens(1) },
|
|
1276
|
+
l2: { count: live(2).length, tokens: sumLevelTokens(2) },
|
|
1277
|
+
l3: { count: live(3).length, tokens: sumLevelTokens(3) },
|
|
1278
|
+
};
|
|
1200
1279
|
return {
|
|
1201
|
-
head
|
|
1202
|
-
tail
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
l2: { count: live(2).length, tokens: sumLevelTokens(2) },
|
|
1206
|
-
l3: { count: live(3).length, tokens: sumLevelTokens(3) },
|
|
1207
|
-
},
|
|
1280
|
+
head,
|
|
1281
|
+
tail,
|
|
1282
|
+
middleRaw: { messages: 0, tokens: 0 },
|
|
1283
|
+
summaries,
|
|
1208
1284
|
pending: {
|
|
1209
1285
|
chunks: this.chunks.filter(c => !c.compressed).length,
|
|
1210
1286
|
merges: this.mergeQueue.length,
|
|
1211
1287
|
},
|
|
1288
|
+
total: {
|
|
1289
|
+
messages: head.messages + tail.messages
|
|
1290
|
+
+ (summaries.l1.count + summaries.l2.count + summaries.l3.count) * 2,
|
|
1291
|
+
tokens: head.tokens + tail.tokens
|
|
1292
|
+
+ summaries.l1.tokens + summaries.l2.tokens + summaries.l3.tokens,
|
|
1293
|
+
},
|
|
1212
1294
|
};
|
|
1213
1295
|
}
|
|
1214
1296
|
|
|
1215
|
-
// ============================================================================
|
|
1216
|
-
// Legacy (single-level) path
|
|
1217
|
-
// ============================================================================
|
|
1218
|
-
|
|
1219
|
-
protected selectLegacy(
|
|
1220
|
-
store: MessageStoreView,
|
|
1221
|
-
_log: ContextLogView,
|
|
1222
|
-
budget: TokenBudget
|
|
1223
|
-
): ContextEntry[] {
|
|
1224
|
-
const entries: ContextEntry[] = [];
|
|
1225
|
-
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
1226
|
-
let totalTokens = 0;
|
|
1227
|
-
const messages = store.getAll();
|
|
1228
|
-
const msgCap = this.config.maxMessageTokens;
|
|
1229
|
-
|
|
1230
|
-
// 1. Head window: preserved verbatim as raw copies
|
|
1231
|
-
const headStart = this.getHeadWindowStartIndex(store);
|
|
1232
|
-
const headEnd = this.getHeadWindowEnd(store);
|
|
1233
|
-
for (let i = headStart; i < headEnd && i < messages.length; i++) {
|
|
1234
|
-
const msg = messages[i];
|
|
1235
|
-
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1236
|
-
const tokens = msgCap > 0 ? Math.min(store.estimateTokens(msg), msgCap + 50) : store.estimateTokens(msg);
|
|
1237
|
-
if (this.isOverBudget(totalTokens + tokens, maxTokens)) break;
|
|
1238
|
-
|
|
1239
|
-
entries.push({
|
|
1240
|
-
index: entries.length,
|
|
1241
|
-
sourceMessageId: msg.id,
|
|
1242
|
-
sourceRelation: 'copy',
|
|
1243
|
-
participant: msg.participant,
|
|
1244
|
-
content,
|
|
1245
|
-
});
|
|
1246
|
-
totalTokens += tokens;
|
|
1247
|
-
}
|
|
1248
|
-
// Mark the last head entry as a cache boundary (even if budget truncated the window)
|
|
1249
|
-
if (entries.length > 0) {
|
|
1250
|
-
entries[entries.length - 1].cacheMarker = true;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
// 2. Middle zone: compressed chunks as diary pairs, uncompressed as raw messages.
|
|
1254
|
-
const rawRecentStart = this.getRecentWindowStart(store);
|
|
1255
|
-
// Track which message IDs are covered by chunks
|
|
1256
|
-
const coveredByChunks = new Set<string>();
|
|
1257
|
-
|
|
1258
|
-
for (const chunk of this.chunks) {
|
|
1259
|
-
for (const m of chunk.messages) coveredByChunks.add(m.id);
|
|
1260
|
-
|
|
1261
|
-
if (chunk.compressed && chunk.diary) {
|
|
1262
|
-
const contextLabel = this.config.summaryContextLabel ?? 'Here is a summary of earlier conversation context:';
|
|
1263
|
-
const summaryParticipant = this.config.summaryParticipant ?? 'Summary';
|
|
1264
|
-
|
|
1265
|
-
const questionEntry: ContextEntry = {
|
|
1266
|
-
index: entries.length,
|
|
1267
|
-
participant: 'Context Manager',
|
|
1268
|
-
content: [{ type: 'text', text: contextLabel }],
|
|
1269
|
-
sourceRelation: 'derived',
|
|
1270
|
-
};
|
|
1271
|
-
|
|
1272
|
-
// Synthesised summary turns must respect maxMessageTokens just like raw
|
|
1273
|
-
// copies do — otherwise a runaway diary can starve recent messages.
|
|
1274
|
-
const answerContent: ContentBlock[] = [{ type: 'text', text: chunk.diary }];
|
|
1275
|
-
const answerEntry: ContextEntry = {
|
|
1276
|
-
index: entries.length + 1,
|
|
1277
|
-
participant: summaryParticipant,
|
|
1278
|
-
content: msgCap > 0 ? this.truncateContent(answerContent, msgCap) : answerContent,
|
|
1279
|
-
sourceRelation: 'derived',
|
|
1280
|
-
};
|
|
1281
|
-
|
|
1282
|
-
const pairTokens = this.estimateTokens(questionEntry.content) +
|
|
1283
|
-
this.estimateTokens(answerEntry.content);
|
|
1284
|
-
|
|
1285
|
-
if (this.isOverBudget(totalTokens + pairTokens, maxTokens)) break;
|
|
1286
|
-
|
|
1287
|
-
entries.push(questionEntry);
|
|
1288
|
-
entries.push(answerEntry);
|
|
1289
|
-
totalTokens += pairTokens;
|
|
1290
|
-
} else {
|
|
1291
|
-
// Uncompressed: emit raw messages so they aren't lost
|
|
1292
|
-
for (const msg of chunk.messages) {
|
|
1293
|
-
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1294
|
-
const tokens = msgCap > 0 ? Math.min(store.estimateTokens(msg), msgCap + 50) : store.estimateTokens(msg);
|
|
1295
|
-
if (this.isOverBudget(totalTokens + tokens, maxTokens)) break;
|
|
1296
|
-
|
|
1297
|
-
entries.push({
|
|
1298
|
-
index: entries.length,
|
|
1299
|
-
sourceMessageId: msg.id,
|
|
1300
|
-
sourceRelation: 'copy',
|
|
1301
|
-
participant: msg.participant,
|
|
1302
|
-
content,
|
|
1303
|
-
});
|
|
1304
|
-
totalTokens += tokens;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// Emit gap messages in the compressible zone not covered by any chunk.
|
|
1310
|
-
// Compressible zone: [0, headStart) ∪ [headEnd, rawRecentStart)
|
|
1311
|
-
for (let i = 0; i < rawRecentStart && i < messages.length; i++) {
|
|
1312
|
-
// Skip head window messages (already emitted verbatim above)
|
|
1313
|
-
if (i >= headStart && i < headEnd) continue;
|
|
1314
|
-
if (coveredByChunks.has(messages[i].id)) continue;
|
|
1315
|
-
const msg = messages[i];
|
|
1316
|
-
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1317
|
-
const tokens = msgCap > 0 ? Math.min(store.estimateTokens(msg), msgCap + 50) : store.estimateTokens(msg);
|
|
1318
|
-
if (this.isOverBudget(totalTokens + tokens, maxTokens)) break;
|
|
1319
|
-
|
|
1320
|
-
entries.push({
|
|
1321
|
-
index: entries.length,
|
|
1322
|
-
sourceMessageId: msg.id,
|
|
1323
|
-
sourceRelation: 'copy',
|
|
1324
|
-
participant: msg.participant,
|
|
1325
|
-
content,
|
|
1326
|
-
});
|
|
1327
|
-
totalTokens += tokens;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// 3. Recent uncompressed messages (skip those already in head window)
|
|
1331
|
-
const recentStart = Math.max(this.getRecentWindowStart(store), headEnd);
|
|
1332
|
-
this.emitRecentNewestFirst(entries, store, messages, recentStart, msgCap, maxTokens, totalTokens);
|
|
1333
|
-
|
|
1334
|
-
this.trimOrphanedToolUse(entries);
|
|
1335
|
-
this.pruneToolEntries(entries);
|
|
1336
|
-
return entries;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
1297
|
/**
|
|
1340
1298
|
* Emit recent-window messages, evicting OLDEST-first when the budget is tight.
|
|
1341
1299
|
*
|
|
@@ -1355,8 +1313,8 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1355
1313
|
msgCap: number,
|
|
1356
1314
|
maxTokens: number,
|
|
1357
1315
|
totalTokensBefore: number,
|
|
1358
|
-
):
|
|
1359
|
-
if (recentStart >= messages.length) return;
|
|
1316
|
+
): { messages: number; tokens: number } {
|
|
1317
|
+
if (recentStart >= messages.length) return { messages: 0, tokens: 0 };
|
|
1360
1318
|
|
|
1361
1319
|
const accepted: number[] = [];
|
|
1362
1320
|
let acceptedTokens = 0;
|
|
@@ -1380,9 +1338,13 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1380
1338
|
accepted.shift();
|
|
1381
1339
|
}
|
|
1382
1340
|
|
|
1341
|
+
let emittedTokens = 0;
|
|
1383
1342
|
for (const i of accepted) {
|
|
1384
1343
|
const msg = messages[i];
|
|
1385
1344
|
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
1345
|
+
const tokens = msgCap > 0
|
|
1346
|
+
? Math.min(store.estimateTokens(msg), msgCap + 50)
|
|
1347
|
+
: store.estimateTokens(msg);
|
|
1386
1348
|
entries.push({
|
|
1387
1349
|
index: entries.length,
|
|
1388
1350
|
sourceMessageId: msg.id,
|
|
@@ -1390,100 +1352,9 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1390
1352
|
participant: msg.participant,
|
|
1391
1353
|
content,
|
|
1392
1354
|
});
|
|
1355
|
+
emittedTokens += tokens;
|
|
1393
1356
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
protected async compressChunkLegacy(chunk: Chunk, ctx: StrategyContext): Promise<void> {
|
|
1397
|
-
if (!ctx.membrane) {
|
|
1398
|
-
throw new Error('No membrane instance for compression');
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
const priorContext = this.buildPriorContextLegacy(chunk, ctx);
|
|
1402
|
-
const chunkContent = this.formatChunkForCompression(chunk);
|
|
1403
|
-
|
|
1404
|
-
const prompt = this.config.diaryUserPrompt ?? this.config.summaryUserPrompt!;
|
|
1405
|
-
const systemPrompt = this.config.diarySystemPrompt ?? this.config.summarySystemPrompt!;
|
|
1406
|
-
|
|
1407
|
-
const messages = [
|
|
1408
|
-
...priorContext,
|
|
1409
|
-
{
|
|
1410
|
-
participant: 'Context Manager',
|
|
1411
|
-
content: [{ type: 'text' as const, text: prompt.replace('{content}', chunkContent) }],
|
|
1412
|
-
},
|
|
1413
|
-
];
|
|
1414
|
-
|
|
1415
|
-
const request: NormalizedRequest = {
|
|
1416
|
-
messages: messages.map((m) => ({
|
|
1417
|
-
participant: m.participant,
|
|
1418
|
-
content: m.content,
|
|
1419
|
-
})),
|
|
1420
|
-
system: systemPrompt,
|
|
1421
|
-
config: {
|
|
1422
|
-
model: this.config.compressionModel ?? 'claude-sonnet-4-20250514',
|
|
1423
|
-
maxTokens: 2000,
|
|
1424
|
-
},
|
|
1425
|
-
};
|
|
1426
|
-
|
|
1427
|
-
try {
|
|
1428
|
-
const response = await ctx.membrane.complete(request);
|
|
1429
|
-
const diaryText = response.content
|
|
1430
|
-
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
1431
|
-
.map((b) => b.text)
|
|
1432
|
-
.join('\n');
|
|
1433
|
-
|
|
1434
|
-
chunk.compressed = true;
|
|
1435
|
-
chunk.diary = diaryText;
|
|
1436
|
-
this._compressionCount++;
|
|
1437
|
-
} catch (error) {
|
|
1438
|
-
console.error('Failed to compress chunk:', error);
|
|
1439
|
-
throw error;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
protected buildPriorContextLegacy(chunk: Chunk, ctx: StrategyContext): Array<{
|
|
1444
|
-
participant: string;
|
|
1445
|
-
content: ContentBlock[];
|
|
1446
|
-
}> {
|
|
1447
|
-
const context: Array<{ participant: string; content: ContentBlock[] }> = [];
|
|
1448
|
-
|
|
1449
|
-
for (const prevChunk of this.chunks) {
|
|
1450
|
-
if (prevChunk.index >= chunk.index) break;
|
|
1451
|
-
if (!prevChunk.compressed || !prevChunk.diary) continue;
|
|
1452
|
-
|
|
1453
|
-
context.push({
|
|
1454
|
-
participant: 'Context Manager',
|
|
1455
|
-
content: [{ type: 'text', text: this.config.summaryContextLabel ?? 'Summary of earlier context:' }],
|
|
1456
|
-
});
|
|
1457
|
-
context.push({
|
|
1458
|
-
participant: this.config.summaryParticipant ?? 'Summary',
|
|
1459
|
-
content: [{ type: 'text', text: prevChunk.diary }],
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// Find the actual position of this chunk's first message in the full array
|
|
1464
|
-
const messages = ctx.messageStore.getAll();
|
|
1465
|
-
const firstMsgId = chunk.messages[0]?.id;
|
|
1466
|
-
const chunkAbsStart = firstMsgId
|
|
1467
|
-
? messages.findIndex(m => m.id === firstMsgId)
|
|
1468
|
-
: -1;
|
|
1469
|
-
|
|
1470
|
-
if (chunkAbsStart > 0) {
|
|
1471
|
-
const precedingStart = Math.max(0, chunkAbsStart - 50);
|
|
1472
|
-
let tokens = 0;
|
|
1473
|
-
|
|
1474
|
-
for (let i = chunkAbsStart - 1; i >= precedingStart && tokens < 15000; i--) {
|
|
1475
|
-
const msg = messages[i];
|
|
1476
|
-
if (!msg) break;
|
|
1477
|
-
|
|
1478
|
-
tokens += ctx.messageStore.estimateTokens(msg);
|
|
1479
|
-
context.unshift({
|
|
1480
|
-
participant: msg.participant,
|
|
1481
|
-
content: msg.content,
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
return context;
|
|
1357
|
+
return { messages: accepted.length, tokens: emittedTokens };
|
|
1487
1358
|
}
|
|
1488
1359
|
|
|
1489
1360
|
// ============================================================================
|
|
@@ -1731,6 +1602,10 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
1731
1602
|
|
|
1732
1603
|
try {
|
|
1733
1604
|
const response = await ctx.membrane.complete(request, { formatter: this.nativeFormatter });
|
|
1605
|
+
// Text-only on purpose: this is the SUMMARIZER's one-shot response —
|
|
1606
|
+
// its thinking/redacted_thinking blocks are scratch work, not part of
|
|
1607
|
+
// the agent's history, and signed thinking is never valid inside a
|
|
1608
|
+
// rewritten summary anyway.
|
|
1734
1609
|
const summaryText = response.content
|
|
1735
1610
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
1736
1611
|
.map(b => b.text)
|
|
@@ -2188,6 +2063,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2188
2063
|
|
|
2189
2064
|
try {
|
|
2190
2065
|
const response = await ctx.membrane.complete(request, { formatter: this.nativeFormatter });
|
|
2066
|
+
// Text-only on purpose: summarizer scratch thinking is not agent history
|
|
2191
2067
|
const mergedText = response.content
|
|
2192
2068
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
2193
2069
|
.map(b => b.text)
|
|
@@ -2272,6 +2148,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2272
2148
|
* See `docs/adaptive-resolution-design.md` §3, §5.
|
|
2273
2149
|
*/
|
|
2274
2150
|
protected selectAdaptive(store: MessageStoreView, budget: TokenBudget): ContextEntry[] {
|
|
2151
|
+
this.rsBegin();
|
|
2275
2152
|
const entries: ContextEntry[] = [];
|
|
2276
2153
|
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
2277
2154
|
const messages = store.getAll();
|
|
@@ -2305,10 +2182,11 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2305
2182
|
totalTokens += tokens;
|
|
2306
2183
|
headMessageIds.add(msg.id);
|
|
2307
2184
|
headTokens += tokens;
|
|
2185
|
+
this.rsRaw('head', tokens);
|
|
2308
2186
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2187
|
+
// (Cache breakpoints are placed in one pass over the FINAL ordered entries
|
|
2188
|
+
// below — see placeCacheMarkers — capturing the stable folded prefix, not
|
|
2189
|
+
// just the head boundary.)
|
|
2312
2190
|
|
|
2313
2191
|
// Compute tail message IDs (will be emitted at end)
|
|
2314
2192
|
const effectiveRecentStart = Math.max(recentStart, headEnd);
|
|
@@ -2412,19 +2290,17 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2412
2290
|
const headSetForPicker = new Set<ChunkId>(headMessageIds);
|
|
2413
2291
|
const tailSetForPicker = new Set<ChunkId>(tailMessageIds);
|
|
2414
2292
|
|
|
2415
|
-
const
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
foldingBudget
|
|
2427
|
-
);
|
|
2293
|
+
const pickerInputs: PickerInputs = {
|
|
2294
|
+
chunks: pickerChunks,
|
|
2295
|
+
summaries: summariesMap,
|
|
2296
|
+
recallPairTokens,
|
|
2297
|
+
headChunkIds: headSetForPicker,
|
|
2298
|
+
tailChunkIds: tailSetForPicker,
|
|
2299
|
+
headTokens,
|
|
2300
|
+
tailTokens,
|
|
2301
|
+
};
|
|
2302
|
+
const picker = this.buildPicker(pickerInputs);
|
|
2303
|
+
const result = picker.run(pickerInputs, foldingBudget);
|
|
2428
2304
|
|
|
2429
2305
|
// Commit the new resolutions back to strategy state for next compile.
|
|
2430
2306
|
// Persist to chronicle only if anything actually changed — avoids
|
|
@@ -2552,6 +2428,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2552
2428
|
content,
|
|
2553
2429
|
});
|
|
2554
2430
|
totalTokens += tokens;
|
|
2431
|
+
this.rsRaw('middleRaw', tokens);
|
|
2555
2432
|
} else {
|
|
2556
2433
|
// summary run — emit Q+A pair, dedup at the strategy level
|
|
2557
2434
|
const ancestor = currentRun.ancestor;
|
|
@@ -2578,6 +2455,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2578
2455
|
entries.push(questionEntry);
|
|
2579
2456
|
entries.push(answerEntry);
|
|
2580
2457
|
totalTokens += pairTokens;
|
|
2458
|
+
this.rsSummary(ancestor.level, pairTokens);
|
|
2581
2459
|
}
|
|
2582
2460
|
}
|
|
2583
2461
|
currentRun = null;
|
|
@@ -2638,6 +2516,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2638
2516
|
content,
|
|
2639
2517
|
});
|
|
2640
2518
|
totalTokens += tokens;
|
|
2519
|
+
this.rsRaw('middleRaw', tokens);
|
|
2641
2520
|
i++;
|
|
2642
2521
|
} else {
|
|
2643
2522
|
const ancestor = this.findAncestorAt(msg.id, resolution, chunksByMessageId, summariesById);
|
|
@@ -2653,6 +2532,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2653
2532
|
content,
|
|
2654
2533
|
});
|
|
2655
2534
|
totalTokens += tokens;
|
|
2535
|
+
this.rsRaw('middleRaw', tokens);
|
|
2656
2536
|
i++;
|
|
2657
2537
|
continue;
|
|
2658
2538
|
}
|
|
@@ -2679,12 +2559,14 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2679
2559
|
entries.push(questionEntry);
|
|
2680
2560
|
entries.push(answerEntry);
|
|
2681
2561
|
totalTokens += pairTokens;
|
|
2562
|
+
this.rsSummary(ancestor.level, pairTokens);
|
|
2682
2563
|
i++;
|
|
2683
2564
|
}
|
|
2684
2565
|
}
|
|
2685
2566
|
|
|
2686
2567
|
// ----- 6. Emit tail entries newest-first eviction (matches existing behavior) -----
|
|
2687
|
-
this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
2568
|
+
const tailStats = this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
2569
|
+
this.rsRaw('tail', tailStats.tokens, tailStats.messages);
|
|
2688
2570
|
|
|
2689
2571
|
// ----- 7. Post-process: merge consecutive raw entries from the same bodyGroup -----
|
|
2690
2572
|
// Both head and tail emission paths emit shards as separate ContextEntries.
|
|
@@ -2694,10 +2576,59 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2694
2576
|
// it falls into (preserves KV cache through region transitions).
|
|
2695
2577
|
const merged = this.mergeAdjacentBodyGroupRaw(entries, store);
|
|
2696
2578
|
|
|
2579
|
+
this.pruneToolEntries(merged);
|
|
2697
2580
|
this.trimOrphanedToolUse(merged);
|
|
2581
|
+
// Strip stale images BEFORE placing markers and committing stats, so both
|
|
2582
|
+
// describe the post-strip context the agent actually receives.
|
|
2583
|
+
this.applyImageStripping(merged, store);
|
|
2584
|
+
// Place ≤4 cache breakpoints across the FINAL ordered entries so the
|
|
2585
|
+
// provider can reuse the stable folded prefix — not just the head. With a
|
|
2586
|
+
// single head marker the cache hit is ~2%; well-placed breakpoints take the
|
|
2587
|
+
// real strategy to ~50% (docs/kv-stable-context-control.md — marker
|
|
2588
|
+
// placement is the dominant KV lever).
|
|
2589
|
+
this.placeCacheMarkers(merged, headMessageIds, tailMessageIds);
|
|
2590
|
+
this.rsEnd();
|
|
2698
2591
|
return merged;
|
|
2699
2592
|
}
|
|
2700
2593
|
|
|
2594
|
+
/**
|
|
2595
|
+
* Place up to four `cache_control` breakpoints across the final ordered
|
|
2596
|
+
* entries: the head/system boundary, the end of the folded history (the
|
|
2597
|
+
* stable prefix that persists turn-to-turn — the most valuable), a mid-history
|
|
2598
|
+
* seam, and the very end (for pure-append reuse). Mirrors `placeMarkers` in
|
|
2599
|
+
* the adaptive layer but operates on emitted entries. Idempotent; clears any
|
|
2600
|
+
* pre-existing markers first.
|
|
2601
|
+
*/
|
|
2602
|
+
protected placeCacheMarkers(
|
|
2603
|
+
entries: ContextEntry[],
|
|
2604
|
+
headMessageIds: ReadonlySet<MessageId>,
|
|
2605
|
+
tailMessageIds: ReadonlySet<MessageId>,
|
|
2606
|
+
): void {
|
|
2607
|
+
for (const e of entries) if (e.cacheMarker) e.cacheMarker = false;
|
|
2608
|
+
const n = entries.length;
|
|
2609
|
+
if (n === 0) return;
|
|
2610
|
+
|
|
2611
|
+
let lastHead = -1;
|
|
2612
|
+
let firstTail = n;
|
|
2613
|
+
for (let i = 0; i < n; i++) {
|
|
2614
|
+
const sid = entries[i].sourceMessageId;
|
|
2615
|
+
if (sid && headMessageIds.has(sid)) lastHead = i;
|
|
2616
|
+
}
|
|
2617
|
+
for (let i = 0; i < n; i++) {
|
|
2618
|
+
const sid = entries[i].sourceMessageId;
|
|
2619
|
+
if (sid && tailMessageIds.has(sid)) { firstTail = i; break; }
|
|
2620
|
+
}
|
|
2621
|
+
const historyEnd = firstTail - 1; // last middle (folded-history) entry
|
|
2622
|
+
|
|
2623
|
+
const marks = new Set<number>();
|
|
2624
|
+
if (lastHead >= 0) marks.add(lastHead); // system / head block
|
|
2625
|
+
if (historyEnd > lastHead) marks.add(historyEnd); // stable folded prefix (the big one)
|
|
2626
|
+
if (historyEnd - lastHead > 2) marks.add(lastHead + Math.floor((historyEnd - lastHead) / 2)); // mid-history
|
|
2627
|
+
marks.add(n - 1); // end → pure-append reuse
|
|
2628
|
+
|
|
2629
|
+
for (const idx of marks) if (idx >= 0 && idx < n) entries[idx].cacheMarker = true;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2701
2632
|
/**
|
|
2702
2633
|
* Walk an entries array; for every run of consecutive entries that
|
|
2703
2634
|
* (a) have sourceRelation: 'copy' (raw, not a synthesized recall pair)
|
|
@@ -2791,6 +2722,23 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2791
2722
|
return this._adaptivePicker;
|
|
2792
2723
|
}
|
|
2793
2724
|
|
|
2725
|
+
/**
|
|
2726
|
+
* Build the picker for this compile. Instance folding strategies (kv-stable)
|
|
2727
|
+
* need the per-compile `PickerInputs` at construction, so they're built fresh
|
|
2728
|
+
* here; stateless ones (flat-profile / oldest-first) reuse the memoized picker.
|
|
2729
|
+
*/
|
|
2730
|
+
protected buildPicker(inputs: PickerInputs): Picker {
|
|
2731
|
+
if (this.config.foldingStrategy === 'kv-stable') {
|
|
2732
|
+
return new Picker(
|
|
2733
|
+
new KvStableStrategy(inputs, {
|
|
2734
|
+
reachTokens: this.config.kvStableReachTokens,
|
|
2735
|
+
mergeThreshold: this.config.mergeThreshold,
|
|
2736
|
+
}),
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
return this.getAdaptivePicker();
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2794
2742
|
/**
|
|
2795
2743
|
* Walk the summary tree to find the L_k ancestor of a message.
|
|
2796
2744
|
* Returns null if no ancestor exists at that level (e.g., L_k not yet produced).
|
|
@@ -2871,6 +2819,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2871
2819
|
* Matches moltbot's budget waterfall: L3 → L2 → L1 with unused budget flowing down.
|
|
2872
2820
|
*/
|
|
2873
2821
|
protected selectHierarchical(store: MessageStoreView, budget: TokenBudget): ContextEntry[] {
|
|
2822
|
+
this.rsBegin();
|
|
2874
2823
|
const entries: ContextEntry[] = [];
|
|
2875
2824
|
const maxTokens = budget.maxTokens - budget.reserveForResponse;
|
|
2876
2825
|
const messages = store.getAll();
|
|
@@ -2895,6 +2844,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
2895
2844
|
content,
|
|
2896
2845
|
});
|
|
2897
2846
|
totalTokens += tokens;
|
|
2847
|
+
this.rsRaw('head', tokens);
|
|
2898
2848
|
}
|
|
2899
2849
|
// Mark the last head entry as a cache boundary (even if budget truncated the window)
|
|
2900
2850
|
if (entries.length > 0) {
|
|
@@ -3054,6 +3004,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3054
3004
|
entries.push(questionEntry);
|
|
3055
3005
|
entries.push(answerEntry);
|
|
3056
3006
|
totalTokens += pairTokens;
|
|
3007
|
+
this.rsSummary(summary.level, pairTokens);
|
|
3057
3008
|
} else {
|
|
3058
3009
|
const msg = item.msg;
|
|
3059
3010
|
const content = msgCap > 0 ? this.truncateContent(msg.content, msgCap) : msg.content;
|
|
@@ -3069,6 +3020,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3069
3020
|
content,
|
|
3070
3021
|
});
|
|
3071
3022
|
totalTokens += tokens;
|
|
3023
|
+
this.rsRaw('middleRaw', tokens);
|
|
3072
3024
|
}
|
|
3073
3025
|
}
|
|
3074
3026
|
} else {
|
|
@@ -3102,6 +3054,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3102
3054
|
entries.push(questionEntry);
|
|
3103
3055
|
entries.push(answerEntry);
|
|
3104
3056
|
totalTokens += pairTokens;
|
|
3057
|
+
for (const s of selectedSummaries) this.rsSummary(s.level, s.tokens);
|
|
3105
3058
|
}
|
|
3106
3059
|
|
|
3107
3060
|
// Sort by position so uncompressed-middle messages and pins both
|
|
@@ -3121,6 +3074,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3121
3074
|
content,
|
|
3122
3075
|
});
|
|
3123
3076
|
totalTokens += tokens;
|
|
3077
|
+
this.rsRaw('middleRaw', tokens);
|
|
3124
3078
|
}
|
|
3125
3079
|
}
|
|
3126
3080
|
}
|
|
@@ -3130,10 +3084,15 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3130
3084
|
// budget, the latest messages (the ones the agent actually needs to act
|
|
3131
3085
|
// on) are preserved and the oldest recent-window messages are dropped.
|
|
3132
3086
|
const effectiveRecentStart = Math.max(recentStart, headEnd);
|
|
3133
|
-
this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
3087
|
+
const tailStats = this.emitRecentNewestFirst(entries, store, messages, effectiveRecentStart, msgCap, maxTokens, totalTokens);
|
|
3088
|
+
this.rsRaw('tail', tailStats.tokens, tailStats.messages);
|
|
3134
3089
|
|
|
3135
3090
|
this.trimOrphanedToolUse(entries);
|
|
3136
3091
|
this.pruneToolEntries(entries);
|
|
3092
|
+
// Strip stale images before committing stats so RenderStats.total reflects
|
|
3093
|
+
// the post-strip context (this path places no cache markers).
|
|
3094
|
+
this.applyImageStripping(entries, store);
|
|
3095
|
+
this.rsEnd();
|
|
3137
3096
|
return entries;
|
|
3138
3097
|
}
|
|
3139
3098
|
|
|
@@ -3388,6 +3347,7 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3388
3347
|
};
|
|
3389
3348
|
|
|
3390
3349
|
const response = await ctx.membrane.complete(request, { formatter: this.nativeFormatter });
|
|
3350
|
+
// Text-only on purpose: summarizer scratch thinking is not agent history
|
|
3391
3351
|
return response.content
|
|
3392
3352
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
3393
3353
|
.map(b => b.text)
|
|
@@ -3561,7 +3521,6 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3561
3521
|
const existing = existingCompressed.get(key);
|
|
3562
3522
|
if (existing) {
|
|
3563
3523
|
chunk.compressed = true;
|
|
3564
|
-
chunk.diary = existing.diary;
|
|
3565
3524
|
chunk.summaryId = existing.summaryId;
|
|
3566
3525
|
}
|
|
3567
3526
|
|
|
@@ -3583,6 +3542,98 @@ export class AutobiographicalStrategy implements ResettableStrategy {
|
|
|
3583
3542
|
return chunk.messages.map((m) => m.id).join(':');
|
|
3584
3543
|
}
|
|
3585
3544
|
|
|
3545
|
+
/** True if any content block is a live image. */
|
|
3546
|
+
protected hasImageBlock(content: ContentBlock[]): boolean {
|
|
3547
|
+
return content.some((b) => b.type === 'image');
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
/** Message index marking the image-strip depth boundary: walks newest→oldest
|
|
3551
|
+
* summing the same per-message estimate as getRecentWindowStart, and returns
|
|
3552
|
+
* the index of the first message still within `depthTokens`. Messages before
|
|
3553
|
+
* this index have their images stripped to placeholders. */
|
|
3554
|
+
protected getImageStripStart(store: MessageStoreView, depthTokens: number): number {
|
|
3555
|
+
const messages = store.getAll();
|
|
3556
|
+
let tokens = 0;
|
|
3557
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3558
|
+
tokens += store.estimateTokens(messages[i]);
|
|
3559
|
+
if (tokens > depthTokens) return i + 1;
|
|
3560
|
+
}
|
|
3561
|
+
return 0;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
/** Text substituted for an image block once it leaves the live-image window. */
|
|
3565
|
+
private static readonly IMAGE_PLACEHOLDER = '[image dropped from live context]';
|
|
3566
|
+
|
|
3567
|
+
/** Post-pass over compiled entries: replace image blocks with a text
|
|
3568
|
+
* placeholder once they fall outside the live-image window — either deeper
|
|
3569
|
+
* than `imageStripDepthTokens` from the newest message, or beyond the
|
|
3570
|
+
* `maxLiveImages` most-recent images (counted newest-first). Summaries are
|
|
3571
|
+
* already text, so they're naturally unaffected. The adjacent
|
|
3572
|
+
* "[image attachment: <name>]" text added at ingest preserves the filename,
|
|
3573
|
+
* so the placeholder itself stays terse. Reduces tokens, so it never pushes
|
|
3574
|
+
* a compiled context back over budget.
|
|
3575
|
+
*
|
|
3576
|
+
* Runs INSIDE each select path, *before* `rsEnd()` and `placeCacheMarkers`,
|
|
3577
|
+
* so the committed render stats (and the cache breakpoints) describe the
|
|
3578
|
+
* post-strip context. As it strips, it decrements the matching raw bucket of
|
|
3579
|
+
* the in-progress render stats by the reclaimed tokens, keeping
|
|
3580
|
+
* `RenderStats.total` equal to the real rendered size. */
|
|
3581
|
+
protected applyImageStripping(entries: ContextEntry[], store: MessageStoreView): void {
|
|
3582
|
+
const maxLive = this.config.maxLiveImages ?? 0; // 0 = unlimited count
|
|
3583
|
+
const depthTokens = this.config.imageStripDepthTokens ?? 0; // 0 = no depth strip
|
|
3584
|
+
if (maxLive === 0 && depthTokens === 0) return; // policy disabled
|
|
3585
|
+
|
|
3586
|
+
const messages = store.getAll();
|
|
3587
|
+
const posById = new Map<string, number>();
|
|
3588
|
+
for (let i = 0; i < messages.length; i++) posById.set(messages[i].id, i);
|
|
3589
|
+
const stripStart = depthTokens > 0 ? this.getImageStripStart(store, depthTokens) : 0;
|
|
3590
|
+
|
|
3591
|
+
// Same region windows select() bucketed by, so a stripped image's reclaimed
|
|
3592
|
+
// tokens come back out of the bucket it was originally tallied into.
|
|
3593
|
+
const headStart = this.getHeadWindowStartIndex(store);
|
|
3594
|
+
const headEnd = this.getHeadWindowEnd(store);
|
|
3595
|
+
const recentStart = Math.max(this.getRecentWindowStart(store), headEnd);
|
|
3596
|
+
const bucketAt = (pos: number): 'head' | 'tail' | 'middleRaw' => {
|
|
3597
|
+
if (pos < 0) return 'middleRaw'; // no resolvable region — keep total == Σbuckets
|
|
3598
|
+
if (pos >= headStart && pos < headEnd) return 'head';
|
|
3599
|
+
if (pos >= recentStart) return 'tail';
|
|
3600
|
+
return 'middleRaw';
|
|
3601
|
+
};
|
|
3602
|
+
const placeholderTokens = Math.ceil(AutobiographicalStrategy.IMAGE_PLACEHOLDER.length / 4);
|
|
3603
|
+
|
|
3604
|
+
// Image-bearing entries, newest-first by source position. Entries with no
|
|
3605
|
+
// resolvable source position sort last (pos -1) and never count as "live".
|
|
3606
|
+
const ordered = entries
|
|
3607
|
+
.map((entry, idx) => ({
|
|
3608
|
+
idx,
|
|
3609
|
+
pos: entry.sourceMessageId !== undefined ? posById.get(entry.sourceMessageId) ?? -1 : -1,
|
|
3610
|
+
}))
|
|
3611
|
+
.filter(({ idx }) => this.hasImageBlock(entries[idx].content))
|
|
3612
|
+
.sort((a, b) => b.pos - a.pos);
|
|
3613
|
+
|
|
3614
|
+
let keptImages = 0;
|
|
3615
|
+
for (const { idx, pos } of ordered) {
|
|
3616
|
+
const entry = entries[idx];
|
|
3617
|
+
const tooDeep = depthTokens > 0 && (pos < 0 || pos < stripStart);
|
|
3618
|
+
const bucket = bucketAt(pos);
|
|
3619
|
+
entry.content = entry.content.map((block) => {
|
|
3620
|
+
if (block.type !== 'image') return block;
|
|
3621
|
+
const overCount = maxLive > 0 && keptImages >= maxLive;
|
|
3622
|
+
if (tooDeep || overCount) {
|
|
3623
|
+
// The renderer estimates an image at `tokenEstimate ?? 1600` (see
|
|
3624
|
+
// message-store/context-log). Hand that back to the bucket, less the
|
|
3625
|
+
// placeholder text that replaces it, so the stats match the output.
|
|
3626
|
+
const reclaimed =
|
|
3627
|
+
((block as { tokenEstimate?: number }).tokenEstimate ?? 1600) - placeholderTokens;
|
|
3628
|
+
if (this._rs && reclaimed > 0) this._rs[bucket].tokens -= reclaimed;
|
|
3629
|
+
return { type: 'text', text: AutobiographicalStrategy.IMAGE_PLACEHOLDER } as ContentBlock;
|
|
3630
|
+
}
|
|
3631
|
+
keptImages++;
|
|
3632
|
+
return block;
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3586
3637
|
protected getRecentWindowStart(store: MessageStoreView): number {
|
|
3587
3638
|
const messages = store.getAll();
|
|
3588
3639
|
let tokens = 0;
|