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:
@@ -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))
|
||||
}
|
||||
|
||||
+3
-1
@@ -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>
|
||||
}
|
||||
|
||||
+7
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
|
||||
+14
-10
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user