epic-12: after epic audit and init commit
This commit is contained in:
@@ -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>
|
||||
}
|
||||
+76
@@ -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()
|
||||
}
|
||||
}
|
||||
+14
@@ -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() }
|
||||
}
|
||||
+14
@@ -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>
|
||||
}
|
||||
+63
@@ -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
|
||||
}
|
||||
+11
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user