ds4-context-engine 0.3.0-alpha.4 → 0.3.0-beta.1
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/README.md +25 -11
- package/docs/ADR/058-bounded-manifest-storage.md +31 -0
- package/docs/ADR/README.md +1 -0
- package/docs/ARCHITECTURE.md +14 -9
- package/docs/COMPACTION.md +8 -8
- package/docs/CONTEXT_MANIFEST.md +10 -1
- package/docs/CONTEXT_PERSISTENCE_TOOL.md +2 -2
- package/docs/DOGFOODING_0.3.0_ALPHA.md +36 -10
- package/docs/DOGFOODING_0.3.0_BETA.md +331 -0
- package/docs/RELEASE_READINESS_0.2.0.md +1 -1
- package/docs/RELEASING.md +5 -5
- package/docs/STORAGE.md +16 -2
- package/docs/STORAGE_MAINTENANCE.md +121 -0
- package/docs/releases/0.3.0-alpha.4.md +5 -3
- package/docs/releases/0.3.0-alpha.5.md +60 -0
- package/docs/releases/0.3.0-beta.1.md +81 -0
- package/package.json +6 -2
- package/scripts/ds4-context-storage.mjs +148 -0
- package/src/extension/commands.ts +83 -2
- package/src/extension/runtime.ts +81 -24
- package/src/pi-adapter/compaction-adapter.ts +107 -23
- package/src/pi-adapter/compaction-coordinator.ts +425 -117
- package/src/pi-adapter/summary-generator.ts +140 -22
- package/src/pi-adapter/version.ts +1 -1
|
@@ -14,7 +14,14 @@ import {
|
|
|
14
14
|
highestClassification,
|
|
15
15
|
type PrivacyClassification,
|
|
16
16
|
} from "ds4-context-core/privacy/privacy-policy";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
prepareCompactionSource,
|
|
19
|
+
sliceCompactionSource,
|
|
20
|
+
type PreparedCompactionSource,
|
|
21
|
+
type PreparedCompactionSourceSlice,
|
|
22
|
+
} from "./compaction-adapter.ts";
|
|
23
|
+
import { buildCompactionAtomicGroups } from "ds4-context-core/compaction/segmentation";
|
|
24
|
+
import { estimateMessageTokens } from "ds4-context-core/core/token-estimator";
|
|
18
25
|
import { adaptiveRecentTailLimit } from "ds4-context-core/planner/context-planner";
|
|
19
26
|
import type { Logger } from "ds4-context-core/shared/logging";
|
|
20
27
|
import {
|
|
@@ -42,6 +49,10 @@ import {
|
|
|
42
49
|
type SummaryValidationStatus,
|
|
43
50
|
} from "ds4-context-core/compaction/summary-contract";
|
|
44
51
|
|
|
52
|
+
const MAX_SEGMENT_REQUESTS = 32;
|
|
53
|
+
const MAX_AGGREGATE_REQUESTS = 64;
|
|
54
|
+
const MAX_AGGREGATE_LEVELS = 16;
|
|
55
|
+
|
|
45
56
|
export type CompactionPhase =
|
|
46
57
|
| "idle"
|
|
47
58
|
| "requested"
|
|
@@ -66,6 +77,11 @@ export interface CompactionDiagnostics {
|
|
|
66
77
|
requestedAt?: number;
|
|
67
78
|
completedAt?: number;
|
|
68
79
|
lastError?: string;
|
|
80
|
+
inputBudgetTokens?: number;
|
|
81
|
+
sourcePromptTokens?: number;
|
|
82
|
+
segmentCount?: number;
|
|
83
|
+
aggregateCalls?: number;
|
|
84
|
+
transportRetries?: number;
|
|
69
85
|
contextTokens?: number;
|
|
70
86
|
softLimitTokens?: number;
|
|
71
87
|
proactiveThresholdTokens?: number;
|
|
@@ -108,6 +124,26 @@ export interface SessionCompactFailedLike {
|
|
|
108
124
|
fromExtension: boolean;
|
|
109
125
|
}
|
|
110
126
|
|
|
127
|
+
interface SegmentGenerationPlan {
|
|
128
|
+
source: PreparedCompactionSourceSlice;
|
|
129
|
+
prompt: string;
|
|
130
|
+
validationSource: string;
|
|
131
|
+
readFiles: string[];
|
|
132
|
+
modifiedFiles: string[];
|
|
133
|
+
classification: PrivacyClassification;
|
|
134
|
+
promptTokens: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface AggregateGenerationPlan {
|
|
138
|
+
children: EmbeddedSummaryNode[];
|
|
139
|
+
prompt: string;
|
|
140
|
+
validationSource: string;
|
|
141
|
+
readFiles: string[];
|
|
142
|
+
modifiedFiles: string[];
|
|
143
|
+
classification: PrivacyClassification;
|
|
144
|
+
promptTokens: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
111
147
|
interface CompactionCoordinatorDependencies {
|
|
112
148
|
config: Ds4ContextConfig;
|
|
113
149
|
database?: ContextDatabase;
|
|
@@ -176,7 +212,8 @@ export class CompactionCoordinator {
|
|
|
176
212
|
ctx: ExtensionContext,
|
|
177
213
|
): Promise<{ compaction?: CompactionResult } | undefined> {
|
|
178
214
|
const config = this.dependencies.config;
|
|
179
|
-
|
|
215
|
+
const model = ctx.model;
|
|
216
|
+
if (!config.compaction.enabled || !config.enabled || !model || config.context.maxSummaryTokens <= 0) {
|
|
180
217
|
return undefined;
|
|
181
218
|
}
|
|
182
219
|
|
|
@@ -188,12 +225,30 @@ export class CompactionCoordinator {
|
|
|
188
225
|
requestedAt,
|
|
189
226
|
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
190
227
|
tokensBefore: event.preparation.tokensBefore,
|
|
228
|
+
transportRetries: 0,
|
|
191
229
|
};
|
|
192
230
|
|
|
193
231
|
try {
|
|
194
232
|
this.dependencies.syncSessionIndex(ctx);
|
|
195
233
|
const source = prepareCompactionSource(event);
|
|
196
|
-
|
|
234
|
+
const inputBudgetTokens = this.inputBudgetTokens(model);
|
|
235
|
+
if (inputBudgetTokens <= 0) throw new Error("Active model has no safe compaction input budget");
|
|
236
|
+
const wholePlan = this.buildSegmentPlan(source, event, model.provider);
|
|
237
|
+
this.state = {
|
|
238
|
+
...this.state,
|
|
239
|
+
sourceEntries: source.sourceEntryIds.length,
|
|
240
|
+
inputBudgetTokens,
|
|
241
|
+
sourcePromptTokens: wholePlan.promptTokens,
|
|
242
|
+
};
|
|
243
|
+
const segmentPlans = this.partitionSegmentPlans(
|
|
244
|
+
source,
|
|
245
|
+
wholePlan,
|
|
246
|
+
event,
|
|
247
|
+
model.provider,
|
|
248
|
+
inputBudgetTokens,
|
|
249
|
+
);
|
|
250
|
+
this.state.segmentCount = segmentPlans.length;
|
|
251
|
+
|
|
197
252
|
const usedIds = new Set(this.graphRecords.keys());
|
|
198
253
|
if (source.previousNode) usedIds.add(source.previousNode.id);
|
|
199
254
|
const nextId = (): string => {
|
|
@@ -208,129 +263,70 @@ export class CompactionCoordinator {
|
|
|
208
263
|
reason: trigger,
|
|
209
264
|
isSplitTurn: event.preparation.isSplitTurn,
|
|
210
265
|
messageCount: source.messages.length,
|
|
211
|
-
provider:
|
|
212
|
-
model:
|
|
213
|
-
};
|
|
214
|
-
const classifyContent = (text: string): { value: string; classification: PrivacyClassification } => {
|
|
215
|
-
const classified = this.dependencies.classifyContent?.(text, ctx.model!.provider);
|
|
216
|
-
if (classified) return classified;
|
|
217
|
-
return {
|
|
218
|
-
value: this.dependencies.sanitizeContent?.(text, ctx.model!.provider) ?? text,
|
|
219
|
-
classification: "normal",
|
|
220
|
-
};
|
|
221
|
-
};
|
|
222
|
-
const sourcePrivacy = classifyContent(source.conversationText);
|
|
223
|
-
const validationPrivacy = classifyContent(source.sourceText);
|
|
224
|
-
const instructionPrivacy = event.customInstructions
|
|
225
|
-
? classifyContent(event.customInstructions)
|
|
226
|
-
: undefined;
|
|
227
|
-
const readFilePrivacy = source.segmentReadFiles.map(classifyContent);
|
|
228
|
-
const modifiedFilePrivacy = source.segmentModifiedFiles.map(classifyContent);
|
|
229
|
-
const segmentClassification = [
|
|
230
|
-
sourcePrivacy.classification,
|
|
231
|
-
validationPrivacy.classification,
|
|
232
|
-
...(instructionPrivacy ? [instructionPrivacy.classification] : []),
|
|
233
|
-
...readFilePrivacy.map((item) => item.classification),
|
|
234
|
-
...modifiedFilePrivacy.map((item) => item.classification),
|
|
235
|
-
].reduce(highestClassification, "normal" as PrivacyClassification);
|
|
236
|
-
const segmentGenerated = await this.generateSummary({
|
|
237
|
-
stage: "segment",
|
|
238
|
-
prompt: buildSummaryPrompt({
|
|
239
|
-
conversationText: sourcePrivacy.value,
|
|
240
|
-
...(instructionPrivacy ? { customInstructions: instructionPrivacy.value } : {}),
|
|
241
|
-
readFiles: readFilePrivacy.map((item) => item.value),
|
|
242
|
-
modifiedFiles: modifiedFilePrivacy.map((item) => item.value),
|
|
243
|
-
isSplitTurn: event.preparation.isSplitTurn,
|
|
244
|
-
}),
|
|
245
|
-
validationSource: validationPrivacy.value,
|
|
246
|
-
readFiles: readFilePrivacy.map((item) => item.value),
|
|
247
|
-
modifiedFiles: modifiedFilePrivacy.map((item) => item.value),
|
|
248
|
-
event,
|
|
249
|
-
ctx,
|
|
250
|
-
});
|
|
251
|
-
const segmentId = nextId();
|
|
252
|
-
const segmentNode: EmbeddedSummaryNode = {
|
|
253
|
-
id: segmentId,
|
|
254
|
-
kind: "segment",
|
|
255
|
-
content: classifiedSummary(segmentGenerated.content, segmentClassification),
|
|
256
|
-
sourceHash: source.sourceHash,
|
|
257
|
-
sourceEntryIds: [...source.sourceEntryIds],
|
|
258
|
-
childSummaryIds: [],
|
|
259
|
-
graphLevel: 0,
|
|
260
|
-
createdAt: this.dependencies.now(),
|
|
261
|
-
validationStatus: segmentGenerated.validation.status,
|
|
262
|
-
validationIssueCodes: unique(segmentGenerated.validation.issues.map((issue) => issue.code)),
|
|
263
|
-
provider: ctx.model.provider,
|
|
264
|
-
model: ctx.model.id,
|
|
266
|
+
provider: model.provider,
|
|
267
|
+
model: model.id,
|
|
265
268
|
};
|
|
266
269
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
const usages: GeneratedSummary["usage"][] = [];
|
|
271
|
+
const segmentNodes: EmbeddedSummaryNode[] = [];
|
|
272
|
+
for (const plan of segmentPlans) {
|
|
273
|
+
const generated = await this.generateSummary({
|
|
274
|
+
stage: "segment",
|
|
275
|
+
prompt: plan.prompt,
|
|
276
|
+
validationSource: plan.validationSource,
|
|
277
|
+
readFiles: plan.readFiles,
|
|
278
|
+
modifiedFiles: plan.modifiedFiles,
|
|
279
|
+
event,
|
|
280
|
+
ctx,
|
|
281
|
+
});
|
|
282
|
+
usages.push(generated.usage);
|
|
283
|
+
segmentNodes.push({
|
|
271
284
|
id: nextId(),
|
|
272
|
-
|
|
285
|
+
kind: "segment",
|
|
286
|
+
content: classifiedSummary(generated.content, plan.classification),
|
|
287
|
+
sourceHash: plan.source.sourceHash,
|
|
288
|
+
sourceEntryIds: [...plan.source.sourceEntryIds],
|
|
289
|
+
childSummaryIds: [],
|
|
290
|
+
graphLevel: 0,
|
|
273
291
|
createdAt: this.dependencies.now(),
|
|
274
|
-
|
|
275
|
-
|
|
292
|
+
validationStatus: generated.validation.status,
|
|
293
|
+
validationIssueCodes: unique(generated.validation.issues.map((issue) => issue.code)),
|
|
294
|
+
provider: model.provider,
|
|
295
|
+
model: model.id,
|
|
276
296
|
});
|
|
277
|
-
embeddedNodes.push(previousNode);
|
|
278
297
|
}
|
|
298
|
+
const segmentId = segmentNodes[0]?.id;
|
|
299
|
+
if (!segmentId) throw new Error("Compaction fan-out produced no segment summary node");
|
|
279
300
|
|
|
280
|
-
const
|
|
281
|
-
let
|
|
282
|
-
if (previousNode) {
|
|
283
|
-
|
|
284
|
-
const classifiedChildren = aggregateChildren.map((child) => ({
|
|
285
|
-
child,
|
|
286
|
-
privacy: classifyContent(child.content),
|
|
287
|
-
}));
|
|
288
|
-
const promptChildren = classifiedChildren.map(({ child, privacy }) => ({
|
|
289
|
-
...child,
|
|
290
|
-
content: privacy.value,
|
|
291
|
-
}));
|
|
292
|
-
const aggregateReadFiles = source.readFiles.map(classifyContent);
|
|
293
|
-
const aggregateModifiedFiles = source.modifiedFiles.map(classifyContent);
|
|
294
|
-
const aggregateInstruction = event.customInstructions
|
|
295
|
-
? classifyContent(event.customInstructions)
|
|
296
|
-
: undefined;
|
|
297
|
-
const aggregateClassification = [
|
|
298
|
-
...classifiedChildren.map((item) => item.privacy.classification),
|
|
299
|
-
...(aggregateInstruction ? [aggregateInstruction.classification] : []),
|
|
300
|
-
...aggregateReadFiles.map((item) => item.classification),
|
|
301
|
-
...aggregateModifiedFiles.map((item) => item.classification),
|
|
302
|
-
].reduce(highestClassification, "normal" as PrivacyClassification);
|
|
303
|
-
const aggregateGenerated = await this.generateSummary({
|
|
304
|
-
stage: "aggregate",
|
|
305
|
-
prompt: buildAggregateSummaryPrompt({
|
|
306
|
-
children: promptChildren,
|
|
307
|
-
...(aggregateInstruction ? { customInstructions: aggregateInstruction.value } : {}),
|
|
308
|
-
readFiles: aggregateReadFiles.map((item) => item.value),
|
|
309
|
-
modifiedFiles: aggregateModifiedFiles.map((item) => item.value),
|
|
310
|
-
}),
|
|
311
|
-
validationSource: promptChildren.map((child) => child.content).join("\n\n"),
|
|
312
|
-
readFiles: aggregateReadFiles.map((item) => item.value),
|
|
313
|
-
modifiedFiles: aggregateModifiedFiles.map((item) => item.value),
|
|
314
|
-
event,
|
|
315
|
-
ctx,
|
|
316
|
-
});
|
|
317
|
-
usages.push(aggregateGenerated.usage);
|
|
318
|
-
activeNode = {
|
|
301
|
+
const createdNodes: EmbeddedSummaryNode[] = [];
|
|
302
|
+
let previousNode = source.previousNode;
|
|
303
|
+
if (!previousNode && source.previousSummary) {
|
|
304
|
+
previousNode = importUntrackedPreviousSummary({
|
|
319
305
|
id: nextId(),
|
|
320
|
-
|
|
321
|
-
content: classifiedSummary(aggregateGenerated.content, aggregateClassification),
|
|
322
|
-
sourceHash: computeAggregateSourceHash(aggregateChildren),
|
|
323
|
-
sourceEntryIds: unique(aggregateChildren.flatMap((child) => child.sourceEntryIds)),
|
|
324
|
-
childSummaryIds: aggregateChildren.map((child) => child.id),
|
|
325
|
-
graphLevel: Math.max(...aggregateChildren.map((child) => child.graphLevel)) + 1,
|
|
306
|
+
content: source.previousSummary,
|
|
326
307
|
createdAt: this.dependencies.now(),
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
};
|
|
332
|
-
embeddedNodes.push(segmentNode);
|
|
308
|
+
provider: model.provider,
|
|
309
|
+
model: model.id,
|
|
310
|
+
});
|
|
311
|
+
createdNodes.push(previousNode);
|
|
333
312
|
}
|
|
313
|
+
createdNodes.push(...segmentNodes);
|
|
314
|
+
const roots = [...(previousNode ? [previousNode] : []), ...segmentNodes];
|
|
315
|
+
const aggregated = await this.aggregateSummaryNodes({
|
|
316
|
+
roots,
|
|
317
|
+
event,
|
|
318
|
+
ctx,
|
|
319
|
+
provider: model.provider,
|
|
320
|
+
model: model.id,
|
|
321
|
+
readFiles: source.readFiles,
|
|
322
|
+
modifiedFiles: source.modifiedFiles,
|
|
323
|
+
inputBudgetTokens,
|
|
324
|
+
nextId,
|
|
325
|
+
createdNodes,
|
|
326
|
+
usages,
|
|
327
|
+
});
|
|
328
|
+
const activeNode = aggregated.activeNode;
|
|
329
|
+
const embeddedNodes = createdNodes.filter((node) => node.id !== activeNode.id);
|
|
334
330
|
|
|
335
331
|
const records = embeddedNodes.map((node) => createSummaryRecord({
|
|
336
332
|
sessionId: this.dependencies.sessionId,
|
|
@@ -359,6 +355,11 @@ export class CompactionCoordinator {
|
|
|
359
355
|
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
360
356
|
tokensBefore: event.preparation.tokensBefore,
|
|
361
357
|
requestedAt,
|
|
358
|
+
inputBudgetTokens,
|
|
359
|
+
sourcePromptTokens: wholePlan.promptTokens,
|
|
360
|
+
segmentCount: segmentPlans.length,
|
|
361
|
+
aggregateCalls: aggregated.aggregateCalls,
|
|
362
|
+
transportRetries: this.state.transportRetries ?? 0,
|
|
362
363
|
};
|
|
363
364
|
this.dependencies.logger.debug("compaction.summary_graph_prepared", {
|
|
364
365
|
activeSummaryId: activeNode.id,
|
|
@@ -367,6 +368,11 @@ export class CompactionCoordinator {
|
|
|
367
368
|
createdNodes: records.length,
|
|
368
369
|
sourceEntries: activeNode.sourceEntryIds.length,
|
|
369
370
|
validationStatus: activeNode.validationStatus,
|
|
371
|
+
inputBudgetTokens,
|
|
372
|
+
sourcePromptTokens: wholePlan.promptTokens,
|
|
373
|
+
segmentCount: segmentPlans.length,
|
|
374
|
+
aggregateCalls: aggregated.aggregateCalls,
|
|
375
|
+
transportRetries: this.state.transportRetries ?? 0,
|
|
370
376
|
trigger,
|
|
371
377
|
});
|
|
372
378
|
const activeRecord = records.at(-1);
|
|
@@ -420,13 +426,20 @@ export class CompactionCoordinator {
|
|
|
420
426
|
: undefined;
|
|
421
427
|
if (!details || !effectiveEntry) {
|
|
422
428
|
this.failPendingNodes();
|
|
423
|
-
const
|
|
429
|
+
const attempt = this.state;
|
|
430
|
+
const customError = attempt.lastError;
|
|
424
431
|
this.state = {
|
|
425
432
|
phase: "pi-default",
|
|
426
433
|
trigger: event.reason,
|
|
427
434
|
firstKeptEntryId: event.compactionEntry.firstKeptEntryId,
|
|
428
435
|
tokensBefore: event.compactionEntry.tokensBefore,
|
|
429
436
|
completedAt: this.dependencies.now(),
|
|
437
|
+
...(attempt.sourceEntries !== undefined ? { sourceEntries: attempt.sourceEntries } : {}),
|
|
438
|
+
...(attempt.inputBudgetTokens !== undefined ? { inputBudgetTokens: attempt.inputBudgetTokens } : {}),
|
|
439
|
+
...(attempt.sourcePromptTokens !== undefined ? { sourcePromptTokens: attempt.sourcePromptTokens } : {}),
|
|
440
|
+
...(attempt.segmentCount !== undefined ? { segmentCount: attempt.segmentCount } : {}),
|
|
441
|
+
...(attempt.aggregateCalls !== undefined ? { aggregateCalls: attempt.aggregateCalls } : {}),
|
|
442
|
+
...(attempt.transportRetries !== undefined ? { transportRetries: attempt.transportRetries } : {}),
|
|
430
443
|
...(customError ? { lastError: `Custom fallback: ${customError}` } : {}),
|
|
431
444
|
};
|
|
432
445
|
return;
|
|
@@ -459,6 +472,17 @@ export class CompactionCoordinator {
|
|
|
459
472
|
tokensBefore: effectiveEntry.tokensBefore,
|
|
460
473
|
requestedAt: metadata.generatedAt,
|
|
461
474
|
completedAt: this.dependencies.now(),
|
|
475
|
+
...(this.state.inputBudgetTokens !== undefined
|
|
476
|
+
? { inputBudgetTokens: this.state.inputBudgetTokens }
|
|
477
|
+
: {}),
|
|
478
|
+
...(this.state.sourcePromptTokens !== undefined
|
|
479
|
+
? { sourcePromptTokens: this.state.sourcePromptTokens }
|
|
480
|
+
: {}),
|
|
481
|
+
...(this.state.segmentCount !== undefined ? { segmentCount: this.state.segmentCount } : {}),
|
|
482
|
+
...(this.state.aggregateCalls !== undefined ? { aggregateCalls: this.state.aggregateCalls } : {}),
|
|
483
|
+
...(this.state.transportRetries !== undefined
|
|
484
|
+
? { transportRetries: this.state.transportRetries }
|
|
485
|
+
: {}),
|
|
462
486
|
};
|
|
463
487
|
this.dependencies.logger.debug("compaction.summary_graph_committed", {
|
|
464
488
|
activeSummaryId: metadata.summaryId,
|
|
@@ -640,6 +664,286 @@ export class CompactionCoordinator {
|
|
|
640
664
|
};
|
|
641
665
|
}
|
|
642
666
|
|
|
667
|
+
private inputBudgetTokens(model: ModelDescriptor): number {
|
|
668
|
+
const config = this.dependencies.config;
|
|
669
|
+
const resolved = this.dependencies.resolveModelBudget?.(model)
|
|
670
|
+
?? {
|
|
671
|
+
budget: calculateContextBudget(createModelProfile(model), config.context),
|
|
672
|
+
recentTailTokens: adaptiveRecentTailLimit(
|
|
673
|
+
model.contextWindow,
|
|
674
|
+
config.context.recentTailTokens,
|
|
675
|
+
),
|
|
676
|
+
};
|
|
677
|
+
return Math.floor(resolved.budget.activeInputBudget);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private classify(text: string, provider: string): {
|
|
681
|
+
value: string;
|
|
682
|
+
classification: PrivacyClassification;
|
|
683
|
+
} {
|
|
684
|
+
const classified = this.dependencies.classifyContent?.(text, provider);
|
|
685
|
+
if (classified) return classified;
|
|
686
|
+
return {
|
|
687
|
+
value: this.dependencies.sanitizeContent?.(text, provider) ?? text,
|
|
688
|
+
classification: "normal",
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private promptTokens(prompt: string): number {
|
|
693
|
+
return estimateMessageTokens({
|
|
694
|
+
role: "user",
|
|
695
|
+
content: [{ type: "text", text: prompt }],
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private buildSegmentPlan(
|
|
700
|
+
source: PreparedCompactionSourceSlice,
|
|
701
|
+
event: SessionBeforeCompactEvent,
|
|
702
|
+
provider: string,
|
|
703
|
+
): SegmentGenerationPlan {
|
|
704
|
+
const sourcePrivacy = this.classify(source.conversationText, provider);
|
|
705
|
+
const validationPrivacy = this.classify(source.sourceText, provider);
|
|
706
|
+
const instructionPrivacy = event.customInstructions
|
|
707
|
+
? this.classify(event.customInstructions, provider)
|
|
708
|
+
: undefined;
|
|
709
|
+
const readFilePrivacy = source.segmentReadFiles.map((file) => this.classify(file, provider));
|
|
710
|
+
const modifiedFilePrivacy = source.segmentModifiedFiles.map((file) => this.classify(file, provider));
|
|
711
|
+
const classification = [
|
|
712
|
+
sourcePrivacy.classification,
|
|
713
|
+
validationPrivacy.classification,
|
|
714
|
+
...(instructionPrivacy ? [instructionPrivacy.classification] : []),
|
|
715
|
+
...readFilePrivacy.map((item) => item.classification),
|
|
716
|
+
...modifiedFilePrivacy.map((item) => item.classification),
|
|
717
|
+
].reduce(highestClassification, "normal" as PrivacyClassification);
|
|
718
|
+
const readFiles = readFilePrivacy.map((item) => item.value);
|
|
719
|
+
const modifiedFiles = modifiedFilePrivacy.map((item) => item.value);
|
|
720
|
+
const prompt = buildSummaryPrompt({
|
|
721
|
+
conversationText: sourcePrivacy.value,
|
|
722
|
+
...(instructionPrivacy ? { customInstructions: instructionPrivacy.value } : {}),
|
|
723
|
+
readFiles,
|
|
724
|
+
modifiedFiles,
|
|
725
|
+
isSplitTurn: source.isSplitTurn,
|
|
726
|
+
});
|
|
727
|
+
return {
|
|
728
|
+
source,
|
|
729
|
+
prompt,
|
|
730
|
+
validationSource: validationPrivacy.value,
|
|
731
|
+
readFiles,
|
|
732
|
+
modifiedFiles,
|
|
733
|
+
classification,
|
|
734
|
+
promptTokens: this.promptTokens(prompt),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private partitionSegmentPlans(
|
|
739
|
+
source: PreparedCompactionSource,
|
|
740
|
+
wholePlan: SegmentGenerationPlan,
|
|
741
|
+
event: SessionBeforeCompactEvent,
|
|
742
|
+
provider: string,
|
|
743
|
+
inputBudgetTokens: number,
|
|
744
|
+
): SegmentGenerationPlan[] {
|
|
745
|
+
if (wholePlan.promptTokens <= inputBudgetTokens) return [wholePlan];
|
|
746
|
+
|
|
747
|
+
const groups = buildCompactionAtomicGroups(source.messages);
|
|
748
|
+
const plans: SegmentGenerationPlan[] = [];
|
|
749
|
+
let currentIndices: number[] = [];
|
|
750
|
+
let currentPlan: SegmentGenerationPlan | undefined;
|
|
751
|
+
|
|
752
|
+
for (const group of groups) {
|
|
753
|
+
const candidateIndices = [...currentIndices, ...group.messageIndices];
|
|
754
|
+
const candidatePlan = this.buildSegmentPlan(
|
|
755
|
+
sliceCompactionSource(source, candidateIndices),
|
|
756
|
+
event,
|
|
757
|
+
provider,
|
|
758
|
+
);
|
|
759
|
+
if (candidatePlan.promptTokens <= inputBudgetTokens) {
|
|
760
|
+
currentIndices = candidateIndices;
|
|
761
|
+
currentPlan = candidatePlan;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (currentPlan) {
|
|
766
|
+
plans.push(currentPlan);
|
|
767
|
+
if (plans.length >= MAX_SEGMENT_REQUESTS) {
|
|
768
|
+
throw new Error(`Compaction fan-out exceeded the bounded segment request limit (${MAX_SEGMENT_REQUESTS})`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const atomicPlan = this.buildSegmentPlan(
|
|
772
|
+
sliceCompactionSource(source, group.messageIndices),
|
|
773
|
+
event,
|
|
774
|
+
provider,
|
|
775
|
+
);
|
|
776
|
+
if (atomicPlan.promptTokens > inputBudgetTokens) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`Compaction source contains an indivisible atomic group above the model input budget (promptTokens=${atomicPlan.promptTokens}; inputBudgetTokens=${inputBudgetTokens})`,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
currentIndices = [...group.messageIndices];
|
|
782
|
+
currentPlan = atomicPlan;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (currentPlan) plans.push(currentPlan);
|
|
786
|
+
if (plans.length === 0) throw new Error("Compaction fan-out produced no source segments");
|
|
787
|
+
if (plans.length > MAX_SEGMENT_REQUESTS) {
|
|
788
|
+
throw new Error(`Compaction fan-out exceeded the bounded segment request limit (${MAX_SEGMENT_REQUESTS})`);
|
|
789
|
+
}
|
|
790
|
+
return plans;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private buildAggregatePlan(
|
|
794
|
+
children: readonly EmbeddedSummaryNode[],
|
|
795
|
+
event: SessionBeforeCompactEvent,
|
|
796
|
+
provider: string,
|
|
797
|
+
readFiles: readonly string[],
|
|
798
|
+
modifiedFiles: readonly string[],
|
|
799
|
+
): AggregateGenerationPlan {
|
|
800
|
+
const classifiedChildren = children.map((child) => ({
|
|
801
|
+
child,
|
|
802
|
+
privacy: this.classify(child.content, provider),
|
|
803
|
+
}));
|
|
804
|
+
const promptChildren = classifiedChildren.map(({ child, privacy }) => ({
|
|
805
|
+
...child,
|
|
806
|
+
content: privacy.value,
|
|
807
|
+
}));
|
|
808
|
+
const aggregateReadFiles = readFiles.map((file) => this.classify(file, provider));
|
|
809
|
+
const aggregateModifiedFiles = modifiedFiles.map((file) => this.classify(file, provider));
|
|
810
|
+
const aggregateInstruction = event.customInstructions
|
|
811
|
+
? this.classify(event.customInstructions, provider)
|
|
812
|
+
: undefined;
|
|
813
|
+
const classification = [
|
|
814
|
+
...classifiedChildren.map((item) => item.privacy.classification),
|
|
815
|
+
...(aggregateInstruction ? [aggregateInstruction.classification] : []),
|
|
816
|
+
...aggregateReadFiles.map((item) => item.classification),
|
|
817
|
+
...aggregateModifiedFiles.map((item) => item.classification),
|
|
818
|
+
].reduce(highestClassification, "normal" as PrivacyClassification);
|
|
819
|
+
const sanitizedReadFiles = aggregateReadFiles.map((item) => item.value);
|
|
820
|
+
const sanitizedModifiedFiles = aggregateModifiedFiles.map((item) => item.value);
|
|
821
|
+
const prompt = buildAggregateSummaryPrompt({
|
|
822
|
+
children: promptChildren,
|
|
823
|
+
...(aggregateInstruction ? { customInstructions: aggregateInstruction.value } : {}),
|
|
824
|
+
readFiles: sanitizedReadFiles,
|
|
825
|
+
modifiedFiles: sanitizedModifiedFiles,
|
|
826
|
+
});
|
|
827
|
+
return {
|
|
828
|
+
children: [...children],
|
|
829
|
+
prompt,
|
|
830
|
+
validationSource: promptChildren.map((child) => child.content).join("\n\n"),
|
|
831
|
+
readFiles: sanitizedReadFiles,
|
|
832
|
+
modifiedFiles: sanitizedModifiedFiles,
|
|
833
|
+
classification,
|
|
834
|
+
promptTokens: this.promptTokens(prompt),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private async aggregateSummaryNodes(input: {
|
|
839
|
+
roots: EmbeddedSummaryNode[];
|
|
840
|
+
event: SessionBeforeCompactEvent;
|
|
841
|
+
ctx: ExtensionContext;
|
|
842
|
+
provider: string;
|
|
843
|
+
model: string;
|
|
844
|
+
readFiles: readonly string[];
|
|
845
|
+
modifiedFiles: readonly string[];
|
|
846
|
+
inputBudgetTokens: number;
|
|
847
|
+
nextId: () => string;
|
|
848
|
+
createdNodes: EmbeddedSummaryNode[];
|
|
849
|
+
usages: GeneratedSummary["usage"][];
|
|
850
|
+
}): Promise<{ activeNode: EmbeddedSummaryNode; aggregateCalls: number }> {
|
|
851
|
+
let layer = [...input.roots];
|
|
852
|
+
let aggregateCalls = 0;
|
|
853
|
+
let graphPasses = 0;
|
|
854
|
+
|
|
855
|
+
while (layer.length > 1) {
|
|
856
|
+
graphPasses++;
|
|
857
|
+
if (graphPasses > MAX_AGGREGATE_LEVELS) {
|
|
858
|
+
throw new Error(`Compaction aggregation exceeded the bounded graph depth (${MAX_AGGREGATE_LEVELS})`);
|
|
859
|
+
}
|
|
860
|
+
const batches: EmbeddedSummaryNode[][] = [];
|
|
861
|
+
let current: EmbeddedSummaryNode[] = [];
|
|
862
|
+
for (const node of layer) {
|
|
863
|
+
if (current.length === 0) {
|
|
864
|
+
current = [node];
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const candidate = [...current, node];
|
|
868
|
+
const candidatePlan = this.buildAggregatePlan(
|
|
869
|
+
candidate,
|
|
870
|
+
input.event,
|
|
871
|
+
input.provider,
|
|
872
|
+
input.readFiles,
|
|
873
|
+
input.modifiedFiles,
|
|
874
|
+
);
|
|
875
|
+
if (candidatePlan.promptTokens <= input.inputBudgetTokens) {
|
|
876
|
+
current = candidate;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (current.length === 1) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`Compaction child summaries cannot be aggregated within the model input budget (inputBudgetTokens=${input.inputBudgetTokens})`,
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
batches.push(current);
|
|
885
|
+
current = [node];
|
|
886
|
+
}
|
|
887
|
+
if (current.length > 0) batches.push(current);
|
|
888
|
+
|
|
889
|
+
const nextLayer: EmbeddedSummaryNode[] = [];
|
|
890
|
+
for (const batch of batches) {
|
|
891
|
+
if (batch.length === 1) {
|
|
892
|
+
nextLayer.push(batch[0] as EmbeddedSummaryNode);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
aggregateCalls++;
|
|
896
|
+
if (aggregateCalls > MAX_AGGREGATE_REQUESTS) {
|
|
897
|
+
throw new Error(`Compaction fan-in exceeded the bounded aggregate request limit (${MAX_AGGREGATE_REQUESTS})`);
|
|
898
|
+
}
|
|
899
|
+
const plan = this.buildAggregatePlan(
|
|
900
|
+
batch,
|
|
901
|
+
input.event,
|
|
902
|
+
input.provider,
|
|
903
|
+
input.readFiles,
|
|
904
|
+
input.modifiedFiles,
|
|
905
|
+
);
|
|
906
|
+
if (plan.promptTokens > input.inputBudgetTokens) {
|
|
907
|
+
throw new Error("Compaction aggregate prompt exceeded its preflight input budget");
|
|
908
|
+
}
|
|
909
|
+
const generated = await this.generateSummary({
|
|
910
|
+
stage: "aggregate",
|
|
911
|
+
prompt: plan.prompt,
|
|
912
|
+
validationSource: plan.validationSource,
|
|
913
|
+
readFiles: plan.readFiles,
|
|
914
|
+
modifiedFiles: plan.modifiedFiles,
|
|
915
|
+
event: input.event,
|
|
916
|
+
ctx: input.ctx,
|
|
917
|
+
});
|
|
918
|
+
input.usages.push(generated.usage);
|
|
919
|
+
const aggregateNode: EmbeddedSummaryNode = {
|
|
920
|
+
id: input.nextId(),
|
|
921
|
+
kind: "aggregate",
|
|
922
|
+
content: classifiedSummary(generated.content, plan.classification),
|
|
923
|
+
sourceHash: computeAggregateSourceHash(batch),
|
|
924
|
+
sourceEntryIds: unique(batch.flatMap((child) => child.sourceEntryIds)),
|
|
925
|
+
childSummaryIds: batch.map((child) => child.id),
|
|
926
|
+
graphLevel: Math.max(...batch.map((child) => child.graphLevel)) + 1,
|
|
927
|
+
createdAt: this.dependencies.now(),
|
|
928
|
+
validationStatus: generated.validation.status,
|
|
929
|
+
validationIssueCodes: unique(generated.validation.issues.map((issue) => issue.code)),
|
|
930
|
+
provider: input.provider,
|
|
931
|
+
model: input.model,
|
|
932
|
+
};
|
|
933
|
+
input.createdNodes.push(aggregateNode);
|
|
934
|
+
nextLayer.push(aggregateNode);
|
|
935
|
+
}
|
|
936
|
+
if (nextLayer.length >= layer.length) {
|
|
937
|
+
throw new Error("Compaction aggregation could not reduce the summary graph within the input budget");
|
|
938
|
+
}
|
|
939
|
+
layer = nextLayer;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const activeNode = layer[0];
|
|
943
|
+
if (!activeNode) throw new Error("Compaction aggregation produced no active summary node");
|
|
944
|
+
return { activeNode, aggregateCalls };
|
|
945
|
+
}
|
|
946
|
+
|
|
643
947
|
private generateSummary(input: {
|
|
644
948
|
stage: "segment" | "aggregate";
|
|
645
949
|
prompt: string;
|
|
@@ -654,6 +958,10 @@ export class CompactionCoordinator {
|
|
|
654
958
|
validate: this.dependencies.config.compaction.validate,
|
|
655
959
|
maxSummaryTokens: this.dependencies.config.context.maxSummaryTokens,
|
|
656
960
|
now: this.dependencies.now,
|
|
961
|
+
onTransportRetry: (diagnostic) => {
|
|
962
|
+
this.state.transportRetries = (this.state.transportRetries ?? 0) + 1;
|
|
963
|
+
this.dependencies.logger.debug("compaction.transport_retry", { ...diagnostic });
|
|
964
|
+
},
|
|
657
965
|
});
|
|
658
966
|
}
|
|
659
967
|
|