Skip to content

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" with GATE_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-token Yes/No at position 1. The prompt must end with no_think and no trailing newline — a trailing newline re-arms Qwen3 thinking mode (top-1 think ~0.80) and destroys the signal.
  • Sampling constants (gate.rs:22): n_predict = 3, n_probs = 16, temperature = 0.0.
  • Transport: the raw /completion path — LlmBackend::complete(prompt, n_predict) (crates/kova-router/src/backend.rs:196) — not the chat path.
  • Decisions (decision.rs): three readers over a LogitVector (TokenProb { token, logprob } list — prob() = e^logprob, and mass() sums the probs of selected tokens, logit.rs:5-15):
  • decide_noul — boolean gate → GateDecision { decision, confidence, position }; confidence is the normalized p_yes / (p_yes + p_no). Fail-closed: position is None (so is_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 over 1..=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: ResearchRunner trait (research/mod.rs:41) — async fn run(&self, task: &str, sink: &dyn EventSink) -> Result<RunOutcome, KovaError>, installed via KovaEngine::with_runner; the engine passes its StateStore as the sink. RunOutcome { summary, plan, report } (research/mod.rs:19-26); status() is ReadyToAdvance only when plan.is_some() and the report is Ok, otherwise Failed (research/mod.rs:31-36).
  • Tool loop: the model is given 9 research tools (codebase reads, plan write, etc. — the McpServer in kova-mcp is the dispatch layer) and must call write_plan exactly 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 always research/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_plans inventories 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 with PipelineState::initial() on first open() (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, and recent_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.