feat(kernel): RepoKnowledgeRetriever port + relevance-retrieved repo context with recency fallback
This commit is contained in:
@@ -276,6 +276,22 @@ fun main() {
|
||||
val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg ->
|
||||
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(
|
||||
repositories = repositories,
|
||||
engines = engines,
|
||||
@@ -285,13 +301,8 @@ fun main() {
|
||||
decisionJournalRepository = decisionJournalRepository,
|
||||
compactionService = journalCompactionService,
|
||||
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(
|
||||
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).
|
||||
|
||||
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.ContextLayer
|
||||
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 {
|
||||
val content = buildString {
|
||||
append("## Project profile\n")
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ class DefaultSessionOrchestrator(
|
||||
private val decisionJournalRepository: DefaultDecisionJournalRepository,
|
||||
private val compactionService: JournalCompactionService? = 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 cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
|
||||
ConcurrentHashMap<SessionId, AtomicBoolean>()
|
||||
|
||||
+8
@@ -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>
|
||||
}
|
||||
+27
-1
@@ -35,6 +35,7 @@ import com.correx.core.events.events.ArtifactValidatingEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.EventPayload
|
||||
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.InferenceFailedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
@@ -155,6 +156,7 @@ abstract class SessionOrchestrator(
|
||||
private val decisionJournalRepository: DefaultDecisionJournalRepository,
|
||||
private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(),
|
||||
private val artifactKindRegistry: ArtifactKindRegistry? = null,
|
||||
private val repoKnowledgeRetriever: RepoKnowledgeRetriever? = null,
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
private val eventStore: EventStore = repositories.eventStore
|
||||
@@ -349,7 +351,7 @@ abstract class SessionOrchestrator(
|
||||
role = EntryRole.SYSTEM,
|
||||
),
|
||||
)
|
||||
val repoMapEntries = buildRepoMapEntries(sessionId)
|
||||
val repoMapEntries = buildContextualRepoEntries(sessionId, stageId, stageConfig)
|
||||
val sessionEvents = eventStore.read(sessionId)
|
||||
val criticIds = criticArtifactIds(sessionEvents, graph, stageId)
|
||||
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(
|
||||
sessionId: SessionId,
|
||||
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.StageId
|
||||
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.buildProjectProfileEntry
|
||||
import com.correx.core.kernel.orchestration.buildRelevantFilesEntry
|
||||
import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry
|
||||
import com.correx.core.kernel.orchestration.criticArtifactIds
|
||||
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}")
|
||||
}
|
||||
|
||||
@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
|
||||
fun `no refinement events yields empty set`() {
|
||||
assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())
|
||||
|
||||
Reference in New Issue
Block a user