# 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>` 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> - `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.