refactor(tui): event-loop foundation — actions, reducers, effects, Panel

Tasks 1.1–3.3 of docs/plans/2026-05-17-tui-refactor.md:

- Mosaic 0.13 → 0.18; Kotlin 2.0.21 → 2.2.10 (required by Mosaic).
  Fix MaxLineLength regressions in core/kernel orchestration logs.
- Input: drop raw System.in reader and ANSI escape parser. Wire
  Modifier.onKeyEvent at root; MosaicKeyMapper → KeyResolver → Action.
- State: split TuiState into Connection/Sessions/Input/Approval/Provider
  slices. Add InputMode.SteeringNote; delete handleSteer/System.console.
- Reducers: pure RootReducer composes slice reducers returning
  (state, List<Effect>). Effect is sealed (SendWs, Quit). Single
  dispatch(action) point in TuiApp; one coroutine drains effects.
- TuiWsClient now exposes messages/connection SharedFlows; suspend
  callbacks gone. ConnectionEvent sealed type added.
- Rendering: Panel(title){…} composable with LocalTerminalSize
  composition local; drop BOX_WIDTH and hand-padding from every
  component. Poll stty size every 500ms.
- Tests: KeyResolver (18) + 5 reducer suites (37). 55 total in :apps:tui.

Pending tasks 4.1–5.2 tracked in
docs/plans/2026-05-17-tui-refactor.progress.md.
This commit is contained in:
2026-05-17 02:57:54 +04:00
parent f77efce10b
commit b95135eb3b
42 changed files with 1512 additions and 291 deletions
@@ -31,12 +31,17 @@ class DefaultSessionOrchestrator(
graph: WorkflowGraph,
config: OrchestrationConfig,
): WorkflowResult {
System.err.println("[Orchestrator] session=${sessionId.value} workflow=${graph.id} start=${graph.start.value}")
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 {
System.err.println(
"[Orchestrator] step session=${ctx.sessionId.value} stage=${ctx.currentStageId.value} " +
"stageCount=${ctx.stageCount}"
)
if (isCancelled(ctx.sessionId)) return handleCancellation(ctx.sessionId, ctx.currentStageId)
val enriched = EnrichedExecutionContext(
@@ -49,7 +54,17 @@ class DefaultSessionOrchestrator(
session = repositories.sessionRepository.getSession(ctx.sessionId),
)
return when (val decision = resolveTransition(enriched.graph, enriched.sessionId, enriched.currentStageId)) {
val stageConfig = enriched.graph.stages[enriched.currentStageId]
val stageArtifacts = stageConfig?.produces
?.let { repositories.artifactRepository.getByIds(enriched.sessionId, it) }
?: emptyMap()
val decision = resolveTransition(enriched.graph, enriched.sessionId, enriched.currentStageId, stageArtifacts)
System.err.println(
"[Orchestrator] transition session=${enriched.sessionId.value} " +
"stage=${enriched.currentStageId.value} decision=${decision::class.simpleName}"
)
return when (decision) {
is TransitionDecision.Move -> when (val r = executeMove(enriched, decision)) {
is StepResult.Continue -> step(r.ctx)
is StepResult.Terminal -> r.result
@@ -100,6 +115,7 @@ class DefaultSessionOrchestrator(
)
}
System.err.println("[Orchestrator] executeStage session=${ctx.sessionId.value} stage=${nextStageId.value}")
return when (val result = executeStage(ctx.sessionId, nextStageId, ctx.graph, ctx.session, ctx.config)) {
is StageExecutionResult.Success -> StepResult.Continue(
ctx.copy(
@@ -109,6 +125,10 @@ class DefaultSessionOrchestrator(
)
is StageExecutionResult.Failure -> {
System.err.println(
"[Orchestrator] stage failed session=${ctx.sessionId.value} " +
"stage=${nextStageId.value} reason=${result.reason} retryable=${result.retryable}"
)
val shouldRetry = retryCoordinator.shouldRetry(
sessionId = ctx.sessionId,
stageId = nextStageId,
@@ -5,8 +5,11 @@ import com.correx.core.approvals.ApprovalStatus
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.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.EventMetadata
@@ -23,9 +26,11 @@ 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.artifacts.ArtifactState
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.ArtifactId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.InferenceRequestId
@@ -43,6 +48,7 @@ import com.correx.core.risk.RiskContext
import com.correx.core.risk.toApprovalTier
import com.correx.core.sessions.Session
import com.correx.core.transitions.evaluation.EvaluationContext
import com.correx.core.transitions.evaluation.PromptResolver
import com.correx.core.transitions.execution.StageExecutionResult
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.WorkflowGraph
@@ -67,6 +73,7 @@ import kotlin.coroutines.cancellation.CancellationException
"TooGenericExceptionCaught",
"TooManyFunctions",
"NestedBlockDepth",
"LongMethod",
)
abstract class SessionOrchestrator(
repositories: OrchestratorRepositories,
@@ -78,6 +85,7 @@ abstract class SessionOrchestrator(
private val inferenceRouter: InferenceRouter = engines.inferenceRouter
private val validationPipeline: ValidationPipeline = engines.validationPipeline
private val riskAssessor: RiskAssessor = engines.riskAssessor
private val promptResolver: PromptResolver = engines.promptResolver
private val inferenceRepository: InferenceRepository = repositories.inferenceRepository
internal val orchestrationRepository: OrchestrationRepository = repositories.orchestrationRepository
internal abstract val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean>
@@ -104,11 +112,35 @@ abstract class SessionOrchestrator(
val stageConfig = requireNotNull(graph.stages[stageId]) {
"Stage '${stageId.value}' not declared in workflow graph"
}
val promptEntries = stageConfig.metadata["prompt"]
?.let { path ->
runCatching { promptResolver.resolve(path) }
.onFailure {
System.err.println(
"[SessionOrchestrator] stage=${stageId.value}: failed to load prompt '$path': ${it.message}"
)
}
.getOrNull()
}
?.takeIf { it.isNotBlank() }
?.let { text ->
listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
content = text,
sourceType = "systemPrompt",
sourceId = stageId.value,
tokenEstimate = text.length / 4,
),
)
} ?: emptyList()
val contextPack = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId,
stageId = stageId,
entries = emptyList(),
entries = promptEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
@@ -143,6 +175,10 @@ abstract class SessionOrchestrator(
timeoutMs: Long,
): InferenceResult {
val provider = inferenceRouter.route(stageId, stageConfig.requiredCapabilities)
System.err.println(
"[Orchestrator] inference session=${sessionId.value} stage=${stageId.value} " +
"provider=${provider.id.value} timeoutMs=$timeoutMs"
)
val requestId = InferenceRequestId(UUID.randomUUID().toString())
val request = InferenceRequest(
requestId = requestId,
@@ -158,6 +194,10 @@ abstract class SessionOrchestrator(
return try {
val response = withTimeout(timeoutMs) { provider.infer(request) }
System.err.println(
"[Orchestrator] inference done session=${sessionId.value} stage=${stageId.value} " +
"tokens=${response.tokensUsed} latencyMs=${response.latencyMs}"
)
if (isCancelled(sessionId)) return InferenceResult.Cancelled
@@ -209,8 +249,9 @@ abstract class SessionOrchestrator(
graph: WorkflowGraph,
sessionId: SessionId,
currentStageId: StageId,
artifacts: Map<ArtifactId, ArtifactState> = emptyMap(),
): TransitionDecision {
val ctx = EvaluationContext(sessionId, currentStageId, emptyMap())
val ctx = EvaluationContext(sessionId, currentStageId, artifacts = artifacts)
return transitionResolver.resolve(graph, ctx)
}
@@ -244,6 +285,10 @@ abstract class SessionOrchestrator(
terminalStageId: StageId,
stageCount: Int,
): WorkflowResult.Completed {
System.err.println(
"[Orchestrator] COMPLETED session=${sessionId.value} " +
"terminalStage=${terminalStageId.value} stages=$stageCount"
)
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount))
cancellations.remove(sessionId)
return WorkflowResult.Completed(sessionId, terminalStageId)
@@ -255,6 +300,10 @@ abstract class SessionOrchestrator(
reason: String,
retryExhausted: Boolean,
): WorkflowResult.Failed {
System.err.println(
"[Orchestrator] FAILED session=${sessionId.value} stage=${stageId.value} " +
"reason=$reason retryExhausted=$retryExhausted"
)
emit(sessionId, WorkflowFailedEvent(sessionId, stageId, reason, retryExhausted))
cancellations.remove(sessionId)
return WorkflowResult.Failed(sessionId, reason, retryExhausted)
@@ -264,6 +313,7 @@ abstract class SessionOrchestrator(
sessionId: SessionId,
stageId: StageId,
): WorkflowResult.Cancelled {
System.err.println("[Orchestrator] CANCELLED session=${sessionId.value} stage=${stageId.value}")
emit(sessionId, WorkflowFailedEvent(sessionId, stageId, "CANCELLED", retryExhausted = false))
cancellations.remove(sessionId)
return WorkflowResult.Cancelled(sessionId)
@@ -299,6 +349,7 @@ abstract class SessionOrchestrator(
stageId: StageId,
outcome: ValidationOutcome.NeedsApproval,
): StageExecutionResult {
System.err.println("[Orchestrator] approval required session=${sessionId.value} stage=${stageId.value}")
emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING"))
val domainRequest = buildApprovalRequest(sessionId, stageId, outcome)
@@ -319,7 +370,15 @@ abstract class SessionOrchestrator(
pendingApprovals[domainRequest.id] = deferred
return try {
System.err.println(
"[Orchestrator] awaiting approval session=${sessionId.value} " +
"requestId=${domainRequest.id.value}"
)
val decision = deferred.await()
System.err.println(
"[Orchestrator] approval resolved session=${sessionId.value} " +
"approved=${decision.isApproved}"
)
emitDecisionResolved(sessionId, domainRequest, decision)
if (decision.isApproved) {
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))