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

- fixed the tool overlay in tui.
- fixed tool calling - added schemas.
- getting all sessionIds via event store.
- instead of sending all events on the replay - there's a new way for replaying.
- session snapshot is now a thing getting sent for previously created sessions.
- added logging to important parts.
- revert workflowId from WorkflowCompletedEvent.
This commit is contained in:
2026-05-24 18:50:00 +04:00
parent f827685ed0
commit fc7b879891
43 changed files with 525 additions and 211 deletions
@@ -26,7 +26,9 @@ class DefaultApprovalReducer : ApprovalReducer {
timestamp = event.metadata.timestamp,
causationId = event.metadata.causationId,
correlationId = event.metadata.correlationId,
userSteering = payload.userSteering
userSteering = payload.userSteering,
toolName = payload.toolName,
preview = payload.preview,
)
state.copy(requests = state.requests + (request.id to request))
}
@@ -39,11 +41,11 @@ class DefaultApprovalReducer : ApprovalReducer {
identity = ApprovalScopeIdentity(
sessionId = event.metadata.sessionId,
stageId = null,
projectId = null
projectId = null,
),
mode = ApprovalMode.PROMPT,
causationId = event.metadata.causationId,
correlationId = event.metadata.correlationId
correlationId = event.metadata.correlationId,
)
val decision = ApprovalDecision(
id = decisionId,
@@ -54,7 +56,7 @@ class DefaultApprovalReducer : ApprovalReducer {
contextSnapshot = contextSnapshot,
resolutionTimestamp = payload.resolutionTimestamp,
reason = payload.reason,
userSteering = payload.userSteering
userSteering = payload.userSteering,
)
state.copy(decisions = state.decisions + (decisionId to decision))
}
@@ -66,7 +68,7 @@ class DefaultApprovalReducer : ApprovalReducer {
permittedTiers = payload.permittedTiers,
reason = payload.reason,
timestamp = event.metadata.timestamp,
expiresAt = payload.expiresAt
expiresAt = payload.expiresAt,
)
state.copy(grants = state.grants + (grant.id to grant))
}
@@ -19,5 +19,7 @@ data class DomainApprovalRequest(
val timestamp: Instant,
val causationId: CausationId? = null,
val correlationId: CorrelationId? = null,
val userSteering: UserSteering? = null
val userSteering: UserSteering? = null,
val toolName: String? = null,
val preview: String? = null,
)
@@ -25,7 +25,9 @@ data class ApprovalRequestedEvent(
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?,
val userSteering: UserSteering? = null
val userSteering: UserSteering? = null,
val toolName: String? = null,
val preview: String? = null,
) : EventPayload
@Serializable
@@ -37,7 +39,7 @@ data class ApprovalDecisionResolvedEvent(
val tier: Tier,
val resolutionTimestamp: Instant,
val reason: String?,
val userSteering: UserSteering? = null
val userSteering: UserSteering? = null,
) : EventPayload
@Serializable
@@ -49,10 +51,10 @@ data class ApprovalGrantCreatedEvent(
val expiresAt: Instant?,
val sessionId: SessionId,
val stageId: StageId?,
val projectId: ProjectId?
val projectId: ProjectId?,
) : EventPayload
@Serializable
data class ApprovalGrantExpiredEvent(
val grantId: GrantId
val grantId: GrantId,
) : EventPayload
@@ -18,7 +18,6 @@ data class WorkflowCompletedEvent(
val sessionId: SessionId,
val terminalStageId: StageId,
val totalStages: Int,
val workflowId: String,
) : EventPayload
@Serializable
@@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class OrchestrationState(
val workflowId: String = "",
val currentStageId: StageId? = null,
val status: OrchestrationStatus = OrchestrationStatus.IDLE,
val retryCount: Int = 0,
@@ -47,4 +47,10 @@ interface EventStore {
* Intended for batch jobs (e.g. CAS liveness scans). Implementations should stream lazily.
*/
fun allEvents(): Sequence<StoredEvent>
/**
* Return all known session ids in the store.
* Implementations should derive this from the session index, not by scanning allEvents().
*/
fun allSessionIds(): Set<SessionId>
}
@@ -16,16 +16,22 @@ class DefaultOrchestrationReducer : OrchestrationReducer {
event: StoredEvent,
): OrchestrationState = when (val p = event.payload) {
is WorkflowStartedEvent -> state.copy(
workflowId = p.workflowId,
status = OrchestrationStatus.RUNNING,
currentStageId = p.startStageId,
retryPolicy = p.retryPolicy,
)
is WorkflowCompletedEvent -> state.copy(
status = OrchestrationStatus.COMPLETED,
currentStageId = p.terminalStageId,
)
is WorkflowFailedEvent -> state.copy(status = OrchestrationStatus.FAILED, failureReason = p.reason)
is WorkflowFailedEvent -> state.copy(
status = OrchestrationStatus.FAILED,
failureReason = p.reason,
)
is OrchestrationPausedEvent -> state.copy(
status = OrchestrationStatus.PAUSED,
pendingApproval = true,
@@ -94,7 +94,7 @@ class DefaultSessionOrchestrator(
}
is TransitionDecision.Stay -> if (isTerminal(enriched.graph, enriched.currentStageId)) {
completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount, enriched.graph.id)
completeWorkflow(enriched.sessionId, enriched.currentStageId, enriched.stageCount)
} else {
failWorkflow(
sessionId = enriched.sessionId,
@@ -140,7 +140,7 @@ class DefaultSessionOrchestrator(
if (isTerminal(ctx.graph, nextStageId)) {
// Terminal stage is a sentinel — not executed, so don't count it.
return StepResult.Terminal(
completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount, ctx.graph.id),
completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount),
)
}
@@ -1,5 +1,6 @@
package com.correx.core.kernel.orchestration
import com.correx.core.approvals.DefaultApprovalRepository
import com.correx.core.artifacts.repository.ArtifactRepository
import com.correx.core.events.stores.EventStore
import com.correx.core.inference.InferenceRepository
@@ -11,4 +12,5 @@ data class OrchestratorRepositories(
val orchestrationRepository: OrchestrationRepository,
val sessionRepository: DefaultSessionRepository,
val artifactRepository: ArtifactRepository,
val approvalRepository: DefaultApprovalRepository,
)
@@ -87,7 +87,7 @@ class ReplayOrchestrator(
val nextStageId = decision.to
if (isTerminal(ctx.graph, nextStageId)) {
return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount, ctx.graph.id)
return completeWorkflow(ctx.sessionId, nextStageId, ctx.stageCount)
}
when (val result = enterStage(ctx, nextStageId, session)) {
@@ -101,7 +101,7 @@ class ReplayOrchestrator(
}
is TransitionDecision.Stay -> if (isTerminal(ctx.graph, ctx.currentStageId)) {
completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount, ctx.graph.id)
completeWorkflow(ctx.sessionId, ctx.currentStageId, ctx.stageCount)
} else {
step(ctx)
}
@@ -112,7 +112,6 @@ abstract class SessionOrchestrator(
private val toolExecutor: ToolExecutor? = engines.toolExecutor
private val toolRegistry: ToolRegistry? = engines.toolRegistry
private val inferenceRepository: InferenceRepository = repositories.inferenceRepository
private val artifactRepository = repositories.artifactRepository
internal val orchestrationRepository: OrchestrationRepository = repositories.orchestrationRepository
internal abstract val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean>
internal val pendingApprovals: ConcurrentHashMap<ApprovalRequestId, CompletableDeferred<ApprovalDecision>> =
@@ -145,7 +144,7 @@ abstract class SessionOrchestrator(
.onFailure {
log.error(
"[SessionOrchestrator] stage=${stageId.value}: " +
"failed to load prompt '$path': ${it.message}",
"failed to load prompt '$path': ${it.message}",
)
}
.getOrNull()
@@ -168,7 +167,7 @@ abstract class SessionOrchestrator(
.onFailure {
log.error(
"[SessionOrchestrator] stage=${stageId.value}: " +
"failed to load default system prompt '$path': ${it.message}",
"failed to load default system prompt '$path': ${it.message}",
)
}
.getOrNull()
@@ -192,7 +191,7 @@ abstract class SessionOrchestrator(
.onFailure {
log.error(
"[SessionOrchestrator] stage=${stageId.value}: " +
"failed to load prompt '$path': ${it.message}",
"failed to load prompt '$path': ${it.message}",
)
}
.getOrNull()
@@ -214,7 +213,7 @@ abstract class SessionOrchestrator(
val llmEmittedSlots = stageConfig.produces.filter { it.kind.llmEmitted }
require(llmEmittedSlots.size <= 1) {
"Stage '${stageId.value}' declares ${llmEmittedSlots.size} LLM-emitted artifact kinds; " +
"only 0 or 1 is supported"
"only 0 or 1 is supported"
}
val responseFormat = llmEmittedSlots.firstOrNull()
?.let { ResponseFormat.Json(it.kind.deriveJsonSchema()) }
@@ -269,6 +268,7 @@ abstract class SessionOrchestrator(
emitToolArtifacts(sessionId, stageId, stageConfig)
verifyProduces(sessionId, stageId, stageConfig)
}
is StageExecutionResult.Failure -> outcome
}
}
@@ -321,6 +321,8 @@ abstract class SessionOrchestrator(
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
riskSummaryId = null,
timestamp = Clock.System.now(),
toolName = toolCall.function.name,
preview = toolCall.function.arguments.take(200),
)
emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING"))
emit(
@@ -333,6 +335,8 @@ abstract class SessionOrchestrator(
sessionId = sessionId,
stageId = stageId,
projectId = null,
toolName = toolCall.function.name,
preview = toolCall.function.arguments.take(200),
),
)
val deferred = CompletableDeferred<ApprovalDecision>()
@@ -399,8 +403,8 @@ abstract class SessionOrchestrator(
if (responseFormat !is ResponseFormat.Json) return emptyList()
val compactSchema = Json.encodeToString(JsonSchema.serializer(), responseFormat.schema)
val instruction = "Respond with a single JSON object matching this schema. " +
"Do not include markdown, code fences, or commentary outside the JSON. " +
"Schema: $compactSchema"
"Do not include markdown, code fences, or commentary outside the JSON. " +
"Schema: $compactSchema"
return listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
@@ -442,7 +446,7 @@ abstract class SessionOrchestrator(
val missingIds = missing.joinToString(", ") { it.value }
log.error(
"[Orchestrator] stage missing produced artifacts " +
"session=${sessionId.value} stage=${stageId.value} missing=$missingIds",
"session=${sessionId.value} stage=${stageId.value} missing=$missingIds",
)
return StageExecutionResult.Failure(
"stage ${stageId.value} did not produce declared artifacts: $missingIds",
@@ -598,13 +602,12 @@ abstract class SessionOrchestrator(
sessionId: SessionId,
terminalStageId: StageId,
stageCount: Int,
workflowId: String,
): WorkflowResult.Completed {
log.debug(
"[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
sessionId.value, terminalStageId.value, stageCount,
)
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount, workflowId))
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount))
cancellations.remove(sessionId)
return WorkflowResult.Completed(sessionId, terminalStageId)
}
@@ -642,6 +645,7 @@ abstract class SessionOrchestrator(
// --- event emission ---
internal suspend fun emit(sessionId: SessionId, payload: EventPayload) {
log.debug("[session {}] emitting event, payload: {}", sessionId.value.substring(0, 7), payload)
eventStore.append(
NewEvent(
metadata = EventMetadata(