feat(server,kernel): bind ProjectProfile per session and inject as L0 context
This commit is contained in:
@@ -11,10 +11,12 @@ import com.correx.core.approvals.DefaultApprovalReducer
|
|||||||
import com.correx.core.approvals.DefaultApprovalRepository
|
import com.correx.core.approvals.DefaultApprovalRepository
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.config.OperatorProfile
|
import com.correx.core.config.OperatorProfile
|
||||||
|
import com.correx.core.config.ProjectProfileLoader
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.events.EventMetadata
|
import com.correx.core.events.events.EventMetadata
|
||||||
import com.correx.core.events.events.NewEvent
|
import com.correx.core.events.events.NewEvent
|
||||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||||
|
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
@@ -217,6 +219,35 @@ class ServerModule(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Bind the per-repo project profile (curated .correx/project.toml) as an event so
|
||||||
|
// stages and replay read the recorded snapshot, never the live file (invariants #8/#9).
|
||||||
|
val workspaceRoot = runCatching {
|
||||||
|
sessionRepository.getSession(sessionId).state.boundWorkspace?.workspaceRoot
|
||||||
|
}.getOrNull() ?: projectMemory?.repoRoot()
|
||||||
|
workspaceRoot?.let { root ->
|
||||||
|
val projectProfile = ProjectProfileLoader.load(root)
|
||||||
|
if (!projectProfile.isEmpty()) {
|
||||||
|
eventStore.append(
|
||||||
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = EventId(java.util.UUID.randomUUID().toString()),
|
||||||
|
sessionId = sessionId,
|
||||||
|
timestamp = Clock.System.now(),
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null,
|
||||||
|
),
|
||||||
|
payload = ProjectProfileBoundEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
workspaceRoot = root,
|
||||||
|
about = projectProfile.about,
|
||||||
|
conventions = projectProfile.conventions,
|
||||||
|
commands = projectProfile.commands,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
orchestrator.run(sessionId, graph, sessionConfig)
|
orchestrator.run(sessionId, graph, sessionConfig)
|
||||||
// After a successful freestyle planning run, lock the plan and execute phase 2.
|
// After a successful freestyle planning run, lock the plan and execute phase 2.
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.correx.core.kernel.orchestration
|
||||||
|
|
||||||
|
// Context-entry builders kept top-level (not orchestrator members) for unit-testability
|
||||||
|
// and to stay under detekt's function-count threshold (same pattern as isBackEdge).
|
||||||
|
|
||||||
|
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.types.ContextEntryId
|
||||||
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
|
||||||
|
val content = buildString {
|
||||||
|
append("## Project profile\n")
|
||||||
|
if (profile.about.isNotBlank()) append(profile.about).append("\n")
|
||||||
|
if (profile.conventions.isNotEmpty()) {
|
||||||
|
append("### Conventions\n")
|
||||||
|
profile.conventions.forEach { append("- ").append(it).append("\n") }
|
||||||
|
}
|
||||||
|
if (profile.commands.isNotEmpty()) {
|
||||||
|
append("### Commands\n")
|
||||||
|
profile.commands.forEach { (name, cmd) -> append(name).append(": ").append(cmd).append("\n") }
|
||||||
|
}
|
||||||
|
}.trimEnd()
|
||||||
|
return ContextEntry(
|
||||||
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
|
layer = ContextLayer.L0,
|
||||||
|
content = content,
|
||||||
|
sourceType = "projectProfile",
|
||||||
|
sourceId = "project-profile",
|
||||||
|
tokenEstimate = content.length / 4,
|
||||||
|
role = EntryRole.SYSTEM,
|
||||||
|
)
|
||||||
|
}
|
||||||
+4
-2
@@ -363,9 +363,11 @@ abstract class SessionOrchestrator(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
|
val projectProfileEntries = session.state.boundProjectProfile
|
||||||
|
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
||||||
var accumulatedEntries =
|
var accumulatedEntries =
|
||||||
systemPrompt + profileEntries + journalEntries + repoMapEntries + needsEntries +
|
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
||||||
schemaEntries + promptEntries + steeringEntries
|
needsEntries + schemaEntries + promptEntries + steeringEntries
|
||||||
val contextPack = contextPackBuilder.build(
|
val contextPack = contextPackBuilder.build(
|
||||||
id = ContextPackId(UUID.randomUUID().toString()),
|
id = ContextPackId(UUID.randomUUID().toString()),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import com.correx.core.context.model.ContextLayer
|
||||||
|
import com.correx.core.context.model.EntryRole
|
||||||
|
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
||||||
|
import com.correx.core.sessions.BoundProjectProfile
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ContextFeedbackTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `project profile renders as single L0 entry`() {
|
||||||
|
val entry = buildProjectProfileEntry(
|
||||||
|
BoundProjectProfile(
|
||||||
|
about = "Event-sourced kernel",
|
||||||
|
conventions = listOf("no bare try-catch"),
|
||||||
|
commands = mapOf("test" to "./gradlew check"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(ContextLayer.L0, entry.layer)
|
||||||
|
assertEquals(EntryRole.SYSTEM, entry.role)
|
||||||
|
assertTrue(entry.content.startsWith("## Project profile"), "content: ${entry.content}")
|
||||||
|
assertTrue(entry.content.contains("- no bare try-catch"), "content: ${entry.content}")
|
||||||
|
assertTrue(entry.content.contains("test: ./gradlew check"), "content: ${entry.content}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `project profile with only about renders without conventions or commands sections`() {
|
||||||
|
val entry = buildProjectProfileEntry(
|
||||||
|
BoundProjectProfile(
|
||||||
|
about = "Simple project",
|
||||||
|
conventions = emptyList(),
|
||||||
|
commands = emptyMap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertTrue(entry.content.contains("Simple project"), "content: ${entry.content}")
|
||||||
|
assertTrue(!entry.content.contains("### Conventions"), "should not contain Conventions: ${entry.content}")
|
||||||
|
assertTrue(!entry.content.contains("### Commands"), "should not contain Commands: ${entry.content}")
|
||||||
|
assertEquals("projectProfile", entry.sourceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user