epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
implementation(project(":core:artifacts"))
implementation(project(":core:sessions"))
}
@@ -0,0 +1,13 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.StoredEvent
import com.correx.core.sessions.projections.Projection
class ContextProjector(
private val reducer: ContextReducer
) : Projection<ContextState> {
override fun initial(): ContextState = ContextState()
override fun apply(state: ContextState, event: StoredEvent): ContextState =
reducer.reduce(state, event)
}
@@ -0,0 +1,8 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.StoredEvent
interface ContextReducer {
fun reduce(state: ContextState, event: StoredEvent): ContextState
}
@@ -0,0 +1,23 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.ContextBuildingFailedEvent
import com.correx.core.events.events.ContextBuildingInterruptedEvent
import com.correx.core.events.events.ContextBuildingStartedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.StoredEvent
class DefaultContextReducer : ContextReducer {
override fun reduce(state: ContextState, event: StoredEvent): ContextState =
when (val p = event.payload) {
is ContextBuildingStartedEvent -> state.copy(buildingInProgress = true, interrupted = false)
is ContextPackBuiltEvent -> state.copy(
buildingInProgress = false,
interrupted = false,
builtPackIds = state.builtPackIds + p.contextPackId,
)
is ContextBuildingFailedEvent -> state.copy(buildingInProgress = false, interrupted = false)
is ContextBuildingInterruptedEvent -> state.copy(buildingInProgress = false, interrupted = true)
else -> state
}
}
@@ -0,0 +1,12 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.EventReplayer
class DefaultContextRepository(
private val replayer: EventReplayer<ContextState>
) {
fun getContextState(sessionId: SessionId): ContextState =
replayer.rebuild(sessionId)
}
@@ -0,0 +1,3 @@
package com.correx.core.context
object Module
@@ -0,0 +1,18 @@
package com.correx.core.context.builder
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
interface ContextPackBuilder {
fun build(
id: ContextPackId,
sessionId: SessionId,
stageId: StageId,
entries: List<ContextEntry>,
budget: TokenBudget
): ContextPack
}
@@ -0,0 +1,8 @@
package com.correx.core.context.builder
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextPack
interface DecisionPointBuilder {
fun build(pack: ContextPack): List<ContextEntry>
}
@@ -0,0 +1,76 @@
package com.correx.core.context.builder
import com.correx.core.context.compression.CompressionStrategy
import com.correx.core.context.compression.ContextCompressor
import com.correx.core.context.model.CompressionMetadata
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
class DefaultContextPackBuilder(
private val compressor: ContextCompressor
) : ContextPackBuilder {
private val neverDropSourceTypes = setOf("steeringNote", "eventHistory")
override fun build(
id: ContextPackId,
sessionId: SessionId,
stageId: StageId,
entries: List<ContextEntry>,
budget: TokenBudget
): ContextPack {
val (pinned, compressible) = entries.partition { it.sourceType in neverDropSourceTypes }
val pinnedTokens = pinned.sumOf { it.tokenEstimate }
var remainingTokens = (budget.limit - pinnedTokens).coerceAtLeast(0)
// Dispatch compression strategy by entry sourceType — applying Conversation
// strategy uniformly mangles tool logs and artifacts (their shape isn't conversational).
val strategiesUsed = linkedSetOf<String>()
val compressed = compressible
.groupBy { it.sourceType }
.flatMap { (sourceType, group) ->
val strategy = strategyFor(sourceType)
strategiesUsed += strategy::class.simpleName ?: "Unknown"
val groupBudget = TokenBudget(limit = remainingTokens.coerceAtLeast(0))
val result = compressor.compress(group, groupBudget, strategy)
remainingTokens -= result.sumOf { it.tokenEstimate }
result
}
val retained = pinned + compressed
val layers = retained.groupBy { it.layer }
val budgetUsed = retained.sumOf { it.tokenEstimate }
val droppedCount = entries.size - retained.size
val truncatedLayers = if (droppedCount > 0) {
entries.map { it.layer }.distinct().filter { layer ->
entries.count { it.layer == layer } > retained.count { it.layer == layer }
}
} else emptyList()
return ContextPack(
id = id,
sessionId = sessionId,
stageId = stageId,
layers = layers,
budgetUsed = budgetUsed,
budgetLimit = budget.limit,
compressionMetadata = CompressionMetadata(
appliedStrategies = strategiesUsed.toList().ifEmpty { listOf("Conversation") },
truncatedLayers = truncatedLayers,
entriesDropped = droppedCount
)
)
}
private fun strategyFor(sourceType: String): CompressionStrategy = when (sourceType) {
"toolLog" -> CompressionStrategy.ToolLog
"artifact" -> CompressionStrategy.Artifact
"steeringNote" -> CompressionStrategy.SteeringNote
"eventHistory" -> CompressionStrategy.EventHistory
else -> CompressionStrategy.Conversation()
}
}
@@ -0,0 +1,14 @@
package com.correx.core.context.builder
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.ContextPack
class DefaultDecisionPointBuilder : DecisionPointBuilder {
// L0 (live execution) and L1 (stage-local) are the only layers sent to the model for inference.
// L2+ are too large or already compressed into DecisionPoints.
private val inferenceLayers = setOf(ContextLayer.L0, ContextLayer.L1)
override fun build(pack: ContextPack): List<ContextEntry> =
inferenceLayers.flatMap { layer -> pack.layers[layer] ?: emptyList() }
}
@@ -0,0 +1,14 @@
package com.correx.core.context.compression
sealed interface CompressionStrategy {
// Tool log entries: deduplicate identical content, drop oldest when over budget
object ToolLog : CompressionStrategy
// Conversation turns: retain the last `keepLast` entries verbatim, drop oldest first when over budget. Default: 10.
data class Conversation(val keepLast: Int = 10) : CompressionStrategy
// Artifact entries: keep most recent entry per sourceId
object Artifact : CompressionStrategy
// Steering notes: always retained verbatim, never dropped
object SteeringNote : CompressionStrategy
// Event history DecisionPoints: always retained, already compressed facts
object EventHistory : CompressionStrategy
}
@@ -0,0 +1,12 @@
package com.correx.core.context.compression
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.TokenBudget
interface ContextCompressor {
fun compress(
entries: List<ContextEntry>,
budget: TokenBudget,
strategy: CompressionStrategy
): List<ContextEntry>
}
@@ -0,0 +1,63 @@
package com.correx.core.context.compression
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.TokenBudget
class DefaultContextCompressor : ContextCompressor {
// L0 (live execution), L3 (durable project memory), and L4 (archival) are never dropped — only L2 then L1.
private val dropOrder = listOf(ContextLayer.L2, ContextLayer.L1)
override fun compress(
entries: List<ContextEntry>,
budget: TokenBudget,
strategy: CompressionStrategy
): List<ContextEntry> = when (strategy) {
CompressionStrategy.SteeringNote,
CompressionStrategy.EventHistory -> entries
CompressionStrategy.Artifact -> compressArtifacts(entries, budget)
CompressionStrategy.ToolLog -> compressToolLogs(entries, budget)
is CompressionStrategy.Conversation -> compressConversation(entries, budget, strategy.keepLast)
}
// Keeps the last `keepLast` entries (oldest-first order preserved), then enforces budget.
// L0 is never truncated — overflow removes L1 from oldest first.
private fun compressConversation(
entries: List<ContextEntry>,
budget: TokenBudget,
keepLast: Int
): List<ContextEntry> {
val kept = if (entries.size > keepLast) entries.takeLast(keepLast) else entries
return trimToFit(kept, budget)
}
private fun compressArtifacts(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
val deduplicated = entries
.groupBy { it.sourceId }
.map { (_, group) -> group.last() }
return trimToFit(deduplicated, budget)
}
private fun compressToolLogs(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
val deduplicated = entries.distinctBy { it.content }
return trimToFit(deduplicated, budget)
}
private fun trimToFit(entries: List<ContextEntry>, budget: TokenBudget): List<ContextEntry> {
var total = entries.sumOf { it.tokenEstimate }
if (total <= budget.limit) return entries
val mutable = entries.toMutableList()
for (layer in dropOrder) {
if (total <= budget.limit) break
val layerEntries = mutable.filter { it.layer == layer }
for (entry in layerEntries) {
if (total <= budget.limit) break
mutable.remove(entry)
total -= entry.tokenEstimate
}
}
return mutable
}
}
@@ -0,0 +1,10 @@
package com.correx.core.context.model
import kotlinx.serialization.Serializable
@Serializable
data class CompressionMetadata(
val appliedStrategies: List<String> = emptyList(),
val truncatedLayers: List<ContextLayer> = emptyList(),
val entriesDropped: Int = 0
)
@@ -0,0 +1,14 @@
package com.correx.core.context.model
import com.correx.core.events.types.ContextEntryId
import kotlinx.serialization.Serializable
@Serializable
data class ContextEntry(
val id: ContextEntryId,
val layer: ContextLayer,
val content: String,
val sourceType: String,
val sourceId: String,
val tokenEstimate: Int
)
@@ -0,0 +1,12 @@
package com.correx.core.context.model
import kotlinx.serialization.Serializable
@Serializable
enum class ContextLayer {
L0, // live execution: current stage inputs/outputs
L1, // stage-local: recent artifacts and events within this stage
L2, // compressed session memory: summarised prior stages
L3, // durable project memory: cross-session retained context
L4 // archival history: stored externally, referenced by ID only
}
@@ -0,0 +1,17 @@
package com.correx.core.context.model
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class ContextPack(
val id: ContextPackId,
val sessionId: SessionId,
val stageId: StageId,
val layers: Map<ContextLayer, List<ContextEntry>> = emptyMap(),
val budgetUsed: Int,
val budgetLimit: Int,
val compressionMetadata: CompressionMetadata = CompressionMetadata()
)
@@ -0,0 +1,12 @@
package com.correx.core.context.model
data class TokenBudget(
val limit: Int,
val used: Int = 0
) {
val remaining: Int get() = limit - used
fun consume(tokens: Int): TokenBudget = copy(used = used + tokens)
fun canFit(tokens: Int): Boolean = tokens <= remaining
}
@@ -0,0 +1,11 @@
package com.correx.core.context.serialization
import com.correx.core.context.model.ContextPack
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
val contextModule: SerializersModule = SerializersModule {
polymorphic(ContextPack::class) {
// no subclasses — ContextPack is a concrete data class
}
}
@@ -0,0 +1,11 @@
package com.correx.core.context.state
import com.correx.core.events.types.ContextPackId
import kotlinx.serialization.Serializable
@Serializable
data class ContextState(
val builtPackIds: List<ContextPackId> = emptyList(),
val buildingInProgress: Boolean = false,
val interrupted: Boolean = false,
)