251 lines
11 KiB
Markdown
251 lines
11 KiB
Markdown
# Epic 13 — Interfaces (TUI + CLI + API)
|
|
|
|
## completed deliverables
|
|
|
|
### task 0 — orchestrator resume mechanism
|
|
|
|
fixed the approval gate in `SessionOrchestrator` to truly suspend and resume rather than immediately failing.
|
|
|
|
changes:
|
|
- `handleApproval` is now `suspend` — holds a `CompletableDeferred<ApprovalDecision>` per pending request, keyed by `ApprovalRequestId`
|
|
- emits `ApprovalRequestedEvent` before suspending so the server layer can surface the request id to clients
|
|
- `DefaultSessionOrchestrator.submitApprovalDecision(requestId, decision)` completes the deferred and resumes the workflow coroutine
|
|
- `ReplayOrchestrator.mapValidationOutcome` updated to `suspend` to match the parent signature
|
|
- `approvalEngine` removed from `SessionOrchestrator` (was evaluated synchronously; now decisions arrive externally)
|
|
|
|
files changed:
|
|
- `core/kernel/.../orchestration/SessionOrchestrator.kt`
|
|
- `core/kernel/.../orchestration/DefaultSessionOrchestrator.kt`
|
|
- `core/kernel/.../orchestration/ReplayOrchestrator.kt`
|
|
|
|
---
|
|
|
|
### task 1 — websocket protocol
|
|
|
|
defined the wire protocol as sealed class hierarchies with kotlinx.serialization in `apps/server/protocol/`.
|
|
|
|
final structures:
|
|
|
|
* `ServerMessage` — 16 subtypes across session lifecycle, stage execution, inference, tool execution, approval, provider status, and error
|
|
* `ClientMessage` — 5 subtypes: StartSession, ResumeSession, CancelSession, ApprovalResponse, Ping
|
|
* `ApprovalDecision` (protocol enum) — `APPROVE`, `REJECT`, `STEER` — separate from the domain `ApprovalDecision` data class
|
|
* `RiskSummaryDto`, `ProviderHealthDto`, `SessionConfigDto`, `PauseReason` — protocol-only DTOs
|
|
* `ProtocolSerializer` — encodes `ServerMessage` to JSON, decodes `ClientMessage` from JSON; unknown/malformed input throws `ProtocolException` (never crashes)
|
|
|
|
key properties:
|
|
- `Json { classDiscriminator = "type"; ignoreUnknownKeys = true }`
|
|
- no domain types leak into the protocol layer except pure value aliases (`SessionId`, `StageId`, `ApprovalRequestId`, `Tier`)
|
|
|
|
files created:
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/protocol/ClientMessage.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/protocol/Dtos.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt`
|
|
|
|
---
|
|
|
|
### task 2 — ktor server + websocket
|
|
|
|
implemented the full Ktor 3.x server with REST and WebSocket endpoints.
|
|
|
|
rest endpoints:
|
|
- `GET /health` — provider status, 200 in degraded mode
|
|
- `GET/POST /sessions` — list and start sessions
|
|
- `GET /sessions/{id}` — single session state
|
|
- `POST /sessions/{id}/cancel`
|
|
- `GET /sessions/{id}/events?from=<eventId>&limit=50` — paginated event replay
|
|
- `GET /workflows` — available workflows
|
|
- `GET /providers` — provider health
|
|
|
|
websocket endpoints:
|
|
- `WS /sessions/{id}/stream` — session-scoped stream
|
|
- `WS /stream` — global stream across all sessions
|
|
|
|
websocket behavior:
|
|
- on connect: snapshot of current state, then live events
|
|
- on reconnect with `?lastEventId=<id>`: replays missed events from that point
|
|
- 30s server heartbeat; client must respond within 10s or connection is dropped
|
|
- unknown client message → `ProtocolError` sent, connection kept open
|
|
|
|
`ServerModule` holds all dependencies as interfaces; no concrete instantiation inside it.
|
|
|
|
files created:
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/Application.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/routes/WorkflowRoutes.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/routes/ProviderRoutes.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/registry/WorkflowRegistry.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt`
|
|
|
|
---
|
|
|
|
### task 3 — approval interaction
|
|
|
|
implemented the full approval lifecycle bridging the orchestrator suspension and the WebSocket clients.
|
|
|
|
`ApprovalCoordinator` responsibilities:
|
|
- tracks connected WS clients per session
|
|
- on `ApprovalRequestedEvent`: broadcasts `ServerMessage.ApprovalRequired` to all clients for that session
|
|
- schedules a timeout job (`coroutineScope.launch { delay(timeoutMs) }`) that auto-denies if no response within `ApprovalConfig.timeoutMs` (default 5 minutes)
|
|
- on `ClientMessage.ApprovalResponse`: cancels timeout job, translates protocol enum to domain `ApprovalDecision`, calls `orchestrator.submitApprovalDecision()`
|
|
- double-response guard via `ConcurrentHashMap<ApprovalRequestId, Boolean>` — second response returns `ProtocolError`, no double-resume
|
|
|
|
protocol → domain mapping:
|
|
- `APPROVE` → `ApprovalOutcome.APPROVED`
|
|
- `REJECT` → `ApprovalOutcome.REJECTED`
|
|
- `STEER` → `ApprovalOutcome.APPROVED` + `UserSteering(text = steeringNote, ...)`
|
|
|
|
rest fallback: `POST /sessions/{id}/approve` for non-WS clients.
|
|
|
|
files created:
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalConfig.kt`
|
|
- `apps/server/src/main/kotlin/com/correx/apps/server/routes/ApprovalRoutes.kt`
|
|
|
|
---
|
|
|
|
### task 4 — mosaic tui
|
|
|
|
implemented a new `apps/tui` Gradle module with a Mosaic-based interactive terminal UI.
|
|
|
|
architecture:
|
|
- single WS listener coroutine updates `mutableStateOf(TuiState)` — no polling
|
|
- separate stdin reader coroutine sends `KeyEvent` to a `Channel`
|
|
- key handler coroutine dispatches protocol messages to the server
|
|
- exponential-backoff reconnect (1s → 30s max) on server disconnect
|
|
- `applyServerMessage` pure reducer maps `ServerMessage` to state updates
|
|
|
|
layout: status bar / session list (last 5) / active session panel / approval panel (collapsed when no pending approval) / input bar
|
|
|
|
keybinds: `n` new, `c` cancel, `a/r/s` approve/reject/steer (only active when approval pending), `↑↓` navigate, `q` quit — inactive binds are no-ops
|
|
|
|
files created:
|
|
- `apps/tui/build.gradle`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/StateReducer.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/ws/TuiWsClient.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/components/ActiveSession.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/components/ApprovalPanel.kt`
|
|
- `apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt`
|
|
|
|
---
|
|
|
|
### task 5 — clikt cli
|
|
|
|
implemented the full `apps/cli` command suite using Clikt.
|
|
|
|
commands:
|
|
- `correx run --workflow <path> [--session <id>] [--auto-approve]`
|
|
- `correx session list/resume/cancel/events`
|
|
- `correx approve <session-id> --decision <approve|reject|steer> [--note]`
|
|
- `correx status`
|
|
- `correx provider list`
|
|
|
|
output modes: `--json` (valid parseable JSON), `--quiet` (errors only), default (human-readable with ANSI color).
|
|
|
|
`correx run` behavior:
|
|
- starts session via `POST /sessions`, subscribes to WS stream
|
|
- prints stage transitions and tool calls to stdout
|
|
- on `ApprovalRequired`: interactive TTY prompt if stdin is a terminal; auto-deny with warning if no TTY and `--auto-approve` absent
|
|
- exit codes: `0` complete, `1` failed, `2` approval denied
|
|
|
|
`CliWsClient` wraps the Ktor WebSocket client for stream subscriptions.
|
|
|
|
files created:
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/CorrexCli.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/CliConstants.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/ws/CliWsClient.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/RunCommand.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/SessionCommand.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/ApproveCommand.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/StatusCommand.kt`
|
|
- `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/ProviderCommand.kt`
|
|
|
|
---
|
|
|
|
### task 6 — shared config
|
|
|
|
implemented configuration loading in `core/config`.
|
|
|
|
schema (`~/.config/correx/config.toml`, override via `$CORREX_CONFIG`):
|
|
|
|
```toml
|
|
[server]
|
|
host = "localhost"
|
|
port = 8080
|
|
|
|
[tui]
|
|
theme = "dark"
|
|
session_list_limit = 5
|
|
|
|
[cli]
|
|
default_output = "human"
|
|
|
|
[approval]
|
|
timeout_ms = 300000
|
|
```
|
|
|
|
`ConfigLoader.load()` — reads from file, returns `CorrexConfig()` defaults on missing file, logs warning and returns defaults on malformed input. no new library dependencies — manual TOML parser for the required subset (sections + key=value pairs).
|
|
|
|
files created:
|
|
- `core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt`
|
|
- `core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt`
|
|
|
|
---
|
|
|
|
# final architecture after Epic 13
|
|
|
|
```text
|
|
apps/
|
|
├── server ← Ktor 3.x, REST + WebSocket, approval coordinator
|
|
├── tui ← Mosaic reactive terminal (new module)
|
|
├── cli ← Clikt commands, WS stream client
|
|
└── shared ← (config lives in core/config)
|
|
|
|
core/
|
|
└── config ← CorrexConfig, ConfigLoader (new module)
|
|
```
|
|
|
|
---
|
|
|
|
# major architectural outcomes
|
|
|
|
Epic 13 established:
|
|
|
|
* real approval pause/resume — orchestrator now truly suspends and resumes externally
|
|
* a wire protocol separating transport from domain (no domain types in protocol layer)
|
|
* a Ktor server as the single point of access — TUI and CLI are pure clients
|
|
* WebSocket event streaming with snapshot-on-connect and missed-event replay
|
|
* approval timeout and double-response safety
|
|
* a Mosaic TUI with reactive state and context-aware keybinds
|
|
* a full Clikt CLI with machine-readable JSON output and correct exit codes
|
|
* shared config via TOML with graceful fallback to defaults
|
|
|
|
---
|
|
|
|
# what Epic 13 intentionally does NOT include
|
|
|
|
not implemented:
|
|
|
|
* router context isolation (explicitly deferred — see CLAUDE.md)
|
|
* streaming inference responses (explicitly deferred)
|
|
* parallel agent execution (explicitly deferred)
|
|
* GPU residency scheduling (explicitly deferred)
|
|
* TUI config editor (`,` keybind present, editor not implemented)
|
|
* `correx config --edit` command (not in server surface)
|
|
|
|
---
|
|
|
|
# final state
|
|
|
|
Correx now has:
|
|
|
|
> a complete interface layer — server, TUI, and CLI — all backed by a single Ktor WebSocket bus, with approval decisions flowing from any client through to the orchestrator's suspended coroutine and resuming execution cleanly.
|