refactor(context): make block activation/rendering exposure-aware and use rendered projections in aggregation

This commit is contained in:
2026-03-26 16:49:56 +08:00
parent 201addbc64
commit 54320dbfde
2 changed files with 280 additions and 35 deletions

View File

@@ -51,7 +51,12 @@ class ContextWorkspace {
continue 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) { if (activationScore <= 0.0) {
iterator.remove() iterator.remove()
continue continue
@@ -61,7 +66,7 @@ class ContextWorkspace {
block = block, block = block,
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) }, domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
activationScore = activationScore, activationScore = activationScore,
forceFullRender = primaryDomain in matchedDomains renderedBlock = block.render(exposure)
) )
} }
@@ -86,11 +91,7 @@ class ContextWorkspace {
} }
private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent { private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent {
return if (resolved.forceFullRender) { return resolved.renderedBlock
resolved.block.blockContent
} else {
resolved.block.render()
}
} }
@@ -128,7 +129,7 @@ private data class ResolvedContextBlock(
val block: ContextBlock, val block: ContextBlock,
val domainWeight: Int, val domainWeight: Int,
val activationScore: Double, val activationScore: Double,
val forceFullRender: Boolean val renderedBlock: BlockContent
) )
data class ContextBlock @JvmOverloads constructor( data class ContextBlock @JvmOverloads constructor(
@@ -155,6 +156,16 @@ data class ContextBlock @JvmOverloads constructor(
*/ */
private val activateFactor: Double private val activateFactor: Double
) { ) {
internal enum class Exposure {
PRIMARY,
SECONDARY
}
private enum class ProjectionLevel {
ABSTRACT,
COMPACT,
FULL
}
/** /**
* 默认活跃分数降低至0时将在 [ContextWorkspace] 中移除该 block * 默认活跃分数降低至0时将在 [ContextWorkspace] 中移除该 block
@@ -185,9 +196,23 @@ data class ContextBlock @JvmOverloads constructor(
return activationScore return activationScore
} }
fun activate(): Double { internal fun activate(exposure: Exposure): Double {
refreshByElapsedTime() 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 return activationScore
} }
@@ -202,11 +227,48 @@ data class ContextBlock @JvmOverloads constructor(
return this.sourceKey == contextBlock.sourceKey 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 { fun render(): BlockContent {
return when (currentProjectionLevel()) {
ProjectionLevel.ABSTRACT -> abstractBlock
ProjectionLevel.COMPACT -> compactBlock
ProjectionLevel.FULL -> blockContent
}
}
private fun currentProjectionLevel(): ProjectionLevel {
return when { return when {
activationScore < 30 -> abstractBlock activationScore < ABSTRACT_TO_COMPACT_THRESHOLD -> ProjectionLevel.ABSTRACT
activationScore < 70 -> compactBlock activationScore < COMPACT_TO_FULL_THRESHOLD -> ProjectionLevel.COMPACT
else -> blockContent 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 blockName: String,
val source: 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( private class AggregatedBlockContent(
@@ -221,11 +290,7 @@ private class AggregatedBlockContent(
) : BlockContent( ) : BlockContent(
groupedBlocks.first().block.sourceKey.blockName, groupedBlocks.first().block.sourceKey.blockName,
groupedBlocks.first().block.sourceKey.source, groupedBlocks.first().block.sourceKey.source,
groupedBlocks.maxByOrNull { groupedBlocks.maxByOrNull { it.renderedBlock.urgency.ordinal }?.renderedBlock?.urgency ?: Urgency.NORMAL
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
) { ) {
override fun fillXml(document: Document, root: Element) { override fun fillXml(document: Document, root: Element) {
@@ -238,11 +303,7 @@ private class AggregatedBlockContent(
groupedBlocks.forEachIndexed { index, groupedBlock -> groupedBlocks.forEachIndexed { index, groupedBlock ->
val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot" val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot"
val wrapper = document.createElement(tagName) val wrapper = document.createElement(tagName)
val renderedBlock = if (groupedBlock.forceFullRender) { val renderedBlock = groupedBlock.renderedBlock
groupedBlock.block.blockContent
} else {
groupedBlock.block.render()
}
wrapper.setAttribute("source", renderedBlock.source) wrapper.setAttribute("source", renderedBlock.source)
wrapper.setAttribute("urgency", renderedBlock.urgency.name.lowercase(Locale.ROOT)) wrapper.setAttribute("urgency", renderedBlock.urgency.name.lowercase(Locale.ROOT))
root.appendChild(wrapper) root.appendChild(wrapper)

View File

@@ -64,13 +64,13 @@ class ContextWorkspaceTest {
) )
assertEquals( assertEquals(
listOf("low", "mid", "high"), listOf("low-compact", "mid-compact", "high"),
resolved.map { (it as TestBlockContent).content } resolved.blocks.map { (it as TestBlockContent).content }
) )
} }
@Test @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 manager = ContextWorkspace()
val older = contextBlock( val older = contextBlock(
blockName = "memory", blockName = "memory",
@@ -99,10 +99,14 @@ class ContextWorkspaceTest {
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
assertEquals( assertEquals(2, resolved.blocks.size)
listOf("older", "newer", "other-source"), assertEquals("other-source", (resolved.blocks[1] as TestBlockContent).content)
resolved.map { (it as TestBlockContent).content }
) val aggregatedXml = resolved.blocks[0].encodeToXmlString()
assertTrue(aggregatedXml.contains("<snapshot"))
assertTrue(aggregatedXml.contains("<content>newer</content>"))
assertTrue(aggregatedXml.contains("<history_snapshot"))
assertTrue(aggregatedXml.contains("<content>older</content>"))
} }
@Test @Test
@@ -128,7 +132,7 @@ class ContextWorkspaceTest {
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) 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 @Test
@@ -158,7 +162,7 @@ class ContextWorkspaceTest {
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) 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 @Test
@@ -181,7 +185,7 @@ class ContextWorkspaceTest {
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY)) 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 @Test
@@ -215,6 +219,185 @@ class ContextWorkspaceTest {
assertSame(summary, summaryBlock.render()) 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("<content>compact-low</content>"))
assertFalse(xml.contains("<content>full-critical</content>"))
}
private fun assertContent(expected: String, rendered: BlockContent) {
assertEquals(expected, (rendered as TestBlockContent).content)
}
private fun contextBlock( private fun contextBlock(
blockName: String, blockName: String,
source: String, source: String,
@@ -240,8 +423,9 @@ class ContextWorkspaceTest {
private class TestBlockContent( private class TestBlockContent(
blockName: String, blockName: String,
source: String, source: String,
val content: String val content: String,
) : BlockContent(blockName, source) { urgency: Urgency = Urgency.NORMAL
) : BlockContent(blockName, source, urgency) {
override fun fillXml(document: org.w3c.dom.Document, root: org.w3c.dom.Element) { override fun fillXml(document: org.w3c.dom.Document, root: org.w3c.dom.Element) {
appendTextElement(document, root, "content", content) appendTextElement(document, root, "content", content)
} }