feat(kernel): label reviewer artifacts as critic feedback on back-edge re-entry
This commit is contained in:
@@ -6,11 +6,14 @@ package com.correx.core.kernel.orchestration
|
|||||||
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.EntryRole
|
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.RetryAttemptedEvent
|
||||||
import com.correx.core.events.events.StoredEvent
|
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.ContextEntryId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.sessions.BoundProjectProfile
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
fun buildRetryFeedbackEntry(events: List<StoredEvent>, stageId: StageId): ContextEntry? {
|
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 {
|
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
|
||||||
val content = buildString {
|
val content = buildString {
|
||||||
append("## Project profile\n")
|
append("## Project profile\n")
|
||||||
|
|||||||
+19
-4
@@ -23,6 +23,7 @@ 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.context.model.EntryRole
|
import com.correx.core.context.model.EntryRole
|
||||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
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.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.events.ArtifactContentStoredEvent
|
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||||
@@ -347,7 +348,13 @@ abstract class SessionOrchestrator(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val repoMapEntries = buildRepoMapEntries(sessionId)
|
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
|
val profileEntries = session.state.boundProfile?.about
|
||||||
?.takeIf { it.isNotBlank() }
|
?.takeIf { it.isNotBlank() }
|
||||||
?.let { about ->
|
?.let { about ->
|
||||||
@@ -365,7 +372,7 @@ abstract class SessionOrchestrator(
|
|||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
val projectProfileEntries = session.state.boundProjectProfile
|
val projectProfileEntries = session.state.boundProjectProfile
|
||||||
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
||||||
val retryFeedbackEntries = buildRetryFeedbackEntry(eventStore.read(sessionId), stageId)
|
val retryFeedbackEntries = buildRetryFeedbackEntry(sessionEvents, stageId)
|
||||||
?.let { listOf(it) } ?: emptyList()
|
?.let { listOf(it) } ?: emptyList()
|
||||||
var accumulatedEntries =
|
var accumulatedEntries =
|
||||||
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
||||||
@@ -1015,16 +1022,24 @@ abstract class SessionOrchestrator(
|
|||||||
private suspend fun buildNeedsArtifactEntries(
|
private suspend fun buildNeedsArtifactEntries(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
stageConfig: StageConfig,
|
stageConfig: StageConfig,
|
||||||
|
criticIds: Set<ArtifactId> = emptySet(),
|
||||||
|
criticFromStage: String? = null,
|
||||||
): List<ContextEntry> {
|
): List<ContextEntry> {
|
||||||
return stageConfig.needs.mapNotNull { needed ->
|
return stageConfig.needs.mapNotNull { needed ->
|
||||||
val cached = artifactContentCache["${sessionId.value}:${needed.value}"]
|
val cached = artifactContentCache["${sessionId.value}:${needed.value}"]
|
||||||
?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
?.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(
|
ContextEntry(
|
||||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
layer = ContextLayer.L1,
|
layer = ContextLayer.L1,
|
||||||
content = content,
|
content = content,
|
||||||
sourceType = "neededArtifact",
|
sourceType = if (needed in criticIds) "criticFeedback" else "neededArtifact",
|
||||||
sourceId = needed.value,
|
sourceId = needed.value,
|
||||||
tokenEstimate = estimateTokens(content),
|
tokenEstimate = estimateTokens(content),
|
||||||
role = EntryRole.USER,
|
role = EntryRole.USER,
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
import com.correx.core.artifacts.kind.FileWrittenKind
|
||||||
|
import com.correx.core.artifacts.kind.TypedArtifactSlot
|
||||||
import com.correx.core.context.model.ContextLayer
|
import com.correx.core.context.model.ContextLayer
|
||||||
import com.correx.core.context.model.EntryRole
|
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.RetryAttemptedEvent
|
||||||
|
import com.correx.core.events.types.ArtifactId
|
||||||
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.TransitionId
|
||||||
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
||||||
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
||||||
|
import com.correx.core.kernel.orchestration.criticArtifactIds
|
||||||
import com.correx.core.sessions.BoundProjectProfile
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
|
import com.correx.core.transitions.graph.StageConfig
|
||||||
|
import com.correx.core.transitions.graph.TransitionEdge
|
||||||
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
import com.correx.testing.fixtures.EventFixtures.stored
|
import com.correx.testing.fixtures.EventFixtures.stored
|
||||||
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
|
||||||
@@ -72,4 +81,49 @@ class ContextFeedbackTest {
|
|||||||
assertTrue(!entry.content.contains("### Commands"), "should not contain Commands: ${entry.content}")
|
assertTrue(!entry.content.contains("### Commands"), "should not contain Commands: ${entry.content}")
|
||||||
assertEquals("projectProfile", entry.sourceType)
|
assertEquals("projectProfile", entry.sourceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `criticArtifactIds resolves the from-stage produces on a back-edge`() {
|
||||||
|
val graph = graphWith(
|
||||||
|
"implement" to producesSlots("patch"),
|
||||||
|
"review" to producesSlots("review_report"),
|
||||||
|
)
|
||||||
|
val events = listOf(
|
||||||
|
stored(payload = RefinementIterationEvent(SessionId("s"), "review->implement", 1, 3)),
|
||||||
|
)
|
||||||
|
assertEquals(setOf(ArtifactId("review_report")), criticArtifactIds(events, graph, StageId("implement")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no refinement events yields empty set`() {
|
||||||
|
assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `criticArtifactIds ignores events targeting a different stage`() {
|
||||||
|
val graph = graphWith(
|
||||||
|
"implement" to producesSlots("patch"),
|
||||||
|
"review" to producesSlots("review_report"),
|
||||||
|
)
|
||||||
|
val events = listOf(
|
||||||
|
stored(payload = RefinementIterationEvent(SessionId("s"), "review->other", 1, 3)),
|
||||||
|
)
|
||||||
|
assertTrue(criticArtifactIds(events, graph, StageId("implement")).isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun producesSlots(vararg names: String): List<TypedArtifactSlot> =
|
||||||
|
names.map { TypedArtifactSlot(name = ArtifactId(it), kind = FileWrittenKind) }
|
||||||
|
|
||||||
|
private fun graphWith(vararg stages: Pair<String, List<TypedArtifactSlot>>): WorkflowGraph {
|
||||||
|
val stageMap = stages.associate { (id, slots) ->
|
||||||
|
StageId(id) to StageConfig(produces = slots)
|
||||||
|
}.toMutableMap()
|
||||||
|
if (stageMap.isEmpty()) stageMap[StageId("start")] = StageConfig()
|
||||||
|
return WorkflowGraph(
|
||||||
|
id = "test-graph",
|
||||||
|
stages = stageMap,
|
||||||
|
transitions = emptySet(),
|
||||||
|
start = stageMap.keys.first(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user