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:
+21
-1
@@ -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,
|
||||
|
||||
+61
-2
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user