fix: complete P4 audit findings — steering wiring, generation config
P4-1: Wire steering to actually work (advisory-only):
- RouterFacade emits SteeringNoteAddedEvent via event store in STEERING mode
- SessionOrchestrator reads steering notes from event store and includes them
as ContextEntry objects (sourceType=steeringNote) in stage context packs
- Delete dead SteeringNote data class; single vocabulary = SteeringNoteAddedEvent
- Add optional validateSteering callback to DefaultRouterFacade for future
ValidationPipeline wiring (avoids cross-core dependency)
- Update RouterFacadeTest to assert event emission
P4-2: Router FACE — move hardcoded generation config:
- Add GenerationConfig field to RouterConfig with same defaults
- RouterFacade uses config.generationConfig instead of inline literals
- (Remaining P4-2 items: conversation persistence and coordination
semantics are design decisions requiring ADRs, deferred)
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
package com.correx.core.context.steering
|
|
||||||
|
|
||||||
import com.correx.core.events.types.SessionId
|
|
||||||
import com.correx.core.events.types.StageId
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SteeringNote(
|
|
||||||
val sessionId: SessionId,
|
|
||||||
val content: String,
|
|
||||||
val stageId: StageId? = null,
|
|
||||||
val createdAt: Instant = Clock.System.now(),
|
|
||||||
)
|
|
||||||
+20
-1
@@ -35,6 +35,7 @@ import com.correx.core.events.events.NewEvent
|
|||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.RiskAssessedEvent
|
import com.correx.core.events.events.RiskAssessedEvent
|
||||||
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
import com.correx.core.events.events.ToolExecutionRejectedEvent
|
||||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
@@ -237,12 +238,13 @@ abstract class SessionOrchestrator(
|
|||||||
?: ResponseFormat.Text
|
?: ResponseFormat.Text
|
||||||
|
|
||||||
val schemaEntries = buildSchemaEntries(responseFormat, stageId)
|
val schemaEntries = buildSchemaEntries(responseFormat, stageId)
|
||||||
|
val steeringEntries = buildSteeringNoteEntries(sessionId)
|
||||||
|
|
||||||
val contextPack = contextPackBuilder.build(
|
val contextPack = contextPackBuilder.build(
|
||||||
id = ContextPackId(UUID.randomUUID().toString()),
|
id = ContextPackId(UUID.randomUUID().toString()),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
entries = systemPrompt + schemaEntries + promptEntries,
|
entries = systemPrompt + schemaEntries + promptEntries + steeringEntries,
|
||||||
budget = TokenBudget(limit = stageConfig.tokenBudget),
|
budget = TokenBudget(limit = stageConfig.tokenBudget),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -544,6 +546,23 @@ abstract class SessionOrchestrator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun buildSteeringNoteEntries(sessionId: SessionId): List<ContextEntry> {
|
||||||
|
val events = eventStore.read(sessionId)
|
||||||
|
return events.mapNotNull { event ->
|
||||||
|
(event.payload as? SteeringNoteAddedEvent)?.let { steering ->
|
||||||
|
ContextEntry(
|
||||||
|
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||||
|
layer = ContextLayer.L2,
|
||||||
|
content = steering.content,
|
||||||
|
sourceType = "steeringNote",
|
||||||
|
sourceId = steering.stageId?.value ?: sessionId.value,
|
||||||
|
tokenEstimate = estimateTokens(steering.content),
|
||||||
|
role = EntryRole.SYSTEM,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun emitToolArtifacts(
|
private suspend fun emitToolArtifacts(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
stageId: StageId,
|
stageId: StageId,
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.correx.core.router
|
package com.correx.core.router
|
||||||
|
|
||||||
|
import com.correx.core.events.events.EventMetadata
|
||||||
|
import com.correx.core.events.events.NewEvent
|
||||||
|
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
|
import com.correx.core.events.types.CausationId
|
||||||
|
import com.correx.core.events.types.CorrelationId
|
||||||
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.InferenceRequestId
|
import com.correx.core.events.types.InferenceRequestId
|
||||||
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.inference.GenerationConfig
|
|
||||||
import com.correx.core.inference.InferenceRequest
|
import com.correx.core.inference.InferenceRequest
|
||||||
import com.correx.core.inference.InferenceRouter
|
import com.correx.core.inference.InferenceRouter
|
||||||
import com.correx.core.inference.ModelCapability
|
import com.correx.core.inference.ModelCapability
|
||||||
@@ -31,6 +36,7 @@ class DefaultRouterFacade(
|
|||||||
private val inferenceRouter: InferenceRouter,
|
private val inferenceRouter: InferenceRouter,
|
||||||
private val eventStore: EventStore,
|
private val eventStore: EventStore,
|
||||||
private val config: RouterConfig,
|
private val config: RouterConfig,
|
||||||
|
private val validateSteering: (suspend (String) -> String?)? = null,
|
||||||
) : RouterFacade {
|
) : RouterFacade {
|
||||||
|
|
||||||
private val histories = ConcurrentHashMap<SessionId, MutableList<RouterTurn>>()
|
private val histories = ConcurrentHashMap<SessionId, MutableList<RouterTurn>>()
|
||||||
@@ -55,11 +61,7 @@ class DefaultRouterFacade(
|
|||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = effectiveStageId,
|
stageId = effectiveStageId,
|
||||||
contextPack = contextPack,
|
contextPack = contextPack,
|
||||||
generationConfig = GenerationConfig(
|
generationConfig = config.generationConfig,
|
||||||
temperature = 0.7,
|
|
||||||
topP = 0.9,
|
|
||||||
maxTokens = 512,
|
|
||||||
),
|
|
||||||
responseFormat = ResponseFormat.Text,
|
responseFormat = ResponseFormat.Text,
|
||||||
)
|
)
|
||||||
val inferenceResponse = provider.infer(inferenceRequest)
|
val inferenceResponse = provider.infer(inferenceRequest)
|
||||||
@@ -67,11 +69,28 @@ class DefaultRouterFacade(
|
|||||||
|
|
||||||
history.add(RouterTurn(role = TurnRole.ROUTER, content = content, timestamp = Clock.System.now()))
|
history.add(RouterTurn(role = TurnRole.ROUTER, content = content, timestamp = Clock.System.now()))
|
||||||
|
|
||||||
// NOTE: SteeringNoteAddedEvent emission is deferred to P4-1, which will wire
|
if (mode == ChatMode.STEERING) {
|
||||||
// proper validation (via ValidationPipeline) and consumption by the context builder.
|
val validationError = validateSteering?.invoke(content)
|
||||||
// The current SteeringNoteAddedEvent had wrong content (user raw input instead of
|
if (validationError == null) {
|
||||||
// LLM-processed steering text) and was emitted without validation (P0-6).
|
eventStore.append(
|
||||||
// The steeringEmitted flag below still informs the TUI that steering mode was used.
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = EventId(UUID.randomUUID().toString()),
|
||||||
|
sessionId = sessionId,
|
||||||
|
timestamp = Clock.System.now(),
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null,
|
||||||
|
),
|
||||||
|
payload = SteeringNoteAddedEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
content = content,
|
||||||
|
stageId = effectiveStageId.takeIf { it != StageId.NONE },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return RouterResponse(content = content, steeringEmitted = (mode == ChatMode.STEERING))
|
return RouterResponse(content = content, steeringEmitted = (mode == ChatMode.STEERING))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package com.correx.core.router.model
|
package com.correx.core.router.model
|
||||||
|
|
||||||
import com.correx.core.context.model.TokenBudget
|
import com.correx.core.context.model.TokenBudget
|
||||||
|
import com.correx.core.inference.GenerationConfig
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RouterConfig(
|
data class RouterConfig(
|
||||||
val conversationKeepLast: Int = 6,
|
val conversationKeepLast: Int = 6,
|
||||||
val tokenBudget: TokenBudget = TokenBudget(limit = 4096),
|
val tokenBudget: TokenBudget = TokenBudget(limit = 4096),
|
||||||
|
val generationConfig: GenerationConfig = GenerationConfig(
|
||||||
|
temperature = 0.7,
|
||||||
|
topP = 0.9,
|
||||||
|
maxTokens = 512,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -81,17 +81,18 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `STEERING mode does not append events to store`(): Unit = runBlocking {
|
fun `STEERING mode appends SteeringNoteAddedEvent to store`(): Unit = runBlocking {
|
||||||
// SteeringNoteAddedEvent emission is deferred to P4-1 (see P0-6).
|
|
||||||
// The steeringEmitted flag still informs the TUI that steering mode was used.
|
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
|
||||||
facade.onUserInput(sessionId = SessionId("session-xyz"), input = "steer this way")
|
facade.onUserInput(sessionId = SessionId("session-xyz"), input = "steer this way")
|
||||||
assertTrue(mockStore.appendedEvents.isEmpty())
|
assertEquals(1, mockStore.appendedEvents.size)
|
||||||
|
val payload = mockStore.appendedEvents.single().payload
|
||||||
|
assertTrue(payload is com.correx.core.events.events.SteeringNoteAddedEvent)
|
||||||
|
assertEquals("inference response", (payload as com.correx.core.events.events.SteeringNoteAddedEvent).content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `STEERING mode does not append events regardless of state stageId`(): Unit = runBlocking {
|
fun `STEERING mode appends SteeringNoteAddedEvent with current stageId`(): Unit = runBlocking {
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val stageId = StageId("stage-A")
|
val stageId = StageId("stage-A")
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
@@ -106,12 +107,15 @@ class RouterFacadeTest {
|
|||||||
routerContextBuilder = object : RouterContextBuilder {
|
routerContextBuilder = object : RouterContextBuilder {
|
||||||
override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack()
|
||||||
},
|
},
|
||||||
inferenceRouter = mockInferenceRouter("response"),
|
inferenceRouter = mockInferenceRouter("steering response"),
|
||||||
eventStore = mockStore,
|
eventStore = mockStore,
|
||||||
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
|
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
|
||||||
)
|
)
|
||||||
facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!", mode = ChatMode.STEERING)
|
facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!", mode = ChatMode.STEERING)
|
||||||
assertTrue(mockStore.appendedEvents.isEmpty())
|
assertEquals(1, mockStore.appendedEvents.size)
|
||||||
|
val payload = mockStore.appendedEvents.single().payload
|
||||||
|
assertTrue(payload is com.correx.core.events.events.SteeringNoteAddedEvent)
|
||||||
|
assertEquals("steering response", (payload as com.correx.core.events.events.SteeringNoteAddedEvent).content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -449,8 +453,7 @@ class RouterFacadeTest {
|
|||||||
assertNotNull(response)
|
assertNotNull(response)
|
||||||
assertEquals("inference response", response.content)
|
assertEquals("inference response", response.content)
|
||||||
assertTrue(response.steeringEmitted)
|
assertTrue(response.steeringEmitted)
|
||||||
// Steering events are deferred to P4-1; no events appended yet.
|
assertEquals(1, mockStore.appendedEvents.size)
|
||||||
assertTrue(mockStore.appendedEvents.isEmpty())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user