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
+18
View File
@@ -0,0 +1,18 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation project(':core:events')
implementation project(':core:sessions')
implementation project(':core:transitions')
implementation project(':core:validation')
implementation project(':core:approvals')
implementation project(':core:context')
implementation project(':core:inference')
implementation project(':core:tools')
implementation project(':core:artifacts')
implementation project(':core:risk')
}
@@ -0,0 +1,7 @@
package com.correx.core.kernel.execution
sealed interface ReplayStrategy {
data object Full : ReplayStrategy
data object SkipInference : ReplayStrategy // use recorded InferenceCompletedEvent artifacts
data object SkipValidation : ReplayStrategy // trust recorded outcomes
}
@@ -0,0 +1 @@
package com.correx.core.kernel.execution
@@ -0,0 +1,18 @@
package com.correx.core.kernel.execution
import com.correx.core.approvals.model.DomainApprovalRequest
import com.correx.core.artifacts.model.Artifact
import com.correx.core.validation.model.ValidationReport
sealed interface StageOutcome {
data class Success(val artifact: Artifact) : StageOutcome
/**
* @param retryable set by the validator based on failure type; the orchestrator must read this
* and never override it.
* Structural errors (e.g. invalid graph topology) are not retryable. Transient errors may be.
*/
data class ValidationFailure(val report: ValidationReport, val retryable: Boolean) : StageOutcome
data class InferenceFailure(val reason: String, val retryable: Boolean) : StageOutcome
data class ApprovalRequired(val request: DomainApprovalRequest) : StageOutcome
data object Cancelled : StageOutcome
}
@@ -0,0 +1,10 @@
package com.correx.core.kernel.execution
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
sealed interface WorkflowResult {
data class Completed(val sessionId: SessionId, val terminalStageId: StageId) : WorkflowResult
data class Failed(val sessionId: SessionId, val reason: String, val retryExhausted: Boolean) : WorkflowResult
data class Cancelled(val sessionId: SessionId) : WorkflowResult
}
@@ -0,0 +1,49 @@
package com.correx.core.kernel.orchestration
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.StoredEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.orchestration.OrchestrationStatus
class DefaultOrchestrationReducer : OrchestrationReducer {
override fun reduce(
state: OrchestrationState,
event: StoredEvent,
): OrchestrationState = when (val p = event.payload) {
is WorkflowStartedEvent -> state.copy(
status = OrchestrationStatus.RUNNING,
currentStageId = p.startStageId,
retryPolicy = p.retryPolicy,
)
is WorkflowCompletedEvent -> state.copy(
status = OrchestrationStatus.COMPLETED,
currentStageId = p.terminalStageId,
)
is WorkflowFailedEvent -> state.copy(status = OrchestrationStatus.FAILED, failureReason = p.reason)
is OrchestrationPausedEvent -> state.copy(
status = OrchestrationStatus.PAUSED,
pendingApproval = true,
pauseReason = p.reason,
)
is OrchestrationResumedEvent -> state.copy(
status = OrchestrationStatus.RUNNING,
pendingApproval = false,
pauseReason = null,
)
is RetryAttemptedEvent -> state.copy(
status = OrchestrationStatus.RUNNING,
retryCount = p.attemptNumber,
failureReason = null,
)
else -> state
}
}
@@ -0,0 +1,157 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.kernel.retry.RetryCoordinator
import com.correx.core.sessions.Session
import com.correx.core.transitions.execution.StageExecutionResult
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.resolution.TransitionDecision
import java.util.concurrent.*
import java.util.concurrent.atomic.*
@SuppressWarnings(
"ReturnCount",
"NestedBlockDepth",
)
class DefaultSessionOrchestrator(
private val repositories: OrchestratorRepositories,
engines: OrchestratorEngines,
private val retryCoordinator: RetryCoordinator,
) : SessionOrchestrator(repositories, engines) {
override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
ConcurrentHashMap<SessionId, AtomicBoolean>()
override suspend fun run(
sessionId: SessionId,
graph: WorkflowGraph,
config: OrchestrationConfig,
): WorkflowResult {
emitWorkflowStarted(sessionId, graph, config)
val base = ExecutionContext(graph, sessionId, 0, graph.start, config, null, null)
return step(base.enrich())
}
private tailrec suspend fun step(ctx: EnrichedExecutionContext): WorkflowResult {
if (isCancelled(ctx.sessionId)) return handleCancellation(ctx.sessionId, ctx.currentStageId)
val enriched = EnrichedExecutionContext(
graph = ctx.graph,
sessionId = ctx.sessionId,
stageCount = ctx.stageCount,
currentStageId = ctx.currentStageId,
config = ctx.config,
state = orchestrationRepository.getState(ctx.sessionId),
session = repositories.sessionRepository.getSession(ctx.sessionId),
)
return when (val decision = resolveTransition(enriched.graph, enriched.sessionId, enriched.currentStageId)) {
is TransitionDecision.Move -> when (val r = executeMove(enriched, decision)) {
is StepResult.Continue -> step(r.ctx)
is StepResult.Terminal -> r.result
}
is TransitionDecision.Stay -> if (isTerminal(enriched.graph, enriched.currentStageId)) {
completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount)
} else {
step(enriched)
}
is TransitionDecision.Blocked -> failWorkflow(
sessionId = enriched.sessionId,
stageId = enriched.currentStageId,
reason = decision.reason,
retryExhausted = false,
)
is TransitionDecision.NoMatch -> failWorkflow(
sessionId = enriched.sessionId,
stageId = enriched.currentStageId,
reason = "no matching transition from stage ${enriched.currentStageId.value}",
retryExhausted = false,
)
}
}
override suspend fun cancel(sessionId: SessionId) {
cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true)
}
private suspend fun executeMove(
ctx: EnrichedExecutionContext,
decision: TransitionDecision.Move,
): StepResult {
val nextStageId = decision.to
if (isTerminal(ctx.graph, nextStageId)) {
return StepResult.Terminal(
completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount + 1),
)
}
return when (val result = executeStage(ctx.sessionId, nextStageId, ctx.graph, ctx.session, ctx.config)) {
is StageExecutionResult.Success -> StepResult.Continue(
ctx.copy(
currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision),
stageCount = ctx.stageCount + 1,
),
)
is StageExecutionResult.Failure -> {
val shouldRetry = retryCoordinator.shouldRetry(
sessionId = ctx.sessionId,
stageId = nextStageId,
currentAttempt = ctx.state.retryCount,
policy = ctx.config.retryPolicy,
failureReason = result.reason,
)
if (shouldRetry) {
StepResult.Continue(ctx) // retryCount updated via projection on next iteration
} else {
StepResult.Terminal(
failWorkflow(
ctx.sessionId,
ctx.currentStageId,
result.reason,
retryExhausted = result.retryable,
),
)
}
}
}
}
private fun ExecutionContext.enrich() = EnrichedExecutionContext(
graph, sessionId, stageCount, currentStageId, config,
session = repositories.sessionRepository.getSession(sessionId),
state = orchestrationRepository.getState(sessionId),
)
}
private sealed class StepResult {
data class Continue(val ctx: EnrichedExecutionContext) : StepResult()
data class Terminal(val result: WorkflowResult) : StepResult()
}
private data class ExecutionContext(
val graph: WorkflowGraph,
val sessionId: SessionId,
val stageCount: Int,
val currentStageId: StageId,
val config: OrchestrationConfig,
val session: Session?,
val state: OrchestrationState?,
)
private data class EnrichedExecutionContext(
val graph: WorkflowGraph,
val sessionId: SessionId,
val stageCount: Int,
val currentStageId: StageId,
val config: OrchestrationConfig,
val session: Session,
val state: OrchestrationState,
)
@@ -0,0 +1,10 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.kernel.execution.ReplayStrategy
data class OrchestrationConfig(
val retryPolicy: RetryPolicy = RetryPolicy(maxAttempts = 3),
val replayStrategy: ReplayStrategy = ReplayStrategy.Full,
val stageTimeoutMs: Long = 60_000L,
)
@@ -0,0 +1,16 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.sessions.projections.Projection
class OrchestrationProjector(
private val orchestrationReducer: OrchestrationReducer,
) : Projection<OrchestrationState> {
override fun initial(): OrchestrationState = OrchestrationState()
override fun apply(
state: OrchestrationState,
event: StoredEvent,
): OrchestrationState = orchestrationReducer.reduce(state, event)
}
@@ -0,0 +1,8 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.orchestration.OrchestrationState
interface OrchestrationReducer {
fun reduce(state: OrchestrationState, event: StoredEvent): OrchestrationState
}
@@ -0,0 +1,11 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.EventReplayer
class OrchestrationRepository(
private val replayer: EventReplayer<OrchestrationState>,
) {
fun getState(sessionId: SessionId): OrchestrationState = replayer.rebuild(sessionId)
}
@@ -0,0 +1 @@
package com.correx.core.kernel.orchestration
@@ -0,0 +1 @@
package com.correx.core.kernel.orchestration
@@ -0,0 +1,17 @@
package com.correx.core.kernel.orchestration
import com.correx.core.approvals.domain.ApprovalEngine
import com.correx.core.context.builder.ContextPackBuilder
import com.correx.core.inference.InferenceRouter
import com.correx.core.risk.RiskAssessor
import com.correx.core.transitions.resolution.TransitionResolver
import com.correx.core.validation.pipeline.ValidationPipeline
data class OrchestratorEngines(
val transitionResolver: TransitionResolver,
val contextPackBuilder: ContextPackBuilder,
val inferenceRouter: InferenceRouter,
val validationPipeline: ValidationPipeline,
val approvalEngine: ApprovalEngine,
val riskAssessor: RiskAssessor,
)
@@ -0,0 +1,12 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.stores.EventStore
import com.correx.core.inference.InferenceRepository
import com.correx.core.sessions.DefaultSessionRepository
data class OrchestratorRepositories(
val eventStore: EventStore,
val inferenceRepository: InferenceRepository,
val orchestrationRepository: OrchestrationRepository,
val sessionRepository: DefaultSessionRepository,
)
@@ -0,0 +1,170 @@
package com.correx.core.kernel.orchestration
import com.correx.core.approvals.domain.NoOpApprovalEngine
import com.correx.core.context.model.ContextPack
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.inference.GenerationConfig
import com.correx.core.inference.InferenceRequest
import com.correx.core.kernel.execution.ReplayStrategy
import com.correx.core.kernel.replay.ReplayInferenceProvider
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
import com.correx.core.risk.NoOpRiskAssessor
import com.correx.core.transitions.execution.StageExecutionResult
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.resolution.TransitionDecision
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.validation.model.ValidationContext
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
private data class ReplayContext(
val graph: WorkflowGraph,
val sessionId: SessionId,
val currentStageId: StageId,
val stageCount: Int,
val config: OrchestrationConfig,
)
/**
* Replay is environment-independent. No live risk assessment or approval evaluation —
* both are reconstructed from the event log (ApprovalGrantedEvent / ApprovalDeniedEvent).
*/
@SuppressWarnings("ReturnCount")
class ReplayOrchestrator(
private val repositories: OrchestratorRepositories,
engines: OrchestratorEngines,
override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean>,
private val strategy: ReplayStrategy,
) : SessionOrchestrator(
repositories,
engines.copy(approvalEngine = NoOpApprovalEngine(), riskAssessor = NoOpRiskAssessor()),
) {
private val replayProvider = ReplayInferenceProvider(repositories.eventStore)
override suspend fun run(
sessionId: SessionId,
graph: WorkflowGraph,
config: OrchestrationConfig,
): WorkflowResult {
emitWorkflowStarted(sessionId, graph, config)
return step(ReplayContext(graph, sessionId, graph.start, 0, config))
}
private tailrec suspend fun step(ctx: ReplayContext): WorkflowResult {
if (isCancelled(ctx.sessionId)) return handleCancellation(ctx.sessionId, ctx.currentStageId)
val session = repositories.sessionRepository.getSession(ctx.sessionId)
return when (val decision = resolveTransition(ctx.graph, ctx.sessionId, ctx.currentStageId)) {
is TransitionDecision.Move -> {
val nextStageId = decision.to
if (isTerminal(ctx.graph, nextStageId)) {
return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount + 1)
}
when (val result = executeStage(ctx.sessionId, nextStageId, ctx.graph, session, ctx.config)) {
is StageExecutionResult.Success -> step(
ctx.copy(
currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision),
stageCount = ctx.stageCount + 1,
),
)
is StageExecutionResult.Failure ->
failWorkflow(ctx.sessionId, ctx.currentStageId, result.reason, retryExhausted = false)
}
}
is TransitionDecision.Stay -> if (isTerminal(ctx.graph, ctx.currentStageId)) {
completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount)
} else {
step(ctx)
}
is TransitionDecision.Blocked -> failWorkflow(
sessionId = ctx.sessionId,
stageId = ctx.currentStageId,
reason = decision.reason,
retryExhausted = false,
)
is TransitionDecision.NoMatch -> failWorkflow(
sessionId = ctx.sessionId,
stageId = ctx.currentStageId,
reason = "no matching transition from stage ${ctx.currentStageId.value}",
retryExhausted = false,
)
}
}
override suspend fun cancel(sessionId: SessionId) {
cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true)
}
override suspend fun runInference(
sessionId: SessionId,
stageId: StageId,
contextPack: ContextPack,
stageConfig: StageConfig,
timeoutMs: Long,
): InferenceResult = when (strategy) {
is ReplayStrategy.SkipInference -> {
// bypass router entirely — use recorded artifact
val request = InferenceRequest(
requestId = InferenceRequestId(UUID.randomUUID().toString()),
sessionId = sessionId,
stageId = stageId,
contextPack = contextPack,
generationConfig = GenerationConfig(
temperature = 0.0,
topP = 1.0,
maxTokens = 0,
seed = 0L,
),
)
runCatching { replayProvider.infer(request) }
.fold(
onSuccess = { InferenceResult.Success(it) },
onFailure = { e ->
if (e is ReplayArtifactMissingException) {
InferenceResult.Failed(e.message ?: "replay artifact missing")
} else {
throw e
}
},
)
}
else -> super.runInference(sessionId, stageId, contextPack, stageConfig, timeoutMs)
}
override fun mapValidationOutcome(
sessionId: SessionId,
stageId: StageId,
context: ValidationContext,
): StageExecutionResult = when (strategy) {
is ReplayStrategy.SkipValidation -> {
// Replay-only: emit a synthetic ArtifactValidatedEvent for every artifact left in
// VALIDATING at this stage. Without this, artifacts are permanently stuck because
// the real validation path (which emits ArtifactValidatedEvent) is bypassed.
val events = repositories.eventStore.read(sessionId)
val alreadyValidated = events
.mapNotNull { (it.payload as? ArtifactValidatedEvent) }
.filter { it.stageId == stageId }
.map { it.artifactId }
.toSet()
events
.mapNotNull { (it.payload as? ArtifactValidatingEvent)?.takeIf { e -> e.stageId == stageId } }
.filter { it.artifactId !in alreadyValidated }
.forEach { emit(sessionId, ArtifactValidatedEvent(it.artifactId, sessionId, stageId)) }
StageExecutionResult.Success(emptyList())
}
else -> super.mapValidationOutcome(sessionId, stageId, context)
}
}
@@ -0,0 +1,344 @@
package com.correx.core.kernel.orchestration
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.domain.ApprovalEngine
import com.correx.core.approvals.model.ApprovalContext
import com.correx.core.approvals.model.ApprovalScopeIdentity
import com.correx.core.approvals.model.DomainApprovalRequest
import com.correx.core.context.builder.ContextPackBuilder
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.events.EventMetadata
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.NewEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RiskAssessedEvent
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 com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.InferenceRequestId
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 com.correx.core.inference.InferenceRepository
import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceResponse
import com.correx.core.inference.InferenceRouter
import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.risk.RiskAssessor
import com.correx.core.risk.RiskContext
import com.correx.core.risk.toApprovalTier
import com.correx.core.sessions.ApprovalMode
import com.correx.core.sessions.Session
import com.correx.core.transitions.evaluation.EvaluationContext
import com.correx.core.transitions.execution.StageExecutionResult
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.resolution.TransitionDecision
import com.correx.core.transitions.resolution.TransitionResolver
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.pipeline.ValidationOutcome
import com.correx.core.validation.pipeline.ValidationPipeline
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Clock
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.*
import kotlin.coroutines.cancellation.CancellationException
@SuppressWarnings(
"ForbiddenComment",
"UnusedParameter",
"ReturnCount",
"TooGenericExceptionCaught",
"TooManyFunctions",
"NestedBlockDepth",
)
abstract class SessionOrchestrator(
repositories: OrchestratorRepositories,
engines: OrchestratorEngines,
) {
private val eventStore: EventStore = repositories.eventStore
private val transitionResolver: TransitionResolver = engines.transitionResolver
private val contextPackBuilder: ContextPackBuilder = engines.contextPackBuilder
private val inferenceRouter: InferenceRouter = engines.inferenceRouter
private val validationPipeline: ValidationPipeline = engines.validationPipeline
private val approvalEngine: ApprovalEngine = engines.approvalEngine
private val riskAssessor: RiskAssessor = engines.riskAssessor
private val inferenceRepository: InferenceRepository = repositories.inferenceRepository
internal val orchestrationRepository: OrchestrationRepository = repositories.orchestrationRepository
internal abstract val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean>
abstract suspend fun run(
sessionId: SessionId,
graph: WorkflowGraph,
config: OrchestrationConfig,
): WorkflowResult
abstract suspend fun cancel(sessionId: SessionId)
// --- stage execution ---
internal suspend fun executeStage(
sessionId: SessionId,
stageId: StageId,
graph: WorkflowGraph,
session: Session,
config: OrchestrationConfig,
): StageExecutionResult {
val stageConfig = requireNotNull(graph.stages[stageId]) {
"Stage '${stageId.value}' not declared in workflow graph"
}
val contextPack = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId,
stageId = stageId,
entries = emptyList(),
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
val inferenceResult = runInference(sessionId, stageId, contextPack, stageConfig, config.stageTimeoutMs)
return when (inferenceResult) {
is InferenceResult.Cancelled -> StageExecutionResult.Failure("CANCELLED", retryable = false)
is InferenceResult.Failed -> StageExecutionResult.Failure(inferenceResult.reason, retryable = true)
is InferenceResult.Success -> {
if (isCancelled(sessionId)) return StageExecutionResult.Failure("CANCELLED", retryable = false)
val validationCtx = ValidationContext(graph = graph, sessionState = session.state)
mapValidationOutcome(sessionId, stageId, validationCtx)
}
}
}
internal open fun mapValidationOutcome(
sessionId: SessionId,
stageId: StageId,
context: ValidationContext,
): StageExecutionResult = when (val outcome = validationPipeline.validate(context)) {
is ValidationOutcome.Passed -> StageExecutionResult.Success(emptyList())
is ValidationOutcome.Rejected ->
StageExecutionResult.Failure("validation failed", retryable = outcome.retryable)
is ValidationOutcome.NeedsApproval -> handleApproval(sessionId, stageId, outcome)
}
internal open suspend fun runInference(
sessionId: SessionId,
stageId: StageId,
contextPack: ContextPack,
stageConfig: StageConfig,
timeoutMs: Long,
): InferenceResult {
val provider = inferenceRouter.route(stageId, stageConfig.requiredCapabilities)
val requestId = InferenceRequestId(UUID.randomUUID().toString())
val request = InferenceRequest(
requestId = requestId,
sessionId = sessionId,
stageId = stageId,
contextPack = contextPack,
generationConfig = stageConfig.generationConfig,
)
emit(sessionId, InferenceStartedEvent(requestId, sessionId, stageId, provider.id))
if (isCancelled(sessionId)) return InferenceResult.Cancelled
return try {
val response = withTimeout(timeoutMs) { provider.infer(request) }
if (isCancelled(sessionId)) return InferenceResult.Cancelled
emit(
sessionId,
InferenceCompletedEvent(
requestId = requestId,
sessionId = sessionId,
stageId = stageId,
providerId = provider.id,
tokensUsed = response.tokensUsed,
latencyMs = response.latencyMs,
),
)
InferenceResult.Success(response)
} catch (e: TimeoutCancellationException) {
emit(
sessionId,
InferenceTimeoutEvent(
requestId = requestId,
sessionId = sessionId,
stageId = stageId,
providerId = provider.id,
timeoutMs = timeoutMs,
),
)
InferenceResult.Failed("TIMEOUT: ${e.message}")
} catch (e: CancellationException) {
throw e // never swallow
} catch (e: Exception) {
emit(
sessionId,
InferenceFailedEvent(
requestId = requestId,
sessionId = sessionId,
stageId = stageId,
providerId = provider.id,
reason = e.message ?: "unknown",
),
)
InferenceResult.Failed(e.message ?: "inference failed")
}
}
// --- transition helpers ---
internal fun resolveTransition(
graph: WorkflowGraph,
sessionId: SessionId,
currentStageId: StageId,
): TransitionDecision {
val ctx = EvaluationContext(sessionId, currentStageId, emptyMap())
return transitionResolver.resolve(graph, ctx)
}
internal fun isTerminal(graph: WorkflowGraph, stageId: StageId): Boolean =
graph.transitions.none { it.from == stageId }
internal fun advanceStage(
sessionId: SessionId,
fromStageId: StageId,
decision: TransitionDecision.Move,
): StageId {
emit(sessionId, TransitionExecutedEvent(sessionId, fromStageId, decision.to, decision.transitionId))
return decision.to
}
// --- terminal state helpers ---
internal fun emitWorkflowStarted(sessionId: SessionId, graph: WorkflowGraph, config: OrchestrationConfig) {
emit(
sessionId,
WorkflowStartedEvent(
sessionId = sessionId,
startStageId = graph.start,
retryPolicy = config.retryPolicy,
),
)
}
internal fun completeWorkflow(
sessionId: SessionId,
terminalStageId: StageId,
stageCount: Int,
): WorkflowResult.Completed {
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount))
cancellations.remove(sessionId)
return WorkflowResult.Completed(sessionId, terminalStageId)
}
internal fun failWorkflow(
sessionId: SessionId,
stageId: StageId,
reason: String,
retryExhausted: Boolean,
): WorkflowResult.Failed {
emit(sessionId, WorkflowFailedEvent(sessionId, stageId, reason, retryExhausted))
cancellations.remove(sessionId)
return WorkflowResult.Failed(sessionId, reason, retryExhausted)
}
internal fun handleCancellation(
sessionId: SessionId,
stageId: StageId,
): WorkflowResult.Cancelled {
emit(sessionId, WorkflowFailedEvent(sessionId, stageId, "CANCELLED", retryExhausted = false))
cancellations.remove(sessionId)
return WorkflowResult.Cancelled(sessionId)
}
// --- cancellation ---
internal fun isCancelled(sessionId: SessionId): Boolean =
cancellations[sessionId]?.get() == true
// --- event emission ---
internal fun emit(sessionId: SessionId, payload: EventPayload) {
eventStore.append(
NewEvent(
metadata = EventMetadata(
eventId = EventId(UUID.randomUUID().toString()),
sessionId = sessionId,
timestamp = Clock.System.now(),
schemaVersion = 1,
causationId = null,
correlationId = null,
),
payload = payload,
),
)
}
// --- private functions ---
private fun handleApproval(
sessionId: SessionId,
stageId: StageId,
outcome: ValidationOutcome.NeedsApproval,
): StageExecutionResult {
emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING"))
val state = orchestrationRepository.getState(sessionId)
val inferenceState = inferenceRepository.getInferenceState(sessionId)
val riskSummary = riskAssessor.assess(
RiskContext(
validationReport = outcome.request.validationReport,
orchestrationState = state,
inferenceState = inferenceState,
),
)
val riskSummaryId = RiskSummaryId(UUID.randomUUID().toString())
emit(
sessionId,
RiskAssessedEvent(sessionId, stageId, riskSummaryId, riskSummary.level, riskSummary.recommendedAction),
)
val domainRequest = DomainApprovalRequest(
id = ApprovalRequestId(UUID.randomUUID().toString()),
tier = riskSummary.level.toApprovalTier(),
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
riskSummaryId = riskSummaryId,
timestamp = Clock.System.now(),
)
val approvalCtx = ApprovalContext(
identity = ApprovalScopeIdentity(sessionId, stageId, null),
mode = ApprovalMode.PROMPT,
)
val decision = approvalEngine.evaluate(domainRequest, approvalCtx, emptyList(), Clock.System.now())
return if (decision.state == ApprovalStatus.COMPLETED) {
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
StageExecutionResult.Success(emptyList())
} else {
StageExecutionResult.Failure("approval pending or rejected", retryable = false)
}
}
}
internal sealed interface InferenceResult {
data class Success(val response: InferenceResponse) : InferenceResult
data class Failed(val reason: String) : InferenceResult
data object Cancelled : InferenceResult
}
@@ -0,0 +1,52 @@
package com.correx.core.kernel.replay
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ProviderId
import com.correx.core.inference.CapabilityScore
import com.correx.core.inference.FinishReason
import com.correx.core.inference.InferenceProvider
import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceResponse
import com.correx.core.inference.ProviderHealth
import com.correx.core.inference.Token
import com.correx.core.inference.Tokenizer
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
class ReplayInferenceProvider(
private val eventStore: EventStore,
override val name: String = "Replay Provider",
) : InferenceProvider {
override val id: ProviderId = ProviderId("replay-provider")
@Suppress("MagicNumber")
override val tokenizer: Tokenizer = object : Tokenizer {
override suspend fun tokenize(text: String): List<Token> =
List(text.chunked(4).size) { i -> Token(i) } // 1 token ≈ 4 chars
override suspend fun countTokens(text: String): Int =
(text.length + 3) / 4
}
override suspend fun infer(request: InferenceRequest): InferenceResponse {
val recorded = eventStore.read(request.sessionId)
.map { it.payload }
.filterIsInstance<InferenceCompletedEvent>()
.firstOrNull { it.stageId == request.stageId }
?: throw ReplayArtifactMissingException(request.sessionId, request.stageId)
return InferenceResponse(
requestId = request.requestId,
text = "", // inference text is not recorded — replay produces empty text by design
finishReason = FinishReason.Stop,
tokensUsed = recorded.tokensUsed,
latencyMs = recorded.latencyMs,
)
}
override suspend fun healthCheck(): ProviderHealth = ProviderHealth.Healthy
override fun capabilities(): Set<CapabilityScore> = emptySet()
}
@@ -0,0 +1,58 @@
package com.correx.core.kernel.retry
import com.correx.core.events.events.NewEvent
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.stores.EventStore
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.events.EventMetadata
import kotlinx.datetime.Clock
import kotlinx.coroutines.delay
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
import kotlin.coroutines.cancellation.CancellationException
class DefaultRetryCoordinator(
private val eventStore: EventStore,
) : RetryCoordinator {
override suspend fun shouldRetry(
sessionId: SessionId,
stageId: StageId,
currentAttempt: Int,
policy: RetryPolicy,
failureReason: String,
): Boolean {
if (currentAttempt >= policy.maxAttempts) return false
val eventId = EventId(UUID.randomUUID().toString())
val event = NewEvent(
metadata = EventMetadata(
eventId = eventId,
sessionId = sessionId,
timestamp = Clock.System.now(),
schemaVersion = 1,
causationId = null,
correlationId = null,
),
payload = RetryAttemptedEvent(
sessionId = sessionId,
stageId = stageId,
attemptNumber = currentAttempt + 1,
maxAttempts = policy.maxAttempts,
failureReason = failureReason,
),
)
eventStore.append(event)
if (policy.backoffMs > 0L) {
delay(policy.backoffMs)
}
return true
}
}
@@ -0,0 +1,15 @@
package com.correx.core.kernel.retry
import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
interface RetryCoordinator {
suspend fun shouldRetry(
sessionId: SessionId,
stageId: StageId,
currentAttempt: Int,
policy: RetryPolicy,
failureReason: String,
): Boolean
}
@@ -0,0 +1,9 @@
package com.correx.core.kernel.retry.exception
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
class ReplayArtifactMissingException(
val sessionId: SessionId,
val stageId: StageId,
) : Exception("no recorded inference artifact for session=${sessionId.value} stage=${stageId.value}")