feat(cognition): add ContextWorkspace to manage context blocks

This commit is contained in:
2026-03-22 21:39:25 +08:00
parent 6aa96c33ac
commit 977d92881c
4 changed files with 567 additions and 0 deletions

View File

@@ -11,6 +11,8 @@ public interface CognitionCapability {
String initiateTurn(String input, String target);
ContextWorkspace contextWorkspace();
List<Message> getChatMessages();
List<Message> snapshotChatMessages();

View File

@@ -35,9 +35,16 @@ public class CognitionCore extends PartnerCore<CognitionCore> {
*/
private List<Message> chatMessages = new ArrayList<>();
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
public CognitionCore() throws IOException, ClassNotFoundException {
}
@CapabilityMethod
public ContextWorkspace contextWorkspace() {
return contextWorkspace;
}
@CapabilityMethod
public String initiateTurn(String input, String target) {
PartnerRunningFlowContext primaryContext = PartnerRunningFlowContext.Companion.fromSelf(input);

View File

@@ -0,0 +1,309 @@
package work.slhaf.partner.core.cognition
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.StringWriter
import java.time.Duration
import java.time.Instant
import java.util.concurrent.locks.ReentrantReadWriteLock
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.concurrent.write
import kotlin.math.max
import kotlin.math.min
class ContextWorkspace {
private val stateSet = mutableSetOf<ContextBlock>()
private val lock = ReentrantReadWriteLock()
/**
* 根据传入的 [ContextBlock.VisibleDomain] 列表,获取上下文块
* @param domains 需要获取上下文的域列表,顺序将决定权重优先级,按照列表排序将具备线性权重分层,最终反映到 blockContent 列表的排序上
*/
fun resolve(domains: List<ContextBlock.VisibleDomain>): List<BlockContent> = lock.write {
if (domains.isEmpty()) {
return@write emptyList()
}
val domainWeights = domains
.distinct()
.withIndex()
.associate { (index, domain) -> domain to (domains.size - index) }
val activeBlocks = mutableListOf<ResolvedContextBlock>()
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val block = iterator.next()
val activationScore = block.applyTimeFade()
if (activationScore <= 0.0) {
iterator.remove()
continue
}
val matchedDomains = block.visibleTo.intersect(domainWeights.keys)
if (matchedDomains.isEmpty()) {
continue
}
activeBlocks += ResolvedContextBlock(
block = block,
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
activationScore = activationScore
)
}
activeBlocks
.sortedWith(
compareBy<ResolvedContextBlock> { it.domainWeight }
.thenBy { it.block.sourceKey.blockName }
.thenBy { it.block.sourceKey.source }
.thenBy { it.activationScore }
.thenBy { it.block.blockContent.encodeToXmlString() }
)
.map { it.block.render() }
}
/**
* @param contextBlock 注册的新上下文块
*/
fun register(contextBlock: ContextBlock) = lock.write {
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
val currentBlock = iterator.next()
if (!currentBlock.sameWith(contextBlock)) {
continue
}
if (currentBlock.applyReplaceFade() <= 0.0) {
iterator.remove()
}
}
stateSet += contextBlock
}
fun expire(blockName: String, source: String) = lock.write {
val sourceKey = ContextBlock.SourceKey(blockName, source)
val iterator = stateSet.iterator()
while (iterator.hasNext()) {
if (iterator.next().sourceKey == sourceKey) {
iterator.remove()
}
}
}
}
private data class ResolvedContextBlock(
val block: ContextBlock,
val domainWeight: Int,
val activationScore: Double
)
data class ContextBlock @JvmOverloads constructor(
val blockContent: BlockContent,
val compactBlock: BlockContent = blockContent,
val abstractBlock: BlockContent = blockContent,
/**
* 对哪些域可见
*/
val visibleTo: Set<VisibleDomain>,
/**
* 新的 [blockContent] 属性与其相同时,发生的衰退步长
*/
private val replaceFadeFactor: Double,
/**
* 随时间发生的衰退步长,按照分钟定义
*/
private val timeFadeFactor: Double,
/**
* 触发一次激活时,发生的强化步长
*/
private val activateFactor: Double
) {
/**
* 默认活跃分数降低至0时将在 [ContextWorkspace] 中移除该 block
* 此外还参与到当同源 block 存在时的排序,按该分数升序排列,只影响同源 block 间的顺序
*/
private var activationScore = 100.0
private var lastTouchedAt = Instant.now()
enum class VisibleDomain {
ACTION,
MEMORY,
PERCEIVE,
COGNITION,
COMMUNICATION,
}
internal val sourceKey: SourceKey
get() = SourceKey(blockContent.blockName, blockContent.source)
fun applyTimeFade(): Double {
val now = Instant.now()
val elapsedSeconds = Duration.between(lastTouchedAt, now).toMillis() / 1000.0
activationScore = max(0.0, activationScore - elapsedSeconds * (timeFadeFactor / 60.0))
lastTouchedAt = now
return activationScore
}
fun applyReplaceFade(): Double {
applyTimeFade()
activationScore = max(0.0, activationScore - replaceFadeFactor)
return activationScore
}
fun activate(): Double {
applyTimeFade()
activationScore = min(100.0, activationScore + activateFactor)
return activationScore
}
fun sameWith(contextBlock: ContextBlock): Boolean {
return this.sourceKey == contextBlock.sourceKey
}
fun render(): BlockContent {
return when {
activationScore < 30 -> abstractBlock
activationScore < 70 -> compactBlock
else -> blockContent
}
}
data class SourceKey(
val blockName: String,
val source: String
)
}
abstract class BlockContent protected constructor(
val blockName: String,
val source: String,
) {
fun encodeToXml(): Element {
val document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument()
val root = document.createElement(blockName)
root.setAttribute("source", source)
document.appendChild(root)
fillXml(document, root)
return root
}
fun encodeToXmlString(): String {
val transformer = TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
}
return StringWriter().use { writer ->
transformer.transform(DOMSource(encodeToXml()), StreamResult(writer))
writer.toString()
}
}
protected abstract fun fillXml(document: Document, root: Element)
protected fun appendTextElement(
document: Document,
parent: Element,
tagName: String,
value: Any?
): Element {
val element = document.createElement(tagName)
element.textContent = value?.toString() ?: ""
parent.appendChild(element)
return element
}
protected fun appendChildElement(
document: Document,
parent: Element,
tagName: String,
block: Element.() -> Unit = {}
): Element {
val element = document.createElement(tagName)
parent.appendChild(element)
element.block()
return element
}
protected fun appendCDataElement(
document: Document,
parent: Element,
tagName: String,
value: String?
): Element {
val element = document.createElement(tagName)
element.appendChild(document.createCDATASection(value ?: ""))
parent.appendChild(element)
return element
}
protected fun <T> appendListElement(
document: Document,
parent: Element,
wrapperTagName: String,
itemTagName: String,
values: Iterable<T>,
block: Element.(T) -> Unit = { value ->
textContent = value?.toString() ?: ""
}
): Element {
val wrapper = document.createElement(wrapperTagName)
parent.appendChild(wrapper)
for (value in values) {
val item = document.createElement(itemTagName)
wrapper.appendChild(item)
item.block(value)
}
return wrapper
}
protected fun <T> appendRepeatedElements(
document: Document,
parent: Element,
itemTagName: String,
values: Iterable<T>,
block: Element.(T) -> Unit = { value ->
textContent = value?.toString() ?: ""
}
) {
for (value in values) {
val item = document.createElement(itemTagName)
parent.appendChild(item)
item.block(value)
}
}
}
abstract class CommunicationBlockContent(
blockName: String,
source: String,
) : BlockContent(
blockName,
source,
) {
enum class Projection {
SUPPLY,
CONTEXT
}
}

