From 54320dbfdeda3db1d11864b1303d70a352e6549f Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Thu, 26 Mar 2026 16:49:56 +0800 Subject: [PATCH] refactor(context): make block activation/rendering exposure-aware and use rendered projections in aggregation --- .../core/cognition/ContextWorkspace.kt | 107 +++++++-- .../core/cognition/ContextWorkspaceTest.kt | 208 +++++++++++++++++- 2 files changed, 280 insertions(+), 35 deletions(-) diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextWorkspace.kt b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextWorkspace.kt index ba8e1faf..c4ffec5d 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextWorkspace.kt +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/ContextWorkspace.kt @@ -51,7 +51,12 @@ class ContextWorkspace { continue } - val activationScore = block.activate() + val exposure = if (primaryDomain in matchedDomains) { + ContextBlock.Exposure.PRIMARY + } else { + ContextBlock.Exposure.SECONDARY + } + val activationScore = block.activate(exposure) if (activationScore <= 0.0) { iterator.remove() continue @@ -61,7 +66,7 @@ class ContextWorkspace { block = block, domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) }, activationScore = activationScore, - forceFullRender = primaryDomain in matchedDomains + renderedBlock = block.render(exposure) ) } @@ -86,11 +91,7 @@ class ContextWorkspace { } private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent { - return if (resolved.forceFullRender) { - resolved.block.blockContent - } else { - resolved.block.render() - } + return resolved.renderedBlock } @@ -128,7 +129,7 @@ private data class ResolvedContextBlock( val block: ContextBlock, val domainWeight: Int, val activationScore: Double, - val forceFullRender: Boolean + val renderedBlock: BlockContent ) data class ContextBlock @JvmOverloads constructor( @@ -155,6 +156,16 @@ data class ContextBlock @JvmOverloads constructor( */ private val activateFactor: Double ) { + internal enum class Exposure { + PRIMARY, + SECONDARY + } + + private enum class ProjectionLevel { + ABSTRACT, + COMPACT, + FULL + } /** * 默认活跃分数,降低至0时将在 [ContextWorkspace] 中移除该 block @@ -185,9 +196,23 @@ data class ContextBlock @JvmOverloads constructor( return activationScore } - fun activate(): Double { + internal fun activate(exposure: Exposure): Double { refreshByElapsedTime() - activationScore = min(100.0, activationScore + activateFactor) + val currentLevel = currentProjectionLevel() + val increasedScore = when (exposure) { + Exposure.PRIMARY -> activationScore + when (currentLevel) { + ProjectionLevel.FULL -> activateFactor + ProjectionLevel.COMPACT -> activateFactor * 0.6 + ProjectionLevel.ABSTRACT -> activateFactor * 0.6 + } + + Exposure.SECONDARY -> activationScore + when (currentLevel) { + ProjectionLevel.COMPACT -> activateFactor * 0.2 + ProjectionLevel.ABSTRACT -> activateFactor * 0.1 + ProjectionLevel.FULL -> 0.0 + } + } + activationScore = min(activationCeiling(exposure, currentLevel), increasedScore) return activationScore } @@ -202,11 +227,48 @@ data class ContextBlock @JvmOverloads constructor( return this.sourceKey == contextBlock.sourceKey } + internal fun render(exposure: Exposure): BlockContent { + return when (exposure) { + Exposure.PRIMARY -> when (currentProjectionLevel()) { + ProjectionLevel.FULL -> blockContent + ProjectionLevel.COMPACT, ProjectionLevel.ABSTRACT -> compactBlock + } + + Exposure.SECONDARY -> when (currentProjectionLevel()) { + ProjectionLevel.ABSTRACT -> abstractBlock + ProjectionLevel.COMPACT, ProjectionLevel.FULL -> compactBlock + } + } + } + fun render(): BlockContent { + return when (currentProjectionLevel()) { + ProjectionLevel.ABSTRACT -> abstractBlock + ProjectionLevel.COMPACT -> compactBlock + ProjectionLevel.FULL -> blockContent + } + } + + private fun currentProjectionLevel(): ProjectionLevel { return when { - activationScore < 30 -> abstractBlock - activationScore < 70 -> compactBlock - else -> blockContent + activationScore < ABSTRACT_TO_COMPACT_THRESHOLD -> ProjectionLevel.ABSTRACT + activationScore < COMPACT_TO_FULL_THRESHOLD -> ProjectionLevel.COMPACT + else -> ProjectionLevel.FULL + } + } + + private fun activationCeiling(exposure: Exposure, currentLevel: ProjectionLevel): Double { + return when (exposure) { + Exposure.PRIMARY -> when (currentLevel) { + ProjectionLevel.ABSTRACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON + ProjectionLevel.COMPACT, ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE + } + + Exposure.SECONDARY -> when (currentLevel) { + ProjectionLevel.ABSTRACT -> ABSTRACT_TO_COMPACT_THRESHOLD - PROJECTION_EPSILON + ProjectionLevel.COMPACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON + ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE + } } } @@ -214,6 +276,13 @@ data class ContextBlock @JvmOverloads constructor( val blockName: String, val source: String ) + + companion object { + private const val MAX_ACTIVATION_SCORE = 100.0 + private const val ABSTRACT_TO_COMPACT_THRESHOLD = 30.0 + private const val COMPACT_TO_FULL_THRESHOLD = 70.0 + private const val PROJECTION_EPSILON = 0.000001 + } } private class AggregatedBlockContent( @@ -221,11 +290,7 @@ private class AggregatedBlockContent( ) : BlockContent( groupedBlocks.first().block.sourceKey.blockName, groupedBlocks.first().block.sourceKey.source, - groupedBlocks.maxByOrNull { - if (it.forceFullRender) it.block.blockContent.urgency.ordinal else it.block.render().urgency.ordinal - }?.let { - if (it.forceFullRender) it.block.blockContent.urgency else it.block.render().urgency - } ?: Urgency.NORMAL + groupedBlocks.maxByOrNull { it.renderedBlock.urgency.ordinal }?.renderedBlock?.urgency ?: Urgency.NORMAL ) { override fun fillXml(document: Document, root: Element) { @@ -238,11 +303,7 @@ private class AggregatedBlockContent( groupedBlocks.forEachIndexed { index, groupedBlock -> val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot" val wrapper = document.createElement(tagName) - val renderedBlock = if (groupedBlock.forceFullRender) { - groupedBlock.block.blockContent - } else { - groupedBlock.block.render() - } + val renderedBlock = groupedBlock.renderedBlock wrapper.setAttribute("source", renderedBlock.source) wrapper.setAttribute("urgency", renderedBlock.urgency.name.lowercase(Locale.ROOT)) root.appendChild(wrapper) diff --git a/Partner-Core/src/test/java/work/slhaf/partner/core/cognition/ContextWorkspaceTest.kt b/Partner-Core/src/test/java/work/slhaf/partner/core/cognition/ContextWorkspaceTest.kt index 610f2fa1..ace14a99 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/core/cognition/ContextWorkspaceTest.kt +++ b/Partner-Core/src/test/java/work/slhaf/partner/core/cognition/ContextWorkspaceTest.kt @@ -64,13 +64,13 @@ class ContextWorkspaceTest { ) assertEquals( - listOf("low", "mid", "high"), - resolved.map { (it as TestBlockContent).content } + listOf("low-compact", "mid-compact", "high"), + resolved.blocks.map { (it as TestBlockContent).content } ) } @Test - fun `resolve uses activation score only within same source`() { + fun `resolve aggregates same source blocks while preserving activation ordering within the group`() { val manager = ContextWorkspace() val older = contextBlock( blockName = "memory", @@ -99,10 +99,14 @@ class ContextWorkspaceTest { val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) - assertEquals( - listOf("older", "newer", "other-source"), - resolved.map { (it as TestBlockContent).content } - ) + assertEquals(2, resolved.blocks.size) + assertEquals("other-source", (resolved.blocks[1] as TestBlockContent).content) + + val aggregatedXml = resolved.blocks[0].encodeToXmlString() + assertTrue(aggregatedXml.contains("newer")) + assertTrue(aggregatedXml.contains("older")) } @Test @@ -128,7 +132,7 @@ class ContextWorkspaceTest { val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) - assertEquals(listOf("replacement"), resolved.map { (it as TestBlockContent).content }) + assertEquals(listOf("replacement"), resolved.blocks.map { (it as TestBlockContent).content }) } @Test @@ -158,7 +162,7 @@ class ContextWorkspaceTest { val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) - assertEquals(listOf("survivor"), resolved.map { (it as TestBlockContent).content }) + assertEquals(listOf("survivor"), resolved.blocks.map { (it as TestBlockContent).content }) } @Test @@ -181,7 +185,7 @@ class ContextWorkspaceTest { val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) - assertEquals(listOf("restored"), resolved.map { (it as TestBlockContent).content }) + assertEquals(listOf("restored"), resolved.blocks.map { (it as TestBlockContent).content }) } @Test @@ -215,6 +219,185 @@ class ContextWorkspaceTest { assertSame(summary, summaryBlock.render()) } + @Test + fun `primary exposure renders at least compact and can render full`() { + val lowActivationBlock = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract", + replaceFadeFactor = 80.0 + ) + lowActivationBlock.applyReplaceFade() + + val highActivationBlock = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract" + ) + + assertContent("compact", lowActivationBlock.render(ContextBlock.Exposure.PRIMARY)) + assertContent("full", highActivationBlock.render(ContextBlock.Exposure.PRIMARY)) + } + + @Test + fun `secondary exposure never renders full`() { + val block = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract" + ) + + assertContent("compact", block.render(ContextBlock.Exposure.SECONDARY)) + } + + @Test + fun `primary exposure promotes abstract only to compact in one activation`() { + val block = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract", + replaceFadeFactor = 80.0, + activateFactor = 50.0 + ) + block.applyReplaceFade() + + block.activate(ContextBlock.Exposure.PRIMARY) + + assertContent("compact", block.render()) + assertContent("compact", block.render(ContextBlock.Exposure.PRIMARY)) + } + + @Test + fun `primary exposure promotes compact to full`() { + val block = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract", + replaceFadeFactor = 40.0, + activateFactor = 20.0 + ) + block.applyReplaceFade() + + block.activate(ContextBlock.Exposure.PRIMARY) + + assertContent("full", block.render()) + } + + @Test + fun `secondary exposure keeps abstract within abstract tier`() { + val block = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract", + replaceFadeFactor = 80.0, + activateFactor = 200.0 + ) + block.applyReplaceFade() + + block.activate(ContextBlock.Exposure.SECONDARY) + + assertContent("abstract", block.render()) + assertContent("abstract", block.render(ContextBlock.Exposure.SECONDARY)) + } + + @Test + fun `secondary exposure keeps compact within compact tier`() { + val block = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract", + replaceFadeFactor = 40.0, + activateFactor = 200.0 + ) + block.applyReplaceFade() + + block.activate(ContextBlock.Exposure.SECONDARY) + + assertContent("compact", block.render()) + assertContent("compact", block.render(ContextBlock.Exposure.SECONDARY)) + } + + @Test + fun `resolve uses exposure-specific rendering for primary and secondary domains`() { + val manager = ContextWorkspace() + val block = contextBlock( + blockName = "memory", + source = "main", + content = "full", + compactContent = "compact", + abstractContent = "abstract", + visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY, ContextBlock.VisibleDomain.ACTION), + replaceFadeFactor = 80.0 + ) + block.applyReplaceFade() + manager.register(block) + + val primaryResolved = manager.resolve( + listOf(ContextBlock.VisibleDomain.MEMORY, ContextBlock.VisibleDomain.ACTION) + ) + val secondaryResolved = manager.resolve( + listOf(ContextBlock.VisibleDomain.PERCEIVE, ContextBlock.VisibleDomain.ACTION) + ) + + assertEquals(listOf("compact"), primaryResolved.blocks.map { (it as TestBlockContent).content }) + assertEquals(listOf("abstract"), secondaryResolved.blocks.map { (it as TestBlockContent).content }) + } + + @Test + fun `aggregated blocks use rendered projection for urgency and snapshot`() { + val manager = ContextWorkspace() + manager.register( + ContextBlock( + blockContent = TestBlockContent("memory", "main", "full-critical", BlockContent.Urgency.CRITICAL), + compactBlock = TestBlockContent("memory", "main", "compact-low", BlockContent.Urgency.LOW), + abstractBlock = TestBlockContent("memory", "main", "abstract-low", BlockContent.Urgency.LOW), + visibleTo = setOf(ContextBlock.VisibleDomain.ACTION), + replaceFadeFactor = 20.0, + timeFadeFactor = 0.0, + activateFactor = 0.0 + ) + ) + manager.register( + ContextBlock( + blockContent = TestBlockContent("memory", "main", "full-normal", BlockContent.Urgency.NORMAL), + compactBlock = TestBlockContent("memory", "main", "compact-normal", BlockContent.Urgency.NORMAL), + abstractBlock = TestBlockContent("memory", "main", "abstract-normal", BlockContent.Urgency.NORMAL), + visibleTo = setOf(ContextBlock.VisibleDomain.ACTION), + replaceFadeFactor = 80.0, + timeFadeFactor = 0.0, + activateFactor = 0.0 + ) + ) + + val resolved = manager.resolve( + listOf(ContextBlock.VisibleDomain.PERCEIVE, ContextBlock.VisibleDomain.ACTION) + ) + + val aggregated = resolved.blocks.single() + val xml = aggregated.encodeToXmlString() + assertEquals(BlockContent.Urgency.NORMAL, aggregated.urgency) + assertTrue(xml.contains("compact-low")) + assertFalse(xml.contains("full-critical")) + } + + private fun assertContent(expected: String, rendered: BlockContent) { + assertEquals(expected, (rendered as TestBlockContent).content) + } + private fun contextBlock( blockName: String, source: String, @@ -240,8 +423,9 @@ class ContextWorkspaceTest { private class TestBlockContent( blockName: String, source: String, - val content: String - ) : BlockContent(blockName, source) { + val content: String, + urgency: Urgency = Urgency.NORMAL + ) : BlockContent(blockName, source, urgency) { override fun fillXml(document: org.w3c.dom.Document, root: org.w3c.dom.Element) { appendTextElement(document, root, "content", content) }