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
@@ -0,0 +1,10 @@
package com.correx.core.approvals
import kotlinx.serialization.Serializable
@Serializable
enum class ApprovalOutcome {
APPROVED,
REJECTED,
AUTO_APPROVED,
}
@@ -0,0 +1,10 @@
package com.correx.core.approvals
import kotlinx.serialization.Serializable
@Serializable
sealed interface ApprovalStatus {
@Serializable data object PENDING : ApprovalStatus
@Serializable data object COMPLETED : ApprovalStatus
@Serializable data object TIMED_OUT : ApprovalStatus
}
@@ -0,0 +1,12 @@
package com.correx.core.approvals
import com.correx.core.events.types.ProjectId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
sealed interface GrantScope {
@Serializable data object SESSION : GrantScope
@Serializable data class STAGE(val stageId: StageId) : GrantScope
@Serializable data class PROJECT(val projectId: ProjectId) : GrantScope
}
@@ -0,0 +1,16 @@
package com.correx.core.approvals
import kotlinx.serialization.Serializable
@Suppress("MagicNumber")
@Serializable
enum class Tier(val level: Int) {
T0(0),
T1(1),
T2(2),
T3(3),
T4(4)
}
fun Tier.isAtLeast(other: Tier): Boolean = this.level >= other.level
fun Tier.isAtMost(other: Tier): Boolean = this.level <= other.level
@@ -0,0 +1,12 @@
package com.correx.core.approvals
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class UserSteering(
val text: String,
val sessionId: SessionId,
val timestamp: Instant
)
@@ -0,0 +1,34 @@
package com.correx.core.events
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.NewEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Clock
import java.util.UUID
class EventDispatcher(private val eventStore: EventStore) {
suspend fun emit(
payload: EventPayload,
sessionId: SessionId,
causationId: CausationId? = null,
correlationId: CorrelationId? = null,
) {
val event = NewEvent(
metadata = EventMetadata(
eventId = EventId(UUID.randomUUID().toString()),
sessionId = sessionId,
timestamp = Clock.System.now(),
schemaVersion = 1,
causationId = causationId,
correlationId = correlationId,
),
payload = payload
)
eventStore.append(event)
}
}
@@ -0,0 +1,58 @@
package com.correx.core.events.events
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.Tier
import com.correx.core.approvals.UserSteering
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.GrantId
import com.correx.core.events.types.ProjectId
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ValidationReportId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ApprovalRequestedEvent(
val requestId: ApprovalRequestId,
val tier: Tier,
val validationReportId: ValidationReportId,
val riskSummaryId: RiskSummaryId?,
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?,
val userSteering: UserSteering? = null
) : EventPayload
@Serializable
data class ApprovalDecisionResolvedEvent(
val decisionId: ApprovalDecisionId,
val requestId: ApprovalRequestId,
val outcome: ApprovalOutcome,
val status: ApprovalStatus,
val tier: Tier,
val resolutionTimestamp: Instant,
val reason: String?,
val userSteering: UserSteering? = null
) : EventPayload
@Serializable
data class ApprovalGrantCreatedEvent(
val grantId: GrantId,
val scope: GrantScope,
val permittedTiers: Set<Tier>,
val reason: String,
val expiresAt: Instant?,
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?
) : EventPayload
@Serializable
data class ApprovalGrantExpiredEvent(
val grantId: GrantId
) : EventPayload
@@ -0,0 +1,60 @@
package com.correx.core.events.events
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactRelationshipType
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class ArtifactValidatingEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ArtifactValidatedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ArtifactSupersededEvent(
val artifactId: ArtifactId,
val supersededById: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ArtifactRelationshipAddedEvent(
val sourceId: ArtifactId,
val targetId: ArtifactId,
val relationshipType: ArtifactRelationshipType,
val sessionId: SessionId,
) : EventPayload
@Serializable
data class ArtifactRejectedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
@Serializable
data class ArtifactCreatedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
val schemaVersion: Int,
) : EventPayload
@Serializable
data class ArtifactArchivedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@@ -0,0 +1,51 @@
package com.correx.core.events.events
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 LayerTruncatedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesDropped: Int,
val reason: String,
) : EventPayload
@Serializable
data class ContextBuildingStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class ContextBuildingFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
// Emitted during replay when a session ended with buildingInProgress=true and no completion/failure event followed.
@Serializable
data class ContextBuildingInterruptedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class CompressionAppliedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesRemoved: Int,
val strategyApplied: String,
) : EventPayload
@Serializable
data class ContextPackBuiltEvent(
val contextPackId: ContextPackId,
val sessionId: SessionId,
val stageId: StageId,
val budgetUsed: Int,
val budgetLimit: Int,
) : EventPayload
@@ -0,0 +1,16 @@
package com.correx.core.events.events
import kotlinx.serialization.Serializable
@Serializable
data class NewEvent(
val metadata: EventMetadata,
val payload: EventPayload
)
@Serializable
data class StoredEvent(
val metadata: EventMetadata,
val sequence: Long,
val payload: EventPayload
)
@@ -0,0 +1,18 @@
package com.correx.core.events.events
import com.correx.core.events.types.CausationId
import com.correx.core.events.types.CorrelationId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class EventMetadata(
val eventId: EventId,
val sessionId: SessionId,
val timestamp: Instant,
val schemaVersion: Int,
val causationId: CausationId?,
val correlationId: CorrelationId?
)
@@ -0,0 +1,6 @@
package com.correx.core.events.events
import kotlinx.serialization.Serializable
@Serializable
sealed interface EventPayload
@@ -0,0 +1,60 @@
package com.correx.core.events.events
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.ProviderId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.TokenUsage
import kotlinx.serialization.Serializable
@Serializable
data class InferenceStartedEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
) : EventPayload
@Serializable
data class InferenceCompletedEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val tokensUsed: TokenUsage,
val latencyMs: Long,
) : EventPayload
@Serializable
data class InferenceFailedEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val reason: String, // human-readable; structured cause lives in CancellationReason
) : EventPayload
@Serializable
data class InferenceTimeoutEvent(
val requestId: InferenceRequestId,
val sessionId: SessionId,
val stageId: StageId,
val providerId: ProviderId,
val timeoutMs: Long,
) : EventPayload
@Serializable
data class ModelLoadedEvent(
val modelId: String,
val providerId: ProviderId,
val sessionId: SessionId,
) : EventPayload
@Serializable
data class ModelUnloadedEvent(
val modelId: String,
val providerId: ProviderId,
val sessionId: SessionId,
val cancellationReason: String? = null, // serialized label, not the sealed class
) : EventPayload
@@ -0,0 +1,50 @@
package com.correx.core.events.events
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class WorkflowStartedEvent(
val sessionId: SessionId,
val startStageId: StageId,
val retryPolicy: RetryPolicy? = null,
) : EventPayload
@Serializable
data class WorkflowCompletedEvent(
val sessionId: SessionId,
val terminalStageId: StageId,
val totalStages: Int,
) : EventPayload
@Serializable
data class WorkflowFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
val retryExhausted: Boolean,
) : EventPayload
@Serializable
data class OrchestrationPausedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String, // e.g. "APPROVAL_PENDING", "USER_REQUESTED"
) : EventPayload
@Serializable
data class OrchestrationResumedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
data class RetryAttemptedEvent(
val sessionId: SessionId,
val stageId: StageId,
val attemptNumber: Int,
val maxAttempts: Int,
val failureReason: String,
) : EventPayload
@@ -0,0 +1,17 @@
package com.correx.core.events.events
import com.correx.core.events.risk.RiskAction
import com.correx.core.events.risk.RiskLevel
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class RiskAssessedEvent(
val sessionId: SessionId,
val stageId: StageId,
val riskSummaryId: RiskSummaryId,
val level: RiskLevel,
val action: RiskAction,
) : EventPayload
@@ -0,0 +1,34 @@
package com.correx.core.events.events
import com.correx.core.events.types.SessionId
import kotlinx.serialization.Serializable
@Serializable
data class SessionStartedEvent(
val sessionId: SessionId,
val initialContextId: String? = null
) : EventPayload
@Serializable
data class SessionPausedEvent(
val sessionId: SessionId,
val reason: String? = null
) : EventPayload
@Serializable
data class SessionResumedEvent(
val sessionId: SessionId,
) : EventPayload
@Serializable
data class SessionCompletedEvent(
val sessionId: SessionId,
val summary: String? = null
) : EventPayload
@Serializable
data class SessionFailedEvent(
val sessionId: SessionId,
val errorCode: String? = null,
val errorMessage: String? = null
) : EventPayload
@@ -0,0 +1,36 @@
package com.correx.core.events.events
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import kotlinx.serialization.Serializable
@Serializable
data class StageStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId
) : EventPayload
@Serializable
data class StageCompletedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId
) : EventPayload
@Serializable
data class StageFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId,
val reason: String
) : EventPayload
@Serializable
data class TransitionExecutedEvent(
val sessionId: SessionId,
val from: StageId,
val to: StageId,
val transitionId: TransitionId
) : EventPayload
@@ -0,0 +1,54 @@
package com.correx.core.events.events
import com.correx.core.approvals.Tier
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.serialization.Serializable
@Serializable
data class ToolExecutionCompletedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
val receipt: ToolReceipt,
) : EventPayload
@Serializable
data class ToolExecutionFailedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
val reason: String,
) : EventPayload
@Serializable
data class ToolExecutionRejectedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
val tier: Tier,
val reason: String,
) : EventPayload
@Serializable
data class ToolExecutionStartedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val toolName: String,
) : EventPayload
@Serializable
data class ToolInvocationRequestedEvent(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val stageId: StageId,
val toolName: String,
val tier: Tier,
val request: ToolRequest,
) : EventPayload
@Serializable
data class ToolInvokedEvent(
val toolId: String,
) : EventPayload
@@ -0,0 +1,21 @@
package com.correx.core.events.events
import com.correx.core.approvals.Tier
import com.correx.core.events.serialization.AnyMapSerializer
import com.correx.core.events.types.ToolInvocationId
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
data class ToolReceipt(
val invocationId: ToolInvocationId,
val toolName: String,
val exitCode: Int,
val outputSummary: String,
@Serializable(with = AnyMapSerializer::class)
val structuredOutput: Map<String, Any> = emptyMap(),
val affectedEntities: List<String> = emptyList(),
val durationMs: Long,
val tier: Tier,
val timestamp: Instant
)
@@ -0,0 +1,17 @@
package com.correx.core.events.events
import com.correx.core.events.serialization.AnyMapSerializer
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.serialization.Serializable
@Serializable
data class ToolRequest(
val invocationId: ToolInvocationId,
val sessionId: SessionId,
val stageId: StageId,
val toolName: String,
@Serializable(with = AnyMapSerializer::class)
val parameters: Map<String, Any> = emptyMap()
)
@@ -0,0 +1,13 @@
package com.correx.core.events.execution
import kotlinx.serialization.Serializable
@Serializable
data class RetryPolicy(
val maxAttempts: Int,
val backoffMs: Long = 0L,
) {
init {
require(maxAttempts >= 1) { "maxAttempts must be >= 1" }
}
}
@@ -0,0 +1,16 @@
package com.correx.core.events.orchestration
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.StageId
import kotlinx.serialization.Serializable
@Serializable
data class OrchestrationState(
val currentStageId: StageId? = null,
val status: OrchestrationStatus = OrchestrationStatus.IDLE,
val retryCount: Int = 0,
val pauseReason: String? = null,
val pendingApproval: Boolean = false,
val failureReason: String? = null,
val retryPolicy: RetryPolicy? = null,
)
@@ -0,0 +1,11 @@
package com.correx.core.events.orchestration
enum class OrchestrationStatus {
IDLE,
RUNNING,
PAUSED,
COMPLETED,
FAILED,
CANCELED,
;
}
@@ -0,0 +1,3 @@
package com.correx.core.events.risk
enum class RiskAction { PROCEED, PROMPT_USER, BLOCK }
@@ -0,0 +1,3 @@
package com.correx.core.events.risk
enum class RiskLevel { LOW, MEDIUM, HIGH, CRITICAL }
@@ -0,0 +1,8 @@
package com.correx.core.events.risk
sealed class RiskSignal {
data class CycleWithoutExit(val cycleId: String) : RiskSignal()
data class RepeatedFailure(val reason: String, val count: Int) : RiskSignal()
data class ValidationErrors(val errorCount: Int) : RiskSignal()
data class InferenceTimeout(val elapsedMs: Long) : RiskSignal()
}
@@ -0,0 +1,7 @@
package com.correx.core.events.risk
data class RiskSummary(
val level: RiskLevel,
val signals: List<RiskSignal>,
val recommendedAction: RiskAction,
)
@@ -0,0 +1,71 @@
package com.correx.core.events.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
private object AnySerializer : KSerializer<Any> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AnyValue")
override fun serialize(encoder: Encoder, value: Any) {
val jsonEncoder = encoder as? JsonEncoder
?: throw SerializationException("AnySerializer requires a JSON encoder")
jsonEncoder.encodeJsonElement(value.toJsonElement())
}
override fun deserialize(decoder: Decoder): Any {
val jsonDecoder = decoder as? JsonDecoder
?: throw SerializationException("AnySerializer requires a JSON decoder")
return jsonDecoder.decodeJsonElement().toAny()
}
}
private fun Any?.toJsonElement(): JsonElement = when (this) {
null -> JsonNull
is String -> JsonPrimitive(this)
is Number -> JsonPrimitive(this)
is Boolean -> JsonPrimitive(this)
is Map<*, *> -> JsonObject(entries.associate { (k, v) -> k.toString() to v.toJsonElement() })
is List<*> -> JsonArray(map { it.toJsonElement() })
else -> throw SerializationException("Unsupported type: ${this::class.simpleName}")
}
private fun JsonElement.toAny(): Any = when (this) {
is JsonNull -> throw SerializationException("null values are not supported in Map<String, Any>")
is JsonPrimitive -> {
val content = this.content
when {
content == "true" || content == "false" -> content == "true"
content == "null" -> throw SerializationException("null values are not supported in Map<String, Any>")
content.toDoubleOrNull() != null && content.contains('.') -> content.toDouble()
content.toLongOrNull() != null -> content.toLong()
else -> content // fallback to string
}
}
is JsonObject -> entries.associate { (k, v) -> k to v.toAny() }
is JsonArray -> map { it.toAny() }
}
object AnyMapSerializer : KSerializer<Map<String, Any>> {
private val delegate = MapSerializer(String.serializer(), AnySerializer)
override val descriptor: SerialDescriptor = delegate.descriptor
@Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder): Map<String, Any> =
delegate.deserialize(decoder) as Map<String, Any>
override fun serialize(encoder: Encoder, value: Map<String, Any>) =
delegate.serialize(encoder, value)
}
@@ -0,0 +1,8 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
interface EventSerializer {
fun serialize(payload: EventPayload): String
fun deserialize(raw: String): EventPayload
}
@@ -0,0 +1,15 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import kotlinx.serialization.json.Json
class JsonEventSerializer(
private val json: Json = eventJson
) : EventSerializer {
override fun serialize(payload: EventPayload): String =
json.encodeToString(EventPayload.serializer(), payload)
override fun deserialize(raw: String): EventPayload =
json.decodeFromString(EventPayload.serializer(), raw)
}
@@ -0,0 +1,107 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRejectedEvent
import com.correx.core.events.events.ArtifactRelationshipAddedEvent
import com.correx.core.events.events.ArtifactSupersededEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.CompressionAppliedEvent
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.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.ModelLoadedEvent
import com.correx.core.events.events.ModelUnloadedEvent
import com.correx.core.events.events.LayerTruncatedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SessionCompletedEvent
import com.correx.core.events.events.SessionFailedEvent
import com.correx.core.events.events.SessionPausedEvent
import com.correx.core.events.events.SessionResumedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StageStartedEvent
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.ToolInvokedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
val eventModule = SerializersModule {
polymorphic(EventPayload::class) {
// Legacy stub event — predates the full tool lifecycle model (ToolInvocationRequestedEvent et al.)
subclass(ToolInvokedEvent::class)
subclass(ToolInvocationRequestedEvent::class)
subclass(ToolExecutionStartedEvent::class)
subclass(ToolExecutionCompletedEvent::class)
subclass(ToolExecutionFailedEvent::class)
subclass(ToolExecutionRejectedEvent::class)
subclass(SessionStartedEvent::class)
subclass(SessionPausedEvent::class)
subclass(SessionResumedEvent::class)
subclass(SessionCompletedEvent::class)
subclass(SessionFailedEvent::class)
subclass(StageStartedEvent::class)
subclass(StageFailedEvent::class)
subclass(StageCompletedEvent::class)
subclass(TransitionExecutedEvent::class)
subclass(ApprovalRequestedEvent::class)
subclass(ApprovalDecisionResolvedEvent::class)
subclass(ApprovalGrantCreatedEvent::class)
subclass(ApprovalGrantExpiredEvent::class)
subclass(ContextBuildingStartedEvent::class)
subclass(ContextPackBuiltEvent::class)
subclass(CompressionAppliedEvent::class)
subclass(LayerTruncatedEvent::class)
subclass(ContextBuildingFailedEvent::class)
subclass(ContextBuildingInterruptedEvent::class)
subclass(ArtifactCreatedEvent::class)
subclass(ArtifactValidatingEvent::class)
subclass(ArtifactValidatedEvent::class)
subclass(ArtifactRejectedEvent::class)
subclass(ArtifactSupersededEvent::class)
subclass(ArtifactArchivedEvent::class)
subclass(ArtifactRelationshipAddedEvent::class)
subclass(InferenceFailedEvent::class)
subclass(InferenceCompletedEvent::class)
subclass(InferenceStartedEvent::class)
subclass(InferenceTimeoutEvent::class)
subclass(ModelLoadedEvent::class)
subclass(ModelUnloadedEvent::class)
subclass(OrchestrationResumedEvent::class)
subclass(OrchestrationPausedEvent::class)
subclass(WorkflowStartedEvent::class)
subclass(WorkflowFailedEvent::class)
subclass(WorkflowCompletedEvent::class)
subclass(RetryAttemptedEvent::class)
subclass(RiskAssessedEvent::class)
}
}
val eventJson = Json {
serializersModule = eventModule
}
@@ -0,0 +1,37 @@
package com.correx.core.events.stores
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.SessionId
interface EventStore {
/**
* Append a single event to the store.
* Must enforce:
* - monotonic per-session sequence
* - idempotency by eventId
* - causal consistency rules (if enforced here, otherwise in kernel)
*/
fun append(event: NewEvent): StoredEvent
/**
* Append multiple events atomically (same session preferred).
*/
fun appendAll(events: List<NewEvent>): List<StoredEvent>
/**
* Read events for a session in strict sequence order.
*/
fun read(sessionId: SessionId): List<StoredEvent>
/**
* Read events from a specific sequence offset (for replay/resume).
*/
fun readFrom(sessionId: SessionId, fromSequence: Long): List<StoredEvent>
/**
* Get last sequence number for session.
*/
fun lastSequence(sessionId: SessionId): Long?
}
@@ -0,0 +1,13 @@
package com.correx.core.events.types
import kotlinx.serialization.Serializable
@Serializable
enum class ArtifactLifecyclePhase {
CREATED,
VALIDATING,
VALIDATED,
REJECTED,
SUPERSEDED,
ARCHIVED
}
@@ -0,0 +1,14 @@
package com.correx.core.events.types
import kotlinx.serialization.Serializable
@Serializable
enum class ArtifactRelationshipType {
PARENT,
CHILD,
SUPERSEDES,
DERIVED_FROM,
VALIDATED_BY,
APPROVED_BY,
GENERATED_FROM
}
@@ -0,0 +1,17 @@
package com.correx.core.events.types
object EventTypes {
const val SESSION_STARTED = "session.started"
const val SESSION_PAUSED = "session.paused"
const val SESSION_COMPLETED = "session.completed"
const val TOOL_INVOKED = "tool.invoked"
const val TOOL_COMPLETED = "tool.completed"
const val APPROVAL_REQUESTED = "approval.requested"
const val APPROVAL_DECISION_RESOLVED = "approval.decision_resolved"
const val APPROVAL_GRANT_CREATED = "approval.grant_created"
const val APPROVAL_GRANT_EXPIRED = "approval.grant_expired"
const val ARTIFACT_CREATED = "artifact.created"
}
@@ -0,0 +1,42 @@
package com.correx.core.events.types
import com.correx.core.utils.TypeId
// Events types
typealias EventId = TypeId
typealias SessionId = TypeId
typealias CorrelationId = TypeId
typealias CausationId = TypeId
// Transitions types
typealias StageId = TypeId
typealias TransitionId = TypeId
// Artifacts types
typealias ArtifactId = TypeId
// Context types
typealias ContextPackId = TypeId
typealias ContextEntryId = TypeId
// Approvals types
typealias ValidationReportId = TypeId
typealias RiskSummaryId = TypeId
typealias GrantId = TypeId
typealias ApprovalRequestId = TypeId
typealias ProjectId = TypeId
typealias ApprovalDecisionId = TypeId
// Tools types
typealias ToolInvocationId = TypeId
// Inference
typealias InferenceRequestId = TypeId
typealias ProviderId = TypeId
@@ -0,0 +1 @@
package com.correx.core.events.types
@@ -0,0 +1,11 @@
package com.correx.core.inference
import kotlinx.serialization.Serializable
@Serializable
data class TokenUsage(
val promptTokens: Int,
val completionTokens: Int,
) {
val totalTokens: Int get() = promptTokens + completionTokens
}
@@ -0,0 +1,14 @@
package com.correx.core.sessions.projections
import com.correx.core.events.events.StoredEvent
class DefaultStateBuilder<S>(
private val projection: Projection<S>
) : StateBuilder<S> {
override fun build(events: List<StoredEvent>): S {
return events.fold(projection.initial()) { state, event ->
projection.apply(state, event)
}
}
}
@@ -0,0 +1,10 @@
package com.correx.core.sessions.projections
import com.correx.core.events.events.StoredEvent
interface Projection<S> {
fun initial(): S
fun apply(state: S, event: StoredEvent): S
}
@@ -0,0 +1,7 @@
package com.correx.core.sessions.projections
import com.correx.core.events.events.StoredEvent
interface StateBuilder<S> {
fun build(events: List<StoredEvent>): S
}
@@ -0,0 +1,19 @@
package com.correx.core.sessions.projections.replay
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.DefaultStateBuilder
import com.correx.core.sessions.projections.Projection
class DefaultEventReplayer<S>(
private val store: EventStore,
private val projection: Projection<S>
) : EventReplayer<S> {
override fun rebuild(sessionId: SessionId): S {
val events = store.read(sessionId)
return DefaultStateBuilder(projection)
.build(events)
}
}
@@ -0,0 +1,7 @@
package com.correx.core.sessions.projections.replay
import com.correx.core.events.types.SessionId
interface EventReplayer<S> {
fun rebuild(sessionId: SessionId): S
}
@@ -0,0 +1,14 @@
package com.correx.core.utils
import kotlinx.serialization.Serializable
@JvmInline
@Serializable
value class TypeId(val value: String) {
init {
require(value.isNotBlank()) { "id must not be blank" }
require(value.trim() == value) { "id must not contain leading/trailing whitespace" }
}
override fun toString(): String = value
}
@@ -0,0 +1,99 @@
package com.correx.core.events
import com.correx.core.approvals.Tier
import com.correx.core.events.events.EventPayload
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.ToolReceipt
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.serialization.eventJson
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.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ToolEventsSerializationTest {
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",
parameters = mapOf("key" to "value", "count" to 42L)
)
private val receipt = ToolReceipt(
invocationId = invocationId,
toolName = "echo",
exitCode = 0,
outputSummary = "done",
structuredOutput = mapOf("result" to "ok"),
affectedEntities = listOf("file.txt"),
durationMs = 100L,
tier = Tier.T0,
timestamp = Instant.parse("2026-01-01T00:00:00Z")
)
@Test
fun `ToolInvocationRequestedEvent round-trips`() {
val event = ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request)
val json = eventJson.encodeToString(ToolInvocationRequestedEvent.serializer(), event)
assertEquals(event, eventJson.decodeFromString(ToolInvocationRequestedEvent.serializer(), json))
}
@Test
fun `ToolExecutionStartedEvent round-trips`() {
val event = ToolExecutionStartedEvent(invocationId, sessionId, "echo")
val json = eventJson.encodeToString(ToolExecutionStartedEvent.serializer(), event)
assertEquals(event, eventJson.decodeFromString(ToolExecutionStartedEvent.serializer(), json))
}
@Test
fun `ToolExecutionCompletedEvent round-trips`() {
val event = ToolExecutionCompletedEvent(invocationId, sessionId, "echo", receipt)
val json = eventJson.encodeToString(ToolExecutionCompletedEvent.serializer(), event)
assertEquals(event, eventJson.decodeFromString(ToolExecutionCompletedEvent.serializer(), json))
}
@Test
fun `ToolExecutionFailedEvent round-trips`() {
val event = ToolExecutionFailedEvent(invocationId, sessionId, "echo", "something broke")
val json = eventJson.encodeToString(ToolExecutionFailedEvent.serializer(), event)
assertEquals(event, eventJson.decodeFromString(ToolExecutionFailedEvent.serializer(), json))
}
@Test
fun `ToolExecutionRejectedEvent round-trips`() {
val event = ToolExecutionRejectedEvent(invocationId, sessionId, "echo", Tier.T4, "tier too high")
val json = eventJson.encodeToString(ToolExecutionRejectedEvent.serializer(), event)
assertEquals(event, eventJson.decodeFromString(ToolExecutionRejectedEvent.serializer(), json))
}
@Test
fun `ToolRequest with parameters round-trips`() {
val json = eventJson.encodeToString(ToolRequest.serializer(), request)
assertEquals(request, eventJson.decodeFromString(ToolRequest.serializer(), json))
}
@Test
fun `ToolReceipt with structuredOutput round-trips`() {
val json = eventJson.encodeToString(ToolReceipt.serializer(), receipt)
assertEquals(receipt, eventJson.decodeFromString(ToolReceipt.serializer(), json))
}
@Test
fun `ToolInvocationRequestedEvent round-trips through polymorphic EventPayload`() {
val event = ToolInvocationRequestedEvent(invocationId, sessionId, stageId, "echo", Tier.T0, request)
val json = eventJson.encodeToString(EventPayload.serializer(), event)
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json)
assertEquals(event, decoded)
}
}