feat(context): static-doc compression — prune CLAUDE.md/AGENTS.md doc entries
The big win the pipeline missed: projectProfile + agentInstructions doc dumps (~14k chars) are L0-SYSTEM -> STATIC -> never pruned, re-sent verbatim every turn. Now pruned via the same TokenPruner at a gentler ratio (keep 60% vs 45% freeform), protected spans (code fences/paths/keys) kept so directives survive. Gated by TOKEN_PRUNE (level 3+); base systemPrompt stays verbatim.
This commit is contained in:
+16
-2
@@ -51,7 +51,13 @@ class DefaultContextPackBuilder(
|
|||||||
const val CHARS_PER_TOKEN = 4
|
const val CHARS_PER_TOKEN = 4
|
||||||
const val FACT_SHEET_ORDINAL = -1
|
const val FACT_SHEET_ORDINAL = -1
|
||||||
const val DEFAULT_PRUNE_RATIO = 0.45
|
const val DEFAULT_PRUNE_RATIO = 0.45
|
||||||
|
const val DOC_PRUNE_RATIO = 0.6
|
||||||
|
const val DOC_MIN_CHARS = 1200
|
||||||
const val TIER0_TURNS = 3
|
const val TIER0_TURNS = 3
|
||||||
|
// Large static instruction docs (CLAUDE.md project profile, AGENTS.md / DOX framework)
|
||||||
|
// injected verbatim every turn. Pruned at DOC_PRUNE_RATIO — gentler than freeform, with
|
||||||
|
// protected spans (code fences, paths, config keys) kept, so directives survive.
|
||||||
|
val DOC_SOURCE_TYPES = setOf("projectProfile", "agentInstructions")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun build(
|
override suspend fun build(
|
||||||
@@ -142,11 +148,19 @@ class DefaultContextPackBuilder(
|
|||||||
.map { it.ordinal }.sortedDescending()
|
.map { it.ordinal }.sortedDescending()
|
||||||
val tier0 = if (tierSplit) freeformOrdinals.take(TIER0_TURNS).toSet() else emptySet()
|
val tier0 = if (tierSplit) freeformOrdinals.take(TIER0_TURNS).toSet() else emptySet()
|
||||||
return entries.map { entry ->
|
return entries.map { entry ->
|
||||||
if (classifier.classify(entry) != ContextClass.FREEFORM || entry.ordinal in tier0) entry
|
val isFreeform = classifier.classify(entry) == ContextClass.FREEFORM && entry.ordinal !in tier0
|
||||||
else reencode(entry, tokenPruner.prune(entry.content, tagger.protectedSpans(entry.content), DEFAULT_PRUNE_RATIO))
|
val isLargeDoc = entry.sourceType in DOC_SOURCE_TYPES && entry.content.length >= DOC_MIN_CHARS
|
||||||
|
when {
|
||||||
|
isFreeform -> reencode(entry, tokenPruner.prune(entry.content, spansOf(entry), DEFAULT_PRUNE_RATIO))
|
||||||
|
// Static instruction docs: gentler ratio so directives survive the classifier.
|
||||||
|
isLargeDoc -> reencode(entry, tokenPruner.prune(entry.content, spansOf(entry), DOC_PRUNE_RATIO))
|
||||||
|
else -> entry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun spansOf(entry: ContextEntry) = tagger.protectedSpans(entry.content)
|
||||||
|
|
||||||
private fun applyToMe(entries: List<ContextEntry>): List<ContextEntry> {
|
private fun applyToMe(entries: List<ContextEntry>): List<ContextEntry> {
|
||||||
val (freeform, rest) = entries.partition { classifier.classify(it) == ContextClass.FREEFORM }
|
val (freeform, rest) = entries.partition { classifier.classify(it) == ContextClass.FREEFORM }
|
||||||
return (rest + toMeMerger.merge(freeform)).sortedBy { it.ordinal }
|
return (rest + toMeMerger.merge(freeform)).sortedBy { it.ordinal }
|
||||||
|
|||||||
@@ -137,6 +137,28 @@ class CompressionPipelineStagesTest {
|
|||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `large static doc entries are pruned at level 3 while base system prompt is untouched`() = kotlinx.coroutines.runBlocking {
|
||||||
|
val pruner = object : com.correx.core.context.compression.TokenPruner {
|
||||||
|
override suspend fun prune(content: String, protectedSpans: List<String>, targetRatio: Double): String = "DOC-PRUNED"
|
||||||
|
}
|
||||||
|
val builder = com.correx.core.context.builder.DefaultContextPackBuilder(
|
||||||
|
com.correx.core.context.compression.DefaultContextCompressor(), CompressionPolicy(3), tokenPruner = pruner,
|
||||||
|
)
|
||||||
|
val bigDoc = "# CLAUDE.md\n" + "guidance line ".repeat(120) // > DOC_MIN_CHARS
|
||||||
|
val entries = listOf(
|
||||||
|
entry("proj", ContextLayer.L0, EntryRole.SYSTEM, bigDoc).copy(sourceType = "projectProfile"),
|
||||||
|
entry("sys", ContextLayer.L0, EntryRole.SYSTEM, "you are an agent").copy(sourceType = "systemPrompt"),
|
||||||
|
)
|
||||||
|
val flat = builder.build(
|
||||||
|
com.correx.core.events.types.ContextPackId("p"), com.correx.core.events.types.SessionId("s"),
|
||||||
|
com.correx.core.events.types.StageId("st"), entries, com.correx.core.context.model.TokenBudget(limit = 4000),
|
||||||
|
).layers.values.flatten()
|
||||||
|
assertEquals("DOC-PRUNED", flat.first { it.sourceType == "projectProfile" }.content)
|
||||||
|
assertEquals("you are an agent", flat.first { it.sourceType == "systemPrompt" }.content)
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `classifier separates static structured freeform`() {
|
fun `classifier separates static structured freeform`() {
|
||||||
val c = ContextClassifier()
|
val c = ContextClassifier()
|
||||||
|
|||||||
Reference in New Issue
Block a user