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 ->
|
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")
|
||||||
|
|||||||
+2
-1
@@ -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>()
|
||||||
|
|||||||
+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.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())
|
||||||
|
|||||||
Reference in New Issue
Block a user