From 3981d8443df0f006e4acfd4f03c27e1658cf5746 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 29 May 2026 01:19:18 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20complete=20P4=20audit=20findings=20?= =?UTF-8?q?=E2=80=94=20steering=20wiring,=20generation=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../core/context/steering/SteeringNote.kt | 15 ------- .../orchestration/SessionOrchestrator.kt | 21 ++++++++- .../com/correx/core/router/RouterFacade.kt | 43 +++++++++++++------ .../correx/core/router/model/RouterConfig.kt | 6 +++ .../src/test/kotlin/RouterFacadeTest.kt | 21 +++++---- 5 files changed, 69 insertions(+), 37 deletions(-) delete mode 100644 core/context/src/main/kotlin/com/correx/core/context/steering/SteeringNote.kt diff --git a/core/context/src/main/kotlin/com/correx/core/context/steering/SteeringNote.kt b/core/context/src/main/kotlin/com/correx/core/context/steering/SteeringNote.kt deleted file mode 100644 index 9c5ff56a..00000000 --- a/core/context/src/main/kotlin/com/correx/core/context/steering/SteeringNote.kt +++ /dev/null @@ -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(), -) diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 0d2783d2..c882605e 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -35,6 +35,7 @@ import com.correx.core.events.events.NewEvent import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent 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.ToolInvocationRequestedEvent import com.correx.core.events.events.ToolRequest @@ -237,12 +238,13 @@ abstract class SessionOrchestrator( ?: ResponseFormat.Text val schemaEntries = buildSchemaEntries(responseFormat, stageId) + val steeringEntries = buildSteeringNoteEntries(sessionId) val contextPack = contextPackBuilder.build( id = ContextPackId(UUID.randomUUID().toString()), sessionId = sessionId, stageId = stageId, - entries = systemPrompt + schemaEntries + promptEntries, + entries = systemPrompt + schemaEntries + promptEntries + steeringEntries, budget = TokenBudget(limit = stageConfig.tokenBudget), ) @@ -544,6 +546,23 @@ abstract class SessionOrchestrator( ) } + private suspend fun buildSteeringNoteEntries(sessionId: SessionId): List { + 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( sessionId: SessionId, stageId: StageId, diff --git a/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt b/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt index bd4b5fa8..59fea505 100644 --- a/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt +++ b/core/router/src/main/kotlin/com/correx/core/router/RouterFacade.kt @@ -1,10 +1,15 @@ 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.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.SessionId import com.correx.core.events.types.StageId -import com.correx.core.inference.GenerationConfig import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceRouter import com.correx.core.inference.ModelCapability @@ -31,6 +36,7 @@ class DefaultRouterFacade( private val inferenceRouter: InferenceRouter, private val eventStore: EventStore, private val config: RouterConfig, + private val validateSteering: (suspend (String) -> String?)? = null, ) : RouterFacade { private val histories = ConcurrentHashMap>() @@ -55,11 +61,7 @@ class DefaultRouterFacade( sessionId = sessionId, stageId = effectiveStageId, contextPack = contextPack, - generationConfig = GenerationConfig( - temperature = 0.7, - topP = 0.9, - maxTokens = 512, - ), + generationConfig = config.generationConfig, responseFormat = ResponseFormat.Text, ) val inferenceResponse = provider.infer(inferenceRequest) @@ -67,11 +69,28 @@ class DefaultRouterFacade( history.add(RouterTurn(role = TurnRole.ROUTER, content = content, timestamp = Clock.System.now())) - // NOTE: SteeringNoteAddedEvent emission is deferred to P4-1, which will wire - // proper validation (via ValidationPipeline) and consumption by the context builder. - // The current SteeringNoteAddedEvent had wrong content (user raw input instead of - // LLM-processed steering text) and was emitted without validation (P0-6). - // The steeringEmitted flag below still informs the TUI that steering mode was used. + if (mode == ChatMode.STEERING) { + val validationError = validateSteering?.invoke(content) + if (validationError == null) { + eventStore.append( + 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)) } @@ -80,4 +99,4 @@ class DefaultRouterFacade( enum class ChatMode { CHAT, STEERING, -} +} \ No newline at end of file diff --git a/core/router/src/main/kotlin/com/correx/core/router/model/RouterConfig.kt b/core/router/src/main/kotlin/com/correx/core/router/model/RouterConfig.kt index 748b6f68..af577567 100644 --- a/core/router/src/main/kotlin/com/correx/core/router/model/RouterConfig.kt +++ b/core/router/src/main/kotlin/com/correx/core/router/model/RouterConfig.kt @@ -1,10 +1,16 @@ package com.correx.core.router.model import com.correx.core.context.model.TokenBudget +import com.correx.core.inference.GenerationConfig import kotlinx.serialization.Serializable @Serializable data class RouterConfig( val conversationKeepLast: Int = 6, val tokenBudget: TokenBudget = TokenBudget(limit = 4096), + val generationConfig: GenerationConfig = GenerationConfig( + temperature = 0.7, + topP = 0.9, + maxTokens = 512, + ), ) diff --git a/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt b/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt index bbbb3774..3ee4ab93 100644 --- a/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt +++ b/testing/deterministic/src/test/kotlin/RouterFacadeTest.kt @@ -81,17 +81,18 @@ class RouterFacadeTest { } @Test - fun `STEERING mode does not append events 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. + fun `STEERING mode appends SteeringNoteAddedEvent to store`(): Unit = runBlocking { val mockStore = mockEventStore() val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING) 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 - 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 stageId = StageId("stage-A") val facade = DefaultRouterFacade( @@ -106,12 +107,15 @@ class RouterFacadeTest { routerContextBuilder = object : RouterContextBuilder { override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack = emptyContextPack() }, - inferenceRouter = mockInferenceRouter("response"), + inferenceRouter = mockInferenceRouter("steering response"), eventStore = mockStore, config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)), ) 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) assertEquals("inference response", response.content) assertTrue(response.steeringEmitted) - // Steering events are deferred to P4-1; no events appended yet. - assertTrue(mockStore.appendedEvents.isEmpty()) + assertEquals(1, mockStore.appendedEvents.size) } // --------------------------------------------------------------------------