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