feature(event-sourcing): baseline for the architecture review fixes.

- fixed the tool overlay in tui.
- fixed tool calling - added schemas.
- getting all sessionIds via event store.
- instead of sending all events on the replay - there's a new way for replaying.
- session snapshot is now a thing getting sent for previously created sessions.
- added logging to important parts.
- revert workflowId from WorkflowCompletedEvent.
This commit is contained in:
2026-05-24 18:50:00 +04:00
parent f827685ed0
commit fc7b879891
43 changed files with 525 additions and 211 deletions
@@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
private const val DEFAULT_PORT = 8080
@@ -32,6 +33,8 @@ private const val STATUS_HEIGHT = 1
private const val TOP_ROW_HEIGHT = 9
private const val INPUT_HEIGHT = 4
private val log = LoggerFactory.getLogger("com.correx.apps.tui.main")
fun main(args: Array<String>) {
val host = args.getOrElse(0) { "localhost" }
val port = args.getOrElse(1) { DEFAULT_PORT.toString() }.toIntOrNull() ?: DEFAULT_PORT
@@ -43,6 +46,13 @@ fun main(args: Array<String>) {
TuiRunner.create().use { runner ->
fun dispatch(action: Action) {
val (next, effects) = RootReducer.reduce(state, action)
log.debug("got connection state after reducers: {}", next.connection)
log.debug("got approval state after reducers: {}", next.approval)
log.debug("got input state after reducers: {}", next.input)
log.debug(
"got session state after reducers: {}",
next.sessions.sessions.firstOrNull() { it.status != "COMPLETED" },
)
state = next
effects.forEach { effect ->
effectScope.launch {
@@ -27,10 +27,10 @@ fun routerPanelWidget(state: TuiState): Paragraph {
state.approvalOverlayVisible && state.approval.active != null -> {
val a = state.approval.active
buildList {
add(Line.from(Span.styled("╭─ approval [ctrl+h to hide] ─────────────╮", Style.create().yellow())))
val tierLine = "│ ⚠ T${a.tier}${a.toolName ?: ""}${a.riskSummary.take(12)}... │"
add(Line.from(Span.styled("╭─ approval [alt+h to hide] ─────────────╮", Style.create().yellow())))
val tierLine = "│ ⚠ ${a.tier}${a.toolName ?: "no tool name"}${a.riskSummary.take(12)}... │"
add(Line.from(Span.styled(tierLine, Style.create().yellow())))
val previewLine = "${(a.preview ?: "").take(40)}… │"
val previewLine = "${(a.preview ?: "no preview").take(40)}… │"
add(Line.from(Span.styled(previewLine, dimStyle)))
add(Line.from(Span.styled("╰──────────────────────────────────────────╯", Style.create().yellow())))
}
@@ -24,4 +24,5 @@ sealed interface Action {
data object ToggleEventOverlay : Action
data object EnterSteer : Action
data object CycleMode : Action
data object NoOp : Action
}
@@ -2,12 +2,24 @@ package com.correx.apps.tui.input
import com.correx.apps.tui.KeyEvent
import dev.tamboui.tui.event.KeyCode
import org.slf4j.LoggerFactory
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
private val log = LoggerFactory.getLogger("com.correx.apps.tui.input")
@Suppress("CyclomaticComplexMethod", "ReturnCount")
fun mapKey(event: TambouiKeyEvent): KeyEvent? {
log.debug("got event: {}", event)
if (event.isCtrlC) return KeyEvent.Quit
if (event.hasAlt()) return null
if (event.hasAlt()) {
return when {
event.isChar('h') -> KeyEvent.ToggleApprovalOverlay.also {
log.debug("got event for toggle approval")
}
else -> null
}
}
if (event.hasCtrl()) {
return when {
event.isChar('q') -> KeyEvent.Quit
@@ -16,7 +28,8 @@ fun mapKey(event: TambouiKeyEvent): KeyEvent? {
event.isChar('a') -> KeyEvent.Approve
event.isChar('r') -> KeyEvent.Reject
event.isChar('s') -> KeyEvent.Steer
event.isChar('h') -> KeyEvent.ToggleApprovalOverlay
event.isChar('e') -> KeyEvent.ToggleEventOverlay
else -> null
}
@@ -32,6 +45,7 @@ fun mapKey(event: TambouiKeyEvent): KeyEvent? {
val ch = event.character()
if (ch.isISOControl()) null else KeyEvent.CharInput(ch)
}
else -> null
}
}
@@ -8,6 +8,9 @@ import com.correx.apps.tui.state.ApprovalInfo
import com.correx.apps.tui.state.ApprovalState
import com.correx.apps.tui.state.InputMode
import com.correx.core.events.types.ApprovalRequestId
import org.slf4j.LoggerFactory
private val log = LoggerFactory.getLogger(ApprovalReducer::class.simpleName)
object ApprovalReducer {
fun reduce(
@@ -31,6 +34,7 @@ object ApprovalReducer {
approval to emptyList()
}
}
is Action.RejectActive -> {
val active = approval.active
if (active != null) {
@@ -47,13 +51,19 @@ object ApprovalReducer {
approval to emptyList()
}
}
is Action.SubmitInput -> if (inputMode == InputMode.STEER) {
ApprovalState(active = null) to emptyList()
} else {
approval to emptyList()
}
is Action.ServerEventReceived -> when (val msg = action.message) {
is ServerMessage.ApprovalRequired -> {
log.debug(
"ApprovalRequired received requestId={} sessionId={} toolName={}",
msg.requestId.value, msg.sessionId.value, msg.toolName,
)
val info = ApprovalInfo(
requestId = msg.requestId.value,
sessionId = msg.sessionId.value,
@@ -64,8 +74,27 @@ object ApprovalReducer {
)
ApprovalState(active = info) to emptyList()
}
is ServerMessage.SessionSnapshot -> {
val requestId = msg.approvalRequestId
if (msg.pendingApproval && requestId != null) {
val info = ApprovalInfo(
requestId = requestId,
sessionId = msg.sessionId.value,
tier = msg.approvalTier ?: "T2",
riskSummary = "unknown",
toolName = msg.approvalToolName,
preview = msg.approvalPreview,
)
ApprovalState(active = info) to emptyList()
} else {
approval to emptyList()
}
}
else -> approval to emptyList()
}
else -> approval to emptyList()
}
}
@@ -22,12 +22,14 @@ object InputReducer {
},
inputBuffer = "",
) to emptyList()
is Action.OpenNewSessionPrompt -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
is Action.OpenSteeringPrompt -> if (state.approval.active != null) {
state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
} else {
state to emptyList()
}
is Action.EnterSteer -> state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
is Action.OpenFilter -> state.copy(inputMode = InputMode.FILTER, inputBuffer = "") to emptyList()
is Action.AppendChar -> state.copy(inputBuffer = state.inputBuffer + action.ch) to emptyList()
@@ -44,6 +46,7 @@ object InputReducer {
state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
}
}
InputMode.STEER -> {
val text = state.inputBuffer.trim()
val active = state.approval.active
@@ -61,9 +64,11 @@ object InputReducer {
state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
}
}
InputMode.FILTER -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
InputMode.NAVIGATE -> state to emptyList()
}
else -> state to emptyList()
}
}
@@ -1,7 +1,11 @@
package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.TuiState
import org.slf4j.LoggerFactory
private val log = LoggerFactory.getLogger(RootReducer::class.java.name)
object RootReducer {
fun reduce(
@@ -9,13 +13,25 @@ object RootReducer {
action: Action,
clock: () -> Long = System::currentTimeMillis,
): Pair<TuiState, List<Effect>> {
log.debug("action={}", action::class.simpleName)
val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList()
val prevInputMode = state.inputMode
val prevInputBuffer = state.inputBuffer
val approvalAction = if (
action is Action.ServerEventReceived &&
action.message is ServerMessage.SessionSnapshot &&
action.message.sessionId.value != state.sessions.selectedId
) Action.NoOp else action
log.debug("input state before reducers={}", state.input)
val (afterInput, ie) = InputReducer.reduce(state, action)
log.debug("sessions state before reducers={}", state.sessions)
val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock)
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action)
log.debug("approval state before reducers={}", state.approval)
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, approvalAction)
log.debug("connection state before reducers={}", state.connection)
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
log.debug("provider state before reducers={}", state.provider)
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
val approvalJustArrived = approval.active != null && afterInput.approval.active == null
val withSubReducers = afterInput.copy(
@@ -30,10 +46,12 @@ object RootReducer {
approvalOverlayVisible = !withSubReducers.approvalOverlayVisible,
eventOverlayVisible = false,
)
is Action.ToggleEventOverlay -> withSubReducers.copy(
eventOverlayVisible = !withSubReducers.eventOverlayVisible,
approvalOverlayVisible = false,
)
else -> withSubReducers
}
return final to (quitEffects + ie + se + ae + ce + pe)
@@ -11,10 +11,13 @@ import com.correx.apps.tui.state.ToolDisplayStatus
import com.correx.apps.tui.state.TuiEventEntry
import com.correx.apps.tui.state.TuiToolRecord
import com.correx.core.events.types.SessionId
import org.slf4j.LoggerFactory
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
private val log = LoggerFactory.getLogger(SessionsReducer::class.simpleName)
object SessionsReducer {
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneOffset.UTC)
@@ -103,7 +106,32 @@ object SessionsReducer {
is ServerMessage.ToolCompleted -> processToolCompletedMessage(msg, sessions)
is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions)
is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg)
is ServerMessage.SessionSnapshot -> processSessionSnapshotMessage(clock, sessions, msg)
else -> sessions to emptyList()
}.also { log.debug("processed server message: {}", msg) }
private fun processSessionSnapshotMessage(
clock: () -> Long,
sessionState: SessionsState,
msg: ServerMessage.SessionSnapshot,
): Pair<SessionsState, List<Effect>> {
val summary = SessionSummary(
id = msg.sessionId.value,
status = when {
msg.pendingApproval -> "PAUSED awaiting approval"
else -> msg.status
},
workflowId = msg.workflowId,
name = msg.workflowId,
lastEventAt = clock(),
currentStage = msg.currentStageId,
currentStageId = msg.currentStageId,
)
val selected = sessionState.selectedId ?: msg.sessionId.value
return sessionState.copy(
sessions = sessionState.sessions + summary,
selectedId = selected,
) to listOf(Effect.ConnectSession(msg.sessionId))
}
private fun processToolRejectedMessage(