chore(damn): detekt, build, tests, formatting
fixed detekt issues where possible. fixed disttar failing build because tools is added twice in the server module. added workflowId where required. fixed some tests not being recognized because of runBlocking without explicit return type. formatting + imports.
This commit is contained in:
@@ -8,6 +8,13 @@ application {
|
|||||||
mainClass = 'com.correx.apps.server.MainKt'
|
mainClass = 'com.correx.apps.server.MainKt'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('distTar') {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
tasks.named('distZip') {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core:config')
|
implementation project(':core:config')
|
||||||
implementation project(':core:events')
|
implementation project(':core:events')
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import io.ktor.serialization.kotlinx.json.json
|
|||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.plugins.calllogging.CallLogging
|
import io.ktor.server.plugins.calllogging.CallLogging
|
||||||
import org.slf4j.event.Level
|
|
||||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.server.plugins.statuspages.StatusPages
|
import io.ktor.server.plugins.statuspages.StatusPages
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
@@ -18,6 +17,7 @@ import io.ktor.server.routing.routing
|
|||||||
import io.ktor.server.websocket.WebSockets
|
import io.ktor.server.websocket.WebSockets
|
||||||
import io.ktor.server.websocket.webSocket
|
import io.ktor.server.websocket.webSocket
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.slf4j.event.Level
|
||||||
|
|
||||||
fun Application.configureServer(module: ServerModule) {
|
fun Application.configureServer(module: ServerModule) {
|
||||||
install(CallLogging) { level = Level.INFO }
|
install(CallLogging) { level = Level.INFO }
|
||||||
|
|||||||
@@ -49,9 +49,20 @@ import java.nio.file.Paths
|
|||||||
private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")
|
private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
log.info("=== correx server starting ===")
|
||||||
|
log.info(" port : 8080")
|
||||||
val artifactStore = InfrastructureModule.createArtifactStore()
|
val artifactStore = InfrastructureModule.createArtifactStore()
|
||||||
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
|
val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))
|
||||||
|
|
||||||
|
logStoresInfo(eventStore, artifactStore)
|
||||||
|
|
||||||
val infraRegistry = InfrastructureModule.createProviderRegistry(listOf(buildLlamaProvider()))
|
val infraRegistry = InfrastructureModule.createProviderRegistry(listOf(buildLlamaProvider()))
|
||||||
|
val modelId = System.getenv("CORREX_MODEL_ID") ?: "default"
|
||||||
|
val modelPath = System.getenv("CORREX_MODEL_PATH") ?: "(not set)"
|
||||||
|
val llamaUrl = System.getenv("CORREX_LLAMA_URL") ?: "http://127.0.0.1:10000"
|
||||||
|
|
||||||
|
logModelInfo(modelId, modelPath, llamaUrl, infraRegistry)
|
||||||
|
|
||||||
val repositories = buildRepositories(eventStore)
|
val repositories = buildRepositories(eventStore)
|
||||||
val approvalEngine = DefaultApprovalEngine()
|
val approvalEngine = DefaultApprovalEngine()
|
||||||
val correxConfig = ConfigLoader.load()
|
val correxConfig = ConfigLoader.load()
|
||||||
@@ -78,6 +89,9 @@ fun main() {
|
|||||||
eventDispatcher = EventDispatcher(eventStore),
|
eventDispatcher = EventDispatcher(eventStore),
|
||||||
workDir = sandboxRoot,
|
workDir = sandboxRoot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logToolInfo(sandboxRoot, workingDir, shellAllowedExecutables, toolRegistry)
|
||||||
|
|
||||||
val inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
|
val inferenceRouter = DefaultInferenceRouter(infraRegistry, FirstAvailableRoutingStrategy())
|
||||||
val engines = OrchestratorEngines(
|
val engines = OrchestratorEngines(
|
||||||
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
|
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
|
||||||
@@ -116,52 +130,46 @@ fun main() {
|
|||||||
defaultOrchestrationConfig = defaultOrchestrationConfig,
|
defaultOrchestrationConfig = defaultOrchestrationConfig,
|
||||||
routerFacade = routerFacade,
|
routerFacade = routerFacade,
|
||||||
)
|
)
|
||||||
val modelId = System.getenv("CORREX_MODEL_ID") ?: "default"
|
log.info("==============================")
|
||||||
val modelPath = System.getenv("CORREX_MODEL_PATH") ?: "(not set)"
|
|
||||||
val llamaUrl = System.getenv("CORREX_LLAMA_URL") ?: "http://127.0.0.1:10000"
|
|
||||||
logServerStartup(
|
|
||||||
modelId,
|
|
||||||
modelPath,
|
|
||||||
llamaUrl,
|
|
||||||
sandboxRoot,
|
|
||||||
workingDir,
|
|
||||||
shellAllowedExecutables,
|
|
||||||
eventStore,
|
|
||||||
artifactStore,
|
|
||||||
toolRegistry,
|
|
||||||
infraRegistry,
|
|
||||||
)
|
|
||||||
|
|
||||||
embeddedServer(Netty, port = 8080) { configureServer(module) }.start(wait = true)
|
embeddedServer(Netty, port = 8080) { configureServer(module) }.start(wait = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logServerStartup(
|
private fun logModelInfo(
|
||||||
modelId: String,
|
modelId: String,
|
||||||
modelPath: String,
|
modelPath: String,
|
||||||
llamaUrl: String,
|
llamaUrl: String,
|
||||||
sandboxRoot: Path?,
|
|
||||||
workingDir: Path?,
|
|
||||||
shellAllowedExecutables: Set<String>,
|
|
||||||
eventStore: LoggingEventStore,
|
|
||||||
artifactStore: ArtifactStore,
|
|
||||||
toolRegistry: ToolRegistry,
|
|
||||||
infraRegistry: DefaultProviderRegistry,
|
infraRegistry: DefaultProviderRegistry,
|
||||||
) {
|
) {
|
||||||
log.info("=== correx server starting ===")
|
log.info("==========MODEL INFO==========")
|
||||||
log.info(" port : 8080")
|
|
||||||
log.info(" model id : {}", modelId)
|
log.info(" model id : {}", modelId)
|
||||||
log.info(" model path : {}", modelPath)
|
log.info(" model path : {}", modelPath)
|
||||||
log.info(" llama url : {}", llamaUrl)
|
log.info(" llama url : {}", llamaUrl)
|
||||||
|
log.info(" providers : {} registered", infraRegistry.listAll().size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logToolInfo(
|
||||||
|
sandboxRoot: Path?,
|
||||||
|
workingDir: Path?,
|
||||||
|
shellAllowedExecutables: Set<String>,
|
||||||
|
toolRegistry: ToolRegistry,
|
||||||
|
) {
|
||||||
|
log.info("==========TOOLS INFO==========")
|
||||||
log.info(" sandbox root : {}", sandboxRoot)
|
log.info(" sandbox root : {}", sandboxRoot)
|
||||||
log.info(" working dir : {}", workingDir)
|
log.info(" working dir : {}", workingDir)
|
||||||
if (shellAllowedExecutables.isEmpty()) {
|
if (shellAllowedExecutables.isEmpty()) {
|
||||||
log.warn(" shell tool : no executable allowlist configured — all executables allowed")
|
log.warn(" shell tool : no executable allowlist configured — all executables allowed")
|
||||||
}
|
}
|
||||||
|
log.info(" tools : {}", toolRegistry.all().map { it.name })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logStoresInfo(
|
||||||
|
eventStore: LoggingEventStore,
|
||||||
|
artifactStore: ArtifactStore,
|
||||||
|
) {
|
||||||
|
log.info("==========STORE INFO==========")
|
||||||
log.info(" event store : {}", eventStore::class.simpleName)
|
log.info(" event store : {}", eventStore::class.simpleName)
|
||||||
log.info(" artifact store: {}", artifactStore::class.simpleName)
|
log.info(" artifact store: {}", artifactStore::class.simpleName)
|
||||||
log.info(" tools : {}", toolRegistry.all().map { it.name })
|
|
||||||
log.info(" providers : {} registered", infraRegistry.listAll().size)
|
|
||||||
log.info("==============================")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildLlamaProvider(): LlamaCppInferenceProvider = InfrastructureModule.createLlamaCppProvider(
|
private fun buildLlamaProvider(): LlamaCppInferenceProvider = InfrastructureModule.createLlamaCppProvider(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import com.correx.core.artifactstore.ArtifactStore
|
|||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
||||||
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
||||||
import com.correx.core.sessions.DefaultSessionRepository
|
|
||||||
import com.correx.core.router.RouterFacade
|
import com.correx.core.router.RouterFacade
|
||||||
|
import com.correx.core.sessions.DefaultSessionRepository
|
||||||
|
|
||||||
data class ServerModule(
|
data class ServerModule(
|
||||||
val orchestrator: DefaultSessionOrchestrator,
|
val orchestrator: DefaultSessionOrchestrator,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.correx.core.approvals.ApprovalStatus
|
|||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.UserSteering
|
import com.correx.core.approvals.UserSteering
|
||||||
import com.correx.core.approvals.model.ApprovalContext
|
import com.correx.core.approvals.model.ApprovalContext
|
||||||
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
|
||||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
@@ -25,7 +24,8 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
|
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
||||||
|
|
||||||
class ApprovalCoordinator(
|
class ApprovalCoordinator(
|
||||||
private val orchestrator: ApprovalGateway,
|
private val orchestrator: ApprovalGateway,
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import com.correx.apps.server.protocol.RiskSummaryDto
|
|||||||
import com.correx.apps.server.protocol.ServerMessage
|
import com.correx.apps.server.protocol.ServerMessage
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.types.ArtifactId
|
|
||||||
import com.correx.core.events.events.InferenceCompletedEvent
|
import com.correx.core.events.events.InferenceCompletedEvent
|
||||||
import com.correx.core.events.events.InferenceStartedEvent
|
import com.correx.core.events.events.InferenceStartedEvent
|
||||||
import com.correx.core.events.events.InferenceTimeoutEvent
|
import com.correx.core.events.events.InferenceTimeoutEvent
|
||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||||
@@ -20,7 +20,7 @@ import com.correx.core.events.events.TransitionExecutedEvent
|
|||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.events.StoredEvent
|
import com.correx.core.events.types.ArtifactId
|
||||||
|
|
||||||
class DomainEventMapper(private val artifactStore: ArtifactStore = NoopArtifactStore) {
|
class DomainEventMapper(private val artifactStore: ArtifactStore = NoopArtifactStore) {
|
||||||
suspend fun map(event: StoredEvent): ServerMessage? =
|
suspend fun map(event: StoredEvent): ServerMessage? =
|
||||||
@@ -39,47 +39,80 @@ suspend fun domainEventToServerMessage(event: StoredEvent, artifactStore: Artifa
|
|||||||
is WorkflowStartedEvent -> mapWorkflowStarted(p)
|
is WorkflowStartedEvent -> mapWorkflowStarted(p)
|
||||||
is WorkflowCompletedEvent -> ServerMessage.SessionCompleted(sessionId = p.sessionId)
|
is WorkflowCompletedEvent -> ServerMessage.SessionCompleted(sessionId = p.sessionId)
|
||||||
is WorkflowFailedEvent -> ServerMessage.SessionFailed(sessionId = p.sessionId, reason = p.reason)
|
is WorkflowFailedEvent -> ServerMessage.SessionFailed(sessionId = p.sessionId, reason = p.reason)
|
||||||
is TransitionExecutedEvent -> ServerMessage.StageStarted(sessionId = p.sessionId, stageId = p.to)
|
is TransitionExecutedEvent -> ServerMessage.StageStarted(
|
||||||
is StageCompletedEvent -> ServerMessage.StageCompleted(sessionId = p.sessionId, stageId = p.stageId)
|
sessionId = p.sessionId,
|
||||||
is StageFailedEvent -> ServerMessage.StageFailed(
|
stageId = p.to,
|
||||||
sessionId = p.sessionId, stageId = p.stageId, reason = p.reason,
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is StageCompletedEvent -> ServerMessage.StageCompleted(
|
||||||
|
sessionId = p.sessionId,
|
||||||
|
stageId = p.stageId,
|
||||||
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
|
)
|
||||||
|
|
||||||
|
is StageFailedEvent -> ServerMessage.StageFailed(
|
||||||
|
sessionId = p.sessionId,
|
||||||
|
stageId = p.stageId,
|
||||||
|
reason = p.reason,
|
||||||
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
|
)
|
||||||
|
|
||||||
is OrchestrationPausedEvent -> mapOrchestrationPaused(p)
|
is OrchestrationPausedEvent -> mapOrchestrationPaused(p)
|
||||||
is InferenceStartedEvent -> ServerMessage.InferenceStarted(sessionId = p.sessionId, stageId = p.stageId)
|
is InferenceStartedEvent -> ServerMessage.InferenceStarted(sessionId = p.sessionId, stageId = p.stageId)
|
||||||
is InferenceCompletedEvent -> mapInferenceCompleted(p, artifactStore)
|
is InferenceCompletedEvent -> mapInferenceCompleted(event, p, artifactStore)
|
||||||
is InferenceTimeoutEvent -> ServerMessage.InferenceTimedOut(
|
is InferenceTimeoutEvent -> ServerMessage.InferenceTimedOut(
|
||||||
sessionId = p.sessionId, stageId = p.stageId, elapsedMs = p.timeoutMs,
|
sessionId = p.sessionId, stageId = p.stageId, elapsedMs = p.timeoutMs,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ToolInvocationRequestedEvent -> ServerMessage.ToolStarted(
|
is ToolInvocationRequestedEvent -> ServerMessage.ToolStarted(
|
||||||
sessionId = p.sessionId, toolName = p.toolName, tier = p.tier,
|
sessionId = p.sessionId, toolName = p.toolName, tier = p.tier,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ToolExecutionCompletedEvent -> ServerMessage.ToolCompleted(
|
is ToolExecutionCompletedEvent -> ServerMessage.ToolCompleted(
|
||||||
sessionId = p.sessionId, toolName = p.toolName, outputSummary = p.receipt.outputSummary,
|
sessionId = p.sessionId,
|
||||||
|
toolName = p.toolName,
|
||||||
|
outputSummary = p.receipt.outputSummary,
|
||||||
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
)
|
)
|
||||||
|
|
||||||
is ToolExecutionFailedEvent -> ServerMessage.ToolFailed(
|
is ToolExecutionFailedEvent -> ServerMessage.ToolFailed(
|
||||||
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
sessionId = p.sessionId,
|
||||||
|
toolName = p.toolName,
|
||||||
|
reason = p.reason,
|
||||||
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
)
|
)
|
||||||
|
|
||||||
is ToolExecutionRejectedEvent -> ServerMessage.ToolRejected(
|
is ToolExecutionRejectedEvent -> ServerMessage.ToolRejected(
|
||||||
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
sessionId = p.sessionId, toolName = p.toolName, reason = p.reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ApprovalRequestedEvent -> mapApprovalRequested(p)
|
is ApprovalRequestedEvent -> mapApprovalRequested(p)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapWorkflowStarted(p: WorkflowStartedEvent): ServerMessage =
|
private fun mapWorkflowStarted(p: WorkflowStartedEvent): ServerMessage =
|
||||||
ServerMessage.SessionStarted(sessionId = p.sessionId, workflowId = p.startStageId.value)
|
ServerMessage.SessionStarted(sessionId = p.sessionId, workflowId = p.workflowId)
|
||||||
|
|
||||||
private fun mapOrchestrationPaused(p: OrchestrationPausedEvent): ServerMessage {
|
private fun mapOrchestrationPaused(p: OrchestrationPausedEvent): ServerMessage {
|
||||||
val reason = if (p.reason == "APPROVAL_PENDING") PauseReason.APPROVAL_PENDING else PauseReason.USER_REQUESTED
|
val reason = if (p.reason == "APPROVAL_PENDING") PauseReason.APPROVAL_PENDING else PauseReason.USER_REQUESTED
|
||||||
return ServerMessage.SessionPaused(sessionId = p.sessionId, reason = reason)
|
return ServerMessage.SessionPaused(sessionId = p.sessionId, reason = reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun mapInferenceCompleted(p: InferenceCompletedEvent, artifactStore: ArtifactStore): ServerMessage {
|
private suspend fun mapInferenceCompleted(
|
||||||
|
event: StoredEvent,
|
||||||
|
p: InferenceCompletedEvent,
|
||||||
|
artifactStore: ArtifactStore,
|
||||||
|
): ServerMessage {
|
||||||
val text = runCatching {
|
val text = runCatching {
|
||||||
artifactStore.get(p.responseArtifactId)?.toString(Charsets.UTF_8) ?: ""
|
artifactStore.get(p.responseArtifactId)?.toString(Charsets.UTF_8) ?: ""
|
||||||
}.getOrElse { "" }
|
}.getOrElse { "" }
|
||||||
return ServerMessage.InferenceCompleted(
|
return ServerMessage.InferenceCompleted(
|
||||||
sessionId = p.sessionId, stageId = p.stageId, outputSummary = text, responseText = text,
|
sessionId = p.sessionId,
|
||||||
|
stageId = p.stageId,
|
||||||
|
outputSummary = text,
|
||||||
|
responseText = text,
|
||||||
|
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.correx.apps.server.protocol
|
package com.correx.apps.server.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class ProtocolException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
|
class ProtocolException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ sealed class ServerMessage {
|
|||||||
data class SessionFailed(val sessionId: SessionId, val reason: String) : ServerMessage()
|
data class SessionFailed(val sessionId: SessionId, val reason: String) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StageStarted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
|
data class StageStarted(val sessionId: SessionId, val stageId: StageId, val occurredAt: Long) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StageCompleted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
|
data class StageCompleted(val sessionId: SessionId, val stageId: StageId, val occurredAt: Long) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StageFailed(val sessionId: SessionId, val stageId: StageId, val reason: String) :
|
data class StageFailed(val sessionId: SessionId, val stageId: StageId, val reason: String, val occurredAt: Long) :
|
||||||
ServerMessage()
|
ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -39,6 +39,7 @@ sealed class ServerMessage {
|
|||||||
val stageId: StageId,
|
val stageId: StageId,
|
||||||
val outputSummary: String,
|
val outputSummary: String,
|
||||||
val responseText: String = "",
|
val responseText: String = "",
|
||||||
|
val occurredAt: Long,
|
||||||
) : ServerMessage()
|
) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -50,11 +51,15 @@ sealed class ServerMessage {
|
|||||||
ServerMessage()
|
ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ToolCompleted(val sessionId: SessionId, val toolName: String, val outputSummary: String) :
|
data class ToolCompleted(
|
||||||
ServerMessage()
|
val sessionId: SessionId,
|
||||||
|
val toolName: String,
|
||||||
|
val outputSummary: String,
|
||||||
|
val occurredAt: Long,
|
||||||
|
) : ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ToolFailed(val sessionId: SessionId, val toolName: String, val reason: String) :
|
data class ToolFailed(val sessionId: SessionId, val toolName: String, val reason: String, val occurredAt: Long) :
|
||||||
ServerMessage()
|
ServerMessage()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.correx.apps.server.registry
|
package com.correx.apps.server.registry
|
||||||
|
|
||||||
|
import com.correx.core.events.types.ProviderId
|
||||||
import com.correx.core.inference.InferenceProvider
|
import com.correx.core.inference.InferenceProvider
|
||||||
import com.correx.core.inference.ProviderHealth
|
import com.correx.core.inference.ProviderHealth
|
||||||
import com.correx.core.events.types.ProviderId
|
|
||||||
|
|
||||||
interface ProviderRegistry {
|
interface ProviderRegistry {
|
||||||
fun listAll(): List<InferenceProvider>
|
fun listAll(): List<InferenceProvider>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.correx.core.events.types.EventId
|
|||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.utils.TypeId
|
import com.correx.core.utils.TypeId
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import io.ktor.server.request.receive
|
import io.ktor.server.request.receive
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
@@ -21,7 +20,8 @@ import io.ktor.server.websocket.webSocket
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class StartSessionRequest(val workflowId: String, val config: SessionConfigDto? = null)
|
data class StartSessionRequest(val workflowId: String, val config: SessionConfigDto? = null)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(GlobalStreamHandler::class.java)
|
private val log = LoggerFactory.getLogger(GlobalStreamHandler::class.java)
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import com.correx.apps.server.protocol.ServerMessage.RouterResponseMessage
|
|||||||
import com.correx.core.approvals.ApprovalOutcome
|
import com.correx.core.approvals.ApprovalOutcome
|
||||||
import com.correx.core.approvals.ApprovalStatus
|
import com.correx.core.approvals.ApprovalStatus
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.model.ApprovalContext
|
|
||||||
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
|
||||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
|
||||||
import com.correx.core.approvals.UserSteering
|
import com.correx.core.approvals.UserSteering
|
||||||
|
import com.correx.core.approvals.model.ApprovalContext
|
||||||
|
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.router.RouterFacade
|
|
||||||
import com.correx.core.sessions.ApprovalMode
|
import com.correx.core.sessions.ApprovalMode
|
||||||
import com.correx.core.utils.TypeId
|
import com.correx.core.utils.TypeId
|
||||||
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
import io.ktor.server.websocket.DefaultWebSocketServerSession
|
||||||
@@ -24,6 +22,7 @@ import io.ktor.websocket.readText
|
|||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import com.correx.core.approvals.model.ApprovalDecision as DomainApprovalDecision
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(SessionStreamHandler::class.java)
|
private val log = LoggerFactory.getLogger(SessionStreamHandler::class.java)
|
||||||
|
|
||||||
@@ -75,11 +74,20 @@ class SessionStreamHandler(private val module: ServerModule) {
|
|||||||
runCatching { module.orchestrator.cancel(msg.sessionId) }
|
runCatching { module.orchestrator.cancel(msg.sessionId) }
|
||||||
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
|
.onFailure { log.error("cancel failed for session={}: {}", msg.sessionId.value, it.message, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is ClientMessage.ApprovalResponse -> {
|
is ClientMessage.ApprovalResponse -> {
|
||||||
val domainDecision = msg.toDomainDecision(msg.requestId.value)
|
val domainDecision = msg.toDomainDecision(msg.requestId.value)
|
||||||
runCatching { module.orchestrator.submitApprovalDecision(msg.requestId, domainDecision) }
|
runCatching { module.orchestrator.submitApprovalDecision(msg.requestId, domainDecision) }
|
||||||
.onFailure { log.error("submitApprovalDecision failed for request={}: {}", msg.requestId.value, it.message, it) }
|
.onFailure {
|
||||||
|
log.error(
|
||||||
|
"submitApprovalDecision failed for request={}: {}",
|
||||||
|
msg.requestId.value,
|
||||||
|
it.message,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ClientMessage.ChatInput -> {
|
is ClientMessage.ChatInput -> {
|
||||||
runCatching {
|
runCatching {
|
||||||
module.routerFacade.onUserInput(
|
module.routerFacade.onUserInput(
|
||||||
@@ -88,21 +96,24 @@ class SessionStreamHandler(private val module: ServerModule) {
|
|||||||
mode = msg.mode,
|
mode = msg.mode,
|
||||||
)
|
)
|
||||||
}.onSuccess { response ->
|
}.onSuccess { response ->
|
||||||
session.send(Frame.Text(
|
session.send(
|
||||||
ProtocolSerializer.encodeServerMessage(
|
Frame.Text(
|
||||||
RouterResponseMessage(
|
ProtocolSerializer.encodeServerMessage(
|
||||||
sessionId = msg.sessionId,
|
RouterResponseMessage(
|
||||||
content = response.content,
|
sessionId = msg.sessionId,
|
||||||
steeringEmitted = response.steeringEmitted,
|
content = response.content,
|
||||||
)
|
steeringEmitted = response.steeringEmitted,
|
||||||
)
|
),
|
||||||
))
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
log.error("routerFacade.onUserInput failed for session={}: {}", msg.sessionId.value, it.message, it)
|
log.error("routerFacade.onUserInput failed for session={}: {}", msg.sessionId.value, it.message, it)
|
||||||
val error = ServerMessage.ProtocolError("Router error: ${it.message}")
|
val error = ServerMessage.ProtocolError("Router error: ${it.message}")
|
||||||
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
session.send(Frame.Text(ProtocolSerializer.encodeServerMessage(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
log.warn("unexpected message type={} in session stream", msg::class.simpleName)
|
log.warn("unexpected message type={} in session stream", msg::class.simpleName)
|
||||||
val error = ServerMessage.ProtocolError("Unexpected message type in session stream")
|
val error = ServerMessage.ProtocolError("Unexpected message type in session stream")
|
||||||
|
|||||||
+86
-62
@@ -24,8 +24,8 @@ import com.correx.core.events.events.TransitionExecutedEvent
|
|||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.types.ArtifactId
|
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
|
import com.correx.core.events.types.ArtifactId
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
import com.correx.core.events.types.ProviderId
|
import com.correx.core.events.types.ProviderId
|
||||||
@@ -35,8 +35,8 @@ import com.correx.core.events.types.ToolInvocationId
|
|||||||
import com.correx.core.events.types.TransitionId
|
import com.correx.core.events.types.TransitionId
|
||||||
import com.correx.core.events.types.ValidationReportId
|
import com.correx.core.events.types.ValidationReportId
|
||||||
import com.correx.core.inference.TokenUsage
|
import com.correx.core.inference.TokenUsage
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -45,7 +45,9 @@ class DomainEventMapperTest {
|
|||||||
|
|
||||||
private val sessionId = SessionId("session-1")
|
private val sessionId = SessionId("session-1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||||
|
private val occurredAt = timestamp.toEpochMilliseconds()
|
||||||
private val noopStore: ArtifactStore = object : ArtifactStore {
|
private val noopStore: ArtifactStore = object : ArtifactStore {
|
||||||
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
|
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("art-1")
|
||||||
override suspend fun get(id: ArtifactId): ByteArray? = null
|
override suspend fun get(id: ArtifactId): ByteArray? = null
|
||||||
@@ -68,15 +70,21 @@ class DomainEventMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WorkflowStartedEvent maps to SessionStarted`(): Unit = runTest {
|
fun `WorkflowStartedEvent maps to SessionStarted`(): Unit = runTest {
|
||||||
val event = storedEvent(WorkflowStartedEvent(sessionId = sessionId, startStageId = stageId))
|
val event =
|
||||||
|
storedEvent(WorkflowStartedEvent(sessionId = sessionId, startStageId = stageId, workflowId = workflowId))
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), result)
|
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WorkflowCompletedEvent maps to SessionCompleted`(): Unit = runTest {
|
fun `WorkflowCompletedEvent maps to SessionCompleted`(): Unit = runTest {
|
||||||
val event = storedEvent(
|
val event = storedEvent(
|
||||||
WorkflowCompletedEvent(sessionId = sessionId, terminalStageId = stageId, totalStages = 1),
|
WorkflowCompletedEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
terminalStageId = stageId,
|
||||||
|
totalStages = 1,
|
||||||
|
workflowId = workflowId,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.SessionCompleted(sessionId), result)
|
assertEquals(ServerMessage.SessionCompleted(sessionId), result)
|
||||||
@@ -99,7 +107,7 @@ class DomainEventMapperTest {
|
|||||||
TransitionExecutedEvent(sessionId = sessionId, from = from, to = to, transitionId = TransitionId("t1")),
|
TransitionExecutedEvent(sessionId = sessionId, from = from, to = to, transitionId = TransitionId("t1")),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.StageStarted(sessionId, to), result)
|
assertEquals(ServerMessage.StageStarted(sessionId, to, occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,7 +116,7 @@ class DomainEventMapperTest {
|
|||||||
StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1")),
|
StageCompletedEvent(sessionId = sessionId, stageId = stageId, transitionId = TransitionId("t1")),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.StageCompleted(sessionId, stageId), result)
|
assertEquals(ServerMessage.StageCompleted(sessionId, stageId, occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -119,7 +127,7 @@ class DomainEventMapperTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.StageFailed(sessionId, stageId, "err"), result)
|
assertEquals(ServerMessage.StageFailed(sessionId, stageId, "err", occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -142,13 +150,15 @@ class DomainEventMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `InferenceStartedEvent maps to InferenceStarted`(): Unit = runTest {
|
fun `InferenceStartedEvent maps to InferenceStarted`(): Unit = runTest {
|
||||||
val event = storedEvent(InferenceStartedEvent(
|
val event = storedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
InferenceStartedEvent(
|
||||||
sessionId = sessionId,
|
requestId = InferenceRequestId("req-1"),
|
||||||
stageId = stageId,
|
sessionId = sessionId,
|
||||||
providerId = ProviderId("p1"),
|
stageId = stageId,
|
||||||
promptArtifactId = ArtifactId("art-prompt"),
|
providerId = ProviderId("p1"),
|
||||||
))
|
promptArtifactId = ArtifactId("art-prompt"),
|
||||||
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.InferenceStarted(sessionId, stageId), result)
|
assertEquals(ServerMessage.InferenceStarted(sessionId, stageId), result)
|
||||||
}
|
}
|
||||||
@@ -161,45 +171,55 @@ class DomainEventMapperTest {
|
|||||||
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("x")
|
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("x")
|
||||||
override suspend fun get(id: ArtifactId): ByteArray? =
|
override suspend fun get(id: ArtifactId): ByteArray? =
|
||||||
if (id == artifactId) responseText.toByteArray(Charsets.UTF_8) else null
|
if (id == artifactId) responseText.toByteArray(Charsets.UTF_8) else null
|
||||||
|
|
||||||
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
|
override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
|
||||||
}
|
}
|
||||||
val event = storedEvent(InferenceCompletedEvent(
|
val event = storedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
InferenceCompletedEvent(
|
||||||
sessionId = sessionId,
|
requestId = InferenceRequestId("req-1"),
|
||||||
stageId = stageId,
|
sessionId = sessionId,
|
||||||
providerId = ProviderId("p1"),
|
stageId = stageId,
|
||||||
tokensUsed = TokenUsage(10, 5),
|
providerId = ProviderId("p1"),
|
||||||
latencyMs = 100L,
|
tokensUsed = TokenUsage(10, 5),
|
||||||
responseArtifactId = artifactId,
|
latencyMs = 100L,
|
||||||
))
|
responseArtifactId = artifactId,
|
||||||
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, store)
|
val result = domainEventToServerMessage(event, store)
|
||||||
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, responseText, responseText), result)
|
assertEquals(
|
||||||
|
ServerMessage.InferenceCompleted(sessionId, stageId, responseText, responseText, occurredAt),
|
||||||
|
result,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `InferenceCompletedEvent falls back to empty string when artifact missing`(): Unit = runTest {
|
fun `InferenceCompletedEvent falls back to empty string when artifact missing`(): Unit = runTest {
|
||||||
val event = storedEvent(InferenceCompletedEvent(
|
val event = storedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
InferenceCompletedEvent(
|
||||||
sessionId = sessionId,
|
requestId = InferenceRequestId("req-1"),
|
||||||
stageId = stageId,
|
sessionId = sessionId,
|
||||||
providerId = ProviderId("p1"),
|
stageId = stageId,
|
||||||
tokensUsed = TokenUsage(10, 5),
|
providerId = ProviderId("p1"),
|
||||||
latencyMs = 100L,
|
tokensUsed = TokenUsage(10, 5),
|
||||||
responseArtifactId = ArtifactId("missing"),
|
latencyMs = 100L,
|
||||||
))
|
responseArtifactId = ArtifactId("missing"),
|
||||||
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, "", ""), result)
|
assertEquals(ServerMessage.InferenceCompleted(sessionId, stageId, "", "", occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `InferenceTimeoutEvent maps to InferenceTimedOut`(): Unit = runTest {
|
fun `InferenceTimeoutEvent maps to InferenceTimedOut`(): Unit = runTest {
|
||||||
val event = storedEvent(InferenceTimeoutEvent(
|
val event = storedEvent(
|
||||||
requestId = InferenceRequestId("req-1"),
|
InferenceTimeoutEvent(
|
||||||
sessionId = sessionId,
|
requestId = InferenceRequestId("req-1"),
|
||||||
stageId = stageId,
|
sessionId = sessionId,
|
||||||
providerId = ProviderId("p1"),
|
stageId = stageId,
|
||||||
timeoutMs = 5000L,
|
providerId = ProviderId("p1"),
|
||||||
))
|
timeoutMs = 5000L,
|
||||||
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.InferenceTimedOut(sessionId, stageId, 5000L), result)
|
assertEquals(ServerMessage.InferenceTimedOut(sessionId, stageId, 5000L), result)
|
||||||
}
|
}
|
||||||
@@ -207,16 +227,18 @@ class DomainEventMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ToolInvocationRequestedEvent maps to ToolStarted`(): Unit = runTest {
|
fun `ToolInvocationRequestedEvent maps to ToolStarted`(): Unit = runTest {
|
||||||
val invId = ToolInvocationId("inv-1")
|
val invId = ToolInvocationId("inv-1")
|
||||||
val event = storedEvent(ToolInvocationRequestedEvent(
|
val event = storedEvent(
|
||||||
invocationId = invId,
|
ToolInvocationRequestedEvent(
|
||||||
sessionId = sessionId,
|
invocationId = invId,
|
||||||
stageId = stageId,
|
sessionId = sessionId,
|
||||||
toolName = "file_write",
|
stageId = stageId,
|
||||||
tier = Tier.T2,
|
toolName = "file_write",
|
||||||
request = ToolRequest(
|
tier = Tier.T2,
|
||||||
invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
|
request = ToolRequest(
|
||||||
|
invocationId = invId, sessionId = sessionId, stageId = stageId, toolName = "file_write",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
))
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.ToolStarted(sessionId, "file_write", Tier.T2), result)
|
assertEquals(ServerMessage.ToolStarted(sessionId, "file_write", Tier.T2), result)
|
||||||
}
|
}
|
||||||
@@ -239,7 +261,7 @@ class DomainEventMapperTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.ToolCompleted(sessionId, "file_write", "wrote 3 lines"), result)
|
assertEquals(ServerMessage.ToolCompleted(sessionId, "file_write", "wrote 3 lines", occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -253,7 +275,7 @@ class DomainEventMapperTest {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore)
|
val result = domainEventToServerMessage(event, noopStore)
|
||||||
assertEquals(ServerMessage.ToolFailed(sessionId, "file_write", "disk full"), result)
|
assertEquals(ServerMessage.ToolFailed(sessionId, "file_write", "disk full", occurredAt), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -274,15 +296,17 @@ class DomainEventMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ApprovalRequestedEvent maps to ApprovalRequired`(): Unit = runTest {
|
fun `ApprovalRequestedEvent maps to ApprovalRequired`(): Unit = runTest {
|
||||||
val requestId = ApprovalRequestId("req-1")
|
val requestId = ApprovalRequestId("req-1")
|
||||||
val event = storedEvent(ApprovalRequestedEvent(
|
val event = storedEvent(
|
||||||
requestId = requestId,
|
ApprovalRequestedEvent(
|
||||||
tier = Tier.T3,
|
requestId = requestId,
|
||||||
validationReportId = ValidationReportId("vr-1"),
|
tier = Tier.T3,
|
||||||
riskSummaryId = null,
|
validationReportId = ValidationReportId("vr-1"),
|
||||||
sessionId = sessionId,
|
riskSummaryId = null,
|
||||||
stageId = stageId,
|
sessionId = sessionId,
|
||||||
projectId = null,
|
stageId = stageId,
|
||||||
))
|
projectId = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
val result = domainEventToServerMessage(event, noopStore) as ServerMessage.ApprovalRequired
|
val result = domainEventToServerMessage(event, noopStore) as ServerMessage.ApprovalRequired
|
||||||
assertEquals(requestId, result.requestId)
|
assertEquals(requestId, result.requestId)
|
||||||
assertEquals(Tier.T3, result.tier)
|
assertEquals(Tier.T3, result.tier)
|
||||||
|
|||||||
+11
-10
@@ -5,8 +5,8 @@ import com.correx.core.artifactstore.ArtifactStore
|
|||||||
import com.correx.core.events.events.EventMetadata
|
import com.correx.core.events.events.EventMetadata
|
||||||
import com.correx.core.events.events.EventPayload
|
import com.correx.core.events.events.EventPayload
|
||||||
import com.correx.core.events.events.NewEvent
|
import com.correx.core.events.events.NewEvent
|
||||||
import com.correx.core.events.events.StoredEvent
|
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
@@ -27,6 +27,7 @@ class SessionEventBridgeTest {
|
|||||||
|
|
||||||
private val sessionId = SessionId("session-1")
|
private val sessionId = SessionId("session-1")
|
||||||
private val stageId = StageId("stage-1")
|
private val stageId = StageId("stage-1")
|
||||||
|
private val workflowId = "workflow-test"
|
||||||
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
private val timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||||
|
|
||||||
private val noopArtifactStore: ArtifactStore = object : ArtifactStore {
|
private val noopArtifactStore: ArtifactStore = object : ArtifactStore {
|
||||||
@@ -65,8 +66,8 @@ class SessionEventBridgeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `replaySnapshot sends mapped messages for all events`() = runTest {
|
fun `replaySnapshot sends mapped messages for all events`() = runTest {
|
||||||
val events = listOf(
|
val events = listOf(
|
||||||
storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L),
|
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
|
||||||
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
|
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
|
||||||
)
|
)
|
||||||
val store = fakeEventStore(allEventsList = events)
|
val store = fakeEventStore(allEventsList = events)
|
||||||
val sent = mutableListOf<ServerMessage>()
|
val sent = mutableListOf<ServerMessage>()
|
||||||
@@ -75,7 +76,7 @@ class SessionEventBridgeTest {
|
|||||||
bridge.replaySnapshot()
|
bridge.replaySnapshot()
|
||||||
|
|
||||||
assertEquals(2, sent.size)
|
assertEquals(2, sent.size)
|
||||||
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), sent[0])
|
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), sent[0])
|
||||||
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
|
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class SessionEventBridgeTest {
|
|||||||
fun `replaySnapshot skips unmapped events`() = runTest {
|
fun `replaySnapshot skips unmapped events`() = runTest {
|
||||||
val events = listOf(
|
val events = listOf(
|
||||||
storedEvent(OrchestrationResumedEvent(sessionId, stageId), seq = 1L),
|
storedEvent(OrchestrationResumedEvent(sessionId, stageId), seq = 1L),
|
||||||
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
|
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
|
||||||
)
|
)
|
||||||
val store = fakeEventStore(allEventsList = events)
|
val store = fakeEventStore(allEventsList = events)
|
||||||
val sent = mutableListOf<ServerMessage>()
|
val sent = mutableListOf<ServerMessage>()
|
||||||
@@ -98,8 +99,8 @@ class SessionEventBridgeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `streamLive sends mapped messages from live flow`() = runTest {
|
fun `streamLive sends mapped messages from live flow`() = runTest {
|
||||||
val events = listOf(
|
val events = listOf(
|
||||||
storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L),
|
storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L),
|
||||||
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L),
|
storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L),
|
||||||
)
|
)
|
||||||
val liveFlow: Flow<StoredEvent> = flow { events.forEach { emit(it) } }
|
val liveFlow: Flow<StoredEvent> = flow { events.forEach { emit(it) } }
|
||||||
val store = fakeEventStore(liveFlow = liveFlow)
|
val store = fakeEventStore(liveFlow = liveFlow)
|
||||||
@@ -109,7 +110,7 @@ class SessionEventBridgeTest {
|
|||||||
bridge.streamLive(sessionId)
|
bridge.streamLive(sessionId)
|
||||||
|
|
||||||
assertEquals(2, sent.size)
|
assertEquals(2, sent.size)
|
||||||
assertEquals(ServerMessage.SessionStarted(sessionId, stageId.value), sent[0])
|
assertEquals(ServerMessage.SessionStarted(sessionId, workflowId), sent[0])
|
||||||
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
|
assertEquals(ServerMessage.SessionCompleted(sessionId), sent[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +124,10 @@ class SessionEventBridgeTest {
|
|||||||
val sent = mutableListOf<ServerMessage>()
|
val sent = mutableListOf<ServerMessage>()
|
||||||
val bridge = SessionEventBridge(store, noopArtifactStore) { sent.add(it) }
|
val bridge = SessionEventBridge(store, noopArtifactStore) { sent.add(it) }
|
||||||
|
|
||||||
channel.send(storedEvent(WorkflowStartedEvent(sessionId, stageId), seq = 1L))
|
channel.send(storedEvent(WorkflowStartedEvent(sessionId, workflowId, stageId), seq = 1L))
|
||||||
|
|
||||||
val job = launch { bridge.streamLive(sessionId) }
|
val job = launch { bridge.streamLive(sessionId) }
|
||||||
channel.send(storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1), seq = 2L))
|
channel.send(storedEvent(WorkflowCompletedEvent(sessionId, stageId, 1, workflowId), seq = 2L))
|
||||||
channel.close()
|
channel.close()
|
||||||
job.join()
|
job.join()
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import dev.tamboui.terminal.Frame
|
|||||||
import dev.tamboui.tui.EventHandler
|
import dev.tamboui.tui.EventHandler
|
||||||
import dev.tamboui.tui.Renderer
|
import dev.tamboui.tui.Renderer
|
||||||
import dev.tamboui.tui.TuiRunner
|
import dev.tamboui.tui.TuiRunner
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
|
||||||
private const val DEFAULT_PORT = 8080
|
private const val DEFAULT_PORT = 8080
|
||||||
private const val STATUS_HEIGHT = 1
|
private const val STATUS_HEIGHT = 1
|
||||||
@@ -70,7 +70,7 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
val handler = EventHandler { event, _ ->
|
val handler = EventHandler { event, _ ->
|
||||||
if (event is TambouiKeyEvent) {
|
if (event is TambouiKeyEvent) {
|
||||||
mapKey(event, state.inputMode)
|
mapKey(event)
|
||||||
?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) }
|
?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) }
|
||||||
?.let(::dispatch)
|
?.let(::dispatch)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,50 +23,11 @@ fun inputBarWidget(state: TuiState): Paragraph {
|
|||||||
?: "(no session)"
|
?: "(no session)"
|
||||||
|
|
||||||
val (row1, row2) = when (state.inputMode) {
|
val (row1, row2) = when (state.inputMode) {
|
||||||
InputMode.ROUTER -> {
|
InputMode.ROUTER -> createRowsForRouter(state, dimStyle, sessionName, hasApproval, hasSession)
|
||||||
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
InputMode.NAVIGATE -> createRowsForNavigate(dimStyle, sessionName, hasApproval, hasSession)
|
||||||
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
InputMode.FILTER -> createRowsForFilter(state, dimStyle)
|
||||||
} else {
|
InputMode.STEER -> createRowsForSteer(state, dimStyle, sessionName)
|
||||||
Line.from(Span.raw("▌ "), Span.styled("Ask anything…", dimStyle))
|
|
||||||
}
|
|
||||||
val hints = buildList<String> {
|
|
||||||
add("$sessionName · router")
|
|
||||||
add("tab navigate")
|
|
||||||
if (hasApproval) { add("ctrl+a approve"); add("ctrl+r reject") }
|
|
||||||
if (hasSession) add("ctrl+s steer")
|
|
||||||
add("ctrl+e events")
|
|
||||||
add("ctrl+q")
|
|
||||||
}.joinToString(" ")
|
|
||||||
cursor to Line.from(Span.styled(hints, dimStyle))
|
|
||||||
}
|
|
||||||
InputMode.NAVIGATE -> {
|
|
||||||
val row1Line = Line.from(Span.styled(" ↑↓ sessions enter select", dimStyle))
|
|
||||||
val hints = buildList<String> {
|
|
||||||
add("$sessionName · navigate")
|
|
||||||
add("tab router")
|
|
||||||
if (hasApproval) { add("ctrl+a approve"); add("ctrl+r reject") }
|
|
||||||
if (hasSession) add("ctrl+s steer")
|
|
||||||
add("ctrl+e events")
|
|
||||||
add("ctrl+q")
|
|
||||||
}.joinToString(" ")
|
|
||||||
row1Line to Line.from(Span.styled(hints, dimStyle))
|
|
||||||
}
|
|
||||||
InputMode.FILTER -> {
|
|
||||||
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
|
||||||
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
|
||||||
} else {
|
|
||||||
Line.from(Span.raw("▌ "), Span.styled("/filter…", dimStyle))
|
|
||||||
}
|
|
||||||
cursor to Line.from(Span.styled("filtering sessions esc clear enter apply", dimStyle))
|
|
||||||
}
|
|
||||||
InputMode.STEER -> {
|
|
||||||
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
|
||||||
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
|
||||||
} else {
|
|
||||||
Line.from(Span.raw("▌ "), Span.styled("Steer the session…", dimStyle))
|
|
||||||
}
|
|
||||||
cursor to Line.from(Span.styled("$sessionName · steering esc cancel enter send", dimStyle))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val block = Block.builder()
|
val block = Block.builder()
|
||||||
@@ -77,3 +38,73 @@ fun inputBarWidget(state: TuiState): Paragraph {
|
|||||||
|
|
||||||
return Paragraph.builder().text(Text.from(listOf(row1, row2))).block(block).build()
|
return Paragraph.builder().text(Text.from(listOf(row1, row2))).block(block).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createRowsForSteer(
|
||||||
|
state: TuiState,
|
||||||
|
dimStyle: Style?,
|
||||||
|
sessionName: String,
|
||||||
|
): Pair<Line?, Line?> {
|
||||||
|
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
||||||
|
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
||||||
|
} else {
|
||||||
|
Line.from(Span.raw("▌ "), Span.styled("Steer the session…", dimStyle))
|
||||||
|
}
|
||||||
|
return cursor to Line.from(Span.styled("$sessionName · steering esc cancel enter send", dimStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRowsForFilter(
|
||||||
|
state: TuiState,
|
||||||
|
dimStyle: Style?,
|
||||||
|
): Pair<Line?, Line?> {
|
||||||
|
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
||||||
|
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
||||||
|
} else {
|
||||||
|
Line.from(Span.raw("▌ "), Span.styled("/filter…", dimStyle))
|
||||||
|
}
|
||||||
|
return cursor to Line.from(Span.styled("filtering sessions esc clear enter apply", dimStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRowsForNavigate(
|
||||||
|
dimStyle: Style?,
|
||||||
|
sessionName: String,
|
||||||
|
hasApproval: Boolean,
|
||||||
|
hasSession: Boolean,
|
||||||
|
): Pair<Line?, Line?> {
|
||||||
|
val row1Line = Line.from(Span.styled(" ↑↓ sessions enter select", dimStyle))
|
||||||
|
val hints = buildList<String> {
|
||||||
|
add("$sessionName · navigate")
|
||||||
|
add("tab router")
|
||||||
|
if (hasApproval) {
|
||||||
|
add("ctrl+a approve"); add("ctrl+r reject")
|
||||||
|
}
|
||||||
|
if (hasSession) add("ctrl+s steer")
|
||||||
|
add("ctrl+e events")
|
||||||
|
add("ctrl+q")
|
||||||
|
}.joinToString(" ")
|
||||||
|
return row1Line to Line.from(Span.styled(hints, dimStyle))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRowsForRouter(
|
||||||
|
state: TuiState,
|
||||||
|
dimStyle: Style?,
|
||||||
|
sessionName: String,
|
||||||
|
hasApproval: Boolean,
|
||||||
|
hasSession: Boolean,
|
||||||
|
): Pair<Line?, Line?> {
|
||||||
|
val cursor = if (state.inputBuffer.isNotEmpty()) {
|
||||||
|
Line.from(Span.raw("▌ "), Span.raw(state.inputBuffer))
|
||||||
|
} else {
|
||||||
|
Line.from(Span.raw("▌ "), Span.styled("Ask anything…", dimStyle))
|
||||||
|
}
|
||||||
|
val hints = buildList<String> {
|
||||||
|
add("$sessionName · router")
|
||||||
|
add("tab navigate")
|
||||||
|
if (hasApproval) {
|
||||||
|
add("ctrl+a approve"); add("ctrl+r reject")
|
||||||
|
}
|
||||||
|
if (hasSession) add("ctrl+s steer")
|
||||||
|
add("ctrl+e events")
|
||||||
|
add("ctrl+q")
|
||||||
|
}.joinToString(" ")
|
||||||
|
return cursor to Line.from(Span.styled(hints, dimStyle))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.correx.apps.tui.components
|
package com.correx.apps.tui.components
|
||||||
|
|
||||||
import com.correx.apps.tui.state.TuiState
|
import com.correx.apps.tui.state.TuiState
|
||||||
import dev.tamboui.style.Color
|
|
||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
import dev.tamboui.text.Line
|
import dev.tamboui.text.Line
|
||||||
import dev.tamboui.text.Span
|
import dev.tamboui.text.Span
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ fun sessionListWidget(state: TuiState): Paragraph {
|
|||||||
val isSelected = s.id == state.sessions.selectedId
|
val isSelected = s.id == state.sessions.selectedId
|
||||||
val prefix = if (isSelected) Span.styled("▶ ", Style.create().blue()) else Span.raw(" ")
|
val prefix = if (isSelected) Span.styled("▶ ", Style.create().blue()) else Span.raw(" ")
|
||||||
val idSpan = Span.styled("[${s.id.take(ID_LEN)}]", dimStyle)
|
val idSpan = Span.styled("[${s.id.take(ID_LEN)}]", dimStyle)
|
||||||
val statusStr = s.status.uppercase().padEnd(STATUS_WIDTH)
|
|
||||||
val statusSpan = statusSpan(s)
|
val statusSpan = statusSpan(s)
|
||||||
val rawName = s.name.ifEmpty { s.workflowId }
|
val rawName = s.name.ifEmpty { s.workflowId }
|
||||||
val truncated = if (rawName.length > NAME_MAX) rawName.take(NAME_MAX - 1) + "…" else rawName
|
val truncated = if (rawName.length > NAME_MAX) rawName.take(NAME_MAX - 1) + "…" else rawName
|
||||||
@@ -61,6 +60,7 @@ private fun statusSpan(s: SessionSummary): Span {
|
|||||||
|
|
||||||
fun filteredSessions(sessions: SessionsState): List<SessionSummary> {
|
fun filteredSessions(sessions: SessionsState): List<SessionSummary> {
|
||||||
val f = sessions.filter
|
val f = sessions.filter
|
||||||
return if (f.isEmpty()) sessions.sessions
|
val sorted = sessions.sessions.sortedByDescending { it.lastEventAt }
|
||||||
else sessions.sessions.filter { it.workflowId.contains(f, ignoreCase = true) }
|
return if (f.isEmpty()) sorted
|
||||||
|
else sorted.filter { it.workflowId.contains(f, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
package com.correx.apps.tui.input
|
package com.correx.apps.tui.input
|
||||||
|
|
||||||
import com.correx.apps.tui.KeyEvent
|
import com.correx.apps.tui.KeyEvent
|
||||||
import com.correx.apps.tui.state.InputMode
|
|
||||||
import dev.tamboui.tui.event.KeyCode
|
import dev.tamboui.tui.event.KeyCode
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
|
||||||
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
||||||
fun mapKey(event: TambouiKeyEvent, mode: InputMode): KeyEvent? {
|
fun mapKey(event: TambouiKeyEvent): KeyEvent? {
|
||||||
if (event.isCtrlC()) return KeyEvent.Quit
|
if (event.isCtrlC) return KeyEvent.Quit
|
||||||
if (event.hasAlt()) return null
|
if (event.hasAlt()) return null
|
||||||
if (event.hasCtrl()) {
|
if (event.hasCtrl()) {
|
||||||
return when (event.character()) {
|
return when {
|
||||||
'q' -> KeyEvent.Quit
|
event.isChar('q') -> KeyEvent.Quit
|
||||||
'n' -> KeyEvent.NewSession
|
event.isChar('n') -> KeyEvent.NewSession
|
||||||
'c' -> KeyEvent.Cancel
|
event.isChar('c') -> KeyEvent.Cancel
|
||||||
'a' -> KeyEvent.Approve
|
event.isChar('a') -> KeyEvent.Approve
|
||||||
'r' -> KeyEvent.Reject
|
event.isChar('r') -> KeyEvent.Reject
|
||||||
's' -> KeyEvent.Steer
|
event.isChar('s') -> KeyEvent.Steer
|
||||||
'h' -> KeyEvent.ToggleApprovalOverlay
|
event.isChar('h') -> KeyEvent.ToggleApprovalOverlay
|
||||||
'e' -> KeyEvent.ToggleEventOverlay
|
event.isChar('e') -> KeyEvent.ToggleEventOverlay
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ object RootReducer {
|
|||||||
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action)
|
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action)
|
||||||
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
|
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
|
||||||
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
|
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
|
||||||
|
val approvalJustArrived = approval.active != null && afterInput.approval.active == null
|
||||||
val withSubReducers = afterInput.copy(
|
val withSubReducers = afterInput.copy(
|
||||||
sessions = sessions,
|
sessions = sessions,
|
||||||
approval = approval,
|
approval = approval,
|
||||||
connection = connection,
|
connection = connection,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
|
approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible,
|
||||||
)
|
)
|
||||||
val final = when (action) {
|
val final = when (action) {
|
||||||
is Action.ToggleApprovalOverlay -> withSubReducers.copy(
|
is Action.ToggleApprovalOverlay -> withSubReducers.copy(
|
||||||
|
|||||||
@@ -25,8 +25,18 @@ object SessionsReducer {
|
|||||||
action: Action,
|
action: Action,
|
||||||
clock: () -> Long = System::currentTimeMillis,
|
clock: () -> Long = System::currentTimeMillis,
|
||||||
): Pair<SessionsState, List<Effect>> = when (action) {
|
): Pair<SessionsState, List<Effect>> = when (action) {
|
||||||
is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) navigateUp(sessions) to emptyList() else sessions to emptyList()
|
is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) {
|
||||||
is Action.NavigateDown -> if (inputMode == InputMode.NAVIGATE) navigateDown(sessions) to emptyList() else sessions to emptyList()
|
navigateUp(sessions) to emptyList()
|
||||||
|
} else {
|
||||||
|
sessions to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
is Action.NavigateDown -> if (inputMode == InputMode.NAVIGATE) {
|
||||||
|
navigateDown(sessions) to emptyList()
|
||||||
|
} else {
|
||||||
|
sessions to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
is Action.SubmitInput -> if (inputMode == InputMode.FILTER) {
|
is Action.SubmitInput -> if (inputMode == InputMode.FILTER) {
|
||||||
sessions.copy(filter = inputText) to emptyList()
|
sessions.copy(filter = inputText) to emptyList()
|
||||||
} else {
|
} else {
|
||||||
@@ -52,8 +62,12 @@ object SessionsReducer {
|
|||||||
else -> sessions to emptyList()
|
else -> sessions to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun filteredSessions(sessions: SessionsState): List<SessionSummary> =
|
||||||
|
if (sessions.filter.isBlank()) sessions.sessions
|
||||||
|
else sessions.sessions.filter { it.workflowId.contains(sessions.filter, ignoreCase = true) }
|
||||||
|
|
||||||
private fun navigateUp(sessions: SessionsState): SessionsState {
|
private fun navigateUp(sessions: SessionsState): SessionsState {
|
||||||
val list = sessions.sessions
|
val list = filteredSessions(sessions)
|
||||||
if (list.isEmpty()) return sessions
|
if (list.isEmpty()) return sessions
|
||||||
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
||||||
val newIdx = if (idx <= 0) list.lastIndex else idx - 1
|
val newIdx = if (idx <= 0) list.lastIndex else idx - 1
|
||||||
@@ -61,7 +75,7 @@ object SessionsReducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateDown(sessions: SessionsState): SessionsState {
|
private fun navigateDown(sessions: SessionsState): SessionsState {
|
||||||
val list = sessions.sessions
|
val list = filteredSessions(sessions)
|
||||||
if (list.isEmpty()) return sessions
|
if (list.isEmpty()) return sessions
|
||||||
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
val idx = list.indexOfFirst { it.id == sessions.selectedId }
|
||||||
val newIdx = if (idx >= list.lastIndex) 0 else idx + 1
|
val newIdx = if (idx >= list.lastIndex) 0 else idx + 1
|
||||||
@@ -77,194 +91,244 @@ object SessionsReducer {
|
|||||||
msg: ServerMessage,
|
msg: ServerMessage,
|
||||||
clock: () -> Long,
|
clock: () -> Long,
|
||||||
): Pair<SessionsState, List<Effect>> = when (msg) {
|
): Pair<SessionsState, List<Effect>> = when (msg) {
|
||||||
is ServerMessage.SessionStarted -> {
|
is ServerMessage.SessionStarted -> processSessionStartedMessage(msg, clock, sessions)
|
||||||
val summary = SessionSummary(
|
is ServerMessage.SessionPaused -> processSessionPausedMessage(msg, sessions, clock)
|
||||||
id = msg.sessionId.value,
|
is ServerMessage.SessionCompleted -> processSessionCompletedMessage(sessions, msg, clock)
|
||||||
status = "ACTIVE",
|
is ServerMessage.SessionFailed -> processSessionFailedMessage(sessions, msg, clock)
|
||||||
workflowId = msg.workflowId,
|
is ServerMessage.StageStarted -> processStageStartedMessage(msg, sessions)
|
||||||
name = msg.workflowId,
|
is ServerMessage.StageCompleted -> processStageCompletedMessage(msg, sessions)
|
||||||
lastEventAt = clock(),
|
is ServerMessage.StageFailed -> processStageFailedMessage(msg, sessions)
|
||||||
currentStage = null,
|
is ServerMessage.ToolStarted -> processToolStartedMessage(clock, sessions, msg)
|
||||||
lastOutput = null,
|
is ServerMessage.InferenceCompleted -> processInferenceCompletedMessage(sessions, msg)
|
||||||
)
|
is ServerMessage.ToolCompleted -> processToolCompletedMessage(msg, sessions)
|
||||||
val selected = sessions.selectedId ?: msg.sessionId.value
|
is ServerMessage.ToolFailed -> processToolFailedMessage(msg, sessions)
|
||||||
sessions.copy(sessions = sessions.sessions + summary, selectedId = selected) to
|
is ServerMessage.ToolRejected -> processToolRejectedMessage(clock, sessions, msg)
|
||||||
listOf(Effect.ConnectSession(msg.sessionId))
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.SessionPaused -> {
|
|
||||||
val statusLabel = if (msg.reason == PauseReason.APPROVAL_PENDING) {
|
|
||||||
"PAUSED awaiting approval"
|
|
||||||
} else {
|
|
||||||
"PAUSED"
|
|
||||||
}
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) s.copy(status = statusLabel, lastEventAt = clock()) else s
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.SessionCompleted -> touchSession(sessions, msg.sessionId.value, "COMPLETED", clock) to listOf(
|
|
||||||
Effect.DisconnectSession,
|
|
||||||
)
|
|
||||||
|
|
||||||
is ServerMessage.SessionFailed -> touchSession(
|
|
||||||
sessions,
|
|
||||||
msg.sessionId.value,
|
|
||||||
"FAILED",
|
|
||||||
clock,
|
|
||||||
) to listOf(Effect.DisconnectSession)
|
|
||||||
|
|
||||||
is ServerMessage.StageStarted -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val entry = TuiEventEntry(formatTime(now), "StageStarted", msg.stageId.value)
|
|
||||||
s.copy(
|
|
||||||
currentStage = msg.stageId.value,
|
|
||||||
currentStageId = msg.stageId.value,
|
|
||||||
tools = emptyList(),
|
|
||||||
recentEvents = (s.recentEvents + entry).takeLast(7),
|
|
||||||
lastEventAt = now,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.StageCompleted -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val entry = TuiEventEntry(formatTime(now), "StageCompleted", msg.stageId.value)
|
|
||||||
s.copy(
|
|
||||||
currentStage = null,
|
|
||||||
recentEvents = (s.recentEvents + entry).takeLast(7),
|
|
||||||
lastEventAt = now,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.StageFailed -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val entry = TuiEventEntry(formatTime(now), "StageFailed", msg.stageId.value)
|
|
||||||
s.copy(
|
|
||||||
currentStage = null,
|
|
||||||
recentEvents = (s.recentEvents + entry).takeLast(7),
|
|
||||||
lastEventAt = now,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.ToolStarted -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val record = TuiToolRecord(msg.toolName, msg.tier.level, ToolDisplayStatus.STARTED, null)
|
|
||||||
s.copy(
|
|
||||||
tools = (s.tools + record).takeLast(8),
|
|
||||||
lastEventAt = now,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.InferenceCompleted -> applyInferenceCompleted(sessions, msg, clock)
|
|
||||||
is ServerMessage.ToolCompleted -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val updatedTools = s.tools.map { t ->
|
|
||||||
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
|
||||||
t.copy(status = ToolDisplayStatus.COMPLETED)
|
|
||||||
} else {
|
|
||||||
t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val entry = TuiEventEntry(formatTime(now), "ToolCompleted", msg.toolName)
|
|
||||||
s.copy(
|
|
||||||
tools = updatedTools,
|
|
||||||
lastOutput = "${msg.toolName}: ${msg.outputSummary}",
|
|
||||||
recentEvents = (s.recentEvents + entry).takeLast(7),
|
|
||||||
lastEventAt = now,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.ToolFailed -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val updatedTools = s.tools.map { t ->
|
|
||||||
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
|
||||||
t.copy(status = ToolDisplayStatus.FAILED)
|
|
||||||
} else {
|
|
||||||
t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.copy(tools = updatedTools, lastEventAt = now)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
is ServerMessage.ToolRejected -> {
|
|
||||||
val now = clock()
|
|
||||||
sessions.copy(
|
|
||||||
sessions = sessions.sessions.map { s ->
|
|
||||||
if (s.id == msg.sessionId.value) {
|
|
||||||
val updatedTools = s.tools.map { t ->
|
|
||||||
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
|
||||||
t.copy(status = ToolDisplayStatus.REJECTED)
|
|
||||||
} else {
|
|
||||||
t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.copy(tools = updatedTools, lastEventAt = now)
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) to emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> sessions to emptyList()
|
else -> sessions to emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyInferenceCompleted(
|
private fun processToolRejectedMessage(
|
||||||
sessions: SessionsState,
|
|
||||||
msg: ServerMessage.InferenceCompleted,
|
|
||||||
clock: () -> Long,
|
clock: () -> Long,
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.ToolRejected,
|
||||||
): Pair<SessionsState, List<Effect>> {
|
): Pair<SessionsState, List<Effect>> {
|
||||||
val now = clock()
|
val now = clock()
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val updatedTools = s.tools.map { t ->
|
||||||
|
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
||||||
|
t.copy(status = ToolDisplayStatus.REJECTED)
|
||||||
|
} else {
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.copy(tools = updatedTools, lastEventAt = now)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processToolFailedMessage(
|
||||||
|
msg: ServerMessage.ToolFailed,
|
||||||
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val updatedTools = s.tools.map { t ->
|
||||||
|
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
||||||
|
t.copy(status = ToolDisplayStatus.FAILED)
|
||||||
|
} else {
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.copy(tools = updatedTools, lastEventAt = now)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processToolCompletedMessage(
|
||||||
|
msg: ServerMessage.ToolCompleted,
|
||||||
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val updatedTools = s.tools.map { t ->
|
||||||
|
if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) {
|
||||||
|
t.copy(status = ToolDisplayStatus.COMPLETED)
|
||||||
|
} else {
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val entry = TuiEventEntry(formatTime(now), "ToolCompleted", msg.toolName)
|
||||||
|
s.copy(
|
||||||
|
tools = updatedTools,
|
||||||
|
lastOutput = "${msg.toolName}: ${msg.outputSummary}",
|
||||||
|
recentEvents = (s.recentEvents + entry).takeLast(7),
|
||||||
|
lastEventAt = now,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processToolStartedMessage(
|
||||||
|
clock: () -> Long,
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.ToolStarted,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = clock()
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val record = TuiToolRecord(msg.toolName, msg.tier.level, ToolDisplayStatus.STARTED, null)
|
||||||
|
s.copy(
|
||||||
|
tools = (s.tools + record).takeLast(8),
|
||||||
|
lastEventAt = now,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processStageFailedMessage(
|
||||||
|
msg: ServerMessage.StageFailed,
|
||||||
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val entry = TuiEventEntry(formatTime(now), "StageFailed", msg.stageId.value)
|
||||||
|
s.copy(
|
||||||
|
currentStage = null,
|
||||||
|
recentEvents = (s.recentEvents + entry).takeLast(7),
|
||||||
|
lastEventAt = now,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processStageCompletedMessage(
|
||||||
|
msg: ServerMessage.StageCompleted,
|
||||||
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val entry = TuiEventEntry(formatTime(now), "StageCompleted", msg.stageId.value)
|
||||||
|
s.copy(
|
||||||
|
currentStage = null,
|
||||||
|
recentEvents = (s.recentEvents + entry).takeLast(7),
|
||||||
|
lastEventAt = now,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processStageStartedMessage(
|
||||||
|
msg: ServerMessage.StageStarted,
|
||||||
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) {
|
||||||
|
val entry = TuiEventEntry(formatTime(now), "StageStarted", msg.stageId.value)
|
||||||
|
s.copy(
|
||||||
|
currentStage = msg.stageId.value,
|
||||||
|
currentStageId = msg.stageId.value,
|
||||||
|
tools = emptyList(),
|
||||||
|
recentEvents = (s.recentEvents + entry).takeLast(7),
|
||||||
|
lastEventAt = now,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSessionFailedMessage(
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.SessionFailed,
|
||||||
|
clock: () -> Long,
|
||||||
|
): Pair<SessionsState, List<Effect.DisconnectSession>> = touchSession(
|
||||||
|
sessions,
|
||||||
|
msg.sessionId.value,
|
||||||
|
"FAILED",
|
||||||
|
clock,
|
||||||
|
) to listOf(Effect.DisconnectSession)
|
||||||
|
|
||||||
|
private fun processSessionCompletedMessage(
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.SessionCompleted,
|
||||||
|
clock: () -> Long,
|
||||||
|
): Pair<SessionsState, List<Effect.DisconnectSession>> =
|
||||||
|
touchSession(sessions, msg.sessionId.value, "COMPLETED", clock) to listOf(
|
||||||
|
Effect.DisconnectSession,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun processSessionPausedMessage(
|
||||||
|
msg: ServerMessage.SessionPaused,
|
||||||
|
sessions: SessionsState,
|
||||||
|
clock: () -> Long,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val statusLabel = if (msg.reason == PauseReason.APPROVAL_PENDING) {
|
||||||
|
"PAUSED awaiting approval"
|
||||||
|
} else {
|
||||||
|
"PAUSED"
|
||||||
|
}
|
||||||
|
return sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == msg.sessionId.value) s.copy(status = statusLabel, lastEventAt = clock()) else s
|
||||||
|
},
|
||||||
|
) to emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSessionStartedMessage(
|
||||||
|
msg: ServerMessage.SessionStarted,
|
||||||
|
clock: () -> Long,
|
||||||
|
sessions: SessionsState,
|
||||||
|
): Pair<SessionsState, List<Effect.ConnectSession>> {
|
||||||
|
val summary = SessionSummary(
|
||||||
|
id = msg.sessionId.value,
|
||||||
|
status = "ACTIVE",
|
||||||
|
workflowId = msg.workflowId,
|
||||||
|
name = msg.workflowId,
|
||||||
|
lastEventAt = clock(),
|
||||||
|
currentStage = null,
|
||||||
|
lastOutput = null,
|
||||||
|
)
|
||||||
|
val selected = sessions.selectedId ?: msg.sessionId.value
|
||||||
|
return sessions.copy(sessions = sessions.sessions + summary, selectedId = selected) to
|
||||||
|
listOf(Effect.ConnectSession(msg.sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processInferenceCompletedMessage(
|
||||||
|
sessions: SessionsState,
|
||||||
|
msg: ServerMessage.InferenceCompleted,
|
||||||
|
): Pair<SessionsState, List<Effect>> {
|
||||||
|
val now = msg.occurredAt
|
||||||
return sessions.copy(
|
return sessions.copy(
|
||||||
sessions = sessions.sessions.map { s ->
|
sessions = sessions.sessions.map { s ->
|
||||||
if (s.id == msg.sessionId.value) {
|
if (s.id == msg.sessionId.value) {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.correx.apps.tui.input
|
package com.correx.apps.tui.input
|
||||||
|
|
||||||
import com.correx.apps.tui.KeyEvent
|
import com.correx.apps.tui.KeyEvent
|
||||||
import com.correx.apps.tui.state.InputMode
|
|
||||||
import dev.tamboui.tui.event.KeyCode
|
import dev.tamboui.tui.event.KeyCode
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
|
||||||
import dev.tamboui.tui.event.KeyModifiers
|
import dev.tamboui.tui.event.KeyModifiers
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
|
||||||
class TambouiKeyMapperTest {
|
class TambouiKeyMapperTest {
|
||||||
|
|
||||||
@@ -15,156 +14,156 @@ class TambouiKeyMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode ROUTER`() {
|
fun `ctrl-c maps to Quit in InputMode ROUTER`() {
|
||||||
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode FILTER`() {
|
fun `ctrl-c maps to Quit in InputMode FILTER`() {
|
||||||
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.FILTER))
|
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode NAVIGATE`() {
|
fun `ctrl-c maps to Quit in InputMode NAVIGATE`() {
|
||||||
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.NAVIGATE))
|
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-c maps to Quit in InputMode STEER`() {
|
fun `ctrl-c maps to Quit in InputMode STEER`() {
|
||||||
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code), InputMode.STEER))
|
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'c'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Ctrl+key → keybind events ---
|
// --- Ctrl+key → keybind events ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-q maps to Quit`() {
|
fun `ctrl-q maps to Quit`() {
|
||||||
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'q'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.Quit, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'q'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-a maps to Approve`() {
|
fun `ctrl-a maps to Approve`() {
|
||||||
assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.Approve, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'a'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-r maps to Reject`() {
|
fun `ctrl-r maps to Reject`() {
|
||||||
assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'r'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.Reject, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'r'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-n maps to NewSession`() {
|
fun `ctrl-n maps to NewSession`() {
|
||||||
assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'n'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.NewSession, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'n'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-s maps to Steer`() {
|
fun `ctrl-s maps to Steer`() {
|
||||||
assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 's'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.Steer, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 's'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-h maps to ToggleApprovalOverlay`() {
|
fun `ctrl-h maps to ToggleApprovalOverlay`() {
|
||||||
assertEquals(KeyEvent.ToggleApprovalOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'h'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.ToggleApprovalOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'h'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl-e maps to ToggleEventOverlay`() {
|
fun `ctrl-e maps to ToggleEventOverlay`() {
|
||||||
assertEquals(KeyEvent.ToggleEventOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'e'.code), InputMode.ROUTER))
|
assertEquals(KeyEvent.ToggleEventOverlay, mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'e'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ctrl+unbound key maps to null`() {
|
fun `ctrl+unbound key maps to null`() {
|
||||||
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'z'.code), InputMode.ROUTER))
|
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.CTRL, 'z'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Alt always → null ---
|
// --- Alt always → null ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `alt modifier maps to null`() {
|
fun `alt modifier maps to null`() {
|
||||||
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.ALT, 'q'.code), InputMode.ROUTER))
|
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.ALT, 'q'.code)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bare chars always → CharInput (never trigger keybinds) ---
|
// --- Bare chars always → CharInput (never trigger keybinds) ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare a maps to CharInput in ROUTER`() {
|
fun `bare a maps to CharInput in ROUTER`() {
|
||||||
assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a'), InputMode.ROUTER))
|
assertEquals(KeyEvent.CharInput('a'), mapKey(TambouiKeyEvent.ofChar('a')))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare q maps to CharInput in ROUTER`() {
|
fun `bare q maps to CharInput in ROUTER`() {
|
||||||
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.ROUTER))
|
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q')))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare s maps to CharInput in ROUTER`() {
|
fun `bare s maps to CharInput in ROUTER`() {
|
||||||
assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s'), InputMode.ROUTER))
|
assertEquals(KeyEvent.CharInput('s'), mapKey(TambouiKeyEvent.ofChar('s')))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare x maps to CharInput in ROUTER`() {
|
fun `bare x maps to CharInput in ROUTER`() {
|
||||||
assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x'), InputMode.ROUTER))
|
assertEquals(KeyEvent.CharInput('x'), mapKey(TambouiKeyEvent.ofChar('x')))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare q maps to CharInput in FILTER`() {
|
fun `bare q maps to CharInput in FILTER`() {
|
||||||
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.FILTER))
|
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q')))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare q maps to CharInput in STEER`() {
|
fun `bare q maps to CharInput in STEER`() {
|
||||||
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.STEER))
|
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q')))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `bare q maps to CharInput in NAVIGATE`() {
|
fun `bare q maps to CharInput in NAVIGATE`() {
|
||||||
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q'), InputMode.NAVIGATE))
|
assertEquals(KeyEvent.CharInput('q'), mapKey(TambouiKeyEvent.ofChar('q')))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- KeyCode mappings ---
|
// --- KeyCode mappings ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `UP maps to NavUp`() {
|
fun `UP maps to NavUp`() {
|
||||||
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP), InputMode.ROUTER))
|
assertEquals(KeyEvent.NavUp, mapKey(TambouiKeyEvent.ofKey(KeyCode.UP)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `DOWN maps to NavDown`() {
|
fun `DOWN maps to NavDown`() {
|
||||||
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN), InputMode.ROUTER))
|
assertEquals(KeyEvent.NavDown, mapKey(TambouiKeyEvent.ofKey(KeyCode.DOWN)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ENTER maps to Enter`() {
|
fun `ENTER maps to Enter`() {
|
||||||
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER), InputMode.ROUTER))
|
assertEquals(KeyEvent.Enter, mapKey(TambouiKeyEvent.ofKey(KeyCode.ENTER)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ESCAPE maps to Escape`() {
|
fun `ESCAPE maps to Escape`() {
|
||||||
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE), InputMode.ROUTER))
|
assertEquals(KeyEvent.Escape, mapKey(TambouiKeyEvent.ofKey(KeyCode.ESCAPE)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `BACKSPACE maps to Backspace`() {
|
fun `BACKSPACE maps to Backspace`() {
|
||||||
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE), InputMode.ROUTER))
|
assertEquals(KeyEvent.Backspace, mapKey(TambouiKeyEvent.ofKey(KeyCode.BACKSPACE)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `TAB maps to Filter`() {
|
fun `TAB maps to Filter`() {
|
||||||
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.ROUTER))
|
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `TAB maps to Filter in FILTER mode`() {
|
fun `TAB maps to Filter in FILTER mode`() {
|
||||||
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB), InputMode.FILTER))
|
assertEquals(KeyEvent.Filter, mapKey(TambouiKeyEvent.ofKey(KeyCode.TAB)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ISO control chars → null ---
|
// --- ISO control chars → null ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ESC codepoint as CHAR event maps to null`() {
|
fun `ESC codepoint as CHAR event maps to null`() {
|
||||||
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B), InputMode.ROUTER))
|
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x1B)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NUL codepoint as CHAR event maps to null`() {
|
fun `NUL codepoint as CHAR event maps to null`() {
|
||||||
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x00), InputMode.ROUTER))
|
assertNull(mapKey(TambouiKeyEvent(KeyCode.CHAR, KeyModifiers.NONE, 0x00)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ class RootReducerTest {
|
|||||||
)
|
)
|
||||||
val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock)
|
val (state2, _) = RootReducer.reduce(state1, Action.ServerEventReceived(startMsg2), fixedClock)
|
||||||
// selected is s1 (first); navigating down should move to s2
|
// selected is s1 (first); navigating down should move to s2
|
||||||
val (state3, _) = RootReducer.reduce(state2.copy(inputMode = InputMode.NAVIGATE), Action.NavigateDown, fixedClock)
|
val (state3, _) = RootReducer.reduce(
|
||||||
|
state2.copy(inputMode = InputMode.NAVIGATE),
|
||||||
|
Action.NavigateDown,
|
||||||
|
fixedClock,
|
||||||
|
)
|
||||||
assertEquals("s2", state3.sessions.selectedId)
|
assertEquals("s2", state3.sessions.selectedId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,11 @@ class SessionsReducerTest {
|
|||||||
sessions = listOf(session("s1").copy(currentStage = "stage-1")),
|
sessions = listOf(session("s1").copy(currentStage = "stage-1")),
|
||||||
selectedId = "s1",
|
selectedId = "s1",
|
||||||
)
|
)
|
||||||
val msg = ServerMessage.StageCompleted(sessionId = SessionId("s1"), stageId = StageId("stage-1"))
|
val msg = ServerMessage.StageCompleted(
|
||||||
|
sessionId = SessionId("s1"),
|
||||||
|
stageId = StageId("stage-1"),
|
||||||
|
occurredAt = fixedClock(),
|
||||||
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals(null, state.sessions[0].currentStage)
|
assertEquals(null, state.sessions[0].currentStage)
|
||||||
assertEquals(1000L, state.sessions[0].lastEventAt)
|
assertEquals(1000L, state.sessions[0].lastEventAt)
|
||||||
@@ -148,6 +152,7 @@ class SessionsReducerTest {
|
|||||||
sessionId = SessionId("s1"),
|
sessionId = SessionId("s1"),
|
||||||
stageId = StageId("stage-1"),
|
stageId = StageId("stage-1"),
|
||||||
reason = "error",
|
reason = "error",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
)
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals(null, state.sessions[0].currentStage)
|
assertEquals(null, state.sessions[0].currentStage)
|
||||||
@@ -177,6 +182,7 @@ class SessionsReducerTest {
|
|||||||
val msg = ServerMessage.InferenceCompleted(
|
val msg = ServerMessage.InferenceCompleted(
|
||||||
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
||||||
outputSummary = "summary", responseText = "full response",
|
outputSummary = "summary", responseText = "full response",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
)
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("summary", state.sessions[0].lastOutput)
|
assertEquals("summary", state.sessions[0].lastOutput)
|
||||||
@@ -190,6 +196,7 @@ class SessionsReducerTest {
|
|||||||
val msg = ServerMessage.InferenceCompleted(
|
val msg = ServerMessage.InferenceCompleted(
|
||||||
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
sessionId = SessionId("s1"), stageId = StageId("stage-1"),
|
||||||
outputSummary = "", responseText = "",
|
outputSummary = "", responseText = "",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
)
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals(null, state.sessions[0].lastResponseText)
|
assertEquals(null, state.sessions[0].lastResponseText)
|
||||||
@@ -198,7 +205,11 @@ class SessionsReducerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ServerEventReceived StageStarted sets currentStage`() {
|
fun `ServerEventReceived StageStarted sets currentStage`() {
|
||||||
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
||||||
val msg = ServerMessage.StageStarted(sessionId = SessionId("s1"), stageId = StageId("stage-1"))
|
val msg = ServerMessage.StageStarted(
|
||||||
|
sessionId = SessionId("s1"),
|
||||||
|
stageId = StageId("stage-1"),
|
||||||
|
occurredAt = fixedClock(),
|
||||||
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("stage-1", state.sessions[0].currentStage)
|
assertEquals("stage-1", state.sessions[0].currentStage)
|
||||||
assertEquals(1000L, state.sessions[0].lastEventAt)
|
assertEquals(1000L, state.sessions[0].lastEventAt)
|
||||||
@@ -207,7 +218,12 @@ class SessionsReducerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `ServerEventReceived ToolCompleted updates lastOutput`() {
|
fun `ServerEventReceived ToolCompleted updates lastOutput`() {
|
||||||
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
val s = SessionsState(sessions = listOf(session("s1")), selectedId = "s1")
|
||||||
val msg = ServerMessage.ToolCompleted(sessionId = SessionId("s1"), toolName = "file_write", outputSummary = "wrote 3 lines")
|
val msg = ServerMessage.ToolCompleted(
|
||||||
|
sessionId = SessionId("s1"),
|
||||||
|
toolName = "file_write",
|
||||||
|
outputSummary = "wrote 3 lines",
|
||||||
|
occurredAt = fixedClock(),
|
||||||
|
)
|
||||||
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
val (state, _) = reduce(sessions = s, action = Action.ServerEventReceived(msg))
|
||||||
assertEquals("file_write: wrote 3 lines", state.sessions[0].lastOutput)
|
assertEquals("file_write: wrote 3 lines", state.sessions[0].lastOutput)
|
||||||
assertEquals(1000L, state.sessions[0].lastEventAt)
|
assertEquals(1000L, state.sessions[0].lastEventAt)
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ data class StoredEvent(
|
|||||||
val metadata: EventMetadata,
|
val metadata: EventMetadata,
|
||||||
val sequence: Long,
|
val sequence: Long,
|
||||||
val payload: EventPayload
|
val payload: EventPayload
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ data class EventMetadata(
|
|||||||
val schemaVersion: Int,
|
val schemaVersion: Int,
|
||||||
val causationId: CausationId?,
|
val causationId: CausationId?,
|
||||||
val correlationId: CorrelationId?
|
val correlationId: CorrelationId?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ package com.correx.core.events.events
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface EventPayload
|
sealed interface EventPayload
|
||||||
|
|||||||
@@ -60,4 +60,4 @@ data class ModelUnloadedEvent(
|
|||||||
val providerId: ProviderId,
|
val providerId: ProviderId,
|
||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
val cancellationReason: String? = null, // serialized label, not the sealed class
|
val cancellationReason: String? = null, // serialized label, not the sealed class
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class WorkflowStartedEvent(
|
data class WorkflowStartedEvent(
|
||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
|
val workflowId: String,
|
||||||
val startStageId: StageId,
|
val startStageId: StageId,
|
||||||
val retryPolicy: RetryPolicy? = null,
|
val retryPolicy: RetryPolicy? = null,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
@@ -17,6 +18,7 @@ data class WorkflowCompletedEvent(
|
|||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
val terminalStageId: StageId,
|
val terminalStageId: StageId,
|
||||||
val totalStages: Int,
|
val totalStages: Int,
|
||||||
|
val workflowId: String,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -47,4 +49,4 @@ data class RetryAttemptedEvent(
|
|||||||
val attemptNumber: Int,
|
val attemptNumber: Int,
|
||||||
val maxAttempts: Int,
|
val maxAttempts: Int,
|
||||||
val failureReason: String,
|
val failureReason: String,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ import com.correx.core.events.events.EventPayload
|
|||||||
interface EventSerializer {
|
interface EventSerializer {
|
||||||
fun serialize(payload: EventPayload): String
|
fun serialize(payload: EventPayload): String
|
||||||
fun deserialize(raw: String): EventPayload
|
fun deserialize(raw: String): EventPayload
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -12,4 +12,4 @@ class JsonEventSerializer(
|
|||||||
|
|
||||||
override fun deserialize(raw: String): EventPayload =
|
override fun deserialize(raw: String): EventPayload =
|
||||||
json.decodeFromString(EventPayload.serializer(), raw)
|
json.decodeFromString(EventPayload.serializer(), raw)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ import com.correx.core.events.events.SessionFailedEvent
|
|||||||
import com.correx.core.events.events.SessionPausedEvent
|
import com.correx.core.events.events.SessionPausedEvent
|
||||||
import com.correx.core.events.events.SessionResumedEvent
|
import com.correx.core.events.events.SessionResumedEvent
|
||||||
import com.correx.core.events.events.SessionStartedEvent
|
import com.correx.core.events.events.SessionStartedEvent
|
||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
import com.correx.core.events.events.StageStartedEvent
|
import com.correx.core.events.events.StageStartedEvent
|
||||||
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
import com.correx.core.events.events.ToolExecutionCompletedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionFailedEvent
|
import com.correx.core.events.events.ToolExecutionFailedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||||
@@ -106,4 +106,4 @@ val eventModule = SerializersModule {
|
|||||||
|
|
||||||
val eventJson = Json {
|
val eventJson = Json {
|
||||||
serializersModule = eventModule
|
serializersModule = eventModule
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ interface EventStore {
|
|||||||
* Intended for batch jobs (e.g. CAS liveness scans). Implementations should stream lazily.
|
* Intended for batch jobs (e.g. CAS liveness scans). Implementations should stream lazily.
|
||||||
*/
|
*/
|
||||||
fun allEvents(): Sequence<StoredEvent>
|
fun allEvents(): Sequence<StoredEvent>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,4 @@ typealias ToolInvocationId = TypeId
|
|||||||
|
|
||||||
// Inference
|
// Inference
|
||||||
typealias InferenceRequestId = TypeId
|
typealias InferenceRequestId = TypeId
|
||||||
typealias ProviderId = TypeId
|
typealias ProviderId = TypeId
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ data class TokenUsage(
|
|||||||
val completionTokens: Int,
|
val completionTokens: Int,
|
||||||
) {
|
) {
|
||||||
val totalTokens: Int get() = promptTokens + completionTokens
|
val totalTokens: Int get() = promptTokens + completionTokens
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,4 +11,4 @@ class DefaultStateBuilder<S>(
|
|||||||
projection.apply(state, event)
|
projection.apply(state, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ interface Projection<S> {
|
|||||||
fun initial(): S
|
fun initial(): S
|
||||||
|
|
||||||
fun apply(state: S, event: StoredEvent): S
|
fun apply(state: S, event: StoredEvent): S
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ import com.correx.core.events.events.StoredEvent
|
|||||||
|
|
||||||
interface StateBuilder<S> {
|
interface StateBuilder<S> {
|
||||||
fun build(events: List<StoredEvent>): S
|
fun build(events: List<StoredEvent>): S
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,4 +16,4 @@ class DefaultEventReplayer<S>(
|
|||||||
return DefaultStateBuilder(projection)
|
return DefaultStateBuilder(projection)
|
||||||
.build(events)
|
.build(events)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@ import com.correx.core.events.types.SessionId
|
|||||||
|
|
||||||
interface EventReplayer<S> {
|
interface EventReplayer<S> {
|
||||||
fun rebuild(sessionId: SessionId): S
|
fun rebuild(sessionId: SessionId): S
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ value class TypeId(val value: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = value
|
override fun toString(): String = value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,4 @@ class DefaultInferenceReducer : InferenceReducer {
|
|||||||
return state.copy(records = updated)
|
return state.copy(records = updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ sealed class FinishReason {
|
|||||||
object Cancelled : FinishReason()
|
object Cancelled : FinishReason()
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Error(val message: String) : FinishReason()
|
data class Error(val message: String) : FinishReason()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ data class GenerationConfig(
|
|||||||
val maxTokens: Int,
|
val maxTokens: Int,
|
||||||
val stopSequences: List<String> = emptyList(),
|
val stopSequences: List<String> = emptyList(),
|
||||||
val seed: Long? = null, // null = non-deterministic; set for replay
|
val seed: Long? = null, // null = non-deterministic; set for replay
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -28,4 +28,4 @@ package com.correx.core.inference
|
|||||||
interface InferenceCancellationToken {
|
interface InferenceCancellationToken {
|
||||||
val isCancelled: Boolean
|
val isCancelled: Boolean
|
||||||
fun cancel(reason: CancellationReason)
|
fun cancel(reason: CancellationReason)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ class InferenceProjector(
|
|||||||
state: InferenceState,
|
state: InferenceState,
|
||||||
event: StoredEvent
|
event: StoredEvent
|
||||||
): InferenceState = reducer.reduce(state, event)
|
): InferenceState = reducer.reduce(state, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ interface ProviderRegistry {
|
|||||||
fun resolve(capability: ModelCapability): List<InferenceProvider> // ordered by score desc
|
fun resolve(capability: ModelCapability): List<InferenceProvider> // ordered by score desc
|
||||||
fun listAll(): List<InferenceProvider>
|
fun listAll(): List<InferenceProvider>
|
||||||
suspend fun healthCheckAll(): Map<ProviderId, ProviderHealth>
|
suspend fun healthCheckAll(): Map<ProviderId, ProviderHealth>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ data class InferenceRecord(
|
|||||||
val tokensUsed: TokenUsage? = null,
|
val tokensUsed: TokenUsage? = null,
|
||||||
val latencyMs: Long? = null,
|
val latencyMs: Long? = null,
|
||||||
val failureReason: String? = null,
|
val failureReason: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ import com.correx.core.events.events.StoredEvent
|
|||||||
|
|
||||||
interface InferenceReducer {
|
interface InferenceReducer {
|
||||||
fun reduce(state: InferenceState, event: StoredEvent): InferenceState
|
fun reduce(state: InferenceState, event: StoredEvent): InferenceState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ class InferenceRepository(
|
|||||||
fun getInferenceState(sessionId: SessionId): InferenceState {
|
fun getInferenceState(sessionId: SessionId): InferenceState {
|
||||||
return replayer.rebuild(sessionId)
|
return replayer.rebuild(sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ data class InferenceRequest(
|
|||||||
val timeout: InferenceTimeout? = null,
|
val timeout: InferenceTimeout? = null,
|
||||||
val responseFormat: ResponseFormat = ResponseFormat.Text,
|
val responseFormat: ResponseFormat = ResponseFormat.Text,
|
||||||
val tools: List<ToolDefinition> = emptyList(),
|
val tools: List<ToolDefinition> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ data class InferenceResponse(
|
|||||||
val tokensUsed: TokenUsage,
|
val tokensUsed: TokenUsage,
|
||||||
val latencyMs: Long,
|
val latencyMs: Long,
|
||||||
val toolCalls: List<ToolCallRequest> = emptyList(),
|
val toolCalls: List<ToolCallRequest> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,4 +41,4 @@ class ProviderUnavailableException(
|
|||||||
reason: String,
|
reason: String,
|
||||||
) : Exception(
|
) : Exception(
|
||||||
"Provider '${providerId.value}' is unavailable: $reason",
|
"Provider '${providerId.value}' is unavailable: $reason",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class InferenceState(
|
data class InferenceState(
|
||||||
val records: List<InferenceRecord> = emptyList()
|
val records: List<InferenceRecord> = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ package com.correx.core.inference
|
|||||||
|
|
||||||
enum class InferenceStatus {
|
enum class InferenceStatus {
|
||||||
STARTED, COMPLETED, FAILED, TIMED_OUT
|
STARTED, COMPLETED, FAILED, TIMED_OUT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ sealed class ProviderHealth {
|
|||||||
object Healthy : ProviderHealth()
|
object Healthy : ProviderHealth()
|
||||||
data class Degraded(val reason: String) : ProviderHealth()
|
data class Degraded(val reason: String) : ProviderHealth()
|
||||||
data class Unavailable(val reason: String) : ProviderHealth()
|
data class Unavailable(val reason: String) : ProviderHealth()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ value class Token(val id: Int)
|
|||||||
interface Tokenizer {
|
interface Tokenizer {
|
||||||
suspend fun tokenize(text: String): List<Token>
|
suspend fun tokenize(text: String): List<Token>
|
||||||
suspend fun countTokens(text: String): Int
|
suspend fun countTokens(text: String): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ data class ToolCallRequest(
|
|||||||
data class ToolCallFunction(
|
data class ToolCallFunction(
|
||||||
val name: String,
|
val name: String,
|
||||||
val arguments: String,
|
val arguments: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ sealed interface ReplayStrategy {
|
|||||||
data object Full : ReplayStrategy
|
data object Full : ReplayStrategy
|
||||||
data object SkipInference : ReplayStrategy // use recorded InferenceCompletedEvent artifacts
|
data object SkipInference : ReplayStrategy // use recorded InferenceCompletedEvent artifacts
|
||||||
data object SkipValidation : ReplayStrategy // trust recorded outcomes
|
data object SkipValidation : ReplayStrategy // trust recorded outcomes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ sealed interface StageOutcome {
|
|||||||
data class InferenceFailure(val reason: String, val retryable: Boolean) : StageOutcome
|
data class InferenceFailure(val reason: String, val retryable: Boolean) : StageOutcome
|
||||||
data class ApprovalRequired(val request: DomainApprovalRequest) : StageOutcome
|
data class ApprovalRequired(val request: DomainApprovalRequest) : StageOutcome
|
||||||
data object Cancelled : StageOutcome
|
data object Cancelled : StageOutcome
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ sealed interface WorkflowResult {
|
|||||||
data class Completed(val sessionId: SessionId, val terminalStageId: StageId) : 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 Failed(val sessionId: SessionId, val reason: String, val retryExhausted: Boolean) : WorkflowResult
|
||||||
data class Cancelled(val sessionId: SessionId) : WorkflowResult
|
data class Cancelled(val sessionId: SessionId) : WorkflowResult
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -46,4 +46,4 @@ class DefaultOrchestrationReducer : OrchestrationReducer {
|
|||||||
|
|
||||||
else -> state
|
else -> state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -1,14 +1,13 @@
|
|||||||
package com.correx.core.kernel.orchestration
|
package com.correx.core.kernel.orchestration
|
||||||
|
|
||||||
import com.correx.core.approvals.model.ApprovalDecision
|
import com.correx.core.approvals.model.ApprovalDecision
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
|
||||||
import com.correx.core.artifacts.ArtifactState
|
import com.correx.core.artifacts.ArtifactState
|
||||||
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||||
import com.correx.core.events.types.ArtifactId
|
|
||||||
import com.correx.core.events.types.ArtifactLifecyclePhase
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import com.correx.core.events.orchestration.OrchestrationState
|
import com.correx.core.events.orchestration.OrchestrationState
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
|
import com.correx.core.events.types.ArtifactId
|
||||||
|
import com.correx.core.events.types.ArtifactLifecyclePhase
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
import com.correx.core.kernel.execution.WorkflowResult
|
||||||
@@ -17,6 +16,7 @@ import com.correx.core.sessions.Session
|
|||||||
import com.correx.core.transitions.execution.StageExecutionResult
|
import com.correx.core.transitions.execution.StageExecutionResult
|
||||||
import com.correx.core.transitions.graph.WorkflowGraph
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import com.correx.core.transitions.resolution.TransitionDecision
|
import com.correx.core.transitions.resolution.TransitionDecision
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
import java.util.concurrent.atomic.*
|
import java.util.concurrent.atomic.*
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class DefaultSessionOrchestrator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is TransitionDecision.Stay -> if (isTerminal(enriched.graph, enriched.currentStageId)) {
|
is TransitionDecision.Stay -> if (isTerminal(enriched.graph, enriched.currentStageId)) {
|
||||||
completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount)
|
completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount, enriched.graph.id)
|
||||||
} else {
|
} else {
|
||||||
failWorkflow(
|
failWorkflow(
|
||||||
sessionId = enriched.sessionId,
|
sessionId = enriched.sessionId,
|
||||||
@@ -140,7 +140,7 @@ class DefaultSessionOrchestrator(
|
|||||||
if (isTerminal(ctx.graph, nextStageId)) {
|
if (isTerminal(ctx.graph, nextStageId)) {
|
||||||
// Terminal stage is a sentinel — not executed, so don't count it.
|
// Terminal stage is a sentinel — not executed, so don't count it.
|
||||||
return StepResult.Terminal(
|
return StepResult.Terminal(
|
||||||
completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount),
|
completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount, ctx.graph.id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +150,7 @@ class DefaultSessionOrchestrator(
|
|||||||
currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision),
|
currentStageId = advanceStage(ctx.sessionId, ctx.currentStageId, decision),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
is StepResult.Terminal -> result
|
is StepResult.Terminal -> result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +172,7 @@ class DefaultSessionOrchestrator(
|
|||||||
is StageExecutionResult.Failure -> {
|
is StageExecutionResult.Failure -> {
|
||||||
log.debug(
|
log.debug(
|
||||||
"[Orchestrator] stage failed session=${ctx.sessionId.value} " +
|
"[Orchestrator] stage failed session=${ctx.sessionId.value} " +
|
||||||
"stage=${stageId.value} reason=${result.reason} retryable=${result.retryable}",
|
"stage=${stageId.value} reason=${result.reason} retryable=${result.retryable}",
|
||||||
)
|
)
|
||||||
val refreshedState = orchestrationRepository.getState(ctx.sessionId)
|
val refreshedState = orchestrationRepository.getState(ctx.sessionId)
|
||||||
val shouldRetry = retryCoordinator.shouldRetry(
|
val shouldRetry = retryCoordinator.shouldRetry(
|
||||||
|
|||||||
+1
-1
@@ -10,4 +10,4 @@ data class OrchestrationConfig(
|
|||||||
val stageTimeoutMs: Long = 60_000L,
|
val stageTimeoutMs: Long = 60_000L,
|
||||||
val sandboxRoot: Path = Path.of(System.getProperty("java.io.tmpdir"), "correx-sandbox"),
|
val sandboxRoot: Path = Path.of(System.getProperty("java.io.tmpdir"), "correx-sandbox"),
|
||||||
val defaultSystemPromptPath: String? = null,
|
val defaultSystemPromptPath: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -13,4 +13,4 @@ class OrchestrationProjector(
|
|||||||
state: OrchestrationState,
|
state: OrchestrationState,
|
||||||
event: StoredEvent,
|
event: StoredEvent,
|
||||||
): OrchestrationState = orchestrationReducer.reduce(state, event)
|
): OrchestrationState = orchestrationReducer.reduce(state, event)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -5,4 +5,4 @@ import com.correx.core.events.orchestration.OrchestrationState
|
|||||||
|
|
||||||
interface OrchestrationReducer {
|
interface OrchestrationReducer {
|
||||||
fun reduce(state: OrchestrationState, event: StoredEvent): OrchestrationState
|
fun reduce(state: OrchestrationState, event: StoredEvent): OrchestrationState
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,4 +8,4 @@ class OrchestrationRepository(
|
|||||||
private val replayer: EventReplayer<OrchestrationState>,
|
private val replayer: EventReplayer<OrchestrationState>,
|
||||||
) {
|
) {
|
||||||
fun getState(sessionId: SessionId): OrchestrationState = replayer.rebuild(sessionId)
|
fun getState(sessionId: SessionId): OrchestrationState = replayer.rebuild(sessionId)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,6 @@ package com.correx.core.kernel.orchestration
|
|||||||
|
|
||||||
import com.correx.core.approvals.domain.NoOpApprovalEngine
|
import com.correx.core.approvals.domain.NoOpApprovalEngine
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import com.correx.core.context.model.ContextPack
|
import com.correx.core.context.model.ContextPack
|
||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||||
@@ -12,17 +11,18 @@ import com.correx.core.events.types.StageId
|
|||||||
import com.correx.core.inference.GenerationConfig
|
import com.correx.core.inference.GenerationConfig
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
import com.correx.core.inference.ResponseFormat
|
import com.correx.core.inference.ResponseFormat
|
||||||
import com.correx.core.sessions.Session
|
|
||||||
import com.correx.core.kernel.execution.ReplayStrategy
|
import com.correx.core.kernel.execution.ReplayStrategy
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
import com.correx.core.kernel.execution.WorkflowResult
|
||||||
import com.correx.core.kernel.replay.ReplayInferenceProvider
|
import com.correx.core.kernel.replay.ReplayInferenceProvider
|
||||||
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
|
import com.correx.core.kernel.retry.exception.ReplayArtifactMissingException
|
||||||
import com.correx.core.risk.NoOpRiskAssessor
|
import com.correx.core.risk.NoOpRiskAssessor
|
||||||
|
import com.correx.core.sessions.Session
|
||||||
import com.correx.core.transitions.execution.StageExecutionResult
|
import com.correx.core.transitions.execution.StageExecutionResult
|
||||||
import com.correx.core.transitions.graph.StageConfig
|
import com.correx.core.transitions.graph.StageConfig
|
||||||
import com.correx.core.transitions.graph.WorkflowGraph
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import com.correx.core.transitions.resolution.TransitionDecision
|
import com.correx.core.transitions.resolution.TransitionDecision
|
||||||
import com.correx.core.validation.model.ValidationContext
|
import com.correx.core.validation.model.ValidationContext
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
import java.util.concurrent.atomic.*
|
import java.util.concurrent.atomic.*
|
||||||
@@ -87,7 +87,7 @@ class ReplayOrchestrator(
|
|||||||
val nextStageId = decision.to
|
val nextStageId = decision.to
|
||||||
|
|
||||||
if (isTerminal(ctx.graph, nextStageId)) {
|
if (isTerminal(ctx.graph, nextStageId)) {
|
||||||
return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount)
|
return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount, ctx.graph.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val result = enterStage(ctx, nextStageId, session)) {
|
when (val result = enterStage(ctx, nextStageId, session)) {
|
||||||
@@ -101,7 +101,7 @@ class ReplayOrchestrator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is TransitionDecision.Stay -> if (isTerminal(ctx.graph, ctx.currentStageId)) {
|
is TransitionDecision.Stay -> if (isTerminal(ctx.graph, ctx.currentStageId)) {
|
||||||
completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount)
|
completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount, ctx.graph.id)
|
||||||
} else {
|
} else {
|
||||||
step(ctx)
|
step(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -2,22 +2,24 @@ package com.correx.core.kernel.orchestration
|
|||||||
|
|
||||||
import com.correx.core.approvals.ApprovalOutcome
|
import com.correx.core.approvals.ApprovalOutcome
|
||||||
import com.correx.core.approvals.ApprovalStatus
|
import com.correx.core.approvals.ApprovalStatus
|
||||||
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.model.ApprovalDecision
|
import com.correx.core.approvals.model.ApprovalDecision
|
||||||
import com.correx.core.approvals.model.DomainApprovalRequest
|
import com.correx.core.approvals.model.DomainApprovalRequest
|
||||||
|
import com.correx.core.artifacts.ArtifactState
|
||||||
|
import com.correx.core.artifacts.kind.JsonSchema
|
||||||
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.context.builder.ContextPackBuilder
|
import com.correx.core.context.builder.ContextPackBuilder
|
||||||
import com.correx.core.context.model.ContextEntry
|
import com.correx.core.context.model.ContextEntry
|
||||||
import com.correx.core.context.model.ContextLayer
|
import com.correx.core.context.model.ContextLayer
|
||||||
import com.correx.core.context.model.ContextPack
|
import com.correx.core.context.model.ContextPack
|
||||||
import com.correx.core.context.model.TokenBudget
|
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.ArtifactCreatedEvent
|
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||||
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.EventMetadata
|
||||||
import com.correx.core.events.events.EventPayload
|
import com.correx.core.events.events.EventPayload
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
|
||||||
import com.correx.core.events.events.InferenceCompletedEvent
|
import com.correx.core.events.events.InferenceCompletedEvent
|
||||||
import com.correx.core.events.events.InferenceFailedEvent
|
import com.correx.core.events.events.InferenceFailedEvent
|
||||||
import com.correx.core.events.events.InferenceStartedEvent
|
import com.correx.core.events.events.InferenceStartedEvent
|
||||||
@@ -26,27 +28,25 @@ import com.correx.core.events.events.NewEvent
|
|||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.RiskAssessedEvent
|
import com.correx.core.events.events.RiskAssessedEvent
|
||||||
|
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||||
|
import com.correx.core.events.events.ToolRequest
|
||||||
import com.correx.core.events.events.TransitionExecutedEvent
|
import com.correx.core.events.events.TransitionExecutedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.artifacts.ArtifactState
|
|
||||||
import com.correx.core.artifacts.kind.JsonSchema
|
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.events.types.ApprovalDecisionId
|
import com.correx.core.events.types.ApprovalDecisionId
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.ArtifactId
|
import com.correx.core.events.types.ArtifactId
|
||||||
|
import com.correx.core.events.types.ContextEntryId
|
||||||
import com.correx.core.events.types.ContextPackId
|
import com.correx.core.events.types.ContextPackId
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
import com.correx.core.events.types.RiskSummaryId
|
import com.correx.core.events.types.RiskSummaryId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.ValidationReportId
|
|
||||||
import com.correx.core.approvals.Tier
|
|
||||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
|
||||||
import com.correx.core.events.events.ToolRequest
|
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
|
import com.correx.core.events.types.ValidationReportId
|
||||||
import com.correx.core.inference.FinishReason
|
import com.correx.core.inference.FinishReason
|
||||||
import com.correx.core.inference.InferenceRepository
|
import com.correx.core.inference.InferenceRepository
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
@@ -56,14 +56,14 @@ import com.correx.core.inference.ResponseFormat
|
|||||||
import com.correx.core.inference.ToolCallRequest
|
import com.correx.core.inference.ToolCallRequest
|
||||||
import com.correx.core.inference.ToolDefinition
|
import com.correx.core.inference.ToolDefinition
|
||||||
import com.correx.core.inference.ToolFunction
|
import com.correx.core.inference.ToolFunction
|
||||||
import com.correx.core.tools.contract.ToolExecutor
|
|
||||||
import com.correx.core.tools.contract.ToolResult
|
|
||||||
import com.correx.core.tools.registry.ToolRegistry
|
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
import com.correx.core.kernel.execution.WorkflowResult
|
||||||
import com.correx.core.risk.RiskAssessor
|
import com.correx.core.risk.RiskAssessor
|
||||||
import com.correx.core.risk.RiskContext
|
import com.correx.core.risk.RiskContext
|
||||||
import com.correx.core.risk.toApprovalTier
|
import com.correx.core.risk.toApprovalTier
|
||||||
import com.correx.core.sessions.Session
|
import com.correx.core.sessions.Session
|
||||||
|
import com.correx.core.tools.contract.ToolExecutor
|
||||||
|
import com.correx.core.tools.contract.ToolResult
|
||||||
|
import com.correx.core.tools.registry.ToolRegistry
|
||||||
import com.correx.core.transitions.evaluation.EvaluationContext
|
import com.correx.core.transitions.evaluation.EvaluationContext
|
||||||
import com.correx.core.transitions.evaluation.PromptResolver
|
import com.correx.core.transitions.evaluation.PromptResolver
|
||||||
import com.correx.core.transitions.execution.StageExecutionResult
|
import com.correx.core.transitions.execution.StageExecutionResult
|
||||||
@@ -587,6 +587,7 @@ abstract class SessionOrchestrator(
|
|||||||
sessionId,
|
sessionId,
|
||||||
WorkflowStartedEvent(
|
WorkflowStartedEvent(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
workflowId = graph.id,
|
||||||
startStageId = graph.start,
|
startStageId = graph.start,
|
||||||
retryPolicy = config.retryPolicy,
|
retryPolicy = config.retryPolicy,
|
||||||
),
|
),
|
||||||
@@ -597,12 +598,13 @@ abstract class SessionOrchestrator(
|
|||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
terminalStageId: StageId,
|
terminalStageId: StageId,
|
||||||
stageCount: Int,
|
stageCount: Int,
|
||||||
|
workflowId: String,
|
||||||
): WorkflowResult.Completed {
|
): WorkflowResult.Completed {
|
||||||
log.debug(
|
log.debug(
|
||||||
"[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
|
"[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
|
||||||
sessionId.value, terminalStageId.value, stageCount,
|
sessionId.value, terminalStageId.value, stageCount,
|
||||||
)
|
)
|
||||||
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount))
|
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount, workflowId))
|
||||||
cancellations.remove(sessionId)
|
cancellations.remove(sessionId)
|
||||||
return WorkflowResult.Completed(sessionId, terminalStageId)
|
return WorkflowResult.Completed(sessionId, terminalStageId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import com.correx.core.router.model.RouterConfig
|
|||||||
import com.correx.core.router.model.RouterL2Entry
|
import com.correx.core.router.model.RouterL2Entry
|
||||||
import com.correx.core.router.model.RouterState
|
import com.correx.core.router.model.RouterState
|
||||||
import com.correx.core.router.model.RouterTurn
|
import com.correx.core.router.model.RouterTurn
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
interface RouterContextBuilder {
|
interface RouterContextBuilder {
|
||||||
fun build(state: RouterState, budget: TokenBudget): ContextPack
|
fun build(state: RouterState, budget: TokenBudget): ContextPack
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import com.correx.core.events.events.NewEvent
|
|||||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.inference.GenerationConfig
|
import com.correx.core.inference.GenerationConfig
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
|
||||||
import com.correx.core.inference.InferenceRouter
|
import com.correx.core.inference.InferenceRouter
|
||||||
import com.correx.core.inference.ModelCapability
|
import com.correx.core.inference.ModelCapability
|
||||||
import com.correx.core.inference.ResponseFormat
|
import com.correx.core.inference.ResponseFormat
|
||||||
@@ -18,8 +18,8 @@ import com.correx.core.router.model.RouterResponse
|
|||||||
import com.correx.core.router.model.RouterTurn
|
import com.correx.core.router.model.RouterTurn
|
||||||
import com.correx.core.router.model.TurnRole
|
import com.correx.core.router.model.TurnRole
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.*
|
||||||
|
|
||||||
interface RouterFacade {
|
interface RouterFacade {
|
||||||
suspend fun onUserInput(
|
suspend fun onUserInput(
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package com.correx.core.router
|
|||||||
|
|
||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.StoredEvent
|
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.StageFailedEvent
|
import com.correx.core.events.events.StageFailedEvent
|
||||||
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ enum class ApprovalMode {
|
|||||||
PROMPT,
|
PROMPT,
|
||||||
AUTO,
|
AUTO,
|
||||||
YOLO
|
YOLO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,4 @@ class DefaultSessionReducer : SessionReducer {
|
|||||||
updatedAt = event.metadata.timestamp
|
updatedAt = event.metadata.timestamp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ class DefaultSessionRepository(
|
|||||||
sessionId,
|
sessionId,
|
||||||
replayer.rebuild(sessionId),
|
replayer.rebuild(sessionId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ class SessionCounterProjection(
|
|||||||
): SessionCounterState {
|
): SessionCounterState {
|
||||||
return state.copy(count = state.count + 1)
|
return state.copy(count = state.count + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ package com.correx.core.sessions
|
|||||||
data class SessionCounterState(
|
data class SessionCounterState(
|
||||||
val sessionId: String,
|
val sessionId: String,
|
||||||
val count: Int
|
val count: Int
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ class SessionProjector(
|
|||||||
state: SessionState,
|
state: SessionState,
|
||||||
event: StoredEvent
|
event: StoredEvent
|
||||||
): SessionState = reducer.reduce(state, event)
|
): SessionState = reducer.reduce(state, event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ interface SessionReducer {
|
|||||||
state: SessionState,
|
state: SessionState,
|
||||||
event: StoredEvent
|
event: StoredEvent
|
||||||
): SessionState
|
): SessionState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ enum class SessionStatus {
|
|||||||
COMPLETED,
|
COMPLETED,
|
||||||
FAILED,
|
FAILED,
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ package com.correx.core.sessions
|
|||||||
sealed interface TransitionResult {
|
sealed interface TransitionResult {
|
||||||
data class Applied(val newState: SessionStatus) : TransitionResult
|
data class Applied(val newState: SessionStatus) : TransitionResult
|
||||||
data object Rejected : TransitionResult
|
data object Rejected : TransitionResult
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,4 +2,4 @@ package com.correx.core.sessions.projections
|
|||||||
|
|
||||||
import com.correx.testing.contracts.fixtures.projections.CountingProjectionContractTest
|
import com.correx.testing.contracts.fixtures.projections.CountingProjectionContractTest
|
||||||
|
|
||||||
class SessionCounterProjectionTest : CountingProjectionContractTest()
|
class SessionCounterProjectionTest : CountingProjectionContractTest()
|
||||||
|
|||||||
+1
-1
@@ -31,4 +31,4 @@ internal class CycleExtractor(
|
|||||||
|
|
||||||
return canonicalize(cycles)
|
return canonicalize(cycles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ import com.correx.core.transitions.graph.TransitionEdge
|
|||||||
data class DetectedCycle(
|
data class DetectedCycle(
|
||||||
val nodes: List<StageId>,
|
val nodes: List<StageId>,
|
||||||
val edges: List<TransitionEdge>
|
val edges: List<TransitionEdge>
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -18,4 +18,4 @@ internal class DeterministicAdjacencyBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -33,4 +33,4 @@ internal object CycleCanonicalizer {
|
|||||||
}
|
}
|
||||||
.sortedBy { it.nodes.first().value }
|
.sortedBy { it.nodes.first().value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,4 +10,4 @@ data class EvaluationContext(
|
|||||||
val currentStage: StageId,
|
val currentStage: StageId,
|
||||||
val artifacts: Map<ArtifactId, ArtifactState> = emptyMap(),
|
val artifacts: Map<ArtifactId, ArtifactState> = emptyMap(),
|
||||||
val variables: Map<String, String> = emptyMap(),
|
val variables: Map<String, String> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -7,4 +7,4 @@ fun interface TransitionConditionEvaluator {
|
|||||||
condition: TransitionCondition,
|
condition: TransitionCondition,
|
||||||
context: EvaluationContext
|
context: EvaluationContext
|
||||||
): Boolean
|
): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,4 +9,4 @@ sealed interface StageExecutionResult {
|
|||||||
val reason: String,
|
val reason: String,
|
||||||
val retryable: Boolean,
|
val retryable: Boolean,
|
||||||
) : StageExecutionResult
|
) : StageExecutionResult
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@ interface StageExecutor {
|
|||||||
fun execute(
|
fun execute(
|
||||||
request: StageExecutionRequest
|
request: StageExecutionRequest
|
||||||
): StageExecutionResult
|
): StageExecutionResult
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ internal fun WorkflowGraph.sortedTransitions(): List<TransitionEdge> =
|
|||||||
{ it.id.value },
|
{ it.id.value },
|
||||||
{ it.to.value }
|
{ it.to.value }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -4,4 +4,4 @@ import com.correx.core.transitions.evaluation.EvaluationContext
|
|||||||
|
|
||||||
fun interface TransitionCondition {
|
fun interface TransitionCondition {
|
||||||
fun evaluate(context: EvaluationContext): Boolean
|
fun evaluate(context: EvaluationContext): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,4 +8,4 @@ data class TransitionEdge(
|
|||||||
val from: StageId,
|
val from: StageId,
|
||||||
val to: StageId,
|
val to: StageId,
|
||||||
val condition: TransitionCondition
|
val condition: TransitionCondition
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ data class WorkflowGraph(
|
|||||||
) {
|
) {
|
||||||
val stageIds: Set<StageId> get() = stages.keys
|
val stageIds: Set<StageId> get() = stages.keys
|
||||||
init {
|
init {
|
||||||
|
require(id.isNotBlank()) { "workflow id must not be blank" }
|
||||||
require(start in stages) { "start stage must exist in stages" }
|
require(start in stages) { "start stage must exist in stages" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,4 +44,4 @@ class DefaultStageExecutionEventMapper : StageExecutionEventMapper {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,4 +9,4 @@ interface StageExecutionEventMapper {
|
|||||||
request: StageExecutionRequest,
|
request: StageExecutionRequest,
|
||||||
result: StageExecutionResult
|
result: StageExecutionResult
|
||||||
): List<EventPayload>
|
): List<EventPayload>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ sealed interface CyclePolicy {
|
|||||||
data class Approval(
|
data class Approval(
|
||||||
val timeoutMs: Long
|
val timeoutMs: Long
|
||||||
) : CyclePolicy
|
) : CyclePolicy
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ package com.correx.core.transitions.policy
|
|||||||
data class CyclePolicyBinding(
|
data class CyclePolicyBinding(
|
||||||
val cycle: CycleSignature,
|
val cycle: CycleSignature,
|
||||||
val policy: CyclePolicy
|
val policy: CyclePolicy
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -9,4 +9,4 @@ class CyclePolicyResolver(
|
|||||||
.firstOrNull { it.cycle == signature }
|
.firstOrNull { it.cycle == signature }
|
||||||
?.policy
|
?.policy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -20,4 +20,4 @@ class PolicyValidation {
|
|||||||
|
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user