From cb4e41a59fb8bac21f8854773f6779aed056d146 Mon Sep 17 00:00:00 2001 From: kami Date: Wed, 1 Jul 2026 14:57:19 +0400 Subject: [PATCH] =?UTF-8?q?feat(context):=20static-doc=20compression=20?= =?UTF-8?q?=E2=80=94=20prune=20CLAUDE.md/AGENTS.md=20doc=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../builder/DefaultContextPackBuilder.kt | 18 +++++++++++++-- .../kotlin/CompressionPipelineStagesTest.kt | 22 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt b/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt index 5ab6b8a6..8af26c7a 100644 --- a/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt +++ b/core/context/src/main/kotlin/com/correx/core/context/builder/DefaultContextPackBuilder.kt @@ -51,7 +51,13 @@ class DefaultContextPackBuilder( const val CHARS_PER_TOKEN = 4 const val FACT_SHEET_ORDINAL = -1 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 + // 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( @@ -142,11 +148,19 @@ class DefaultContextPackBuilder( .map { it.ordinal }.sortedDescending() val tier0 = if (tierSplit) freeformOrdinals.take(TIER0_TURNS).toSet() else emptySet() return entries.map { entry -> - if (classifier.classify(entry) != ContextClass.FREEFORM || entry.ordinal in tier0) entry - else reencode(entry, tokenPruner.prune(entry.content, tagger.protectedSpans(entry.content), DEFAULT_PRUNE_RATIO)) + val isFreeform = classifier.classify(entry) == ContextClass.FREEFORM && entry.ordinal !in tier0 + 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): List { val (freeform, rest) = entries.partition { classifier.classify(it) == ContextClass.FREEFORM } return (rest + toMeMerger.merge(freeform)).sortedBy { it.ordinal } diff --git a/core/context/src/test/kotlin/CompressionPipelineStagesTest.kt b/core/context/src/test/kotlin/CompressionPipelineStagesTest.kt index 288b582b..3aab42b9 100644 --- a/core/context/src/test/kotlin/CompressionPipelineStagesTest.kt +++ b/core/context/src/test/kotlin/CompressionPipelineStagesTest.kt @@ -137,6 +137,28 @@ class CompressionPipelineStagesTest { 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, 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 fun `classifier separates static structured freeform`() { val c = ContextClassifier()