Pipeline Specification¶
The pipeline is a deterministic state machine in kova-core, built on types
from kova-primitives. The model never mutates pipeline state; it proposes
(through the research loop) and the engine disposes (through gated
transitions, sandboxed checks, and an append-only event log).
Phases¶
kova_primitives::phase::Phase — the four deterministic phases
(crates/kova-primitives/src/phase.rs:7):
| Phase | Worker model (worker_name) |
Deterministic check commands (default_check_commands) |
Circuit-break target on failure |
|---|---|---|---|
Research |
— (model-driven loop, no worker) | none — its check is "a plan artifact exists" | Research (re-run) |
Verification |
verify |
cargo check --workspace --all-targets --message-format=short |
Research |
Scaffolding |
scaffold |
cargo check --workspace --all-targets --message-format=short |
Verification |
Implementation |
implement |
cargo test --workspace |
Scaffolding |
Phase order is Research → Verification → Scaffolding → Implementation
(Phase::ALL, phase.rs:15); Phase::next() / Phase::prev() walk the
array. Phase::circuit_break_target() (phase.rs:32) is the regression
table: a failed phase drops back at most one phase, except Verification,
which drops all the way to Research. The table is unit-tested but is not
yet invoked by the engine — automated backward circuit breaking is defined
and tested, not wired (see the divergence ledger below).
flowchart LR
R[Research] --> V[Verification]
V --> S[Scaffolding]
S --> I[Implementation]
V -. "check failed: regress" .-> R
S -. "check failed: regress" .-> V
I -. "check failed: regress" .-> S
Statuses¶
kova_primitives::phase::PhaseStatus (phase.rs:88) — the status within the
current phase:
| Status | Meaning |
|---|---|
Idle |
Phase entered, nothing running. |
Working |
Model work in flight (research run). |
Checking |
Deterministic check commands running in the sandbox. |
ReadyToAdvance |
Checks passed; the phase may be advanced. |
Failed |
A check or the run failed; the circuit-break target applies. |
Complete |
Terminal (Implementation done). |
The advance rule (PhaseStatus::can_advance(), phase.rs:98): exactly
one status may advance — ReadyToAdvance. KovaEngine::advance
(crates/kova-core/src/engine.rs:94) rejects anything else with the current
status name; on success it applies Phase::next(), transitions the new
phase to Idle, and records a PhaseChanged event. There is no code path that advances
without this gate.
stateDiagram-v2
[*] --> Idle
Idle --> Working: research started
Working --> Checking: checks dispatched
Checking --> ReadyToAdvance: checks pass
Checking --> Failed: checks fail
Working --> Failed: run error
ReadyToAdvance --> Idle: POST /pipeline/advance (user-gated)
Pipeline state¶
kova_primitives::state::PipelineState (crates/kova-primitives/src/state.rs:9):
pub struct PipelineState {
pub phase: Phase,
pub status: PhaseStatus,
pub plan: Option<String>, // plan artifact name once research produced one
}
PipelineState::initial() is Research / Idle / None — the state
StateStore::open() seeds on first use (state.rs:48); a missing or corrupt
row at read time is an error, never a silent fallback.
Events¶
kova_primitives::event::PipelineEvent (crates/kova-primitives/src/event.rs:16)
— the event log records, in order:
Started · PhaseChanged · CheckStarted · CheckFinished ·
ResearchStarted · ResearchFinished · PlanWritten · GateRead ·
StatusChanged · Log · Error.
The log is append-only: each event is keyed by the tree length in big-endian
bytes at insert time (state.rs:117) and nothing is ever trimmed.
GET /pipeline/events?n= reads the tail of the log (default 100, max 1000,
clamped — kova-api/src/engine.rs:37-40).
The logit phase-gate¶
Phase gating reads the model's raw token distribution rather than parsing
prose. The gate lives in kova-primitives::{gate, decision, logit}:
- Prompt shape (
gate.rs): the gate prompt is"{GATE_SYSTEM}\n{question}\nno_think"withGATE_SYSTEM = "You are Kova's deterministic phase gate. Answer with exactly one word: Yes or No.". Verified live against Qwen3.8-27B: the model emits a newline pair (~0.97 mass) then a single-tokenYes/Noat position 1. The prompt must end withno_thinkand no trailing newline — a trailing newline re-arms Qwen3 thinking mode (top-1think~0.80) and destroys the signal. - Sampling constants (
gate.rs:22):n_predict = 3,n_probs = 16,temperature = 0.0. - Transport: the raw
/completionpath —LlmBackend::complete(prompt, n_predict)(crates/kova-router/src/backend.rs:196) — not the chat path. - Decisions (
decision.rs): three readers over aLogitVector(TokenProb { token, logprob }list —prob()= e^logprob, andmass()sums the probs of selected tokens,logit.rs:5-15): decide_noul— boolean gate →GateDecision { decision, confidence, position };confidenceis the normalizedp_yes / (p_yes + p_no). Fail-closed:positionisNone(sois_confident()is false) when no position met the mass floor (decision.rs:45-63).decide_choice— multi-way gate →ChoiceDecision { winner, confidence, position, masses }(decision.rs:89-97).decide_score— score over1..=max(decide_score(vectors, max, cfg),decision.rs:144-147).- Gate config:
GateConfig { mass_floor, threshold }— a decision counts only if the chosen token's mass clears the floor and its confidence clears the threshold; otherwise the gate fails closed.
The gate reader is unit-tested against the live Qwen3.8-27B capture; the engine's wiring of gate reads into phase transitions is part of the verification-phase work (sub-plan 03+).
Research phase¶
Research is the only model-driven phase. Today it runs
PlaceholderRunner (crates/kova-core/src/research/mod.rs:51) — a stub that
returns a fixed summary and no plan artifact. The real loop is specified in
plans/kova-research-phase-mcp-tool-layer.md (sub-plans 03a–03c):
- Runner seam:
ResearchRunnertrait (research/mod.rs:41) —async fn run(&self, task: &str, sink: &dyn EventSink) -> Result<RunOutcome, KovaError>, installed viaKovaEngine::with_runner; the engine passes itsStateStoreas the sink.RunOutcome { summary, plan, report }(research/mod.rs:19-26);status()isReadyToAdvanceonly whenplan.is_some()and the report isOk, otherwiseFailed(research/mod.rs:31-36). - Tool loop: the model is given 9 research tools (codebase reads, plan
write, etc. — the
McpServerinkova-mcpis the dispatch layer) and must callwrite_planexactly once with a kebab-case name before the run can finish; the run is bounded by the research guard (below). - Plan artifact contract (
kova_primitives::plan): names must be[a-z0-9-]+.md— no leading/trailing dashes, stem ≤ 128 chars (validate_plan_name,plan.rs:10-32); the path is alwaysresearch/plans/{name}(plan_rel_path,plan.rs:35-38) — the only fenced write surface of the pipeline.PlanArtifact { name, path, size, modified }(plan.rs:47-52) records what the run produced;list_plansinventories the directory.
Research budget guard¶
kova_core::guard (crates/kova-core/src/guard.rs) bounds every research
run:
Limit (ResearchLimits) |
Meaning |
|---|---|
max_iterations |
maximum model turns per run |
max_tool_calls_per_iteration |
maximum tool calls in one turn |
tool_result_chars |
tool results are truncated to this many characters (truncate) |
wall_timeout_secs |
wall-clock budget for the whole run |
Budget charges up front (begin_turn / begin_call) and returns the
GuardReport that stopped the run: Ok · IterationsExceeded(n) ·
ToolCallsExceeded(n) · WallTimeout · ModelError(String). It enforces the
turn and call budgets; the wall-clock budget is carried in ResearchLimits
but not yet checked. The limits come from the [research] table
(ResearchConfig, config.rs:185-198), not per model endpoint.
State persistence¶
kova_core::state::StateStore (crates/kova-core/src/state.rs:26) persists
to sled under engine.data_dir (default kovadb):
- a single root key
b"phase"— the serialized state row (StateRow { phase, status, plan },state.rs:18-22), seeded withPipelineState::initial()on firstopen()(state.rs:48); a missing or corrupt row at read time is an error, never a silent fallback. - tree
events— the append-only event log: each event is keyed by the tree length in big-endian bytes at insert time (state.rs:117); nothing is ever trimmed, andrecent_events(n)reads the tail.
StateStore::set_status(status, plan) replaces the status (and optionally
the plan) without touching the phase (state.rs:68-87);
StateStore::transition(phase, status) moves phase and status but never
touches the plan (state.rs:94-111) — callers append their own
PhaseChanged / StatusChanged events (KovaEngine::advance records
PhaseChanged with reason "user advance", engine.rs:110-117).
Concurrency: the engine holds an in-memory running flag — a second
run_research while one is in flight is rejected (engine.rs:71-77).
REST surface¶
Implemented in kova-api (crates/kova-api/src/{lib.rs,engine.rs,gui.rs}):
| Method & path | Purpose |
|---|---|
GET /health |
Liveness: {"service":"kova","status":"ok","version":...} |
GET / |
Embedded GUI (rust-embed via kova-gui) |
GET /pipeline/state |
Current PipelineState |
GET /pipeline/events?n= |
Event log tail (default 100, max 1000) |
POST /pipeline/research |
Body {"task": "..."} — start a research run |
POST /pipeline/advance |
User-gated phase advance |
The engine is reached through the kova_api::engine::Engine trait
(state, recent_events, run_research, advance) — KovaEngine
implements it, and tests substitute a fake.
Divergence ledger¶
The v3 spec (plans/kova-qwen-clean-start-v3.md) describes a slightly
different layout; the tree realizes its intent, not its literal shape:
| Spec item | Actual |
|---|---|
GET /api/v1/pipeline/stream (SSE) |
GET /pipeline/events?n= — polling window; no SSE, no tokio-stream usage |
--gui / --port / --headless flags |
config-driven api.addr in kovarc.toml; GUI always mounted at / |
openapi.json + root README.md |
absent |
dependencies/youki static binary |
YoukiRuntime code present; binary not vendored |
| Automated backward circuit breaking | circuit_break_target() defined + unit-tested, never called by the engine |
| Ractor supervision tree / actor pool | ractor declared in the workspace but unused; kova-actor holds only WorkOrder / VramBudget types |
Repo-root kovarc.toml |
v2 layout — [engine].api_addr, [engine].gui, [engine].gate, per-model kind/label/n_probs/optional |
| MCP research loop | kova-mcp dispatch layer implemented (7 tests); the tool loop itself is sub-plan 03a–03c, not in the tree |
Full file:line evidence for every item:
plans/kova-qwen-clean-start-v3.md.