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