feat(kernel): label reviewer artifacts as critic feedback on back-edge re-entry

This commit is contained in:
2026-06-10 21:56:05 +04:00
parent 629b910e7f
commit beed501d60
3 changed files with 88 additions and 4 deletions
@@ -6,11 +6,14 @@ package com.correx.core.kernel.orchestration
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.RefinementIterationEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.StageId
import com.correx.core.sessions.BoundProjectProfile
import com.correx.core.transitions.graph.WorkflowGraph
import java.util.UUID
fun buildRetryFeedbackEntry(events: List<StoredEvent>, stageId: StageId): ContextEntry? {
@@ -32,6 +35,18 @@ fun buildRetryFeedbackEntry(events: List<StoredEvent>, stageId: StageId): Contex
)
}
fun criticArtifactIds(
events: List<StoredEvent>,
graph: WorkflowGraph,
stageId: StageId,
): Set<ArtifactId> {
val cycle = events
.mapNotNull { it.payload as? RefinementIterationEvent }
.lastOrNull { it.cycleKey.endsWith("->${stageId.value}") } ?: return emptySet()
val fromStage = StageId(cycle.cycleKey.substringBefore("->"))
return graph.stages[fromStage]?.produces?.map { it.name }?.toSet() ?: emptySet()
}
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
val content = buildString {
append("## Project profile\n")
@@ -23,6 +23,7 @@ import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.TokenBudget
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.RefinementIterationEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.ArtifactCreatedEvent
@@ -347,7 +348,13 @@ abstract class SessionOrchestrator(
),
)
val repoMapEntries = buildRepoMapEntries(sessionId)
val needsEntries = buildNeedsArtifactEntries(sessionId, stageConfig)
val sessionEvents = eventStore.read(sessionId)
val criticIds = criticArtifactIds(sessionEvents, graph, stageId)
val criticFrom = sessionEvents
.mapNotNull { it.payload as? RefinementIterationEvent }
.lastOrNull { it.cycleKey.endsWith("->${stageId.value}") }
?.cycleKey?.substringBefore("->")
val needsEntries = buildNeedsArtifactEntries(sessionId, stageConfig, criticIds, criticFrom)
val profileEntries = session.state.boundProfile?.about
?.takeIf { it.isNotBlank() }
?.let { about ->
@@ -365,7 +372,7 @@ abstract class SessionOrchestrator(
} ?: emptyList()
val projectProfileEntries = session.state.boundProjectProfile
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
val retryFeedbackEntries = buildRetryFeedbackEntry(eventStore.read(sessionId), stageId)
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
?.let { listOf(it) } ?: emptyList()
var accumulatedEntries =
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
@@ -1015,16 +1022,24 @@ abstract class SessionOrchestrator(
private suspend fun buildNeedsArtifactEntries(
sessionId: SessionId,
stageConfig: StageConfig,
criticIds: Set<ArtifactId> = emptySet(),
criticFromStage: String? = null,
): List<ContextEntry> {
return stageConfig.needs.mapNotNull { needed ->
val cached = artifactContentCache["${sessionId.value}:${needed.value}"]
?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
val content = "## Input artifact: ${needed.value}\n$cached"
val content = if (needed in criticIds) {
"## Critic feedback from stage '${criticFromStage ?: "reviewer"}': ${needed.value}\n" +
"This is reviewer feedback on your previous attempt. " +
"Address every finding before completing the stage.\n$cached"
} else {
"## Input artifact: ${needed.value}\n$cached"
}
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L1,
content = content,
sourceType = "neededArtifact",
sourceType = if (needed in criticIds) "criticFeedback" else "neededArtifact",
sourceId = needed.value,
tokenEstimate = estimateTokens(content),
role = EntryRole.USER,