2c459da009
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
5.3 KiB
5.3 KiB
spec
objective
Add :core:router — a resident conversational facade that maintains isolated L2 memory, routes inference requests to an LLM provider, and emits steering notes, while exposing a WebSocket-protocol interface for chat and steering interaction.
scope
in
SteeringNotedomain object andSteeringNoteAddedEventin:core:context/:core:events(Task 0)RouterState,RouterReducer,RouterProjector,RouterRepositoryin:core:router(Tasks 1-2)RouterContextBuilderthat assembles a budget-constrainedContextPackfrom L2 memory, workflow status, and conversation history (Task 3)RouterFacadethat orchestrates state loading, context building, inference routing, and steering emission (Task 4)RouterConfigandRouterResponsetypes in:core:router(Task 4)- Protocol layer additions:
ClientMessage.ChatInput,ServerMessage.RouterResponseMessage, andChatModeenum (Task 5) - Infrastructure wiring:
createRouterFacade()inInfrastructureModuleand server WebSocket message routing (Task 5) - Deterministic tests for reducer, projector, context builder, and facade
out
- L3 persistence (cross-session memory)
- Streaming inference responses
- Router-initiated workflow control (pause/cancel)
- Router system prompt configuration via
harness.yaml - Multiple concurrent router instances
- TUI changes (handled separately by the TUI module)
- GPU residency scheduling
- Context compression beyond the router's own L2 entries
invariants
RouterStateis always rebuilt from the event log — never persisted independentlyRouterContextPacknever contains rawEventPayloadobjects, artifact content, or tool outputs- The router emits only
SteeringNoteAddedEventto the event store — it never emits events that mutate workflow state - In-memory conversation history is session-scoped (
ConcurrentHashMap<SessionId, ...>) — no cross-session leakage - L0 entries in the router
ContextPack(system prompt, workflow status) are never dropped under budget pressure - L2 entries (stage summaries) are evicted oldest-first under budget pressure
- Replay is environment-independent — no external service calls required for deterministic projection
:core:routerdoes not depend on:core:orchestrationor:core:kernel— it reads state from events only- Every new
EventPayloadmust be registered in theeventModulepolymorphic serialization block - Inference requests from the router use the same cancellation semantics as orchestrator inference
inputs
- User chat messages via
ClientMessage.ChatInputon WebSocket (sessionId, text, mode) - Domain events on the event log:
WorkflowStartedEvent,WorkflowCompletedEvent,WorkflowFailedEvent,OrchestrationPausedEvent,OrchestrationResumedEvent,StageCompletedEvent,StageFailedEvent RouterConfig— conversation keep-last count and token budget
outputs
ServerMessage.RouterResponseMessagesent back over WebSocket (content, steeringEmitted flag)SteeringNoteAddedEventappended to the event store whenmode == ChatMode.STEERINGRouterState— observable projection of router knowledge (L2 memory, workflow status, conversation history)
dependencies
:core:events— event primitives,StoredEvent,EventPayload,EventStore,EventReplayer, serialization:core:context—TokenBudget,ContextPack,CompressionStrategy:core:inference—InferenceRouter, inference provider contract, cancellation semantics:core:sessions—SessionId,StageId, workflow type primitives- Server WebSocket handler (existing infrastructure) for message dispatch
- TUI protocol types (existing) —
PauseReasonreferenced by server message routing
assumptions
InferenceRouter.route()returns an inference provider instance that can be called with aContextPackEventStoresupports both reading events (for replay) and writing events (forSteeringNoteAddedEvent) within the same moduleContextPackalready supports layered priority with L0/L1/L2 classification and budget-aware eviction- The
SessionConfigDtotype referenced byClientMessage.StartSessionalready exists and is shared across modules - A
Clockabstraction (orClock.System) is available for deterministic timestamping - The server WebSocket message router can be extended with a new
whenbranch to dispatchChatInputwithout breaking existing branches - The server already has access to the
InfrastructureModulecomponent graph whereRouterFacadewill be injected
open questions
- Should
ChatInput.sessionIdbe aSessionIddomain type or remain aString? The existing protocol types (ServerMessage,ClientMessage) already use domain types likeSessionId,StageId— consistency suggestsSessionIdwould be preferred - The existing
InputModeenum inapps:tuihas variantsROUTER, NAVIGATE, FILTER, STEER. Should the newChatModeprotocol enum mirror these exactly (e.g.ROUTER, STEER), or use a simpler two-variant enum (CHAT, STEERING)? - Task 0 adds types to
:core:contextand:core:events— are these to be merged before task 1 starts, or is a single PR acceptable? The epic states "complete and merge before starting task 1." - Does the server's
CorrexServer.ktalready have a WebSocket handler structure that can accommodate aChatInputbranch, or does the handler file need review to confirm the extension point?