epic-13: add cli and tui entry point, finish epic.

This commit is contained in:
2026-05-16 13:37:58 +04:00
parent 72d20726ce
commit 2207a37549
60 changed files with 2896 additions and 41 deletions
@@ -1,6 +1,8 @@
package com.correx.core.kernel.orchestration
import com.correx.core.approvals.model.ApprovalDecision
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.kernel.execution.WorkflowResult
@@ -80,6 +82,12 @@ class DefaultSessionOrchestrator(
cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true)
}
fun submitApprovalDecision(requestId: ApprovalRequestId, decision: ApprovalDecision) {
val deferred = pendingApprovals[requestId]
?: error("No pending approval for requestId ${requestId.value}")
deferred.complete(decision)
}
private suspend fun executeMove(
ctx: EnrichedExecutionContext,
decision: TransitionDecision.Move,
@@ -143,7 +143,7 @@ class ReplayOrchestrator(
else -> super.runInference(sessionId, stageId, contextPack, stageConfig, timeoutMs)
}
override fun mapValidationOutcome(
override suspend fun mapValidationOutcome(
sessionId: SessionId,
stageId: StageId,
context: ValidationContext,
@@ -1,13 +1,14 @@
package com.correx.core.kernel.orchestration
import com.correx.core.approvals.ApprovalOutcome
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.ApprovalDecision
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.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent
@@ -23,6 +24,7 @@ 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.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.EventId
@@ -39,7 +41,6 @@ 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
@@ -50,6 +51,7 @@ 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.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Clock
@@ -75,11 +77,12 @@ abstract class SessionOrchestrator(
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>
internal val pendingApprovals: ConcurrentHashMap<ApprovalRequestId, CompletableDeferred<ApprovalDecision>> =
ConcurrentHashMap()
abstract suspend fun run(
sessionId: SessionId,
@@ -121,7 +124,7 @@ abstract class SessionOrchestrator(
}
}
internal open fun mapValidationOutcome(
internal open suspend fun mapValidationOutcome(
sessionId: SessionId,
stageId: StageId,
context: ValidationContext,
@@ -291,13 +294,49 @@ abstract class SessionOrchestrator(
// --- private functions ---
private fun handleApproval(
private suspend fun handleApproval(
sessionId: SessionId,
stageId: StageId,
outcome: ValidationOutcome.NeedsApproval,
): StageExecutionResult {
emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING"))
val domainRequest = buildApprovalRequest(sessionId, stageId, outcome)
emit(
sessionId,
ApprovalRequestedEvent(
requestId = domainRequest.id,
tier = domainRequest.tier,
validationReportId = domainRequest.validationReportId,
riskSummaryId = domainRequest.riskSummaryId,
sessionId = sessionId,
stageId = stageId,
projectId = null,
),
)
val deferred = CompletableDeferred<ApprovalDecision>()
pendingApprovals[domainRequest.id] = deferred
return try {
val decision = deferred.await()
emitDecisionResolved(sessionId, domainRequest, decision)
if (decision.isApproved) {
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
StageExecutionResult.Success(emptyList())
} else {
StageExecutionResult.Failure(decision.reason ?: "approval rejected", retryable = false)
}
} finally {
pendingApprovals.remove(domainRequest.id)
}
}
private fun buildApprovalRequest(
sessionId: SessionId,
stageId: StageId,
outcome: ValidationOutcome.NeedsApproval,
): DomainApprovalRequest {
val state = orchestrationRepository.getState(sessionId)
val inferenceState = inferenceRepository.getInferenceState(sessionId)
val riskSummary = riskAssessor.assess(
@@ -307,33 +346,38 @@ abstract class SessionOrchestrator(
inferenceState = inferenceState,
),
)
val riskSummaryId = RiskSummaryId(UUID.randomUUID().toString())
emit(
sessionId,
RiskAssessedEvent(sessionId, stageId, riskSummaryId, riskSummary.level, riskSummary.recommendedAction),
)
val domainRequest = DomainApprovalRequest(
return 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,
}
private fun emitDecisionResolved(
sessionId: SessionId,
domainRequest: DomainApprovalRequest,
decision: ApprovalDecision,
) {
emit(
sessionId,
ApprovalDecisionResolvedEvent(
decisionId = ApprovalDecisionId(UUID.randomUUID().toString()),
requestId = domainRequest.id,
outcome = decision.outcome ?: ApprovalOutcome.REJECTED,
status = ApprovalStatus.COMPLETED,
tier = domainRequest.tier,
resolutionTimestamp = Clock.System.now(),
reason = decision.reason,
userSteering = decision.userSteering,
),
)
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)
}
}
}