refactor: rename the router subsystem to Talkie

The 'router' name was misleading — it's the always-on conversational front-end
(CHAT triage + STEERING into a running workflow), not a routing layer, and it's
distinct from InferenceRouter. Rename core:router -> core:talkie, package
com.correx.core.router -> com.correx.core.talkie, and RouterFacade/Config/State/
Repository/ContextBuilder/Projector/Reducer/Response -> Talkie*. Config section
[router] -> [talkie] (legacy [router] still read as fallback).

Persisted wire formats are preserved: ChatTurnRole.ROUTER and the
RouterNarrationEvent @SerialName("RouterNarration") stay, so existing event
logs still replay. RouterNarrationEvent the class is now TalkieNarrationEvent.
This commit is contained in:
2026-07-03 13:26:00 +04:00
parent 77c28ba313
commit 595ec187bc
84 changed files with 736 additions and 734 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ dependencies {
implementation project(':infrastructure:inference:llama_cpp')
implementation project(':infrastructure:inference:openai_compat')
implementation project(':infrastructure:inference:commons')
implementation project(':core:router')
implementation project(':core:talkie')
implementation project(':core:tools')
implementation project(':core:toolintent')
implementation project(':infrastructure:tools')
@@ -44,9 +44,9 @@ import com.correx.core.kernel.orchestration.WorkspaceTools
import com.correx.core.kernel.orchestration.WorkspaceToolRegistryProvider
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.router.RouterFacade
import com.correx.core.router.l3.L3MetadataRehydrator
import com.correx.core.router.model.RouterConfig
import com.correx.core.talkie.TalkieFacade
import com.correx.core.talkie.l3.L3MetadataRehydrator
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
@@ -294,7 +294,7 @@ fun main() {
// Constructed before the engines so the context builder can rank freeform turns by relevance
// to the current query (pipeline §6, query-conditioned selection). Reused by L3 retrieval below.
val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder)
val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.talkie.embedder)
val engines = OrchestratorEngines(
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
contextPackBuilder = DefaultContextPackBuilder(
@@ -345,7 +345,7 @@ fun main() {
val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg ->
configArtifactKindsEarly.forEach { reg.register(it) }
}
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3)
val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.talkie.l3)
// Rebuild the L3 metadata map from the ChatTurnEvent log before serving queries:
// persisted vectors survive restart but the metadata map does not (no-op for
// non-rehydratable backends like in_memory).
@@ -390,11 +390,11 @@ fun main() {
InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly),
)
// Builds the router facade from a config snapshot, mapping the [router] block onto the domain
// RouterConfig. Reused by ConfigService's rebuild hook so router knob edits apply live (the
// TalkieConfig. Reused by ConfigService's rebuild hook so router knob edits apply live (the
// embedder and L3 store are construction-time and reused; the facade is what gets swapped).
fun buildRouterFacade(cfg: CorrexConfig): RouterFacade {
val rc = cfg.router
val domainRouterConfig = RouterConfig(
fun buildTalkieFacade(cfg: CorrexConfig): TalkieFacade {
val rc = cfg.talkie
val domainTalkieConfig = TalkieConfig(
conversationKeepLast = rc.conversationKeepLast,
retrievalK = rc.retrievalK,
tokenBudget = TokenBudget(limit = rc.tokenBudget),
@@ -410,16 +410,16 @@ fun main() {
),
narrationModelId = rc.narration.modelId,
)
return InfrastructureModule.createRouterFacade(
return InfrastructureModule.createTalkieFacade(
eventStore = eventStore,
inferenceRouter = inferenceRouter,
config = domainRouterConfig,
config = domainTalkieConfig,
tokenizer = firstProvider.tokenizer,
embedder = embedder,
l3MemoryStore = l3MemoryStore,
workflowSummaryProvider = {
workflowRegistry.listAll().map { s ->
com.correx.core.router.model.WorkflowSummary(s.workflowId, s.description, s.stageIds)
com.correx.core.talkie.model.WorkflowSummary(s.workflowId, s.description, s.stageIds)
}
},
sessionProfileProvider = { sid ->
@@ -429,7 +429,7 @@ fun main() {
},
)
}
val routerFacade = buildRouterFacade(correxConfig)
val routerFacade = buildTalkieFacade(correxConfig)
val sessionUndoService = com.correx.apps.server.undo.SessionUndoService(
eventStore = eventStore,
artifactStore = artifactStore,
@@ -546,7 +546,7 @@ fun main() {
modelSwapper = modelSwapper,
resourceProbe = resourceProbe,
workspaceResolver = workspaceResolver,
narrationMaxPerRun = correxConfig.router.narration.maxPerRun,
narrationMaxPerRun = correxConfig.talkie.narration.maxPerRun,
projectMemory = projectMemory,
architectContradictionChecker = architectContradictionChecker,
configHolder = configHolder,
@@ -564,7 +564,7 @@ fun main() {
// services. Built after the module so the rebuild hook can swap them in place.
module.configService = ConfigService(configHolder, ConfigLoader.configPath()) { cfg ->
module.applyRebuiltServices(
newRouterFacade = buildRouterFacade(cfg),
newTalkieFacade = buildTalkieFacade(cfg),
newProjectMemory = buildProjectMemory(cfg),
newProfileAdaptationService = buildProfileAdaptation(cfg),
)
@@ -39,7 +39,7 @@ import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.kernel.orchestration.OrchestrationRepository
import com.correx.core.router.RouterFacade
import com.correx.core.talkie.TalkieFacade
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionSummary
import com.correx.core.sessions.SessionSummaryProjector
@@ -72,7 +72,7 @@ class ServerModule(
val defaultOrchestrationConfig: OrchestrationConfig,
// Rebuilt by ConfigService on a live config edit (router knobs); read at use-time by
// GlobalStreamHandler.onUserInput so the next chat turn picks up the new facade.
routerFacade: RouterFacade,
routerFacade: TalkieFacade,
val orchestrationRepository: OrchestrationRepository,
val approvalRepository: DefaultApprovalRepository,
val toolRegistry: ToolRegistry,
@@ -134,7 +134,7 @@ class ServerModule(
// Config-derived services that ConfigService swaps on a live edit. Reads happen at use-time
// (next session / next chat turn), so in-flight sessions keep the config they started with.
@Volatile
var routerFacade: RouterFacade = routerFacade
var routerFacade: TalkieFacade = routerFacade
private set
@Volatile
@@ -154,11 +154,11 @@ class ServerModule(
* In-flight sessions are untouched; the next session/turn reads the new instances.
*/
fun applyRebuiltServices(
newRouterFacade: RouterFacade,
newTalkieFacade: TalkieFacade,
newProjectMemory: com.correx.apps.server.memory.ProjectMemoryService?,
newProfileAdaptationService: com.correx.apps.server.personalization.ProfileAdaptationService?,
) {
this.routerFacade = newRouterFacade
this.routerFacade = newTalkieFacade
this.projectMemory = newProjectMemory
this.profileAdaptationService = newProfileAdaptationService
}
@@ -39,7 +39,7 @@ import com.correx.core.events.events.ToolCallAssessedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.TalkieNarrationEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowProposedEvent
import com.correx.core.events.risk.RiskAction
@@ -249,7 +249,7 @@ suspend fun domainEventToServerMessage(
sequence = seq,
sessionSequence = sessionSequence,
)
is RouterNarrationEvent -> ServerMessage.Narration(
is TalkieNarrationEvent -> ServerMessage.Narration(
sessionId = p.sessionId,
content = p.content,
stageId = p.stageId,
@@ -5,8 +5,8 @@ import com.correx.core.events.events.RelatedDecision
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.Embedder
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
/**
* Display-only architect contradiction surfacing (BACKLOG §B-§4). When the architect records a
@@ -4,8 +4,8 @@ import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.types.SessionId
import com.correx.core.inference.Embedder
import com.correx.core.kernel.orchestration.RepoKnowledgeRetriever
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
private const val RETRIEVAL_OVERSAMPLE_FACTOR = 4
@@ -13,9 +13,9 @@ import com.correx.core.events.types.SessionId
import com.correx.core.inference.Embedder
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.journal.ProjectMemoryDistiller
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import kotlinx.coroutines.CancellationException
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
@@ -13,8 +13,8 @@ import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import com.correx.core.router.RouterFacade
import com.correx.core.router.model.NarrationTrigger
import com.correx.core.talkie.TalkieFacade
import com.correx.core.talkie.model.NarrationTrigger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class NarrationSubscriber(
private val eventStore: EventStore,
private val routerFacade: RouterFacade,
private val routerFacade: TalkieFacade,
private val scope: CoroutineScope,
private val maxPerRun: Int = DEFAULT_MAX_PER_RUN,
) {
@@ -8,7 +8,7 @@ import com.correx.core.events.types.ClarificationRequestId
import com.correx.core.events.types.GrantId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.router.ChatMode
import com.correx.core.talkie.ChatMode
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@@ -562,7 +562,7 @@ sealed interface ServerMessage {
@Serializable
@SerialName("router.response")
data class RouterResponseMessage(
data class TalkieResponseMessage(
val sessionId: SessionId,
val content: String,
val steeringEmitted: Boolean,
@@ -35,7 +35,7 @@ import com.correx.core.kernel.orchestration.WorkspaceContext
import com.correx.core.config.ProjectProfileLoader
import com.correx.core.config.ProjectProfileWriter
import com.correx.core.config.withConvention
import com.correx.core.router.IdeaReader
import com.correx.core.talkie.IdeaReader
import com.correx.core.utils.TypeId
import com.correx.infrastructure.inference.commons.ResourceProbe
import com.correx.infrastructure.inference.commons.ResourceSnapshot
@@ -253,7 +253,7 @@ class GlobalStreamHandler(private val module: ServerModule) {
is ClientMessage.ChatInput -> {
// The USER and ROUTER turns are emitted as ChatTurnEvents inside onUserInput
// and reach the client as chat.turn frames via streamGlobal — no direct
// RouterResponseMessage (the conversation is event-derived).
// TalkieResponseMessage (the conversation is event-derived).
withSessionContext(msg.sessionId) {
runCatching {
module.routerFacade.onUserInput(msg.sessionId, msg.text, msg.mode)
@@ -6,7 +6,7 @@ import com.correx.apps.server.logging.withSessionContext
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.server.protocol.ProtocolSerializer
import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.server.protocol.ServerMessage.RouterResponseMessage
import com.correx.apps.server.protocol.ServerMessage.TalkieResponseMessage
import com.correx.core.events.types.SessionId
import io.ktor.server.websocket.DefaultWebSocketServerSession
import io.ktor.websocket.Frame
@@ -93,7 +93,7 @@ class SessionStreamHandler(private val module: ServerModule) {
session.send(
Frame.Text(
ProtocolSerializer.encodeServerMessage(
RouterResponseMessage(
TalkieResponseMessage(
sessionId = msg.sessionId,
content = response.content,
steeringEmitted = response.steeringEmitted,
@@ -14,7 +14,7 @@ import com.correx.core.approvals.GRANT_LEDGER_SESSION_ID
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.ProjectIdentity
import com.correx.core.events.types.ProjectId
import com.correx.core.router.IdeaReader
import com.correx.core.talkie.IdeaReader
import com.correx.core.config.EditableConfig
import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.ArtifactCreatedEvent
@@ -35,11 +35,11 @@ import com.correx.core.kernel.orchestration.OrchestratorEngines
import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.router.l3.L3Hit
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.router.model.RouterConfig
import com.correx.core.talkie.l3.L3Hit
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
@@ -182,10 +182,10 @@ class ArchitectContradictionHookTest {
tokenizer = provider.tokenizer,
decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
)
val routerFacade = InfrastructureModule.createRouterFacade(
val routerFacade = InfrastructureModule.createTalkieFacade(
eventStore = eventStore,
inferenceRouter = inferenceRouter,
config = RouterConfig(),
config = TalkieConfig(),
tokenizer = provider.tokenizer,
)
val noopProviderRegistry = object : ProviderRegistry {
@@ -24,7 +24,7 @@ import com.correx.core.kernel.orchestration.OrchestratorEngines
import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.router.model.RouterConfig
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
@@ -116,10 +116,10 @@ fun buildTestServerModule(
decisionJournalRepository = decisionJournalRepository,
)
val routerFacade = InfrastructureModule.createRouterFacade(
val routerFacade = InfrastructureModule.createTalkieFacade(
eventStore = eventStore,
inferenceRouter = modelSwapper,
config = RouterConfig(),
config = TalkieConfig(),
tokenizer = firstProvider.tokenizer,
)
@@ -3,10 +3,10 @@ package com.correx.apps.server.memory
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.Embedder
import com.correx.core.router.l3.L3Hit
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.talkie.l3.L3Hit
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
@@ -2,8 +2,8 @@ package com.correx.apps.server.memory
import com.correx.core.events.types.SessionId
import com.correx.core.inference.Embedder
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.L3MemoryEntry
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -10,7 +10,7 @@ import com.correx.core.inference.Embedder
import com.correx.core.journal.DecisionJournalProjector
import com.correx.core.journal.DefaultDecisionJournalReducer
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
@@ -6,7 +6,7 @@ import com.correx.core.inference.Embedder
import com.correx.core.journal.DecisionJournalProjector
import com.correx.core.journal.DefaultDecisionJournalReducer
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.events.types.SessionId
import com.correx.infrastructure.persistence.InMemoryEventStore
@@ -13,7 +13,7 @@ import com.correx.core.inference.Embedder
import com.correx.core.journal.DecisionJournalProjector
import com.correx.core.journal.DefaultDecisionJournalReducer
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
@@ -25,10 +25,10 @@ import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.events.types.ValidationReportId
import org.junit.jupiter.api.Assertions.assertTrue
import com.correx.core.router.ChatMode
import com.correx.core.router.RouterFacade
import com.correx.core.router.model.NarrationTrigger
import com.correx.core.router.model.RouterResponse
import com.correx.core.talkie.ChatMode
import com.correx.core.talkie.TalkieFacade
import com.correx.core.talkie.model.NarrationTrigger
import com.correx.core.talkie.model.TalkieResponse
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -72,10 +72,10 @@ class NarrationSubscriberTest {
override suspend fun lastGlobalSequence(): Long = 0L
}
private class RecordingRouterFacade : RouterFacade {
private class RecordingTalkieFacade : TalkieFacade {
val calls = CopyOnWriteArrayList<Pair<SessionId, NarrationTrigger>>()
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): RouterResponse =
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): TalkieResponse =
error("unused in narration tests")
override suspend fun narrate(sessionId: SessionId, trigger: NarrationTrigger) {
@@ -99,7 +99,7 @@ class NarrationSubscriberTest {
@Test
fun `triggers are narrated in order for matching events`(): Unit = runBlocking {
val facade = RecordingRouterFacade()
val facade = RecordingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
// Wait for subscription to be attached
@@ -122,7 +122,7 @@ class NarrationSubscriberTest {
@Test
fun `pause narration is grounded with the pending approval tool and preview`(): Unit = runBlocking {
val facade = RecordingRouterFacade()
val facade = RecordingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
while (liveFlow.subscriptionCount.value == 0) yield()
@@ -158,7 +158,7 @@ class NarrationSubscriberTest {
@Test
fun `maxPerRun=1 skips second trigger after first narration`(): Unit = runBlocking {
val facade = RecordingRouterFacade()
val facade = RecordingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope, maxPerRun = 1).start()
while (liveFlow.subscriptionCount.value == 0) yield()
@@ -180,10 +180,10 @@ class NarrationSubscriberTest {
@Test
fun `a throwing narrate does not prevent subsequent narrations`(): Unit = runBlocking {
var callCount = 0
val facade = object : RouterFacade {
val facade = object : TalkieFacade {
val calls = CopyOnWriteArrayList<String>()
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): RouterResponse =
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): TalkieResponse =
error("unused")
override suspend fun narrate(sessionId: SessionId, trigger: NarrationTrigger) {
@@ -210,7 +210,7 @@ class NarrationSubscriberTest {
@Test
fun `execution plan rejection triggers narration with rejection reason`(): Unit = runBlocking {
val facade = RecordingRouterFacade()
val facade = RecordingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
while (liveFlow.subscriptionCount.value == 0) yield()
@@ -244,7 +244,7 @@ class NarrationSubscriberTest {
* letting the test enqueue further events (and resolve the pause) while the pause narration is
* still pending in the channel.
*/
private class GatingRouterFacade : RouterFacade {
private class GatingTalkieFacade : TalkieFacade {
val calls = CopyOnWriteArrayList<NarrationTrigger>()
val release = CompletableDeferred<Unit>()
// Signals that the worker has pulled the first (warmup) trigger and is now blocked,
@@ -252,7 +252,7 @@ class NarrationSubscriberTest {
val blocked = CompletableDeferred<Unit>()
private var first = true
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): RouterResponse =
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): TalkieResponse =
error("unused in narration tests")
override suspend fun narrate(sessionId: SessionId, trigger: NarrationTrigger) {
@@ -289,7 +289,7 @@ class NarrationSubscriberTest {
@Test
fun `resolved approval drops the still-queued pause narration`(): Unit = runBlocking {
val facade = GatingRouterFacade()
val facade = GatingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
while (liveFlow.subscriptionCount.value == 0) yield()
@@ -320,7 +320,7 @@ class NarrationSubscriberTest {
@Test
fun `unrelated approval resolution leaves the pending pause intact`(): Unit = runBlocking {
val facade = GatingRouterFacade()
val facade = GatingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
while (liveFlow.subscriptionCount.value == 0) yield()
@@ -342,7 +342,7 @@ class NarrationSubscriberTest {
@Test
fun `resume drops every queued pause narration for the session`(): Unit = runBlocking {
val facade = GatingRouterFacade()
val facade = GatingTalkieFacade()
NarrationSubscriber(fakeStore, facade, scope).start()
while (liveFlow.subscriptionCount.value == 0) yield()
@@ -33,7 +33,7 @@ import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.events.types.ProviderId
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.router.model.RouterConfig
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
@@ -165,10 +165,10 @@ class EventInspectRoutesTest {
tokenizer = provider.tokenizer,
decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
)
val routerFacade = InfrastructureModule.createRouterFacade(
val routerFacade = InfrastructureModule.createTalkieFacade(
eventStore = eventStore,
inferenceRouter = inferenceRouter,
config = RouterConfig(),
config = TalkieConfig(),
tokenizer = provider.tokenizer,
)
val noopProviderRegistry = object : ProviderRegistry {
@@ -26,7 +26,7 @@ import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.events.types.ProviderId
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.router.model.RouterConfig
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
@@ -444,10 +444,10 @@ class TaskRoutesTest {
tokenizer = provider.tokenizer,
decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
)
val routerFacade = InfrastructureModule.createRouterFacade(
val routerFacade = InfrastructureModule.createTalkieFacade(
eventStore = eventStore,
inferenceRouter = inferenceRouter,
config = RouterConfig(),
config = TalkieConfig(),
tokenizer = provider.tokenizer,
)
val noopProviderRegistry = object : ProviderRegistry {
@@ -186,10 +186,10 @@ class WorkspaceHandshakeTest {
decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
)
val routerFacade = InfrastructureModule.createRouterFacade(
val routerFacade = InfrastructureModule.createTalkieFacade(
eventStore = eventStore,
inferenceRouter = inferenceRouter,
config = com.correx.core.router.model.RouterConfig(),
config = com.correx.core.talkie.model.TalkieConfig(),
tokenizer = provider.tokenizer,
)
@@ -418,14 +418,14 @@ object ConfigLoader {
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
val routerSection = sections["router"] ?: emptyMap()
val routerSection = sections["talkie"] ?: sections["router"] ?: emptyMap()
// Embedder + L3 are shared infra (the orchestrator uses them for workflow repo-knowledge,
// not just the router chat layer). Prefer the top-level [embedder]/[l3] sections; fall back
// to the legacy [router.embedder]/[router.l3] so existing configs keep working.
val routerEmbedderSection = sections["embedder"] ?: sections["router.embedder"] ?: emptyMap()
val routerL3Section = sections["l3"] ?: sections["router.l3"] ?: emptyMap()
val routerGenerationSection = sections["router.generation"] ?: emptyMap()
val routerNarrationSection = sections["router.narration"] ?: emptyMap()
val routerGenerationSection = sections["talkie.generation"] ?: sections["router.generation"] ?: emptyMap()
val routerNarrationSection = sections["talkie.narration"] ?: sections["router.narration"] ?: emptyMap()
val modelsSection = sections["models"] ?: emptyMap()
val server = ServerConfig(
@@ -575,7 +575,7 @@ object ConfigLoader {
modelId = asStringOrNull(routerNarrationSection["model_id"]),
)
val router = RouterConfig(
val router = TalkieConfig(
embedder = embedder,
l3 = l3,
conversationKeepLast = asInt(routerSection["conversation_keep_last"], 6),
@@ -655,7 +655,7 @@ object ConfigLoader {
cli = cli,
tools = tools,
providers = providers,
router = router,
talkie = router,
models = models,
modelsSettings = modelsSettings,
artifacts = artifacts,
@@ -9,7 +9,7 @@ data class CorrexConfig(
val cli: CliConfig = CliConfig(),
val tools: ToolsConfig = ToolsConfig(),
val providers: List<ProviderConfig> = emptyList(),
val router: RouterConfig = RouterConfig(),
val talkie: TalkieConfig = TalkieConfig(),
val models: List<ModelConfig> = emptyList(),
val modelsSettings: ModelsSettings = ModelsSettings(),
val artifacts: List<ArtifactKindConfig> = emptyList(),
@@ -189,7 +189,7 @@ data class ProviderConfig(
)
@Serializable
data class RouterConfig(
data class TalkieConfig(
val embedder: EmbedderConfig = EmbedderConfig(),
val l3: L3Config = L3Config(),
val conversationKeepLast: Int = 6,
@@ -51,36 +51,36 @@ object CorrexConfigWriter {
b.section("tools.file_edit")
b.kv("enabled", cfg.tools.fileEditEnabled)
b.section("router")
b.kv("conversation_keep_last", cfg.router.conversationKeepLast)
b.kv("retrieval_k", cfg.router.retrievalK)
b.kv("token_budget", cfg.router.tokenBudget)
b.section("talkie")
b.kv("conversation_keep_last", cfg.talkie.conversationKeepLast)
b.kv("retrieval_k", cfg.talkie.retrievalK)
b.kv("token_budget", cfg.talkie.tokenBudget)
b.section("embedder")
b.kv("backend", str(cfg.router.embedder.backend))
b.kv("dimension", cfg.router.embedder.dimension)
cfg.router.embedder.url?.let { b.kv("url", str(it)) }
cfg.router.embedder.modelId?.let { b.kv("model_id", str(it)) }
b.kv("backend", str(cfg.talkie.embedder.backend))
b.kv("dimension", cfg.talkie.embedder.dimension)
cfg.talkie.embedder.url?.let { b.kv("url", str(it)) }
cfg.talkie.embedder.modelId?.let { b.kv("model_id", str(it)) }
b.section("l3")
b.kv("backend", str(cfg.router.l3.backend))
cfg.router.l3.persistPath?.let { b.kv("persist_path", str(it)) }
b.kv("python_executable", str(cfg.router.l3.pythonExecutable))
cfg.router.l3.scriptPath?.let { b.kv("script_path", str(it)) }
b.kv("dim", cfg.router.l3.dim)
b.kv("bit_width", cfg.router.l3.bitWidth)
b.kv("backend", str(cfg.talkie.l3.backend))
cfg.talkie.l3.persistPath?.let { b.kv("persist_path", str(it)) }
b.kv("python_executable", str(cfg.talkie.l3.pythonExecutable))
cfg.talkie.l3.scriptPath?.let { b.kv("script_path", str(it)) }
b.kv("dim", cfg.talkie.l3.dim)
b.kv("bit_width", cfg.talkie.l3.bitWidth)
b.section("router.generation")
b.kv("temperature", cfg.router.generation.temperature)
b.kv("top_p", cfg.router.generation.topP)
b.kv("max_tokens", cfg.router.generation.maxTokens)
b.section("talkie.generation")
b.kv("temperature", cfg.talkie.generation.temperature)
b.kv("top_p", cfg.talkie.generation.topP)
b.kv("max_tokens", cfg.talkie.generation.maxTokens)
b.section("router.narration")
b.kv("temperature", cfg.router.narration.temperature)
b.kv("top_p", cfg.router.narration.topP)
b.kv("max_tokens", cfg.router.narration.maxTokens)
b.kv("max_per_run", cfg.router.narration.maxPerRun)
cfg.router.narration.modelId?.let { b.kv("model_id", str(it)) }
b.section("talkie.narration")
b.kv("temperature", cfg.talkie.narration.temperature)
b.kv("top_p", cfg.talkie.narration.topP)
b.kv("max_tokens", cfg.talkie.narration.maxTokens)
b.kv("max_per_run", cfg.talkie.narration.maxPerRun)
cfg.talkie.narration.modelId?.let { b.kv("model_id", str(it)) }
b.section("orchestration")
b.kv("stage_timeout_ms", cfg.orchestration.stageTimeoutMs)
@@ -87,57 +87,57 @@ object EditableConfig {
),
EditableField(
"router.conversation_keep_last", ConfigFieldType.INT,
getString = { it.router.conversationKeepLast.toString() },
getString = { it.talkie.conversationKeepLast.toString() },
withString = { c, v -> rtr(c) { it.copy(conversationKeepLast = int("router.conversation_keep_last", v)) } },
),
EditableField(
"router.retrieval_k", ConfigFieldType.INT,
getString = { it.router.retrievalK.toString() },
getString = { it.talkie.retrievalK.toString() },
withString = { c, v -> rtr(c) { it.copy(retrievalK = int("router.retrieval_k", v)) } },
),
EditableField(
"router.token_budget", ConfigFieldType.INT,
getString = { it.router.tokenBudget.toString() },
getString = { it.talkie.tokenBudget.toString() },
withString = { c, v -> rtr(c) { it.copy(tokenBudget = int("router.token_budget", v)) } },
),
EditableField(
"router.generation.temperature", ConfigFieldType.DOUBLE,
getString = { it.router.generation.temperature.toString() },
getString = { it.talkie.generation.temperature.toString() },
withString = { c, v -> gen(c) { it.copy(temperature = dbl("router.generation.temperature", v)) } },
),
EditableField(
"router.generation.top_p", ConfigFieldType.DOUBLE,
getString = { it.router.generation.topP.toString() },
getString = { it.talkie.generation.topP.toString() },
withString = { c, v -> gen(c) { it.copy(topP = dbl("router.generation.top_p", v)) } },
),
EditableField(
"router.generation.max_tokens", ConfigFieldType.INT,
getString = { it.router.generation.maxTokens.toString() },
getString = { it.talkie.generation.maxTokens.toString() },
withString = { c, v -> gen(c) { it.copy(maxTokens = int("router.generation.max_tokens", v)) } },
),
EditableField(
"router.narration.temperature", ConfigFieldType.DOUBLE,
getString = { it.router.narration.temperature.toString() },
getString = { it.talkie.narration.temperature.toString() },
withString = { c, v -> nar(c) { it.copy(temperature = dbl("router.narration.temperature", v)) } },
),
EditableField(
"router.narration.top_p", ConfigFieldType.DOUBLE,
getString = { it.router.narration.topP.toString() },
getString = { it.talkie.narration.topP.toString() },
withString = { c, v -> nar(c) { it.copy(topP = dbl("router.narration.top_p", v)) } },
),
EditableField(
"router.narration.max_tokens", ConfigFieldType.INT,
getString = { it.router.narration.maxTokens.toString() },
getString = { it.talkie.narration.maxTokens.toString() },
withString = { c, v -> nar(c) { it.copy(maxTokens = int("router.narration.max_tokens", v)) } },
),
EditableField(
"router.narration.max_per_run", ConfigFieldType.INT,
getString = { it.router.narration.maxPerRun.toString() },
getString = { it.talkie.narration.maxPerRun.toString() },
withString = { c, v -> nar(c) { it.copy(maxPerRun = int("router.narration.max_per_run", v)) } },
),
EditableField(
"router.narration.model_id", ConfigFieldType.STRING,
getString = { it.router.narration.modelId.orEmpty() },
getString = { it.talkie.narration.modelId.orEmpty() },
withString = { c, v -> nar(c) { it.copy(modelId = v.trim().ifBlank { null }) } },
),
EditableField(
@@ -215,7 +215,7 @@ private fun srv(c: CorrexConfig, f: (ServerConfig) -> ServerConfig) = c.copy(ser
private fun tui(c: CorrexConfig, f: (TuiConfig) -> TuiConfig) = c.copy(tui = f(c.tui))
private fun cli(c: CorrexConfig, f: (CliConfig) -> CliConfig) = c.copy(cli = f(c.cli))
private fun tls(c: CorrexConfig, f: (ToolsConfig) -> ToolsConfig) = c.copy(tools = f(c.tools))
private fun rtr(c: CorrexConfig, f: (RouterConfig) -> RouterConfig) = c.copy(router = f(c.router))
private fun rtr(c: CorrexConfig, f: (TalkieConfig) -> TalkieConfig) = c.copy(talkie = f(c.talkie))
private fun gen(c: CorrexConfig, f: (GenerationSettings) -> GenerationSettings) =
rtr(c) { it.copy(generation = f(it.generation)) }
private fun nar(c: CorrexConfig, f: (NarrationSettings) -> NarrationSettings) =
@@ -307,16 +307,16 @@ class ConfigLoaderTest {
parseTomlMethod.isAccessible = true
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
assertEquals("llamacpp", result.router.embedder.backend)
assertEquals(1536, result.router.embedder.dimension)
assertEquals("http://localhost:11000", result.router.embedder.url)
assertEquals("nomic-embed-text", result.router.embedder.modelId)
assertEquals("llamacpp", result.talkie.embedder.backend)
assertEquals(1536, result.talkie.embedder.dimension)
assertEquals("http://localhost:11000", result.talkie.embedder.url)
assertEquals("nomic-embed-text", result.talkie.embedder.modelId)
assertEquals("turbovec", result.router.l3.backend)
assertEquals("/tmp/router/l3.tq", result.router.l3.persistPath)
assertEquals("python3", result.router.l3.pythonExecutable)
assertEquals(1536, result.router.l3.dim)
assertEquals(4, result.router.l3.bitWidth)
assertEquals("turbovec", result.talkie.l3.backend)
assertEquals("/tmp/router/l3.tq", result.talkie.l3.persistPath)
assertEquals("python3", result.talkie.l3.pythonExecutable)
assertEquals(1536, result.talkie.l3.dim)
assertEquals(4, result.talkie.l3.bitWidth)
}
@Test
@@ -331,16 +331,16 @@ class ConfigLoaderTest {
parseTomlMethod.isAccessible = true
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
assertEquals("noop", result.router.embedder.backend)
assertEquals(1536, result.router.embedder.dimension)
assertEquals(null, result.router.embedder.url)
assertEquals(null, result.router.embedder.modelId)
assertEquals("noop", result.talkie.embedder.backend)
assertEquals(1536, result.talkie.embedder.dimension)
assertEquals(null, result.talkie.embedder.url)
assertEquals(null, result.talkie.embedder.modelId)
assertEquals("in_memory", result.router.l3.backend)
assertEquals(null, result.router.l3.persistPath)
assertEquals("python3", result.router.l3.pythonExecutable)
assertEquals(1536, result.router.l3.dim)
assertEquals(4, result.router.l3.bitWidth)
assertEquals("in_memory", result.talkie.l3.backend)
assertEquals(null, result.talkie.l3.persistPath)
assertEquals("python3", result.talkie.l3.pythonExecutable)
assertEquals(1536, result.talkie.l3.dim)
assertEquals(4, result.talkie.l3.bitWidth)
}
@Test
@@ -477,15 +477,15 @@ class ConfigLoaderTest {
parseTomlMethod.isAccessible = true
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
assertEquals(8, result.router.conversationKeepLast)
assertEquals(3, result.router.retrievalK)
assertEquals(8192, result.router.tokenBudget)
assertEquals(0.4, result.router.generation.temperature)
assertEquals(0.8, result.router.generation.topP)
assertEquals(600, result.router.generation.maxTokens)
assertEquals(0.55, result.router.narration.temperature)
assertEquals(2048, result.router.narration.maxTokens)
assertEquals(25, result.router.narration.maxPerRun)
assertEquals(8, result.talkie.conversationKeepLast)
assertEquals(3, result.talkie.retrievalK)
assertEquals(8192, result.talkie.tokenBudget)
assertEquals(0.4, result.talkie.generation.temperature)
assertEquals(0.8, result.talkie.generation.topP)
assertEquals(600, result.talkie.generation.maxTokens)
assertEquals(0.55, result.talkie.narration.temperature)
assertEquals(2048, result.talkie.narration.maxTokens)
assertEquals(25, result.talkie.narration.maxPerRun)
}
@Test
@@ -494,10 +494,10 @@ class ConfigLoaderTest {
parseTomlMethod.isAccessible = true
val result = parseTomlMethod.invoke(ConfigLoader, "[server]\nhost = \"x\"") as CorrexConfig
assertEquals(6, result.router.conversationKeepLast)
assertEquals(512, result.router.generation.maxTokens)
assertEquals(4096, result.router.narration.maxTokens)
assertEquals(100, result.router.narration.maxPerRun)
assertEquals(6, result.talkie.conversationKeepLast)
assertEquals(512, result.talkie.generation.maxTokens)
assertEquals(4096, result.talkie.narration.maxTokens)
assertEquals(100, result.talkie.narration.maxPerRun)
}
@Test
@@ -530,7 +530,7 @@ class ConfigLoaderTest {
val result = ConfigLoader.parseTomlForTest(toml)
assertEquals("llama-cpp:phi-3-mini", result.router.narration.modelId)
assertEquals("llama-cpp:phi-3-mini", result.talkie.narration.modelId)
}
@Test
@@ -542,6 +542,6 @@ class ConfigLoaderTest {
val result = ConfigLoader.parseTomlForTest(toml)
assertNull(result.router.narration.modelId)
assertNull(result.talkie.narration.modelId)
}
}
@@ -26,7 +26,7 @@ class CorrexConfigWriterTest {
shellAllowedExecutables = listOf("git", "ls"),
networkAllowedHosts = listOf("example.com"),
),
router = RouterConfig(
talkie = TalkieConfig(
conversationKeepLast = 7,
retrievalK = 4,
tokenBudget = 5000,
@@ -25,7 +25,7 @@ class EditableConfigTest {
CorrexConfig(),
mapOf("router.generation.temperature" to "0.25", "orchestration.stage_timeout_ms" to "90000"),
).getOrThrow()
assertEquals(0.25, out.router.generation.temperature)
assertEquals(0.25, out.talkie.generation.temperature)
assertEquals(90_000L, out.orchestration.stageTimeoutMs)
}
@@ -53,7 +53,7 @@ data class ContextTruncatedEvent(
@Serializable
@SerialName("RouterNarration")
data class RouterNarrationEvent(
data class TalkieNarrationEvent(
val sessionId: SessionId,
val narrationId: String,
val trigger: String,
@@ -20,7 +20,7 @@ import com.correx.core.events.events.ClarificationRequestedEvent
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.events.CritiqueFindingsRecordedEvent
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.TalkieNarrationEvent
import com.correx.core.events.events.OperatorProfileBoundEvent
import com.correx.core.events.events.ProjectProfileBoundEvent
import com.correx.core.events.events.SessionWorkspaceBoundEvent
@@ -147,7 +147,7 @@ val eventModule = SerializersModule {
subclass(ChatSessionStartedEvent::class)
subclass(SessionWorkingTaskEvent::class)
subclass(ChatTurnEvent::class)
subclass(RouterNarrationEvent::class)
subclass(TalkieNarrationEvent::class)
subclass(SessionWorkspaceBoundEvent::class)
subclass(OperatorProfileBoundEvent::class)
subclass(ProjectProfileBoundEvent::class)
@@ -1,16 +1,16 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.TalkieNarrationEvent
import com.correx.core.events.types.SessionId
import com.correx.core.inference.TokenUsage
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class RouterNarrationEventSerializationTest {
class TalkieNarrationEventSerializationTest {
private val sample = RouterNarrationEvent(
private val sample = TalkieNarrationEvent(
sessionId = SessionId("sess-1"),
narrationId = "narr-42",
trigger = "stage_start",
@@ -24,6 +24,8 @@ class RouterNarrationEventSerializationTest {
@Test
fun `round-trips as polymorphic EventPayload`() {
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
// Wire discriminator is intentionally still "RouterNarration": the class was renamed
// Router->Talkie, but changing a persisted @SerialName would break replay of existing logs.
assertTrue(encoded.contains("\"type\":\"RouterNarration\""), "SerialName must be present: $encoded")
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
assertEquals(sample, decoded)
@@ -31,7 +33,7 @@ class RouterNarrationEventSerializationTest {
@Test
fun `round-trips with optional fields absent`() {
val minimal = RouterNarrationEvent(
val minimal = TalkieNarrationEvent(
sessionId = SessionId("sess-2"),
narrationId = "narr-0",
trigger = "manual",
@@ -1,15 +0,0 @@
package com.correx.core.router
import com.correx.core.events.events.StoredEvent
import com.correx.core.router.model.RouterState
import com.correx.core.sessions.projections.Projection
class RouterProjector(
private val reducer: RouterReducer,
) : Projection<RouterState> {
override fun initial(): RouterState = reducer.initial
override fun apply(state: RouterState, event: StoredEvent): RouterState =
reducer.reduce(state, event)
}
@@ -1,16 +0,0 @@
package com.correx.core.router
import com.correx.core.events.types.SessionId
import com.correx.core.router.model.RouterState
import com.correx.core.sessions.projections.replay.EventReplayer
interface RouterRepository {
suspend fun getRouterState(sessionId: SessionId): RouterState
}
class DefaultRouterRepository(
private val replayer: EventReplayer<RouterState>,
) : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
replayer.rebuild(sessionId)
}
@@ -1,11 +1,11 @@
package com.correx.core.router
package com.correx.core.talkie
import com.correx.core.events.events.IdeaCapturedEvent
import com.correx.core.events.events.IdeaDiscardedEvent
import com.correx.core.events.events.IdeaPromotedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import com.correx.core.router.model.Idea
import com.correx.core.talkie.model.Idea
/**
* Rebuilds the operator's idea board from the event log. Cross-session by design: it folds over
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import com.correx.core.context.model.CompressionMetadata
import com.correx.core.context.model.ContextEntry
@@ -11,20 +11,20 @@ import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.Tokenizer
import com.correx.core.router.model.Idea
import com.correx.core.router.model.NarrationTrigger
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.RouterL2Entry
import com.correx.core.router.model.RouterState
import com.correx.core.router.model.RouterTurn
import com.correx.core.router.model.TurnRole
import com.correx.core.router.model.WorkflowSummary
import com.correx.core.talkie.model.Idea
import com.correx.core.talkie.model.NarrationTrigger
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.talkie.model.RouterL2Entry
import com.correx.core.talkie.model.TalkieState
import com.correx.core.talkie.model.TalkieTurn
import com.correx.core.talkie.model.TurnRole
import com.correx.core.talkie.model.WorkflowSummary
import java.util.*
import kotlinx.coroutines.CancellationException
interface RouterContextBuilder {
interface TalkieContextBuilder {
suspend fun build(
state: RouterState,
state: TalkieState,
budget: TokenBudget,
availableWorkflows: List<WorkflowSummary> = emptyList(),
projectProfileText: String? = null,
@@ -38,18 +38,18 @@ interface RouterContextBuilder {
* Default delegates to [build] so that anonymous test stubs that only override [build] compile
* without needing a stub override.
*/
suspend fun buildNarrationContext(state: RouterState, trigger: NarrationTrigger, budget: TokenBudget): ContextPack =
suspend fun buildNarrationContext(state: TalkieState, trigger: NarrationTrigger, budget: TokenBudget): ContextPack =
build(state, budget, emptyList(), null)
}
class DefaultRouterContextBuilder(
private val config: RouterConfig,
class DefaultTalkieContextBuilder(
private val config: TalkieConfig,
private val tokenizer: Tokenizer? = null,
// Cross-session ideas to surface to the router (router-only context). Read fresh each build so
// newly-captured ideas appear on the next turn; injected via the constructor so the build()
// interface — and its many test doubles — stay unchanged.
private val ideaProvider: () -> List<Idea> = { emptyList() },
) : RouterContextBuilder {
) : TalkieContextBuilder {
companion object {
private val SYSTEM_PROMPT =
@@ -106,7 +106,7 @@ class DefaultRouterContextBuilder(
}
override suspend fun build(
state: RouterState,
state: TalkieState,
budget: TokenBudget,
availableWorkflows: List<WorkflowSummary>,
projectProfileText: String?,
@@ -179,7 +179,7 @@ class DefaultRouterContextBuilder(
// --- Conversation turns (L1) ---
// The current (protected) user turn is the last element in conversationHistory,
// regardless of conversationKeepLast — it must always be present.
val protectedUserTurn: RouterTurn? = state.conversationHistory.lastOrNull()
val protectedUserTurn: TalkieTurn? = state.conversationHistory.lastOrNull()
// Apply conversationKeepLast cap. The protected turn may or may not fall within this window.
val cappedTurns = state.conversationHistory.takeLast(config.conversationKeepLast)
@@ -317,7 +317,7 @@ class DefaultRouterContextBuilder(
)
}
private fun buildWorkflowStatusContent(state: RouterState, stageOverride: String? = null): String {
private fun buildWorkflowStatusContent(state: TalkieState, stageOverride: String? = null): String {
val status = capitalizeFirst(state.workflowStatus.name.lowercase())
val stage = stageOverride ?: state.currentStageId?.value ?: "none"
return "Status: $status, Stage: $stage"
@@ -400,7 +400,7 @@ class DefaultRouterContextBuilder(
* narration lines never enter the conversation flow.
*/
override suspend fun buildNarrationContext(
state: RouterState,
state: TalkieState,
trigger: NarrationTrigger,
budget: TokenBudget,
): ContextPack {
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.events.ChatTurnRole
@@ -8,7 +8,7 @@ import com.correx.core.events.events.IdeaCapturedEvent
import com.correx.core.events.events.L3MemoryRetrievedEvent
import com.correx.core.events.events.L3RetrievedHit
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.TalkieNarrationEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.WorkflowProposedEvent
import com.correx.core.events.stores.EventStore
@@ -23,53 +23,53 @@ import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ModelCapability
import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.TokenUsage
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.router.model.NarrationTrigger
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.RouterResponse
import com.correx.core.router.model.WorkflowSummary
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import com.correx.core.talkie.model.NarrationTrigger
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.talkie.model.TalkieResponse
import com.correx.core.talkie.model.WorkflowSummary
import kotlinx.coroutines.CancellationException
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID
private val log = LoggerFactory.getLogger(DefaultRouterFacade::class.java)
private val log = LoggerFactory.getLogger(DefaultTalkieFacade::class.java)
interface RouterFacade {
interface TalkieFacade {
suspend fun onUserInput(
sessionId: SessionId,
input: String,
mode: ChatMode = ChatMode.CHAT,
): RouterResponse
): TalkieResponse
suspend fun narrate(sessionId: SessionId, trigger: NarrationTrigger)
}
class DefaultRouterFacade(
private val routerRepository: RouterRepository,
private val routerContextBuilder: RouterContextBuilder,
class DefaultTalkieFacade(
private val routerRepository: TalkieRepository,
private val routerContextBuilder: TalkieContextBuilder,
private val inferenceRouter: InferenceRouter,
private val eventStore: EventStore,
private val config: RouterConfig,
private val config: TalkieConfig,
private val validateSteering: (suspend (String) -> String?)? = null,
private val embedder: Embedder,
private val l3MemoryStore: L3MemoryStore,
private val workflowSummaryProvider: () -> List<WorkflowSummary> = { emptyList() },
private val sessionProfileProvider: suspend (SessionId) -> String? = { null },
) : RouterFacade {
) : TalkieFacade {
override suspend fun onUserInput(
sessionId: SessionId,
input: String,
mode: ChatMode,
): RouterResponse {
): TalkieResponse {
// Emit USER turn event and get the turnId for retrieval dedup
val userTurnId = emitChatTurn(sessionId, input, ChatTurnRole.USER)
// Rebuild state with user turn appended
var state = routerRepository.getRouterState(sessionId)
var state = routerRepository.getTalkieState(sessionId)
val effectiveStageId = state.currentStageId ?: StageId.NONE
// L3 retrieval — non-fatal; results fed back into state via event.
@@ -119,7 +119,7 @@ class DefaultRouterFacade(
),
)
// Rebuild state so lastRetrievedMemory reflects this turn
state = routerRepository.getRouterState(sessionId)
state = routerRepository.getTalkieState(sessionId)
val contextPack = routerContextBuilder.build(state, config.tokenBudget, workflowSummaryProvider(), sessionProfileProvider(sessionId))
@@ -199,11 +199,11 @@ class DefaultRouterFacade(
}
}
return RouterResponse(content = content, steeringEmitted = steeringEmitted)
return TalkieResponse(content = content, steeringEmitted = steeringEmitted)
}
override suspend fun narrate(sessionId: SessionId, trigger: NarrationTrigger) {
val state = routerRepository.getRouterState(sessionId)
val state = routerRepository.getTalkieState(sessionId)
val effectiveStageId = trigger.stageId?.let { StageId(it) } ?: state.currentStageId ?: StageId.NONE
val contextPack = routerContextBuilder.buildNarrationContext(state, trigger, config.tokenBudget)
@@ -234,7 +234,7 @@ class DefaultRouterFacade(
causationId = null,
correlationId = null,
),
payload = RouterNarrationEvent(
payload = TalkieNarrationEvent(
sessionId = sessionId,
narrationId = narrationId,
trigger = trigger.kind,
@@ -0,0 +1,15 @@
package com.correx.core.talkie
import com.correx.core.events.events.StoredEvent
import com.correx.core.talkie.model.TalkieState
import com.correx.core.sessions.projections.Projection
class TalkieProjector(
private val reducer: TalkieReducer,
) : Projection<TalkieState> {
override fun initial(): TalkieState = reducer.initial
override fun apply(state: TalkieState, event: StoredEvent): TalkieState =
reducer.reduce(state, event)
}
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.events.L3MemoryRetrievedEvent
@@ -11,22 +11,22 @@ import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.router.model.RouterL2Entry
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.TurnRole
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.talkie.model.RouterL2Entry
import com.correx.core.talkie.model.TalkieState
import com.correx.core.talkie.model.TalkieTurn
import com.correx.core.talkie.model.StageOutcomeKind
import com.correx.core.talkie.model.TurnRole
import com.correx.core.talkie.model.WorkflowStatus
import kotlinx.datetime.Instant
interface RouterReducer {
val initial: RouterState
fun reduce(state: RouterState, event: StoredEvent): RouterState
interface TalkieReducer {
val initial: TalkieState
fun reduce(state: TalkieState, event: StoredEvent): TalkieState
}
class DefaultRouterReducer : RouterReducer {
class DefaultTalkieReducer : TalkieReducer {
override val initial: RouterState = RouterState(
override val initial: TalkieState = TalkieState(
sessionId = null,
workflowStatus = WorkflowStatus.IDLE,
currentStageId = null,
@@ -34,7 +34,7 @@ class DefaultRouterReducer : RouterReducer {
conversationHistory = emptyList(),
)
override fun reduce(state: RouterState, event: StoredEvent): RouterState {
override fun reduce(state: TalkieState, event: StoredEvent): TalkieState {
return when (val payload = event.payload) {
is WorkflowStartedEvent -> handleWorkflowStarted(state, event)
is WorkflowCompletedEvent -> handleWorkflowCompleted(state)
@@ -50,7 +50,7 @@ class DefaultRouterReducer : RouterReducer {
}
}
private fun handleWorkflowStarted(state: RouterState, event: StoredEvent): RouterState {
private fun handleWorkflowStarted(state: TalkieState, event: StoredEvent): TalkieState {
val payload = event.payload as WorkflowStartedEvent
return state.copy(
sessionId = payload.sessionId,
@@ -59,33 +59,33 @@ class DefaultRouterReducer : RouterReducer {
)
}
private fun handleWorkflowCompleted(state: RouterState): RouterState {
private fun handleWorkflowCompleted(state: TalkieState): TalkieState {
return state.copy(
workflowStatus = WorkflowStatus.COMPLETED,
currentStageId = null
)
}
private fun handleWorkflowFailed(state: RouterState): RouterState {
private fun handleWorkflowFailed(state: TalkieState): TalkieState {
return state.copy(
workflowStatus = WorkflowStatus.FAILED,
currentStageId = null
)
}
private fun handleOrchestrationPaused(state: RouterState): RouterState {
private fun handleOrchestrationPaused(state: TalkieState): TalkieState {
return state.copy(
workflowStatus = WorkflowStatus.PAUSED
)
}
private fun handleOrchestrationResumed(state: RouterState): RouterState {
private fun handleOrchestrationResumed(state: TalkieState): TalkieState {
return state.copy(
workflowStatus = WorkflowStatus.RUNNING
)
}
private fun handleStageCompleted(state: RouterState, event: StoredEvent): RouterState {
private fun handleStageCompleted(state: TalkieState, event: StoredEvent): TalkieState {
val payload = event.payload as StageCompletedEvent
val entry = RouterL2Entry(
stageId = payload.stageId,
@@ -100,7 +100,7 @@ class DefaultRouterReducer : RouterReducer {
)
}
private fun handleStageFailed(state: RouterState, event: StoredEvent): RouterState {
private fun handleStageFailed(state: TalkieState, event: StoredEvent): TalkieState {
val payload = event.payload as StageFailedEvent
val entry = RouterL2Entry(
stageId = payload.stageId,
@@ -120,16 +120,16 @@ class DefaultRouterReducer : RouterReducer {
// adr-0003 §4: harvest user steering into the current stage's decision point.
// Accumulate here; handleStageCompleted/Failed attach and clear. Pure field
// extraction from the event stream — no inference.
private fun handleSteeringNote(state: RouterState, payload: SteeringNoteAddedEvent): RouterState =
private fun handleSteeringNote(state: TalkieState, payload: SteeringNoteAddedEvent): TalkieState =
state.copy(pendingSteeringNotes = state.pendingSteeringNotes + payload.content)
private fun handleChatTurn(state: RouterState, event: StoredEvent): RouterState {
private fun handleChatTurn(state: TalkieState, event: StoredEvent): TalkieState {
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(
val turn = TalkieTurn(
role = turnRole,
content = payload.content,
timestamp = Instant.fromEpochMilliseconds(payload.timestampMs),
@@ -0,0 +1,16 @@
package com.correx.core.talkie
import com.correx.core.events.types.SessionId
import com.correx.core.talkie.model.TalkieState
import com.correx.core.sessions.projections.replay.EventReplayer
interface TalkieRepository {
suspend fun getTalkieState(sessionId: SessionId): TalkieState
}
class DefaultTalkieRepository(
private val replayer: EventReplayer<TalkieState>,
) : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
}
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import com.correx.core.events.events.ProposedWorkflow
import kotlinx.serialization.json.Json
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
import com.correx.core.events.types.SessionId
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
/**
* Cross-session memory store interface for vector-based retrieval.
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.stores.EventStore
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
import com.correx.core.events.types.SessionId
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
/**
* An [L3MemoryStore] whose vectors persist independently of the JVM (e.g. a
@@ -1,4 +1,4 @@
package com.correx.core.router.model
package com.correx.core.talkie.model
/** An active idea on the operator's cross-session scratchpad (a captured, not-yet-discarded note). */
data class Idea(
@@ -1,9 +1,9 @@
package com.correx.core.router.model
package com.correx.core.talkie.model
/**
* Describes the event or condition that prompted a narration inference call.
*
* The trigger is stored verbatim in [RouterNarrationEvent.trigger] so it can be
* The trigger is stored verbatim in [TalkieNarrationEvent.trigger] so it can be
* replayed and displayed without re-querying the environment.
*/
data class NarrationTrigger(
@@ -1,11 +1,11 @@
package com.correx.core.router.model
package com.correx.core.talkie.model
import com.correx.core.context.model.TokenBudget
import com.correx.core.inference.GenerationConfig
import kotlinx.serialization.Serializable
@Serializable
data class RouterConfig(
data class TalkieConfig(
val conversationKeepLast: Int = 6,
val retrievalK: Int = 5,
val tokenBudget: TokenBudget = TokenBudget(limit = 4096),
@@ -1,9 +1,9 @@
package com.correx.core.router.model
package com.correx.core.talkie.model
import kotlinx.serialization.Serializable
@Serializable
data class RouterResponse(
data class TalkieResponse(
val content: String,
val steeringEmitted: Boolean = false,
)
@@ -1,4 +1,4 @@
package com.correx.core.router.model
package com.correx.core.talkie.model
import com.correx.core.events.events.L3RetrievedHit
import com.correx.core.events.types.SessionId
@@ -41,7 +41,7 @@ data class RouterL2Entry(
)
@Serializable
data class RouterTurn(
data class TalkieTurn(
val role: TurnRole,
val content: String,
val timestamp: Instant,
@@ -49,12 +49,12 @@ data class RouterTurn(
)
@Serializable
data class RouterState(
data class TalkieState(
val sessionId: SessionId? = null,
val workflowStatus: WorkflowStatus = WorkflowStatus.IDLE,
val currentStageId: StageId? = null,
val l2Memory: List<RouterL2Entry> = emptyList(),
val conversationHistory: List<RouterTurn> = emptyList(),
val conversationHistory: List<TalkieTurn> = emptyList(),
val lastRetrievedMemory: List<L3RetrievedHit> = emptyList(),
/**
* Steering notes accumulated during the current stage, harvested into the
@@ -1,4 +1,4 @@
package com.correx.core.router.model
package com.correx.core.talkie.model
data class WorkflowSummary(
val id: String,
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
@@ -1,4 +1,4 @@
package com.correx.core.router
package com.correx.core.talkie
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
@@ -1,4 +1,4 @@
package com.correx.core.router.l3
package com.correx.core.talkie.l3
import com.correx.core.events.types.SessionId
import com.correx.core.utils.TypeId
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
dependencies {
implementation project(":core:tools")
implementation project(":core:events")
implementation project(":core:router")
implementation project(":core:talkie")
implementation project(":core:inference")
implementation project(":core:approvals")
implementation project(":core:sessions")
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
}
dependencies {
implementation project(':core:router')
implementation project(':core:talkie')
implementation project(':core:events')
testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test"
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
}
dependencies {
implementation project(':core:router')
implementation project(':core:talkie')
implementation project(':core:sessions')
implementation project(':core:events')
testImplementation "org.junit.jupiter:junit-jupiter"
@@ -1,9 +1,9 @@
package com.correx.infrastructure.router.turbovec
import com.correx.core.router.l3.L3Hit
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3Query
import com.correx.core.router.l3.RehydratableL3MemoryStore
import com.correx.core.talkie.l3.L3Hit
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3Query
import com.correx.core.talkie.l3.RehydratableL3MemoryStore
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
@@ -22,17 +22,17 @@ import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ModelCapability
import com.correx.core.inference.NoopEmbedder
import com.correx.core.inference.Tokenizer
import com.correx.core.router.DefaultRouterContextBuilder
import com.correx.core.router.DefaultRouterFacade
import com.correx.core.router.IdeaReader
import com.correx.core.router.DefaultRouterReducer
import com.correx.core.router.DefaultRouterRepository
import com.correx.core.router.RouterFacade
import com.correx.core.router.RouterProjector
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.WorkflowSummary
import com.correx.core.talkie.DefaultTalkieContextBuilder
import com.correx.core.talkie.DefaultTalkieFacade
import com.correx.core.talkie.IdeaReader
import com.correx.core.talkie.DefaultTalkieReducer
import com.correx.core.talkie.DefaultTalkieRepository
import com.correx.core.talkie.TalkieFacade
import com.correx.core.talkie.TalkieProjector
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.talkie.model.WorkflowSummary
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.tools.contract.Tool
@@ -306,22 +306,22 @@ object InfrastructureModule {
}
}
fun createRouterFacade(
fun createTalkieFacade(
eventStore: EventStore,
inferenceRouter: InferenceRouter,
config: RouterConfig = RouterConfig(),
config: TalkieConfig = TalkieConfig(),
tokenizer: Tokenizer? = null,
embedder: Embedder = createNoopEmbedder(),
l3MemoryStore: L3MemoryStore = createInMemoryL3MemoryStore(),
workflowSummaryProvider: () -> List<WorkflowSummary> = { emptyList() },
sessionProfileProvider: suspend (SessionId) -> String? = { null },
): RouterFacade {
val reducer = DefaultRouterReducer()
val projector = RouterProjector(reducer)
): TalkieFacade {
val reducer = DefaultTalkieReducer()
val projector = TalkieProjector(reducer)
val replayer = DefaultEventReplayer(eventStore, projector)
val repository = DefaultRouterRepository(replayer)
val repository = DefaultTalkieRepository(replayer)
val ideaReader = IdeaReader(eventStore)
val contextBuilder = DefaultRouterContextBuilder(config, tokenizer, ideaProvider = { ideaReader.activeIdeas() })
return DefaultRouterFacade(repository, contextBuilder, inferenceRouter, eventStore, config, null, embedder, l3MemoryStore, workflowSummaryProvider, sessionProfileProvider)
val contextBuilder = DefaultTalkieContextBuilder(config, tokenizer, ideaProvider = { ideaReader.activeIdeas() })
return DefaultTalkieFacade(repository, contextBuilder, inferenceRouter, eventStore, config, null, embedder, l3MemoryStore, workflowSummaryProvider, sessionProfileProvider)
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ include ':core:validation'
include ':core:approvals'
include ':core:tools'
include ':core:toolintent'
include ':core:router'
include ':core:talkie'
include ':core:sessions'
include ':core:tasks'
include ':core:config'
+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
dependencies {
testImplementation(project(":core:events"))
testImplementation(project(":core:router"))
testImplementation(project(":core:talkie"))
testImplementation(project(":core:sessions"))
testImplementation(project(":core:transitions"))
testImplementation(project(":core:validation"))
@@ -8,12 +8,12 @@ import com.correx.core.events.events.StoredEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.router.l3.L3Hit
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MetadataRehydrator
import com.correx.core.router.l3.L3Query
import com.correx.core.router.l3.RehydratableL3MemoryStore
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.L3Hit
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MetadataRehydrator
import com.correx.core.talkie.l3.L3Query
import com.correx.core.talkie.l3.RehydratableL3MemoryStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Instant
@@ -5,15 +5,15 @@ import com.correx.core.context.model.TokenBudget
import com.correx.core.events.events.L3RetrievedHit
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.router.DefaultRouterContextBuilder
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.WorkflowSummary
import com.correx.core.router.model.RouterL2Entry
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.TurnRole
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.talkie.DefaultTalkieContextBuilder
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.talkie.model.WorkflowSummary
import com.correx.core.talkie.model.RouterL2Entry
import com.correx.core.talkie.model.TalkieState
import com.correx.core.talkie.model.TalkieTurn
import com.correx.core.talkie.model.StageOutcomeKind
import com.correx.core.talkie.model.TurnRole
import com.correx.core.talkie.model.WorkflowStatus
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
@@ -24,12 +24,12 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RouterContextBuilderTest {
class TalkieContextBuilderTest {
private val config = RouterConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 200))
private val builder = DefaultRouterContextBuilder(config)
private val config = TalkieConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 200))
private val builder = DefaultTalkieContextBuilder(config)
private fun buildPack(state: RouterState, budget: TokenBudget): ContextPack = runBlocking {
private fun buildPack(state: TalkieState, budget: TokenBudget): ContextPack = runBlocking {
builder.build(state, budget)
}
@@ -43,12 +43,12 @@ class RouterContextBuilderTest {
@Test
fun `build fits within budget when all entries are small`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hi", clock.now()),
TalkieTurn(TurnRole.USER, "hi", clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
@@ -60,14 +60,14 @@ class RouterContextBuilderTest {
fun `build drops entries when budget is exceeded`() {
// Build a state where conversation + L2 entries together exceed the tight budget
val longContent = "x".repeat(800) // ~400 tokens each
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longContent, clock.now()),
RouterTurn(TurnRole.ROUTER, longContent, clock.now()),
RouterTurn(TurnRole.USER, longContent, clock.now()),
TalkieTurn(TurnRole.USER, longContent, clock.now()),
TalkieTurn(TurnRole.ROUTER, longContent, clock.now()),
TalkieTurn(TurnRole.USER, longContent, clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
@@ -82,10 +82,10 @@ class RouterContextBuilderTest {
fun `build drops entries oldest-first for L2 memory when budget forces partial drop`() {
// Each L2 entry: "Stage sN (SUCCESS): " + 200-char summary ≈ 55 tokens each.
// Use a config with conversationKeepLast=0 so only L0 + L2 compete for budget.
val configNoConv = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builderNoConv = DefaultRouterContextBuilder(configNoConv)
val configNoConv = TalkieConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builderNoConv = DefaultTalkieContextBuilder(configNoConv)
val summary = "x".repeat(200) // ~50 tokens; with prefix "Stage sN (SUCCESS): " ~55 tokens total
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -115,18 +115,18 @@ class RouterContextBuilderTest {
@Test
fun `build drops conversation turns oldest-first when conversationKeepLast exceeded`() {
val configWide = RouterConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builderWide = DefaultRouterContextBuilder(configWide)
val configWide = TalkieConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builderWide = DefaultTalkieContextBuilder(configWide)
val longContent = "x".repeat(400)
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longContent, Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, longContent, Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-04T00:00:00Z")),
TalkieTurn(TurnRole.USER, longContent, Instant.parse("2026-01-01T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-02T00:00:00Z")),
TalkieTurn(TurnRole.USER, longContent, Instant.parse("2026-01-03T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-04T00:00:00Z")),
),
)
// Budget allows L0 + 2 long conversation turns; oldest 2 turns are dropped.
@@ -144,7 +144,7 @@ class RouterContextBuilderTest {
@Test
fun `L0 system prompt is always present`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.IDLE,
currentStageId = null,
@@ -156,7 +156,7 @@ class RouterContextBuilderTest {
@Test
fun `L0 workflow status is always present`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.FAILED,
currentStageId = StageId("failed-stage"),
@@ -171,7 +171,7 @@ class RouterContextBuilderTest {
@Test
fun `L0 entries survive zero budget`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.COMPLETED,
currentStageId = null,
@@ -185,12 +185,12 @@ class RouterContextBuilderTest {
@Test
fun `L0 entries survive budget that cannot cover L1 or L2`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "x".repeat(500), clock.now()),
TalkieTurn(TurnRole.USER, "x".repeat(500), clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "x".repeat(500), StageOutcomeKind.SUCCESS, clock.now()),
@@ -215,10 +215,10 @@ class RouterContextBuilderTest {
// Each L2 entry: "Stage sN (SUCCESS): " + 100-char summary ≈ 30 tokens each.
// L0 ~30 tokens; budget 90 leaves ~60. Two entries (~30 each = ~60) fit; oldest dropped.
val summary = "x".repeat(100) // ~25 tokens; with prefix ~30 tokens total
val configTight = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builderTight = DefaultRouterContextBuilder(configTight)
val configTight = TalkieConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builderTight = DefaultTalkieContextBuilder(configTight)
val baseTime = Instant.parse("2026-01-01T00:00:00Z")
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -251,9 +251,9 @@ class RouterContextBuilderTest {
@Test
fun `L2 entries fit within budget are all retained`() {
val config = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
val config = TalkieConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultTalkieContextBuilder(config)
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -274,7 +274,7 @@ class RouterContextBuilderTest {
@Test
fun `system prompt is classified as L0`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.IDLE,
)
@@ -286,7 +286,7 @@ class RouterContextBuilderTest {
@Test
fun `workflow status is classified as L0`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -298,13 +298,13 @@ class RouterContextBuilderTest {
@Test
fun `conversation turns are classified as L1`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello", clock.now()),
RouterTurn(TurnRole.ROUTER, "hi", clock.now()),
TalkieTurn(TurnRole.USER, "hello", clock.now()),
TalkieTurn(TurnRole.ROUTER, "hi", clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
@@ -317,7 +317,7 @@ class RouterContextBuilderTest {
@Test
fun `l2 memory entries are classified as L2`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -335,12 +335,12 @@ class RouterContextBuilderTest {
@Test
fun `no entries other than L0 L1 L2 appear in pack`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello", clock.now()),
TalkieTurn(TurnRole.USER, "hello", clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
@@ -359,18 +359,18 @@ class RouterContextBuilderTest {
@Test
fun `conversationHistory respects conversationKeepLast from config`() {
val config = RouterConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
val config = TalkieConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultTalkieContextBuilder(config)
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "one", Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "two", Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, "three", Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "four", Instant.parse("2026-01-04T00:00:00Z")),
RouterTurn(TurnRole.USER, "five", Instant.parse("2026-01-05T00:00:00Z")),
TalkieTurn(TurnRole.USER, "one", Instant.parse("2026-01-01T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, "two", Instant.parse("2026-01-02T00:00:00Z")),
TalkieTurn(TurnRole.USER, "three", Instant.parse("2026-01-03T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, "four", Instant.parse("2026-01-04T00:00:00Z")),
TalkieTurn(TurnRole.USER, "five", Instant.parse("2026-01-05T00:00:00Z")),
),
)
val pack = runBlocking { builder.build(state, TokenBudget(limit = 10000)) }
@@ -381,17 +381,17 @@ class RouterContextBuilderTest {
@Test
fun `conversationHistory takeLast preserves ordering`() {
val config = RouterConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
val config = TalkieConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultTalkieContextBuilder(config)
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "first", Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "second", Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, "third", Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "fourth", Instant.parse("2026-01-04T00:00:00Z")),
TalkieTurn(TurnRole.USER, "first", Instant.parse("2026-01-01T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, "second", Instant.parse("2026-01-02T00:00:00Z")),
TalkieTurn(TurnRole.USER, "third", Instant.parse("2026-01-03T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, "fourth", Instant.parse("2026-01-04T00:00:00Z")),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
@@ -409,7 +409,7 @@ class RouterContextBuilderTest {
@Test
fun `build with empty state produces L0 only`() {
val state = RouterState()
val state = TalkieState()
val pack = buildPack(state, TokenBudget(limit = 10000))
val l0Entries = pack.layers[ContextLayer.L0]
assertNotNull(l0Entries)
@@ -420,7 +420,7 @@ class RouterContextBuilderTest {
@Test
fun `pack contains correct metadata on empty state`() {
val state = RouterState()
val state = TalkieState()
val pack = buildPack(state, TokenBudget(limit = 5000))
assertEquals(5000, pack.budgetLimit)
assertEquals(0, pack.compressionMetadata.entriesDropped)
@@ -430,7 +430,7 @@ class RouterContextBuilderTest {
@Test
fun `pack has correct context pack id and session info`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -447,7 +447,7 @@ class RouterContextBuilderTest {
@Test
fun `system prompt has expected content`() {
val state = RouterState()
val state = TalkieState()
val pack = buildPack(state, TokenBudget(limit = 10000))
val systemEntry = pack.layers[ContextLayer.L0]?.find { it.sourceType == "systemPrompt" }
assertNotNull(systemEntry)
@@ -461,7 +461,7 @@ class RouterContextBuilderTest {
@Test
fun `workflow status entry contains status and stage`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.COMPLETED,
currentStageId = null,
@@ -475,12 +475,12 @@ class RouterContextBuilderTest {
@Test
fun `conversation turn entry is formatted with role prefix`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "user message", clock.now()),
TalkieTurn(TurnRole.USER, "user message", clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
@@ -492,7 +492,7 @@ class RouterContextBuilderTest {
@Test
fun `l2 entry is formatted with stage outcome and summary`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -516,12 +516,12 @@ class RouterContextBuilderTest {
@Test
fun `budgetUsed equals sum of tokenEstimate of all retained entries`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello world", clock.now()),
TalkieTurn(TurnRole.USER, "hello world", clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
@@ -535,14 +535,14 @@ class RouterContextBuilderTest {
@Test
fun `compressionMetadata entriesDropped reflects dropped count`() {
val config = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
val config = TalkieConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10))
val builder = DefaultTalkieContextBuilder(config)
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "x".repeat(200), clock.now()),
TalkieTurn(TurnRole.USER, "x".repeat(200), clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "x".repeat(500), StageOutcomeKind.SUCCESS, clock.now()),
@@ -561,17 +561,17 @@ class RouterContextBuilderTest {
@Test
fun `build with full state including all layers`() {
val config = RouterConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
val config = TalkieConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultTalkieContextBuilder(config)
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "first turn", Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "first reply", Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, "second turn", Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "second reply", Instant.parse("2026-01-04T00:00:00Z")),
TalkieTurn(TurnRole.USER, "first turn", Instant.parse("2026-01-01T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, "first reply", Instant.parse("2026-01-02T00:00:00Z")),
TalkieTurn(TurnRole.USER, "second turn", Instant.parse("2026-01-03T00:00:00Z")),
TalkieTurn(TurnRole.ROUTER, "second reply", Instant.parse("2026-01-04T00:00:00Z")),
),
l2Memory = listOf(
RouterL2Entry(
@@ -614,7 +614,7 @@ class RouterContextBuilderTest {
@Test
fun `build with null sessionId produces unknown pack id`() {
val state = RouterState(
val state = TalkieState(
workflowStatus = WorkflowStatus.IDLE,
)
val pack = buildPack(state, TokenBudget(limit = 1000))
@@ -627,7 +627,7 @@ class RouterContextBuilderTest {
@Test
fun `L3 hits injected into pack ordered by score descending`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -667,7 +667,7 @@ class RouterContextBuilderTest {
@Test
fun `L3 entries carry recalled memory label in content`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
lastRetrievedMemory = listOf(
@@ -694,7 +694,7 @@ class RouterContextBuilderTest {
fun `L3 hits dropped when budget exhausted and counted in entriesDropped`() {
// Budget tight enough for L0 but not L3 entries with long text
val longText = "x".repeat(800) // ~200 tokens
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
lastRetrievedMemory = listOf(
@@ -721,14 +721,14 @@ class RouterContextBuilderTest {
@Test
fun `system prompt and current user turn survive tight budget`() {
val longContent = "x".repeat(800) // ~200 tokens each
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longContent, Instant.parse("2026-01-01T00:00:00Z"), turnId = "old-turn"),
RouterTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-02T00:00:00Z"), turnId = "mid-turn"),
RouterTurn(TurnRole.USER, "current user input", Instant.parse("2026-01-03T00:00:00Z"), turnId = "current-turn"),
TalkieTurn(TurnRole.USER, longContent, Instant.parse("2026-01-01T00:00:00Z"), turnId = "old-turn"),
TalkieTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-02T00:00:00Z"), turnId = "mid-turn"),
TalkieTurn(TurnRole.USER, "current user input", Instant.parse("2026-01-03T00:00:00Z"), turnId = "current-turn"),
),
)
// Budget 60: L0 frame ~30 tokens -> remaining ~30; current user turn ~4 tokens -> protected.
@@ -748,17 +748,17 @@ class RouterContextBuilderTest {
@Test
fun `oldest conversation turn is dropped before newest when budget is tight`() {
val config3 = RouterConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 10000))
val builder3 = DefaultRouterContextBuilder(config3)
val config3 = TalkieConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 10000))
val builder3 = DefaultTalkieContextBuilder(config3)
val longContent = "x".repeat(800) // ~200 tokens each
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "oldest turn $longContent", Instant.parse("2026-01-01T00:00:00Z"), turnId = "t1"),
RouterTurn(TurnRole.ROUTER, "middle turn $longContent", Instant.parse("2026-01-02T00:00:00Z"), turnId = "t2"),
RouterTurn(TurnRole.USER, "newest turn", Instant.parse("2026-01-03T00:00:00Z"), turnId = "t3"),
TalkieTurn(TurnRole.USER, "oldest turn $longContent", Instant.parse("2026-01-01T00:00:00Z"), turnId = "t1"),
TalkieTurn(TurnRole.ROUTER, "middle turn $longContent", Instant.parse("2026-01-02T00:00:00Z"), turnId = "t2"),
TalkieTurn(TurnRole.USER, "newest turn", Instant.parse("2026-01-03T00:00:00Z"), turnId = "t3"),
),
)
// Budget 100: L0 frame ~30 tokens -> remaining ~70; newest turn ~3 tokens -> protected.
@@ -776,12 +776,12 @@ class RouterContextBuilderTest {
@Test
fun `L3 and L2 dropped before recent conversation when budget is tight`() {
val longContent = "x".repeat(200) // ~50 tokens each
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "current user message", clock.now(), turnId = "curr"),
TalkieTurn(TurnRole.USER, "current user message", clock.now(), turnId = "curr"),
),
lastRetrievedMemory = listOf(
L3RetrievedHit(
@@ -816,7 +816,7 @@ class RouterContextBuilderTest {
@Test
fun `available workflows entry is injected into L0 when non-empty`() {
val workflows = listOf(WorkflowSummary("healthcheck", "Runs the health check pipeline", listOf("collect", "report")))
val pack = runBlocking { builder.build(RouterState(), TokenBudget(limit = 10000), availableWorkflows = workflows) }
val pack = runBlocking { builder.build(TalkieState(), TokenBudget(limit = 10000), availableWorkflows = workflows) }
val entry = (pack.layers[ContextLayer.L0] ?: emptyList()).find { it.sourceType == "availableWorkflows" }
assertNotNull(entry)
assertTrue(entry!!.content.contains("## Available workflows"))
@@ -825,27 +825,27 @@ class RouterContextBuilderTest {
@Test
fun `available workflows entry is omitted when list is empty`() {
val pack = runBlocking { builder.build(RouterState(), TokenBudget(limit = 10000)) }
val pack = runBlocking { builder.build(TalkieState(), TokenBudget(limit = 10000)) }
assertNull((pack.layers[ContextLayer.L0] ?: emptyList()).find { it.sourceType == "availableWorkflows" })
}
@Test
fun `project profile text is injected into L0 when non-null`() {
val text = "## Project profile\nA Kotlin service.\n### Conventions\n- Use runCatching"
val pack = runBlocking { builder.build(RouterState(), TokenBudget(limit = 10000), projectProfileText = text) }
val pack = runBlocking { builder.build(TalkieState(), TokenBudget(limit = 10000), projectProfileText = text) }
val entry = (pack.layers[ContextLayer.L0] ?: emptyList()).find { it.sourceType == "projectProfile" }
assertEquals(text, entry!!.content)
}
@Test
fun `project profile text is omitted when null or blank`() {
val pack = runBlocking { builder.build(RouterState(), TokenBudget(limit = 10000), projectProfileText = " ") }
val pack = runBlocking { builder.build(TalkieState(), TokenBudget(limit = 10000), projectProfileText = " ") }
assertNull((pack.layers[ContextLayer.L0] ?: emptyList()).find { it.sourceType == "projectProfile" })
}
@Test
fun `system prompt contains triage directive not bare refusal`() {
val pack = buildPack(RouterState(), TokenBudget(limit = 10000))
val pack = buildPack(TalkieState(), TokenBudget(limit = 10000))
val content = pack.layers[ContextLayer.L0]?.find { it.sourceType == "systemPrompt" }!!.content
assertFalse(content.contains("if something is not in context, say you do not have it"))
assertTrue(content.contains("Available workflows", ignoreCase = true))
@@ -855,13 +855,13 @@ class RouterContextBuilderTest {
@Test
fun `truncatedLayers populated with affected layers when drops occur`() {
val longText = "x".repeat(800)
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longText, clock.now(), turnId = "old"),
RouterTurn(TurnRole.USER, "current", clock.now(), turnId = "curr"),
TalkieTurn(TurnRole.USER, longText, clock.now(), turnId = "old"),
TalkieTurn(TurnRole.USER, "current", clock.now(), turnId = "curr"),
),
lastRetrievedMemory = listOf(
L3RetrievedHit(
@@ -889,12 +889,12 @@ class RouterContextBuilderTest {
// The current user turn must still be included; budgetUsed must report the true total;
// appliedStrategies must contain "BudgetExceeded".
val longUserTurn = "x".repeat(200) // ~50 tokens
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longUserTurn, clock.now(), turnId = "current"),
TalkieTurn(TurnRole.USER, longUserTurn, clock.now(), turnId = "current"),
),
)
// Budget 1: system prompt + workflow status alone exceed this.
@@ -928,16 +928,16 @@ class RouterContextBuilderTest {
@Test
fun `conversationKeepLast zero still includes the current user turn`() {
val configZero = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builderZero = DefaultRouterContextBuilder(configZero)
val state = RouterState(
val configZero = TalkieConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builderZero = DefaultTalkieContextBuilder(configZero)
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "old turn 1", Instant.parse("2026-01-01T00:00:00Z"), turnId = "t1"),
RouterTurn(TurnRole.ROUTER, "old reply", Instant.parse("2026-01-02T00:00:00Z"), turnId = "t2"),
RouterTurn(TurnRole.USER, "current user input", Instant.parse("2026-01-03T00:00:00Z"), turnId = "t3"),
TalkieTurn(TurnRole.USER, "old turn 1", Instant.parse("2026-01-01T00:00:00Z"), turnId = "t1"),
TalkieTurn(TurnRole.ROUTER, "old reply", Instant.parse("2026-01-02T00:00:00Z"), turnId = "t2"),
TalkieTurn(TurnRole.USER, "current user input", Instant.parse("2026-01-03T00:00:00Z"), turnId = "t3"),
),
)
val pack = runBlocking { builderZero.build(state, TokenBudget(limit = 10000)) }
@@ -955,7 +955,7 @@ class RouterContextBuilderTest {
@Test
fun `appliedStrategies includes L3Recall when lastRetrievedMemory is non-empty`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
@@ -978,7 +978,7 @@ class RouterContextBuilderTest {
@Test
fun `appliedStrategies does not include L3Recall when lastRetrievedMemory is empty`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
)
@@ -995,12 +995,12 @@ class RouterContextBuilderTest {
@Test
fun `layers map is sorted by ContextLayer ordinal`() {
val state = RouterState(
val state = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello", clock.now()),
TalkieTurn(TurnRole.USER, "hello", clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
@@ -29,26 +29,26 @@ import com.correx.core.inference.NoopEmbedder
import com.correx.core.inference.ProviderHealth
import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.TokenUsage
import com.correx.core.router.ChatMode
import com.correx.core.router.DefaultRouterContextBuilder
import com.correx.core.router.DefaultRouterFacade
import com.correx.core.router.DefaultRouterReducer
import com.correx.core.router.IdeaReader
import com.correx.core.router.RouterContextBuilder
import com.correx.core.router.RouterFacade
import com.correx.core.router.RouterProjector
import com.correx.core.router.RouterRepository
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.RouterResponse
import com.correx.core.router.model.RouterState
import com.correx.core.router.model.RouterTurn
import com.correx.core.router.model.TurnRole
import com.correx.core.router.model.WorkflowSummary
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.talkie.ChatMode
import com.correx.core.talkie.DefaultTalkieContextBuilder
import com.correx.core.talkie.DefaultTalkieFacade
import com.correx.core.talkie.DefaultTalkieReducer
import com.correx.core.talkie.IdeaReader
import com.correx.core.talkie.TalkieContextBuilder
import com.correx.core.talkie.TalkieFacade
import com.correx.core.talkie.TalkieProjector
import com.correx.core.talkie.TalkieRepository
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.talkie.model.TalkieResponse
import com.correx.core.talkie.model.TalkieState
import com.correx.core.talkie.model.TalkieTurn
import com.correx.core.talkie.model.TurnRole
import com.correx.core.talkie.model.WorkflowSummary
import com.correx.core.talkie.model.WorkflowStatus
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.testing.fixtures.EventFixtures
import com.correx.testing.fixtures.inference.MockTokenizer
@@ -63,7 +63,7 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RouterFacadeTest {
class TalkieFacadeTest {
// --------------------------------------------------------------------------
// CHAT mode
@@ -135,21 +135,21 @@ class RouterFacadeTest {
fun `STEERING mode appends SteeringNoteAddedEvent with current stageId`(): Unit = runBlocking {
val mockStore = mockEventStore()
val stageId = StageId("stage-A")
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
RouterState(
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter("steering response"),
eventStore = mockStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -166,26 +166,26 @@ class RouterFacadeTest {
@Test
fun `conversation history grows per call - user and router turns appended`(): Unit = runBlocking {
val capturedStates = mutableListOf<RouterState>()
val capturedStates = mutableListOf<TalkieState>()
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
capturedStates.add(state)
return emptyContextPack()
}
},
inferenceRouter = mockInferenceRouter("router reply"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -206,26 +206,26 @@ class RouterFacadeTest {
@Test
fun `conversation history is session-scoped - different sessions do not share history`(): Unit = runBlocking {
val capturedStates = mutableListOf<RouterState>()
val capturedStates = mutableListOf<TalkieState>()
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
capturedStates.add(state)
return emptyContextPack()
}
},
inferenceRouter = mockInferenceRouter("response"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -246,17 +246,17 @@ class RouterFacadeTest {
@Test
fun `state is passed through to context builder`(): Unit = runBlocking {
val capturedState = mutableListOf<RouterState>()
val mockContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
val capturedState = mutableListOf<TalkieState>()
val mockContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
capturedState.add(state)
return emptyContextPack()
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
RouterState(
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = StageId("s1"),
@@ -265,7 +265,7 @@ class RouterFacadeTest {
routerContextBuilder = mockContextBuilder,
inferenceRouter = mockInferenceRouter("inference response"),
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -278,20 +278,20 @@ class RouterFacadeTest {
@Test
fun `budget is passed through to context builder`(): Unit = runBlocking {
val capturedBudget = mutableListOf<TokenBudget>()
val mockContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
val mockContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
capturedBudget.add(budget)
return emptyContextPack()
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = mockContextBuilder,
inferenceRouter = mockInferenceRouter("response"),
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 4200)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 4200)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -313,20 +313,20 @@ class RouterFacadeTest {
}
}
val stateStageId = StageId("state-stage")
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState(
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stateStageId,
)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter,
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -346,20 +346,20 @@ class RouterFacadeTest {
return mockProvider("response")
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState(
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.IDLE,
currentStageId = null,
)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter,
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -378,16 +378,16 @@ class RouterFacadeTest {
return mockProviderWithCapture("response", capturedRequestIds)
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter,
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -408,16 +408,16 @@ class RouterFacadeTest {
return mockProviderWithRequestCapture("response", capturedRequests)
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter,
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -431,12 +431,12 @@ class RouterFacadeTest {
@Test
fun `context pack is passed to inference provider`(): Unit = runBlocking {
val capturedContextPacks = mutableListOf<ContextPack>()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
val pack = ContextPack(
id = ContextPackId("test-pack"),
sessionId = state.sessionId ?: SessionId("unknown"),
@@ -478,7 +478,7 @@ class RouterFacadeTest {
}
},
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -488,12 +488,12 @@ class RouterFacadeTest {
@Test
fun `responseFormat defaults to Text`(): Unit = runBlocking {
val capturedRequests = mutableListOf<InferenceRequest>()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = object : InferenceRouter {
override suspend fun route(
@@ -504,7 +504,7 @@ class RouterFacadeTest {
}
},
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -514,7 +514,7 @@ class RouterFacadeTest {
}
@Test
fun `onUserInput returns RouterResponse with content and mode flag`(): Unit = runBlocking {
fun `onUserInput returns TalkieResponse with content and mode flag`(): Unit = runBlocking {
val mockStore = mockEventStore()
val facade = facadeWithMocks(eventStore = mockStore, chatMode = ChatMode.STEERING)
val response = facade.onUserInput(sessionId = SessionId("test-session"), input = "Hello!")
@@ -576,19 +576,19 @@ class RouterFacadeTest {
override fun capabilities(): Set<CapabilityScore> = setOf(CapabilityScore(ModelCapability.General, 1.0))
}
val eventStore = mockEventStore()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = object : InferenceRouter {
override suspend fun route(stageId: StageId, requiredCapabilities: Set<ModelCapability>): InferenceProvider =
metricsProvider
},
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -717,16 +717,16 @@ class RouterFacadeTest {
store: EventStore,
reply: String,
workflows: List<WorkflowSummary>,
): RouterFacade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState(sessionId = sessionId)
): TalkieFacade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState(sessionId = sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter(reply),
eventStore = store,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
workflowSummaryProvider = { workflows },
@@ -739,28 +739,28 @@ class RouterFacadeTest {
chatMode: ChatMode = ChatMode.CHAT,
embedder: Embedder = NoopEmbedder(dimension = 8),
l3MemoryStore: L3MemoryStore = InMemoryL3MemoryStore(),
): RouterFacade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
RouterState(
): TalkieFacade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = StageId("s1"),
)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter("inference response"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = embedder,
l3MemoryStore = l3MemoryStore,
).let { impl ->
object : RouterFacade {
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): RouterResponse =
object : TalkieFacade {
override suspend fun onUserInput(sessionId: SessionId, input: String, mode: ChatMode): TalkieResponse =
impl.onUserInput(sessionId = sessionId, input = input, mode = chatMode)
override suspend fun narrate(sessionId: SessionId, trigger: com.correx.core.router.model.NarrationTrigger) =
override suspend fun narrate(sessionId: SessionId, trigger: com.correx.core.talkie.model.NarrationTrigger) =
impl.narrate(sessionId = sessionId, trigger = trigger)
}
}
@@ -872,21 +872,21 @@ class RouterFacadeTest {
)
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter("router reply"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000), retrievalK = 5),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000), retrievalK = 5),
embedder = stubEmbedder,
l3MemoryStore = l3Store,
)
@@ -928,9 +928,9 @@ class RouterFacadeTest {
)
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
// Bootstrap session with a ChatTurnEvent whose turnId matches the L3 entry
@@ -954,17 +954,17 @@ class RouterFacadeTest {
)
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter("router reply"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000), retrievalK = 5),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000), retrievalK = 5),
embedder = stubEmbedder,
l3MemoryStore = l3Store,
)
@@ -986,16 +986,16 @@ class RouterFacadeTest {
throw RuntimeException("embedding service unavailable")
}
val eventStore = mockEventStore()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter("response"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = throwingEmbedder,
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -1014,9 +1014,9 @@ class RouterFacadeTest {
}
@Test
fun `replay of L3MemoryRetrievedEvent populates lastRetrievedMemory in RouterState`(): Unit = runBlocking {
val reducer = DefaultRouterReducer()
val projector = RouterProjector(reducer)
fun `replay of L3MemoryRetrievedEvent populates lastRetrievedMemory in TalkieState`(): Unit = runBlocking {
val reducer = DefaultTalkieReducer()
val projector = TalkieProjector(reducer)
val sessionId = SessionId("replay-session")
val hits = listOf(
L3RetrievedHit(
@@ -1048,28 +1048,28 @@ class RouterFacadeTest {
@Test
fun `ContextTruncatedEvent emitted when context builder drops entries`(): Unit = runBlocking {
// Use the real DefaultRouterContextBuilder with a tight budget and large conversation
// Use the real DefaultTalkieContextBuilder with a tight budget and large conversation
val longContent = "x".repeat(800) // ~200 tokens each
val sessionId = SessionId("truncation-session")
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = DefaultRouterContextBuilder(
config = RouterConfig(
routerContextBuilder = DefaultTalkieContextBuilder(
config = TalkieConfig(
conversationKeepLast = 4,
tokenBudget = TokenBudget(limit = 80),
),
),
inferenceRouter = mockInferenceRouter("response"),
eventStore = eventStore,
config = RouterConfig(
config = TalkieConfig(
conversationKeepLast = 4,
tokenBudget = TokenBudget(limit = 80),
),
@@ -1112,16 +1112,16 @@ class RouterFacadeTest {
@Test
fun `no ContextTruncatedEvent when budget is generous`(): Unit = runBlocking {
val eventStore = mockEventStore()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = DefaultRouterContextBuilder(
config = RouterConfig(tokenBudget = TokenBudget(limit = 10000)),
routerContextBuilder = DefaultTalkieContextBuilder(
config = TalkieConfig(tokenBudget = TokenBudget(limit = 10000)),
),
inferenceRouter = mockInferenceRouter("response"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 10000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 10000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -1136,9 +1136,9 @@ class RouterFacadeTest {
val longContent = "x".repeat(800)
val sessionId = SessionId("turnid-session")
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
// Pre-seed with a large turn so the budget overflows on the next call
@@ -1162,17 +1162,17 @@ class RouterFacadeTest {
)
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = DefaultRouterContextBuilder(
config = RouterConfig(conversationKeepLast = 4, tokenBudget = TokenBudget(limit = 80)),
routerContextBuilder = DefaultTalkieContextBuilder(
config = TalkieConfig(conversationKeepLast = 4, tokenBudget = TokenBudget(limit = 80)),
),
inferenceRouter = mockInferenceRouter("response"),
eventStore = eventStore,
config = RouterConfig(conversationKeepLast = 4, tokenBudget = TokenBudget(limit = 80)),
config = TalkieConfig(conversationKeepLast = 4, tokenBudget = TokenBudget(limit = 80)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -1192,28 +1192,28 @@ class RouterFacadeTest {
// Each turn must emit L3MemoryRetrievedEvent (with empty hits), so that
// lastRetrievedMemory is always current-turn-scoped and never stale.
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
val capturedStates = mutableListOf<RouterState>()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val capturedStates = mutableListOf<TalkieState>()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
private val real = DefaultRouterContextBuilder(
config = RouterConfig(tokenBudget = TokenBudget(limit = 10000)),
routerContextBuilder = object : TalkieContextBuilder {
private val real = DefaultTalkieContextBuilder(
config = TalkieConfig(tokenBudget = TokenBudget(limit = 10000)),
)
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack {
capturedStates.add(state)
return real.build(state, budget)
}
},
inferenceRouter = mockInferenceRouter("reply"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 10000), retrievalK = 5),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 10000), retrievalK = 5),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -1265,25 +1265,25 @@ class RouterFacadeTest {
val capturedPacks = mutableListOf<ContextPack>()
val eventStore = mockEventStore()
val replayer = DefaultEventReplayer<RouterState>(
val replayer = DefaultEventReplayer<TalkieState>(
store = eventStore,
projection = RouterProjector(DefaultRouterReducer()),
projection = TalkieProjector(DefaultTalkieReducer()),
)
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
replayer.rebuild(sessionId)
},
routerContextBuilder = object : RouterContextBuilder {
private val real = DefaultRouterContextBuilder(
config = RouterConfig(tokenBudget = TokenBudget(limit = 10000)),
routerContextBuilder = object : TalkieContextBuilder {
private val real = DefaultTalkieContextBuilder(
config = TalkieConfig(tokenBudget = TokenBudget(limit = 10000)),
)
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack =
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack =
real.build(state, budget).also { capturedPacks.add(it) }
},
inferenceRouter = mockInferenceRouter("router reply"),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 10000), retrievalK = 5),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 10000), retrievalK = 5),
embedder = stubEmbedder,
l3MemoryStore = l3Store,
)
@@ -1321,16 +1321,16 @@ class RouterFacadeTest {
return mockProvider("narration text")
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter,
eventStore = mockEventStore(),
config = RouterConfig(
config = TalkieConfig(
tokenBudget = TokenBudget(limit = 5000),
narrationModelId = "llama-cpp:phi-3-mini",
),
@@ -1339,7 +1339,7 @@ class RouterFacadeTest {
)
facade.narrate(
sessionId = SessionId("test-session"),
trigger = com.correx.core.router.model.NarrationTrigger(
trigger = com.correx.core.talkie.model.NarrationTrigger(
kind = "test",
instruction = "describe what happened",
),
@@ -1366,16 +1366,16 @@ class RouterFacadeTest {
return mockProvider("response")
}
}
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState()
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState()
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter,
eventStore = mockEventStore(),
config = RouterConfig(
config = TalkieConfig(
tokenBudget = TokenBudget(limit = 5000),
narrationModelId = "llama-cpp:phi-3-mini",
),
@@ -1393,9 +1393,9 @@ class RouterFacadeTest {
@Test
fun `workflowSummaryProvider is called and forwarded to context builder on each turn`(): Unit = runBlocking {
val captured = mutableListOf<List<WorkflowSummary>>()
val capturingBuilder = object : RouterContextBuilder {
val capturingBuilder = object : TalkieContextBuilder {
override suspend fun build(
state: RouterState,
state: TalkieState,
budget: TokenBudget,
availableWorkflows: List<WorkflowSummary>,
projectProfileText: String?,
@@ -1413,9 +1413,9 @@ class RouterFacadeTest {
@Test
fun `sessionProfileProvider is called and forwarded to context builder`(): Unit = runBlocking {
val capturedProfiles = mutableListOf<String?>()
val capturingBuilder = object : RouterContextBuilder {
val capturingBuilder = object : TalkieContextBuilder {
override suspend fun build(
state: RouterState,
state: TalkieState,
budget: TokenBudget,
availableWorkflows: List<WorkflowSummary>,
projectProfileText: String?,
@@ -1433,9 +1433,9 @@ class RouterFacadeTest {
}
private fun facadeWith(
contextBuilder: RouterContextBuilder = object : RouterContextBuilder {
contextBuilder: TalkieContextBuilder = object : TalkieContextBuilder {
override suspend fun build(
state: RouterState,
state: TalkieState,
budget: TokenBudget,
availableWorkflows: List<WorkflowSummary>,
projectProfileText: String?,
@@ -1443,14 +1443,14 @@ class RouterFacadeTest {
},
workflowSummaryProvider: () -> List<WorkflowSummary> = { emptyList() },
sessionProfileProvider: suspend (SessionId) -> String? = { null },
): RouterFacade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState = RouterState(sessionId = sessionId)
): TalkieFacade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState = TalkieState(sessionId = sessionId)
},
routerContextBuilder = contextBuilder,
inferenceRouter = mockInferenceRouter("inference response"),
eventStore = mockEventStore(),
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
workflowSummaryProvider = workflowSummaryProvider,
@@ -4,7 +4,7 @@ import com.correx.core.context.model.TokenBudget
import com.correx.core.events.events.ChatTurnEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.TalkieNarrationEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ContextPackId
@@ -25,19 +25,19 @@ import com.correx.core.inference.NoopEmbedder
import com.correx.core.inference.ProviderHealth
import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.TokenUsage
import com.correx.core.router.DefaultRouterContextBuilder
import com.correx.core.router.DefaultRouterFacade
import com.correx.core.router.RouterContextBuilder
import com.correx.core.router.RouterRepository
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.l3.L3Query
import com.correx.core.router.model.NarrationTrigger
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.RouterState
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.router.model.WorkflowSummary
import com.correx.core.talkie.DefaultTalkieContextBuilder
import com.correx.core.talkie.DefaultTalkieFacade
import com.correx.core.talkie.TalkieContextBuilder
import com.correx.core.talkie.TalkieRepository
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.L3MemoryEntry
import com.correx.core.talkie.l3.L3MemoryStore
import com.correx.core.talkie.l3.L3Query
import com.correx.core.talkie.model.NarrationTrigger
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.talkie.model.TalkieState
import com.correx.core.talkie.model.WorkflowStatus
import com.correx.core.talkie.model.WorkflowSummary
import com.correx.testing.fixtures.inference.MockTokenizer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking
@@ -47,14 +47,14 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RouterNarrationTest {
class TalkieNarrationTest {
// --------------------------------------------------------------------------
// narrate() — event emission
// --------------------------------------------------------------------------
@Test
fun `narrate emits RouterNarrationEvent with inference content and metrics`(): Unit = runBlocking {
fun `narrate emits TalkieNarrationEvent with inference content and metrics`(): Unit = runBlocking {
val knownLatencyMs = 55L
val knownUsage = TokenUsage(promptTokens = 8, completionTokens = 3)
val eventStore = MapBackedEventStore()
@@ -68,7 +68,7 @@ class RouterNarrationTest {
facade.narrate(SessionId("sess-1"), trigger)
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<RouterNarrationEvent>()
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<TalkieNarrationEvent>()
assertEquals(1, narrationEvents.size)
val event = narrationEvents[0]
assertEquals("narration line", event.content)
@@ -112,8 +112,8 @@ class RouterNarrationTest {
facade.narrate(SessionId("sess-4"), trigger)
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<RouterNarrationEvent>()
assertTrue(narrationEvents.isEmpty(), "blank inference must not emit RouterNarrationEvent")
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<TalkieNarrationEvent>()
assertTrue(narrationEvents.isEmpty(), "blank inference must not emit TalkieNarrationEvent")
}
@Test
@@ -124,30 +124,30 @@ class RouterNarrationTest {
facade.narrate(SessionId("sess-5"), trigger)
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<RouterNarrationEvent>()
assertTrue(narrationEvents.isEmpty(), "empty inference must not emit RouterNarrationEvent")
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<TalkieNarrationEvent>()
assertTrue(narrationEvents.isEmpty(), "empty inference must not emit TalkieNarrationEvent")
}
@Test
fun `narrate RouterNarrationEvent carries sessionId and stageId from state`(): Unit = runBlocking {
fun `narrate TalkieNarrationEvent carries sessionId and stageId from state`(): Unit = runBlocking {
val stageId = StageId("my-stage")
val eventStore = MapBackedEventStore()
val facade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
RouterState(
val facade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
)
},
routerContextBuilder = object : RouterContextBuilder {
override suspend fun build(state: RouterState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
override suspend fun buildNarrationContext(state: RouterState, trigger: NarrationTrigger, budget: TokenBudget): ContextPack = emptyContextPack()
routerContextBuilder = object : TalkieContextBuilder {
override suspend fun build(state: TalkieState, budget: TokenBudget, availableWorkflows: List<WorkflowSummary>, projectProfileText: String?): ContextPack = emptyContextPack()
override suspend fun buildNarrationContext(state: TalkieState, trigger: NarrationTrigger, budget: TokenBudget): ContextPack = emptyContextPack()
},
inferenceRouter = mockInferenceRouter("stage narration", latencyMs = 10L, tokensUsed = TokenUsage(1, 1)),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = InMemoryL3MemoryStore(),
)
@@ -155,7 +155,7 @@ class RouterNarrationTest {
facade.narrate(SessionId("session-x"), trigger)
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<RouterNarrationEvent>()
val narrationEvents = eventStore.appendedEvents.map { it.payload }.filterIsInstance<TalkieNarrationEvent>()
assertEquals(1, narrationEvents.size)
assertEquals(SessionId("session-x"), narrationEvents[0].sessionId)
assertEquals("my-stage", narrationEvents[0].stageId)
@@ -167,8 +167,8 @@ class RouterNarrationTest {
@Test
fun `buildNarrationContext includes system entry in L0`(): Unit = runBlocking {
val builder = DefaultRouterContextBuilder(RouterConfig())
val state = RouterState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.IDLE)
val builder = DefaultTalkieContextBuilder(TalkieConfig())
val state = TalkieState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.IDLE)
val trigger = NarrationTrigger(kind = "test", instruction = "Do something.")
val pack = builder.buildNarrationContext(state, trigger, TokenBudget(limit = 5000))
@@ -179,8 +179,8 @@ class RouterNarrationTest {
@Test
fun `buildNarrationContext includes workflow status entry`(): Unit = runBlocking {
val builder = DefaultRouterContextBuilder(RouterConfig())
val state = RouterState(
val builder = DefaultTalkieContextBuilder(TalkieConfig())
val state = TalkieState(
sessionId = SessionId("s"),
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = StageId("my-stage"),
@@ -196,8 +196,8 @@ class RouterNarrationTest {
@Test
fun `buildNarrationContext includes trigger instruction as final entry`(): Unit = runBlocking {
val builder = DefaultRouterContextBuilder(RouterConfig())
val state = RouterState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.IDLE)
val builder = DefaultTalkieContextBuilder(TalkieConfig())
val state = TalkieState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.IDLE)
val instruction = "Narrate the completed action now."
val trigger = NarrationTrigger(kind = "stage_completed", instruction = instruction)
val pack = builder.buildNarrationContext(state, trigger, TokenBudget(limit = 5000))
@@ -214,8 +214,8 @@ class RouterNarrationTest {
// Regression: the trigger instruction must render as a user turn. When it was filed
// under L0 it folded into the system message, leaving zero user messages and the chat
// template rejected the request ("No user query found in messages").
val builder = DefaultRouterContextBuilder(RouterConfig())
val state = RouterState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.RUNNING)
val builder = DefaultTalkieContextBuilder(TalkieConfig())
val state = TalkieState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.RUNNING)
val instruction = "Tell the user the stage just completed."
val trigger = NarrationTrigger(kind = "stage_completed", instruction = instruction)
val pack = builder.buildNarrationContext(state, trigger, TokenBudget(limit = 5000))
@@ -231,8 +231,8 @@ class RouterNarrationTest {
@Test
fun `buildNarrationContext stays within budget`(): Unit = runBlocking {
val builder = DefaultRouterContextBuilder(RouterConfig())
val state = RouterState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.IDLE)
val builder = DefaultTalkieContextBuilder(TalkieConfig())
val state = TalkieState(sessionId = SessionId("s"), workflowStatus = WorkflowStatus.IDLE)
val trigger = NarrationTrigger(kind = "test", instruction = "Short.")
val budget = TokenBudget(limit = 5000)
val pack = builder.buildNarrationContext(state, trigger, budget)
@@ -242,13 +242,13 @@ class RouterNarrationTest {
@Test
fun `buildNarrationContext does not include conversationHistory turns`(): Unit = runBlocking {
val builder = DefaultRouterContextBuilder(RouterConfig())
val state = RouterState(
val builder = DefaultTalkieContextBuilder(TalkieConfig())
val state = TalkieState(
sessionId = SessionId("s"),
workflowStatus = WorkflowStatus.IDLE,
conversationHistory = listOf(
com.correx.core.router.model.RouterTurn(
role = com.correx.core.router.model.TurnRole.USER,
com.correx.core.talkie.model.TalkieTurn(
role = com.correx.core.talkie.model.TurnRole.USER,
content = "secret conversation turn",
timestamp = kotlinx.datetime.Clock.System.now(),
),
@@ -274,19 +274,19 @@ class RouterNarrationTest {
latencyMs: Long = 10L,
tokensUsed: TokenUsage = TokenUsage(5, 5),
l3MemoryStore: L3MemoryStore = InMemoryL3MemoryStore(),
): DefaultRouterFacade = DefaultRouterFacade(
routerRepository = object : RouterRepository {
override suspend fun getRouterState(sessionId: SessionId): RouterState =
RouterState(
): DefaultTalkieFacade = DefaultTalkieFacade(
routerRepository = object : TalkieRepository {
override suspend fun getTalkieState(sessionId: SessionId): TalkieState =
TalkieState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = StageId("s1"),
)
},
routerContextBuilder = DefaultRouterContextBuilder(RouterConfig()),
routerContextBuilder = DefaultTalkieContextBuilder(TalkieConfig()),
inferenceRouter = mockInferenceRouter(responseText, latencyMs, tokensUsed),
eventStore = eventStore,
config = RouterConfig(tokenBudget = TokenBudget(limit = 5000)),
config = TalkieConfig(tokenBudget = TokenBudget(limit = 5000)),
embedder = NoopEmbedder(dimension = 8),
l3MemoryStore = l3MemoryStore,
)
@@ -372,7 +372,7 @@ class RouterNarrationTest {
storedEntries.add(entry)
}
override suspend fun query(query: L3Query): List<com.correx.core.router.l3.L3Hit> = emptyList()
override suspend fun query(query: L3Query): List<com.correx.core.talkie.l3.L3Hit> = emptyList()
override suspend fun existsByTurnIdPrefix(prefix: String): Boolean = false
@@ -9,27 +9,27 @@ import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.router.DefaultRouterReducer
import com.correx.core.router.RouterProjector
import com.correx.core.router.model.RouterState
import com.correx.core.router.model.StageOutcomeKind
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.talkie.DefaultTalkieReducer
import com.correx.core.talkie.TalkieProjector
import com.correx.core.talkie.model.TalkieState
import com.correx.core.talkie.model.StageOutcomeKind
import com.correx.core.talkie.model.WorkflowStatus
import com.correx.testing.fixtures.EventFixtures.stored
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RouterProjectorTest {
class TalkieProjectorTest {
private val reducer = DefaultRouterReducer()
private val projector = RouterProjector(reducer)
private val reducer = DefaultTalkieReducer()
private val projector = TalkieProjector(reducer)
private val sessionId = SessionId("s1")
private val stageId = StageId("stage-1")
private val workflowId = "workflow-test"
@Test
fun `initial() returns RouterState with IDLE status and empty collections`() {
fun `initial() returns TalkieState with IDLE status and empty collections`() {
val state = projector.initial()
assertEquals(WorkflowStatus.IDLE, state.workflowStatus)
assertNull(state.sessionId)
@@ -190,7 +190,7 @@ class RouterProjectorTest {
@Test
fun `full lifecycle through projector mirrors reducer pipeline`() {
var state: RouterState = projector.initial()
var state: TalkieState = projector.initial()
state = projector.apply(
state,
@@ -9,9 +9,9 @@ import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.router.DefaultRouterReducer
import com.correx.core.router.model.StageOutcomeKind
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.talkie.DefaultTalkieReducer
import com.correx.core.talkie.model.StageOutcomeKind
import com.correx.core.talkie.model.WorkflowStatus
import com.correx.testing.fixtures.EventFixtures.stored
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
@@ -20,9 +20,9 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RouterReducerTest {
class TalkieReducerTest {
private val reducer = DefaultRouterReducer()
private val reducer = DefaultTalkieReducer()
private val sessionId = SessionId("s1")
private val stageId = StageId("stage-1")
private val workflowId = "workflow-test"
+1 -1
View File
@@ -29,6 +29,6 @@ dependencies {
testImplementation(project(":infrastructure:tools"))
testImplementation(project(":infrastructure:tools:filesystem"))
testImplementation(project(":apps:server"))
testImplementation(project(":core:router"))
testImplementation(project(":core:talkie"))
testImplementation(project(":core:config"))
}
@@ -10,7 +10,7 @@ import com.correx.core.inference.Embedder
import com.correx.core.journal.DecisionJournalProjector
import com.correx.core.journal.DefaultDecisionJournalReducer
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.router.l3.InMemoryL3MemoryStore
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies {
testImplementation(project(":core:inference"))
testImplementation(project(":core:kernel"))
testImplementation(project(":core:artifacts"))
testImplementation(project(":core:router"))
testImplementation(project(":core:talkie"))
testImplementation(project(":core:journal"))
implementation(project(":testing:fixtures"))
}
@@ -8,19 +8,19 @@ import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.router.DefaultRouterReducer
import com.correx.core.router.RouterProjector
import com.correx.core.router.model.StageOutcomeKind
import com.correx.core.router.model.WorkflowStatus
import com.correx.core.talkie.DefaultTalkieReducer
import com.correx.core.talkie.TalkieProjector
import com.correx.core.talkie.model.StageOutcomeKind
import com.correx.core.talkie.model.WorkflowStatus
import com.correx.testing.fixtures.EventFixtures.stored
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class RouterProjectorTest {
class TalkieProjectorTest {
private val reducer = DefaultRouterReducer()
private val projector = RouterProjector(reducer)
private val reducer = DefaultTalkieReducer()
private val projector = TalkieProjector(reducer)
private val sessionId = SessionId("s1")
private val stageId = StageId("stage-1")
private val workflowId = "workflow-test"