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
@@ -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