View File

@@ -0,0 +1,249 @@
package work.slhaf.partner.core.cognition
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class ContextWorkspaceTest {
@Test
fun `sameWith uses blockName and source only`() {
val left = contextBlock(
blockName = "memory",
source = "main",
content = "left",
compactContent = "left-compact",
abstractContent = "left-abstract"
)
val sameSource = contextBlock(
blockName = "memory",
source = "main",
content = "right"
)
val differentSource = contextBlock(
blockName = "memory",
source = "backup",
content = "right"
)
assertTrue(left.sameWith(sameSource))
assertFalse(left.sameWith(differentSource))
}
@Test
fun `resolve sorts by accumulated domain weight across sources`() {
val manager = ContextWorkspace()
val lowWeight = contextBlock(
blockName = "low",
source = "source-low",
content = "low",
visibleTo = setOf(ContextBlock.VisibleDomain.COGNITION)
)
val midWeight = contextBlock(
blockName = "mid",
source = "source-mid",
content = "mid",
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
)
val highWeight = contextBlock(
blockName = "high",
source = "source-high",
content = "high",
visibleTo = setOf(ContextBlock.VisibleDomain.ACTION, ContextBlock.VisibleDomain.MEMORY)
)
manager.register(highWeight)
manager.register(lowWeight)
manager.register(midWeight)
val resolved = manager.resolve(
listOf(
ContextBlock.VisibleDomain.ACTION,
ContextBlock.VisibleDomain.MEMORY,
ContextBlock.VisibleDomain.COGNITION
)
)
assertEquals(
listOf("low", "mid", "high"),
resolved.map { (it as TestBlockContent).content }
)
}
@Test
fun `resolve uses activation score only within same source`() {
val manager = ContextWorkspace()
val older = contextBlock(
blockName = "memory",
source = "main",
content = "older",
replaceFadeFactor = 20.0,
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
)
val newer = contextBlock(
blockName = "memory",
source = "main",
content = "newer",
replaceFadeFactor = 20.0,
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
)
val otherSource = contextBlock(
blockName = "memory",
source = "secondary",
content = "other-source",
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
)
manager.register(older)
manager.register(newer)
manager.register(otherSource)
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
assertEquals(
listOf("older", "newer", "other-source"),
resolved.map { (it as TestBlockContent).content }
)
}
@Test
fun `register fades matching source blocks and removes zero score ones`() {
val manager = ContextWorkspace()
val evicted = contextBlock(
blockName = "memory",
source = "main",
content = "evicted",
replaceFadeFactor = 100.0,
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
)
val replacement = contextBlock(
blockName = "memory",
source = "main",
content = "replacement",
replaceFadeFactor = 100.0,
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
)
manager.register(evicted)
manager.register(replacement)
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
assertEquals(listOf("replacement"), resolved.map { (it as TestBlockContent).content })
}
@Test
fun `expire removes all matching source blocks`() {
val manager = ContextWorkspace()
val first = contextBlock(
blockName = "memory",
source = "main",
content = "first"
)
val second = contextBlock(
blockName = "memory",
source = "main",
content = "second"
)
val survivor = contextBlock(
blockName = "memory",
source = "secondary",
content = "survivor"
)
manager.register(first)
manager.register(second)
manager.register(survivor)
manager.expire("memory", "main")
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
assertEquals(listOf("survivor"), resolved.map { (it as TestBlockContent).content })
}
@Test
fun `expire allows same source to be registered again`() {
val manager = ContextWorkspace()
val original = contextBlock(
blockName = "memory",
source = "main",
content = "original"
)
val restored = contextBlock(
blockName = "memory",
source = "main",
content = "restored"
)
manager.register(original)
manager.expire("memory", "main")
manager.register(restored)
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
assertEquals(listOf("restored"), resolved.map { (it as TestBlockContent).content })
}
@Test
fun `render switches projections by activation score`() {
val original = TestBlockContent("memory", "main", "full")
val compact = TestBlockContent("memory", "main", "compact")
val summary = TestBlockContent("memory", "main", "summary")
val compactBlock = ContextBlock(
blockContent = original,
compactBlock = compact,
abstractBlock = summary,
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY),
replaceFadeFactor = 40.0,
timeFadeFactor = 0.0,
activateFactor = 0.0
)
compactBlock.applyReplaceFade()
assertSame(compact, compactBlock.render())
val summaryBlock = ContextBlock(
blockContent = original,
compactBlock = compact,
abstractBlock = summary,
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY),
replaceFadeFactor = 80.0,
timeFadeFactor = 0.0,
activateFactor = 0.0
)
summaryBlock.applyReplaceFade()
assertSame(summary, summaryBlock.render())
}
private fun contextBlock(
blockName: String,
source: String,
content: String,
compactContent: String = "${content}-compact",
abstractContent: String = "${content}-abstract",
visibleTo: Set<ContextBlock.VisibleDomain> = setOf(ContextBlock.VisibleDomain.MEMORY),
replaceFadeFactor: Double = 10.0,
timeFadeFactor: Double = 0.0,
activateFactor: Double = 0.0
): ContextBlock {
return ContextBlock(
blockContent = TestBlockContent(blockName, source, content),
compactBlock = TestBlockContent(blockName, source, compactContent),
abstractBlock = TestBlockContent(blockName, source, abstractContent),
visibleTo = visibleTo,
replaceFadeFactor = replaceFadeFactor,
timeFadeFactor = timeFadeFactor,
activateFactor = activateFactor
)
}
private class TestBlockContent(
blockName: String,
source: String,
val content: String
) : BlockContent(blockName, source) {
override fun fillXml(document: org.w3c.dom.Document, root: org.w3c.dom.Element) {
appendTextElement(document, root, "content", content)
}
}
}