feat: wire L3 write path via Embedder into router

Introduces Embedder contract in core:inference with a NoopEmbedder
default (zero vector) and wires both Embedder and L3MemoryStore into
DefaultRouterFacade. Each ChatTurnEvent emission now embeds the turn
content and writes a corresponding L3MemoryEntry, so cross-session
memory is captured at the source of truth (the event emission point).

InfrastructureModule defaults to NoopEmbedder + InMemoryL3MemoryStore
so the system runs without an external embedding model or vector store
wired. The TurboVec adapter stays unwired for now; switching to it is
a configuration concern (Epic 12 config layer).

Also gitignores apps/tui/logs/ (Kotlin TUI runtime logs, pre-Go rewrite).
This commit is contained in:
2026-05-30 01:01:40 +04:00
parent a3f29e6eb9
commit 9f171d3236
6 changed files with 113 additions and 1 deletions
@@ -0,0 +1,20 @@
package com.correx.core.inference
/**
* Interface for computing vector embeddings of text.
* Implementations provide pre-computed vectors for similarity search and retrieval.
*/
interface Embedder {
/**
* Dimensionality of the returned embeddings.
*/
val dimension: Int
/**
* Compute a vector embedding for the given text.
*
* @param text The text to embed.
* @return A vector of [dimension] floats.
*/
suspend fun embed(text: String): FloatArray
}
@@ -0,0 +1,8 @@
package com.correx.core.inference
/**
* Returns a zero vector. Lets the L3 write path run without an embedding model wired.
*/
class NoopEmbedder(override val dimension: Int = 1536) : Embedder {
override suspend fun embed(text: String): FloatArray = FloatArray(dimension)
}
@@ -10,10 +10,13 @@ import com.correx.core.events.types.EventId
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.Embedder
import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ModelCapability
import com.correx.core.inference.ResponseFormat
import com.correx.core.router.l3.L3MemoryEntry
import com.correx.core.router.l3.L3MemoryStore
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.RouterResponse
import kotlinx.datetime.Clock
@@ -34,6 +37,8 @@ class DefaultRouterFacade(
private val eventStore: EventStore,
private val config: RouterConfig,
private val validateSteering: (suspend (String) -> String?)? = null,
private val embedder: Embedder,
private val l3MemoryStore: L3MemoryStore,
) : RouterFacade {
override suspend fun onUserInput(
@@ -96,6 +101,18 @@ class DefaultRouterFacade(
),
),
)
val vector = embedder.embed(content)
l3MemoryStore.store(
L3MemoryEntry(
id = turnId,
sessionId = sessionId,
turnId = turnId,
text = content,
vector = vector,
timestampMs = nowMs,
)
)
}
private suspend fun emitSteeringNote(sessionId: SessionId, content: String, effectiveStageId: StageId) {