feat(kernel): runtime refinement guard on back-edges with iteration cap
This commit is contained in:
+30
@@ -10,6 +10,8 @@ import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.RefinementIterationEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.orchestration.OrchestrationState
|
||||
import com.correx.core.events.types.ApprovalDecisionId
|
||||
import com.correx.core.events.types.ApprovalRequestId
|
||||
@@ -35,6 +37,9 @@ import java.util.concurrent.atomic.*
|
||||
)
|
||||
private val log = LoggerFactory.getLogger(DefaultSessionOrchestrator::class.java)
|
||||
|
||||
// Fallback refinement-loop cap when a stage does not declare maxRetries.
|
||||
private const val DEFAULT_MAX_REFINEMENT = 3
|
||||
|
||||
class DefaultSessionOrchestrator(
|
||||
private val repositories: OrchestratorRepositories,
|
||||
engines: OrchestratorEngines,
|
||||
@@ -206,6 +211,27 @@ class DefaultSessionOrchestrator(
|
||||
)
|
||||
}
|
||||
|
||||
// Runtime refinement guard: a back-edge (re-entering a stage already visited this
|
||||
// run, e.g. reviewer→implementer) increments a per-cycle counter recorded as an
|
||||
// event, so the guard is replay-deterministic. Exceeding the cap escalates to a
|
||||
// terminal failure instead of looping forever.
|
||||
if (isBackEdge(ctx, nextStageId)) {
|
||||
val cycleKey = "${ctx.currentStageId.value}->${nextStageId.value}"
|
||||
val maxIterations = ctx.graph.stages[nextStageId]?.maxRetries ?: DEFAULT_MAX_REFINEMENT
|
||||
val iteration = (orchestrationRepository.getState(ctx.sessionId).refinementIterations[cycleKey] ?: 0) + 1
|
||||
emit(ctx.sessionId, RefinementIterationEvent(ctx.sessionId, cycleKey, iteration, maxIterations))
|
||||
if (iteration > maxIterations) {
|
||||
return StepResult.Terminal(
|
||||
failWorkflow(
|
||||
ctx.sessionId,
|
||||
nextStageId,
|
||||
"refinement loop '$cycleKey' exceeded $maxIterations iterations — escalating",
|
||||
retryExhausted = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the transition before executing the next stage. Otherwise the next stage's
|
||||
// events (InferenceStarted, artifacts, …) are recorded ahead of the TransitionExecuted
|
||||
// that marks entering it, so the log shows the stage running before it was entered.
|
||||
@@ -265,6 +291,10 @@ class DefaultSessionOrchestrator(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBackEdge(ctx: EnrichedExecutionContext, target: StageId): Boolean =
|
||||
repositories.eventStore.read(ctx.sessionId)
|
||||
.any { (it.payload as? TransitionExecutedEvent)?.to == target }
|
||||
|
||||
private fun ExecutionContext.enrich() = EnrichedExecutionContext(
|
||||
graph, sessionId, stageCount, currentStageId, config,
|
||||
session = repositories.sessionRepository.getSession(sessionId),
|
||||
|
||||
Reference in New Issue
Block a user