feat(kernel): RepoKnowledgeRetriever port + relevance-retrieved repo context with recency fallback

This commit is contained in:
2026-06-11 13:39:57 +04:00
parent 9d38a89c88
commit 7a9e060a55
7 changed files with 111 additions and 8 deletions
@@ -276,6 +276,22 @@ fun main() {
val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg -> val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg ->
configArtifactKindsEarly.forEach { reg.register(it) } configArtifactKindsEarly.forEach { reg.register(it) }
} }
val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder)
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3)
// Rebuild the L3 metadata map from the ChatTurnEvent log before serving queries:
// persisted vectors survive restart but the metadata map does not (no-op for
// non-rehydratable backends like in_memory).
runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) }
val repoKnowledgeRetriever: com.correx.apps.server.memory.L3RepoKnowledgeRetriever? =
if (correxConfig.project.enabled) {
com.correx.apps.server.memory.L3RepoKnowledgeRetriever(
embedder = embedder,
l3MemoryStore = l3MemoryStore,
repoRoot = workspaceRoot.toString(),
)
} else {
null
}
val orchestrator = DefaultSessionOrchestrator( val orchestrator = DefaultSessionOrchestrator(
repositories = repositories, repositories = repositories,
engines = engines, engines = engines,
@@ -285,13 +301,8 @@ fun main() {
decisionJournalRepository = decisionJournalRepository, decisionJournalRepository = decisionJournalRepository,
compactionService = journalCompactionService, compactionService = journalCompactionService,
artifactKindRegistry = artifactKindRegistry, artifactKindRegistry = artifactKindRegistry,
repoKnowledgeRetriever = repoKnowledgeRetriever,
) )
val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder)
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3)
// Rebuild the L3 metadata map from the ChatTurnEvent log before serving queries:
// persisted vectors survive restart but the metadata map does not (no-op for
// non-rehydratable backends like in_memory).
runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) }
val workflowRegistry = FileSystemWorkflowRegistry( val workflowRegistry = FileSystemWorkflowRegistry(
InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly), InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly),
) )
@@ -0,0 +1,24 @@
package com.correx.apps.server.memory
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.types.SessionId
import com.correx.core.inference.Embedder
import com.correx.core.kernel.orchestration.RepoKnowledgeRetriever
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
private const val RETRIEVAL_OVERSAMPLE_FACTOR = 4
class L3RepoKnowledgeRetriever(
private val embedder: Embedder,
private val l3MemoryStore: L3MemoryStore,
private val repoRoot: String,
) : RepoKnowledgeRetriever {
override suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List<RepoKnowledgeHit> {
val vector = embedder.embed(query)
return l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR))
.filter { it.entry.turnId.startsWith("repomap:$repoRoot") }
.take(k)
.map { RepoKnowledgeHit(path = it.entry.text.substringBefore(":"), text = it.entry.text, score = it.score) }
}
}
@@ -4,6 +4,7 @@ package com.correx.core.kernel.orchestration
// and to stay under detekt's function-count threshold (same pattern as isBackEdge). // and to stay under detekt's function-count threshold (same pattern as isBackEdge).
import com.correx.core.artifacts.kind.ArtifactKind import com.correx.core.artifacts.kind.ArtifactKind
import com.correx.core.events.events.RepoKnowledgeHit
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
@@ -66,6 +67,22 @@ fun buildArtifactKindVocabularyEntry(kinds: Collection<ArtifactKind>): ContextEn
) )
} }
fun buildRelevantFilesEntry(hits: List<RepoKnowledgeHit>): ContextEntry {
val content = buildString {
append("## Relevant files (retrieved by semantic similarity)\n")
hits.forEach { append("- ").append(it.text).append("\n") }
}.trimEnd()
return ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L3,
content = content,
sourceType = "relevantFiles",
sourceId = "repo-knowledge",
tokenEstimate = content.length / 4,
role = EntryRole.SYSTEM,
)
}
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry { fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
val content = buildString { val content = buildString {
append("## Project profile\n") append("## Project profile\n")
@@ -57,7 +57,8 @@ class DefaultSessionOrchestrator(
private val decisionJournalRepository: DefaultDecisionJournalRepository, private val decisionJournalRepository: DefaultDecisionJournalRepository,
private val compactionService: JournalCompactionService? = null, private val compactionService: JournalCompactionService? = null,
artifactKindRegistry: ArtifactKindRegistry? = null, artifactKindRegistry: ArtifactKindRegistry? = null,
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry), ApprovalGateway { repoKnowledgeRetriever: RepoKnowledgeRetriever? = null,
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever), ApprovalGateway {
override val tokenizer: Tokenizer? = tokenizer override val tokenizer: Tokenizer? = tokenizer
override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> = override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
ConcurrentHashMap<SessionId, AtomicBoolean>() ConcurrentHashMap<SessionId, AtomicBoolean>()
@@ -0,0 +1,8 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.types.SessionId
interface RepoKnowledgeRetriever {
suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List<RepoKnowledgeHit>
}
@@ -35,6 +35,7 @@ import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.RepoKnowledgeRetrievedEvent
import com.correx.core.events.events.RepoMapComputedEvent import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.InferenceFailedEvent import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceStartedEvent import com.correx.core.events.events.InferenceStartedEvent
@@ -155,6 +156,7 @@ abstract class SessionOrchestrator(
private val decisionJournalRepository: DefaultDecisionJournalRepository, private val decisionJournalRepository: DefaultDecisionJournalRepository,
private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(), private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(),
private val artifactKindRegistry: ArtifactKindRegistry? = null, private val artifactKindRegistry: ArtifactKindRegistry? = null,
private val repoKnowledgeRetriever: RepoKnowledgeRetriever? = null,
) { ) {
private val log = LoggerFactory.getLogger(this::class.java) private val log = LoggerFactory.getLogger(this::class.java)
private val eventStore: EventStore = repositories.eventStore private val eventStore: EventStore = repositories.eventStore
@@ -349,7 +351,7 @@ abstract class SessionOrchestrator(
role = EntryRole.SYSTEM, role = EntryRole.SYSTEM,
), ),
) )
val repoMapEntries = buildRepoMapEntries(sessionId) val repoMapEntries = buildContextualRepoEntries(sessionId, stageId, stageConfig)
val sessionEvents = eventStore.read(sessionId) val sessionEvents = eventStore.read(sessionId)
val criticIds = criticArtifactIds(sessionEvents, graph, stageId) val criticIds = criticArtifactIds(sessionEvents, graph, stageId)
val criticFrom = sessionEvents val criticFrom = sessionEvents
@@ -1024,6 +1026,30 @@ abstract class SessionOrchestrator(
) )
} }
private suspend fun buildContextualRepoEntries(
sessionId: SessionId,
stageId: StageId,
stageConfig: StageConfig,
): List<ContextEntry> {
val recorded = eventStore.read(sessionId)
.mapNotNull { it.payload as? RepoKnowledgeRetrievedEvent }
.lastOrNull { it.stageId == stageId }
if (recorded != null) return listOf(buildRelevantFilesEntry(recorded.hits))
val retriever = repoKnowledgeRetriever ?: return buildRepoMapEntries(sessionId)
val query = (stageConfig.metadata["promptInline"] ?: stageConfig.metadata["prompt"] ?: stageId.value)
.trim().ifBlank { stageId.value }
val hits = runCatching { retriever.retrieve(sessionId, query, REPO_MAP_INJECT_TOP_K) }
.getOrElse { e ->
if (e is CancellationException) throw e
log.warn("repo-knowledge retrieval failed for stage {}: {}", stageId.value, e.message)
emptyList()
}
if (hits.isEmpty()) return buildRepoMapEntries(sessionId)
emit(sessionId, RepoKnowledgeRetrievedEvent(sessionId, stageId, query, hits))
return listOf(buildRelevantFilesEntry(hits))
}
private suspend fun buildNeedsArtifactEntries( private suspend fun buildNeedsArtifactEntries(
sessionId: SessionId, sessionId: SessionId,
stageConfig: StageConfig, stageConfig: StageConfig,
@@ -10,8 +10,10 @@ 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.events.types.TransitionId
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry
import com.correx.core.kernel.orchestration.buildProjectProfileEntry import com.correx.core.kernel.orchestration.buildProjectProfileEntry
import com.correx.core.kernel.orchestration.buildRelevantFilesEntry
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.kernel.orchestration.criticArtifactIds
import com.correx.core.sessions.BoundProjectProfile import com.correx.core.sessions.BoundProjectProfile
@@ -109,6 +111,20 @@ class ContextFeedbackTest {
assertTrue(entry.content.contains("report (llm-emitted)"), "content should flag llm-emitted: ${entry.content}") assertTrue(entry.content.contains("report (llm-emitted)"), "content should flag llm-emitted: ${entry.content}")
} }
@Test
fun `buildRelevantFilesEntry produces L3 system entry with Relevant files header`() {
val hits = listOf(
RepoKnowledgeHit(path = "src/Parser.kt", text = "src/Parser.kt: Parser, Lexer", score = 0.9f),
RepoKnowledgeHit(path = "src/Main.kt", text = "src/Main.kt: main", score = 0.7f),
)
val entry = buildRelevantFilesEntry(hits)
assertEquals(ContextLayer.L3, entry.layer)
assertEquals(EntryRole.SYSTEM, entry.role)
assertEquals("relevantFiles", entry.sourceType)
assertTrue(entry.content.startsWith("## Relevant files"), "content: ${entry.content}")
assertTrue(entry.content.contains("src/Parser.kt"), "content: ${entry.content}")
}
@Test @Test
fun `no refinement events yields empty set`() { fun `no refinement events yields empty set`() {
assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty()) assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())