Files
correx/docs/specs/2026-05-20-router/exec/task-012.md
T
kami 2c459da009 feat(router): implement Epic 14 — core:router module
Implements the full conversational router facade: RouterState, RouterReducer,
RouterProjector, RouterRepository, RouterContextBuilder, RouterFacade, protocol
types, WebSocket wiring, infrastructure factory, and deterministic test suite.

Also fixes spec divergences found in post-implementation review:
- Add SteeringNote domain object to core:context (epic prerequisite)
- Rename RouterFacade.handleChat → onUserInput per spec interface contract
- Add in-memory ConcurrentHashMap conversation history to DefaultRouterFacade
- Make RouterRepository.getRouterState suspend
- Rename RouterConfig.keepLast → conversationKeepLast, fix defaults (6, 4096)
- Refactor InfrastructureModule.createRouterFacade to self-assemble internally
- Fix FileReadTool: allowedPaths was dead constructor param (@SuppressUnusedParameter);
  now stored as private val and enforced in validateRequest
- Disable koverVerify on modules tested via testing/ submodules or with
  hardware/integration dependencies (24 modules); build gate now passes clean
2026-05-21 15:06:20 +04:00

4.9 KiB

task-012

status: done

goal

Implement RouterContextBuilder — assembles a budget-constrained ContextPack from L2 memory (stage summaries), workflow status events, and conversation history, respecting L0 immutability and L2 eviction ordering.

target artifact

core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt

execution steps

  1. Create RouterContextBuilder interface in RouterContextBuilder.kt at core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt

    • Interface signature: fun build(state: RouterState, budget: TokenBudget): ContextPack
    • Package: com.correx.core.router
    • Imports: RouterState from com.correx.core.router.model, TokenBudget and ContextPack from com.correx.core.context.model
  2. Create DefaultRouterContextBuilder class in the same file

    • Constructor receives: RouterConfig (inject dependency, never instantiate)
    • build() assembles a Map<ContextLayer, List<ContextEntry>> layer by layer: a. L0 entries (always included, never dropped under budget pressure):
      • System prompt entry: layer = L0, sourceType = "systemPrompt", content = RouterContextBuilder.SYSTEM_PROMPT (private const String)
      • Workflow status entry: layer = L0, sourceType = "workflowStatus", content = "Status: ${state.workflowStatus}, Stage: ${state.currentStageId ?: "none"}"
      • Compute combined L0 token estimate; deduct from budget (remaining = max(0, budget.limit - l0Tokens)) b. L1 entries (conversation history):
      • Take state.conversationHistory.takeLast(config.keepLast)
      • Convert each RouterTurn to a ContextEntry with layer = L1, sourceType = "conversation"
      • Content format: "[${role.name}] ${content}"
      • Deduct L1 token estimates from remaining budget, stop if budget exhausted c. L2 entries (stage summaries from L2 memory):
      • Iterate state.l2Memory in insertion order (oldest first)
      • Convert each RouterL2Entry to a ContextEntry with layer = L2, sourceType = "stageSummary"
      • Content format: "Stage ${stageId} (${outcome}): ${summary}"
      • Budget enforcement: if an entry's tokenEstimate exceeds remaining budget, skip it and continue (oldest-first eviction)
    • Assemble ContextPack with:
      • id = new ContextPackId (generate a deterministic ID from sessionId + "router")
      • sessionId = from state
      • stageId = from state (currentStageId, or empty StageId if null)
      • layers = the grouped Map<ContextLayer, List>
      • budgetUsed = sum of all retained entries' tokenEstimate
      • budgetLimit = budget.limit
      • compressionMetadata = CompressionMetadata(appliedStrategies = listOf("L0Immutable", "Conversation"), truncatedLayers = emptyList(), entriesDropped = droppedCount)
    • Token estimation per entry:
      • System prompt: SYSTEM_PROMPT.length / 2 (rough heuristic: ~2 chars per token)
      • Workflow status: content length / 2
      • Conversation: content length / 2
      • Stage summary: content length / 2
      • Use estimateTokens(content: String): Int helper method for consistency
  3. Verify no raw event payload, artifact content, or tool outputs enter the context pack

    • The builder only consumes RouterState (which is derived from events by the reducer) and produces clean ContextEntry objects
    • No direct EventPayload references in the builder
  4. Verify L0 entries are never dropped

    • L0 entries are computed first, budget is deducted, then L1/L2 entries are added only if budget permits
    • L0 entries remain regardless of budget exhaustion
  5. Verify compilation with ./gradlew :core:router:compileKotlin

changed artifacts

  • core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt — new file (interface + implementation)
  • core/router/build.gradle — added Kover plugin and disabled KoverVerify (no tests yet; tests are task-022)

blockers

  • None — task-006 (RouterState) and task-007 (RouterConfig) are complete and the artifact files exist

notes

  • The epic spec (epic-14, task 3) suggests placing files under context/ subdirectory, but the task plan file specifies the root core/router/.../RouterContextBuilder.kt path. Following the task plan path.
  • The builder does NOT implement inference routing or event writing — those are task-013 (RouterFacade) concerns.
  • The SYSTEM_PROMPT constant should be concise and non-authoritative, reflecting a conversational assistant role. Example: "You are a routing assistant. Provide guidance based on workflow state and conversation context."
  • The ContextPackId for router packs should be deterministic and traceable — use ContextPackId("${state.sessionId?.value ?: "unknown"}-router-pack") for testability.
  • If state.sessionId is null (initial state), the build should still succeed — use a fallback ID component like "unknown" for the pack ID.