feat: event-source router CHAT and STEERING conversation turns
Adds ChatTurnEvent (role USER|ROUTER) so router conversation history survives restart and is rebuilt from the event log via projection, restoring Hard Invariant #1 for the CHAT path. Both CHAT and STEERING now emit user + router turn events; STEERING additionally keeps its existing SteeringNoteAddedEvent so the structured directive record is preserved alongside the conversation turns. RouterFacade drops the in-memory ConcurrentHashMap of histories and rebuilds state through the repository after each emission. Reducer appends turns to conversationHistory. ChatTurnEvent is registered in eventModule.
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
package com.correx.core.events.events
|
||||||
|
|
||||||
|
import com.correx.core.events.types.SessionId
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("ChatTurn")
|
||||||
|
data class ChatTurnEvent(
|
||||||
|
val sessionId: SessionId,
|
||||||
|
val turnId: String,
|
||||||
|
val role: ChatTurnRole,
|
||||||
|
val content: String,
|
||||||
|
val timestampMs: Long,
|
||||||
|
) : EventPayload
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class ChatTurnRole {
|
||||||
|
USER,
|
||||||
|
ROUTER,
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.correx.core.events.events.ArtifactCreatedEvent
|
|||||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||||
import com.correx.core.events.events.ChatSessionStartedEvent
|
import com.correx.core.events.events.ChatSessionStartedEvent
|
||||||
|
import com.correx.core.events.events.ChatTurnEvent
|
||||||
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.InferenceFailedEvent
|
import com.correx.core.events.events.InferenceFailedEvent
|
||||||
@@ -71,6 +72,7 @@ val eventModule = SerializersModule {
|
|||||||
subclass(RetryAttemptedEvent::class)
|
subclass(RetryAttemptedEvent::class)
|
||||||
subclass(RiskAssessedEvent::class)
|
subclass(RiskAssessedEvent::class)
|
||||||
subclass(ChatSessionStartedEvent::class)
|
subclass(ChatSessionStartedEvent::class)
|
||||||
|
subclass(ChatTurnEvent::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.correx.core.router
|
package com.correx.core.router
|
||||||
|
|
||||||
|
import com.correx.core.events.events.ChatTurnEvent
|
||||||
|
import com.correx.core.events.events.ChatTurnRole
|
||||||
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.SteeringNoteAddedEvent
|
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.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
|
||||||
@@ -16,11 +16,8 @@ import com.correx.core.inference.ModelCapability
|
|||||||
import com.correx.core.inference.ResponseFormat
|
import com.correx.core.inference.ResponseFormat
|
||||||
import com.correx.core.router.model.RouterConfig
|
import com.correx.core.router.model.RouterConfig
|
||||||
import com.correx.core.router.model.RouterResponse
|
import com.correx.core.router.model.RouterResponse
|
||||||
import com.correx.core.router.model.RouterTurn
|
|
||||||
import com.correx.core.router.model.TurnRole
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.*
|
|
||||||
|
|
||||||
interface RouterFacade {
|
interface RouterFacade {
|
||||||
suspend fun onUserInput(
|
suspend fun onUserInput(
|
||||||
@@ -39,22 +36,19 @@ class DefaultRouterFacade(
|
|||||||
private val validateSteering: (suspend (String) -> String?)? = null,
|
private val validateSteering: (suspend (String) -> String?)? = null,
|
||||||
) : RouterFacade {
|
) : RouterFacade {
|
||||||
|
|
||||||
private val histories = ConcurrentHashMap<SessionId, MutableList<RouterTurn>>()
|
|
||||||
|
|
||||||
override suspend fun onUserInput(
|
override suspend fun onUserInput(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
input: String,
|
input: String,
|
||||||
mode: ChatMode,
|
mode: ChatMode,
|
||||||
): RouterResponse {
|
): RouterResponse {
|
||||||
val state = routerRepository.getRouterState(sessionId)
|
// Emit USER turn event
|
||||||
|
emitChatTurn(sessionId, input, ChatTurnRole.USER)
|
||||||
|
|
||||||
val history = histories.getOrPut(sessionId) { mutableListOf() }
|
// Rebuild state with user turn appended
|
||||||
history.add(RouterTurn(role = TurnRole.USER, content = input, timestamp = Clock.System.now()))
|
val stateWithUserTurn = routerRepository.getRouterState(sessionId)
|
||||||
|
val effectiveStageId = stateWithUserTurn.currentStageId ?: StageId.NONE
|
||||||
|
|
||||||
val stateWithHistory = state.copy(conversationHistory = history.toList())
|
val contextPack = routerContextBuilder.build(stateWithUserTurn, config.tokenBudget)
|
||||||
val effectiveStageId = state.currentStageId ?: StageId.NONE
|
|
||||||
|
|
||||||
val contextPack = routerContextBuilder.build(stateWithHistory, config.tokenBudget)
|
|
||||||
val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General))
|
val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General))
|
||||||
val inferenceRequest = InferenceRequest(
|
val inferenceRequest = InferenceRequest(
|
||||||
requestId = InferenceRequestId(UUID.randomUUID().toString()),
|
requestId = InferenceRequestId(UUID.randomUUID().toString()),
|
||||||
@@ -67,36 +61,65 @@ class DefaultRouterFacade(
|
|||||||
val inferenceResponse = provider.infer(inferenceRequest)
|
val inferenceResponse = provider.infer(inferenceRequest)
|
||||||
val content = inferenceResponse.text
|
val content = inferenceResponse.text
|
||||||
|
|
||||||
history.add(RouterTurn(role = TurnRole.ROUTER, content = content, timestamp = Clock.System.now()))
|
// Emit ROUTER turn event
|
||||||
|
emitChatTurn(sessionId, content, ChatTurnRole.ROUTER)
|
||||||
|
|
||||||
if (mode == ChatMode.STEERING) {
|
if (mode == ChatMode.STEERING) {
|
||||||
val validationError = validateSteering?.invoke(content)
|
val validationError = validateSteering?.invoke(content)
|
||||||
if (validationError == null) {
|
if (validationError == null) {
|
||||||
eventStore.append(
|
emitSteeringNote(sessionId, content, effectiveStageId)
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitChatTurn(sessionId: SessionId, content: String, role: ChatTurnRole) {
|
||||||
|
val turnId = UUID.randomUUID().toString()
|
||||||
|
val nowMs = Clock.System.now().toEpochMilliseconds()
|
||||||
|
eventStore.append(
|
||||||
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = EventId(UUID.randomUUID().toString()),
|
||||||
|
sessionId = sessionId,
|
||||||
|
timestamp = Clock.System.now(),
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null,
|
||||||
|
),
|
||||||
|
payload = ChatTurnEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
turnId = turnId,
|
||||||
|
role = role,
|
||||||
|
content = content,
|
||||||
|
timestampMs = nowMs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun emitSteeringNote(sessionId: SessionId, content: String, effectiveStageId: StageId) {
|
||||||
|
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 },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ChatMode {
|
enum class ChatMode {
|
||||||
CHAT,
|
CHAT,
|
||||||
STEERING,
|
STEERING,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.core.router
|
package com.correx.core.router
|
||||||
|
|
||||||
|
import com.correx.core.events.events.ChatTurnEvent
|
||||||
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.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
@@ -10,8 +11,11 @@ import com.correx.core.events.events.WorkflowFailedEvent
|
|||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.router.model.RouterL2Entry
|
import com.correx.core.router.model.RouterL2Entry
|
||||||
import com.correx.core.router.model.RouterState
|
import com.correx.core.router.model.RouterState
|
||||||
|
import com.correx.core.router.model.RouterTurn
|
||||||
import com.correx.core.router.model.StageOutcomeKind
|
import com.correx.core.router.model.StageOutcomeKind
|
||||||
|
import com.correx.core.router.model.TurnRole
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
import com.correx.core.router.model.WorkflowStatus
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
interface RouterReducer {
|
interface RouterReducer {
|
||||||
val initial: RouterState
|
val initial: RouterState
|
||||||
@@ -37,6 +41,7 @@ class DefaultRouterReducer : RouterReducer {
|
|||||||
is OrchestrationResumedEvent -> handleOrchestrationResumed(state)
|
is OrchestrationResumedEvent -> handleOrchestrationResumed(state)
|
||||||
is StageCompletedEvent -> handleStageCompleted(state, event)
|
is StageCompletedEvent -> handleStageCompleted(state, event)
|
||||||
is StageFailedEvent -> handleStageFailed(state, event)
|
is StageFailedEvent -> handleStageFailed(state, event)
|
||||||
|
is ChatTurnEvent -> handleChatTurn(state, event)
|
||||||
else -> state
|
else -> state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,4 +107,20 @@ class DefaultRouterReducer : RouterReducer {
|
|||||||
currentStageId = null
|
currentStageId = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleChatTurn(state: RouterState, event: StoredEvent): RouterState {
|
||||||
|
val payload = event.payload as ChatTurnEvent
|
||||||
|
val turnRole = when (payload.role) {
|
||||||
|
com.correx.core.events.events.ChatTurnRole.USER -> TurnRole.USER
|
||||||
|
com.correx.core.events.events.ChatTurnRole.ROUTER -> TurnRole.ROUTER
|
||||||
|
}
|
||||||
|
val turn = RouterTurn(
|
||||||
|
role = turnRole,
|
||||||
|
content = payload.content,
|
||||||
|
timestamp = Instant.fromEpochMilliseconds(payload.timestampMs),
|
||||||
|
)
|
||||||
|
return state.copy(
|
||||||
|
conversationHistory = state.conversationHistory + turn
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,17 @@ import com.correx.core.inference.ResponseFormat
|
|||||||
import com.correx.core.inference.TokenUsage
|
import com.correx.core.inference.TokenUsage
|
||||||
import com.correx.core.router.ChatMode
|
import com.correx.core.router.ChatMode
|
||||||
import com.correx.core.router.DefaultRouterFacade
|
import com.correx.core.router.DefaultRouterFacade
|
||||||
|
import com.correx.core.router.DefaultRouterReducer
|
||||||
import com.correx.core.router.RouterContextBuilder
|
import com.correx.core.router.RouterContextBuilder
|
||||||
import com.correx.core.router.RouterFacade
|
import com.correx.core.router.RouterFacade
|
||||||
|
import com.correx.core.router.RouterProjector
|
||||||
import com.correx.core.router.RouterRepository
|
import com.correx.core.router.RouterRepository
|
||||||
import com.correx.core.router.model.RouterConfig
|
import com.correx.core.router.model.RouterConfig
|
||||||
import com.correx.core.router.model.RouterResponse
|
import com.correx.core.router.model.RouterResponse
|
||||||
import com.correx.core.router.model.RouterState
|
import com.correx.core.router.model.RouterState
|
||||||
import com.correx.core.router.model.TurnRole
|
import com.correx.core.router.model.TurnRole
|
||||||
import com.correx.core.router.model.WorkflowStatus
|
import com.correx.core.router.model.WorkflowStatus
|
||||||
|
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
|
||||||
import com.correx.testing.fixtures.inference.MockTokenizer
|
import com.correx.testing.fixtures.inference.MockTokenizer
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -61,11 +64,19 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CHAT mode does not append to EventStore`(): Unit = runBlocking {
|
fun `CHAT mode appends USER and ROUTER ChatTurnEvent to EventStore`(): Unit = runBlocking {
|
||||||
val mockStore = mockEventStore()
|
val mockStore = mockEventStore()
|
||||||
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.CHAT)
|
||||||
facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
|
||||||
assertTrue(mockStore.appendedEvents.isEmpty())
|
assertEquals(2, mockStore.appendedEvents.size)
|
||||||
|
val userEvent = mockStore.appendedEvents[0].payload
|
||||||
|
val routerEvent = mockStore.appendedEvents[1].payload
|
||||||
|
assertTrue(userEvent is com.correx.core.events.events.ChatTurnEvent)
|
||||||
|
assertTrue(routerEvent is com.correx.core.events.events.ChatTurnEvent)
|
||||||
|
assertEquals("Hello!", (userEvent as com.correx.core.events.events.ChatTurnEvent).content)
|
||||||
|
assertEquals(com.correx.core.events.events.ChatTurnRole.USER, userEvent.role)
|
||||||
|
assertEquals("inference response", (routerEvent as com.correx.core.events.events.ChatTurnEvent).content)
|
||||||
|
assertEquals(com.correx.core.events.events.ChatTurnRole.ROUTER, routerEvent.role)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -81,14 +92,20 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `STEERING mode appends SteeringNoteAddedEvent to store`(): Unit = runBlocking {
|
fun `STEERING mode appends USER ChatTurnEvent, ROUTER ChatTurnEvent, and SteeringNoteAddedEvent to store`(): Unit = runBlocking {
|
||||||
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")
|
||||||
assertEquals(1, mockStore.appendedEvents.size)
|
assertEquals(3, mockStore.appendedEvents.size)
|
||||||
val payload = mockStore.appendedEvents.single().payload
|
val userEvent = mockStore.appendedEvents[0].payload
|
||||||
assertTrue(payload is com.correx.core.events.events.SteeringNoteAddedEvent)
|
val routerEvent = mockStore.appendedEvents[1].payload
|
||||||
assertEquals("inference response", (payload as com.correx.core.events.events.SteeringNoteAddedEvent).content)
|
val steeringEvent = mockStore.appendedEvents[2].payload
|
||||||
|
assertTrue(userEvent is com.correx.core.events.events.ChatTurnEvent)
|
||||||
|
assertTrue(routerEvent is com.correx.core.events.events.ChatTurnEvent)
|
||||||
|
assertTrue(steeringEvent is com.correx.core.events.events.SteeringNoteAddedEvent)
|
||||||
|
assertEquals("steer this way", (userEvent as com.correx.core.events.events.ChatTurnEvent).content)
|
||||||
|
assertEquals("inference response", (routerEvent as com.correx.core.events.events.ChatTurnEvent).content)
|
||||||
|
assertEquals("inference response", (steeringEvent as com.correx.core.events.events.SteeringNoteAddedEvent).content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -112,10 +129,10 @@ class RouterFacadeTest {
|
|||||||
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)
|
||||||
assertEquals(1, mockStore.appendedEvents.size)
|
assertEquals(3, mockStore.appendedEvents.size)
|
||||||
val payload = mockStore.appendedEvents.single().payload
|
val steeringEvent = mockStore.appendedEvents[2].payload
|
||||||
assertTrue(payload is com.correx.core.events.events.SteeringNoteAddedEvent)
|
assertTrue(steeringEvent is com.correx.core.events.events.SteeringNoteAddedEvent)
|
||||||
assertEquals("steering response", (payload as com.correx.core.events.events.SteeringNoteAddedEvent).content)
|
assertEquals("steering response", (steeringEvent as com.correx.core.events.events.SteeringNoteAddedEvent).content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -125,14 +142,15 @@ class RouterFacadeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `conversation history grows per call - user and router turns appended`(): Unit = runBlocking {
|
fun `conversation history grows per call - user and router turns appended`(): Unit = runBlocking {
|
||||||
val capturedStates = mutableListOf<RouterState>()
|
val capturedStates = mutableListOf<RouterState>()
|
||||||
|
val eventStore = mockEventStore()
|
||||||
|
val replayer = DefaultEventReplayer<RouterState>(
|
||||||
|
store = eventStore,
|
||||||
|
projection = RouterProjector(DefaultRouterReducer()),
|
||||||
|
)
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
||||||
RouterState(
|
replayer.rebuild(sessionId)
|
||||||
sessionId = sessionId,
|
|
||||||
workflowStatus = WorkflowStatus.RUNNING,
|
|
||||||
currentStageId = StageId("s1"),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
routerContextBuilder = object : RouterContextBuilder {
|
routerContextBuilder = object : RouterContextBuilder {
|
||||||
override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
||||||
@@ -141,7 +159,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
inferenceRouter = mockInferenceRouter("router reply"),
|
inferenceRouter = mockInferenceRouter("router reply"),
|
||||||
eventStore = mockEventStore(),
|
eventStore = eventStore,
|
||||||
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
|
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
|
||||||
)
|
)
|
||||||
val sessionId = SessionId("history-session")
|
val sessionId = SessionId("history-session")
|
||||||
@@ -162,10 +180,15 @@ class RouterFacadeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `conversation history is session-scoped - different sessions do not share history`(): Unit = runBlocking {
|
fun `conversation history is session-scoped - different sessions do not share history`(): Unit = runBlocking {
|
||||||
val capturedStates = mutableListOf<RouterState>()
|
val capturedStates = mutableListOf<RouterState>()
|
||||||
|
val eventStore = mockEventStore()
|
||||||
|
val replayer = DefaultEventReplayer<RouterState>(
|
||||||
|
store = eventStore,
|
||||||
|
projection = RouterProjector(DefaultRouterReducer()),
|
||||||
|
)
|
||||||
val facade = DefaultRouterFacade(
|
val facade = DefaultRouterFacade(
|
||||||
routerRepository = object : RouterRepository {
|
routerRepository = object : RouterRepository {
|
||||||
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
override suspend fun getRouterState(sessionId: SessionId): RouterState =
|
||||||
RouterState(sessionId = sessionId, workflowStatus = WorkflowStatus.RUNNING)
|
replayer.rebuild(sessionId)
|
||||||
},
|
},
|
||||||
routerContextBuilder = object : RouterContextBuilder {
|
routerContextBuilder = object : RouterContextBuilder {
|
||||||
override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
override suspend fun build(state: RouterState, budget: TokenBudget): ContextPack {
|
||||||
@@ -174,7 +197,7 @@ class RouterFacadeTest {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
inferenceRouter = mockInferenceRouter("response"),
|
inferenceRouter = mockInferenceRouter("response"),
|
||||||
eventStore = mockEventStore(),
|
eventStore = eventStore,
|
||||||
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
|
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
|
||||||
)
|
)
|
||||||
facade.onUserInput(sessionId = SessionId("session-A"), input = "message A")
|
facade.onUserInput(sessionId = SessionId("session-A"), input = "message A")
|
||||||
@@ -453,7 +476,7 @@ class RouterFacadeTest {
|
|||||||
assertNotNull(response)
|
assertNotNull(response)
|
||||||
assertEquals("inference response", response.content)
|
assertEquals("inference response", response.content)
|
||||||
assertTrue(response.steeringEmitted)
|
assertTrue(response.steeringEmitted)
|
||||||
assertEquals(1, mockStore.appendedEvents.size)
|
assertEquals(3, mockStore.appendedEvents.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user