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
+12
View File
@@ -0,0 +1,12 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
implementation(project(":core:approvals"))
implementation(project(":core:sessions"))
testImplementation "org.jetbrains.kotlin:kotlin-test"
}
@@ -0,0 +1,52 @@
package com.correx.core.tools
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
import com.correx.core.events.events.ToolExecutionStartedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.state.ToolInvocationRecord
import com.correx.core.tools.state.ToolInvocationStatus
import com.correx.core.tools.state.ToolState
class DefaultToolReducer : ToolReducer {
override fun reduce(state: ToolState, event: StoredEvent): ToolState =
when (val p = event.payload) {
is ToolInvocationRequestedEvent -> state.copy(
invocations = state.invocations + ToolInvocationRecord(
invocationId = p.invocationId,
toolName = p.toolName,
tier = p.tier,
request = p.request,
status = ToolInvocationStatus.REQUESTED,
requestedAt = event.metadata.timestamp
)
)
is ToolExecutionStartedEvent -> state.updateRecord(p.invocationId) {
it.copy(status = ToolInvocationStatus.STARTED)
}
is ToolExecutionCompletedEvent -> state.updateRecord(p.invocationId) {
it.copy(
status = ToolInvocationStatus.COMPLETED,
receipt = p.receipt,
completedAt = event.metadata.timestamp
)
}
is ToolExecutionFailedEvent -> state.updateRecord(p.invocationId) {
it.copy(status = ToolInvocationStatus.FAILED, completedAt = event.metadata.timestamp)
}
is ToolExecutionRejectedEvent -> state.updateRecord(p.invocationId) {
it.copy(status = ToolInvocationStatus.REJECTED, completedAt = event.metadata.timestamp)
}
else -> state
}
}
private fun ToolState.updateRecord(
invocationId: ToolInvocationId,
transform: (ToolInvocationRecord) -> ToolInvocationRecord
): ToolState = copy(
invocations = invocations.map { if (it.invocationId == invocationId) transform(it) else it }
)
@@ -0,0 +1,9 @@
package com.correx.core.tools
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.EventReplayer
import com.correx.core.tools.state.ToolState
class DefaultToolRepository(private val replayer: EventReplayer<ToolState>) {
fun getToolState(sessionId: SessionId): ToolState = replayer.rebuild(sessionId)
}
@@ -0,0 +1,3 @@
package com.correx.core.tools
object Module
@@ -0,0 +1,11 @@
package com.correx.core.tools
import com.correx.core.events.events.StoredEvent
import com.correx.core.sessions.projections.Projection
import com.correx.core.tools.state.ToolState
class ToolProjector(private val reducer: ToolReducer) : Projection<ToolState> {
override fun initial(): ToolState = ToolState()
override fun apply(state: ToolState, event: StoredEvent): ToolState = reducer.reduce(state, event)
}
@@ -0,0 +1,8 @@
package com.correx.core.tools
import com.correx.core.events.events.StoredEvent
import com.correx.core.tools.state.ToolState
interface ToolReducer {
fun reduce(state: ToolState, event: StoredEvent): ToolState
}
@@ -0,0 +1,8 @@
package com.correx.core.tools.contract
import com.correx.core.events.events.ToolRequest
import java.nio.file.Path
interface FileAffectingTool : Tool {
fun affectedPaths(request: ToolRequest): Set<Path>
}
@@ -0,0 +1,11 @@
package com.correx.core.tools.contract
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
interface Tool {
val name: String
val tier: Tier
val requiredCapabilities: Set<ToolCapability>
fun validateRequest(request: ToolRequest): ValidationResult
}
@@ -0,0 +1,9 @@
package com.correx.core.tools.contract
enum class ToolCapability {
FILE_READ,
FILE_WRITE,
NETWORK_ACCESS,
SHELL_EXEC,
PROCESS_SPAWN
}
@@ -0,0 +1,7 @@
package com.correx.core.tools.contract
import com.correx.core.events.events.ToolRequest
interface ToolExecutor {
suspend fun execute(request: ToolRequest): ToolResult
}
@@ -0,0 +1,21 @@
package com.correx.core.tools.contract
import com.correx.core.events.types.ToolInvocationId
import kotlinx.serialization.Serializable
sealed interface ToolResult {
@Serializable
data class Success(
val invocationId: ToolInvocationId,
val output: String,
val exitCode: Int = 0,
val metadata: Map<String, String> = emptyMap(),
) : ToolResult
@Serializable
data class Failure(
val invocationId: ToolInvocationId,
val reason: String,
val recoverable: Boolean,
) : ToolResult
}
@@ -0,0 +1,6 @@
package com.correx.core.tools.contract
sealed interface ValidationResult {
data object Valid : ValidationResult
data class Invalid(val reason: String) : ValidationResult
}
@@ -0,0 +1,8 @@
package com.correx.core.tools.registry
import com.correx.core.tools.contract.Tool
interface ToolRegistry {
fun resolve(name: String): Tool?
fun all(): List<Tool>
}
@@ -0,0 +1,20 @@
package com.correx.core.tools.state
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ToolInvocationRecord(
val invocationId: ToolInvocationId,
val toolName: String,
val tier: Tier,
val request: ToolRequest,
val status: ToolInvocationStatus,
val receipt: ToolReceipt? = null,
val requestedAt: Instant,
val completedAt: Instant? = null
)
@@ -0,0 +1,12 @@
package com.correx.core.tools.state
import kotlinx.serialization.Serializable
@Serializable
enum class ToolInvocationStatus {
REQUESTED,
STARTED,
COMPLETED,
FAILED,
REJECTED
}
@@ -0,0 +1,8 @@
package com.correx.core.tools.state
import kotlinx.serialization.Serializable
@Serializable
data class ToolState(
val invocations: List<ToolInvocationRecord> = emptyList()
)
@@ -0,0 +1,138 @@
package com.correx.core.tools
import com.correx.core.approvals.Tier
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
import com.correx.core.events.events.ToolExecutionStartedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolInvokedEvent
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.state.ToolInvocationStatus
import com.correx.core.tools.state.ToolState
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class DefaultToolReducerTest {
private val reducer = DefaultToolReducer()
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
private val invocationId = ToolInvocationId("inv-1")
private val sessionId = SessionId("s-1")
private val stageId = StageId("st-1")
private val request = ToolRequest(
invocationId = invocationId,
sessionId = sessionId,
stageId = stageId,
toolName = "echo"
)
private val receipt = ToolReceipt(
invocationId = invocationId,
toolName = "echo",
exitCode = 0,
outputSummary = "done",
durationMs = 50L,
tier = Tier.T0,
timestamp = timestamp
)
private fun storedEvent(payload: com.correx.core.events.events.EventPayload): StoredEvent =
StoredEvent(
metadata = EventMetadata(
eventId = EventId("e-1"),
sessionId = sessionId,
timestamp = timestamp,
schemaVersion = 1,
causationId = null,
correlationId = null
),
sequence = 1L,
payload = payload
)
@Test
fun `ToolInvocationRequestedEvent appends REQUESTED record`() {
val event = storedEvent(
ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request)
)
val state = reducer.reduce(ToolState(), event)
assertEquals(1, state.invocations.size)
assertEquals(ToolInvocationStatus.REQUESTED, state.invocations[0].status)
assertEquals(invocationId, state.invocations[0].invocationId)
}
@Test
fun `ToolExecutionStartedEvent transitions record to STARTED`() {
var state = reducer.reduce(
ToolState(),
storedEvent(ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request))
)
state = reducer.reduce(state, storedEvent(ToolExecutionStartedEvent(invocationId, sessionId, "echo")))
assertEquals(ToolInvocationStatus.STARTED, state.invocations[0].status)
}
@Test
fun `ToolExecutionCompletedEvent transitions to COMPLETED with receipt and completedAt`() {
var state = reducer.reduce(
ToolState(),
storedEvent(ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request))
)
state = reducer.reduce(state, storedEvent(ToolExecutionStartedEvent(invocationId, sessionId, "echo")))
state = reducer.reduce(
state,
storedEvent(ToolExecutionCompletedEvent(invocationId, sessionId, "echo", receipt))
)
val record = state.invocations[0]
assertEquals(ToolInvocationStatus.COMPLETED, record.status)
assertNotNull(record.receipt)
assertEquals(0, record.receipt!!.exitCode)
assertNotNull(record.completedAt)
}
@Test
fun `ToolExecutionFailedEvent transitions to FAILED and sets completedAt`() {
var state = reducer.reduce(
ToolState(),
storedEvent(ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request))
)
state = reducer.reduce(state, storedEvent(ToolExecutionFailedEvent(invocationId, sessionId, "echo", "timeout")))
val record = state.invocations[0]
assertEquals(ToolInvocationStatus.FAILED, record.status)
assertNull(record.receipt)
assertNotNull(record.completedAt)
}
@Test
fun `ToolExecutionRejectedEvent transitions to REJECTED and sets completedAt`() {
var state = reducer.reduce(
ToolState(),
storedEvent(ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request))
)
state = reducer.reduce(
state,
storedEvent(ToolExecutionRejectedEvent(invocationId, sessionId, "echo", Tier.T0, "tier denied"))
)
val record = state.invocations[0]
assertEquals(ToolInvocationStatus.REJECTED, record.status)
assertNotNull(record.completedAt)
}
@Test
fun `unrelated event passes through unchanged`() {
val state = ToolState()
val result = reducer.reduce(state, storedEvent(ToolInvokedEvent("test-tool")))
assertEquals(state, result)
}
}
@@ -0,0 +1,46 @@
package com.correx.core.tools.contract
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
class ToolContractTest {
private val stubTool = object : Tool {
override val name: String = "stub"
override val tier: Tier = Tier.T0
override val requiredCapabilities: Set<ToolCapability> = emptySet()
override fun validateRequest(request: ToolRequest): ValidationResult = ValidationResult.Valid
}
private val request = ToolRequest(
invocationId = ToolInvocationId("inv-1"),
sessionId = SessionId("s-1"),
stageId = StageId("st-1"),
toolName = "stub"
)
@Test
fun `stub tool returns Valid for any request`() {
assertIs<ValidationResult.Valid>(stubTool.validateRequest(request))
}
@Test
fun `ValidationResult Invalid carries reason`() {
val result = ValidationResult.Invalid("missing required parameter")
assertIs<ValidationResult.Invalid>(result)
assertEquals("missing required parameter", result.reason)
}
@Test
fun `tool exposes name tier and capabilities`() {
assertEquals("stub", stubTool.name)
assertEquals(Tier.T0, stubTool.tier)
assertEquals(emptySet(), stubTool.requiredCapabilities)
}
}
@@ -0,0 +1,57 @@
package com.correx.core.tools.state
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ToolStateTest {
private val request = ToolRequest(
invocationId = ToolInvocationId("inv-1"),
sessionId = SessionId("s-1"),
stageId = StageId("st-1"),
toolName = "echo"
)
@Test
fun `initial ToolState has empty invocations`() {
assertTrue(ToolState().invocations.isEmpty())
}
@Test
fun `ToolInvocationRecord has null receipt and completedAt by default`() {
val record = ToolInvocationRecord(
invocationId = ToolInvocationId("inv-1"),
toolName = "echo",
tier = Tier.T0,
request = request,
status = ToolInvocationStatus.REQUESTED,
requestedAt = Instant.parse("2026-01-01T00:00:00Z")
)
assertNull(record.receipt)
assertNull(record.completedAt)
}
@Test
fun `ToolState can accumulate multiple records`() {
val base = ToolInvocationRecord(
invocationId = ToolInvocationId("inv-1"),
toolName = "echo",
tier = Tier.T0,
request = request,
status = ToolInvocationStatus.REQUESTED,
requestedAt = Instant.parse("2026-01-01T00:00:00Z")
)
val state = ToolState(
invocations = listOf(base, base.copy(invocationId = ToolInvocationId("inv-2")))
)
assertEquals(2, state.invocations.size)
}
}