feat(personalization): OperatorProfileBoundEvent + bind-at-start + inject about context
Records operator profile as a snapshot event at session start (invariants #8/#9), populates SessionState.boundProfile via reducer, and injects the about text as a pinned L0 SYSTEM context entry in every stage. Gated by PersonalizationConfig.enabled.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package com.correx.core.sessions
|
||||
|
||||
data class BoundProfile(
|
||||
val about: String,
|
||||
val approvalMode: String,
|
||||
val preferredModels: List<String>,
|
||||
val conventions: List<String>,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.correx.core.sessions
|
||||
|
||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.StageFailedEvent
|
||||
@@ -39,11 +40,23 @@ class DefaultSessionReducer : SessionReducer {
|
||||
else -> state.boundWorkspace
|
||||
}
|
||||
|
||||
val boundProfile = when (payload) {
|
||||
is OperatorProfileBoundEvent ->
|
||||
BoundProfile(
|
||||
about = payload.about,
|
||||
approvalMode = payload.approvalMode,
|
||||
preferredModels = payload.preferredModels,
|
||||
conventions = payload.conventions,
|
||||
)
|
||||
else -> state.boundProfile
|
||||
}
|
||||
|
||||
return state.copy(
|
||||
status = newStatus,
|
||||
createdAt = createdAt,
|
||||
updatedAt = event.metadata.timestamp,
|
||||
boundWorkspace = boundWorkspace,
|
||||
boundProfile = boundProfile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ data class SessionState(
|
||||
val updatedAt: Instant? = null,
|
||||
val invalidTransitions: Int = 0,
|
||||
val boundWorkspace: BoundWorkspace? = null,
|
||||
val boundProfile: BoundProfile? = null,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.correx.core.sessions
|
||||
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||
import com.correx.core.events.events.StoredEvent
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultSessionReducerTest {
|
||||
|
||||
private val reducer = DefaultSessionReducer()
|
||||
private val sessionId = SessionId("s-test")
|
||||
private val initialState = SessionState(status = SessionStatus.ACTIVE)
|
||||
|
||||
private fun stored(payload: com.correx.core.events.events.EventPayload) = StoredEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId("e-1"),
|
||||
sessionId = sessionId,
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z"),
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
sequence = 1L,
|
||||
sessionSequence = 1L,
|
||||
payload = payload,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `OperatorProfileBoundEvent populates boundProfile`() {
|
||||
val event = stored(
|
||||
OperatorProfileBoundEvent(
|
||||
sessionId = sessionId,
|
||||
about = "Expert Kotlin engineer",
|
||||
approvalMode = "auto",
|
||||
preferredModels = listOf("model-x"),
|
||||
conventions = listOf("no var"),
|
||||
),
|
||||
)
|
||||
|
||||
val result = reducer.reduce(initialState, event)
|
||||
|
||||
val profile = result.boundProfile
|
||||
assertEquals("Expert Kotlin engineer", profile?.about)
|
||||
assertEquals("auto", profile?.approvalMode)
|
||||
assertEquals(listOf("model-x"), profile?.preferredModels)
|
||||
assertEquals(listOf("no var"), profile?.conventions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `boundProfile is null by default`() {
|
||||
assertNull(initialState.boundProfile)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unrelated event leaves boundProfile unchanged`() {
|
||||
val state = initialState.copy(
|
||||
boundProfile = BoundProfile(
|
||||
about = "original",
|
||||
approvalMode = "",
|
||||
preferredModels = emptyList(),
|
||||
conventions = emptyList(),
|
||||
),
|
||||
)
|
||||
val event = stored(
|
||||
SessionWorkspaceBoundEvent(
|
||||
sessionId = sessionId,
|
||||
workspaceRoot = "/ws",
|
||||
allowedPaths = listOf("/ws"),
|
||||
),
|
||||
)
|
||||
|
||||
val result = reducer.reduce(state, event)
|
||||
|
||||
assertEquals("original", result.boundProfile?.about)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user