e0d0244fa9
15 root markdown files, ~4,900 lines against ~33,000 lines of Go, with at least three pairs contradicting each other. When five documents describe the architecture, the code becomes the only trustworthy one — which defeats the point of having them. That drift is why the resident-model question had four incompatible answers. SPEC.md, maven.md and ROADMAP.md are deduped into DESIGN.md rather than concatenated, with a "Superseded" section carrying eight retired decisions and what replaced each: classifier-owns-the-route (the cascade is still the live path, but as a stopgap, not a design to extend), faster-whisper/vosk/silero, the small-model phrasing claim, sqlcipher, the Kotlin/Spring sketches, obsidian->chroma, script deployment, and FloorEnrollment. Superseded material is kept and marked rather than deleted, so it cannot read as current. SESSION-05/06-07-2026.md and PLANS.md are removed outright — git history holds them, and both were verified tracked before deletion. Go doc comments citing the deleted files are repointed to the equivalent DESIGN.md sections. Several asserted designs that were already retired, so the claims are corrected and not just relinked: stt.go named faster-whisper as production (it is whisper.cpp), tts.go named silero (it is piper), intent.go still described the classifier as owning the route, and stale vosk/chroma vocabulary is replaced. ECOSYSTEM-SPEC.md references are deliberately untouched — that is a different document, and a naive grep for SPEC.md matches it. Root markdown drops from 4,880 to ~3,700 lines. The review's ~1,500 target is not reachable while keeping the files it also said to keep — those alone are 2,553 lines — so trimming further needs a separate decision on MAVEN_ECOSYSTEM_ARCHITECTURE.md and PROGRESS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
108 lines
5.1 KiB
Go
108 lines
5.1 KiB
Go
// Package router is maven's reactive path — the cascade that turns a free-form
|
|
// utterance into a deterministic Decision.
|
|
//
|
|
// Spec contract (from DESIGN.md § Reactive path — routing):
|
|
//
|
|
// - the TARGET design is LLM-as-router: the resident model (Qwen3-1.7B)
|
|
// emits GBNF-constrained structured JSON for the route, and the same
|
|
// model phrases replies; the embedder is a RAG hint, not a routing gate.
|
|
// the classifier/embedder cascade below is the committed default today,
|
|
// but it is an interim stopgap (DESIGN.md § Superseded, "classifier-owns-
|
|
// the-route") and the known cause of weak RU query handling — not a
|
|
// design to extend.
|
|
// - a CASCADE, not one decider — layers:
|
|
// stage 0 — exact match (regex/grammar). wake-word + known command
|
|
// grammar. "maven, restart nginx" hits the allowlist directly,
|
|
// skips the classifier. lowest latency — the client-side wake-word/
|
|
// command-grammar path (cmd/mavwaked).
|
|
// stage 1 — route decision. the resident model (target), else the
|
|
// nearest-centroid classifier over embedded labeled intents (today's
|
|
// stopgap: one forward pass, ~30ms cpu, similarity score). any LLM
|
|
// error falls through to the classifier so a turn never breaks.
|
|
// stage 2 — slot extraction, per intent. classification gives *what kind*,
|
|
// not *the args*. reminders need a datetime, acts need fn+params.
|
|
// stage 3 — confidence gate. below threshold → clarify, don't guess.
|
|
// same pattern as since(key)==null → don't fire: a misrouted
|
|
// fact is a confident wrong write — worse than a gap.
|
|
// - save-where is the routing axis: act | reminder | fact | note | query.
|
|
// - misroute correction = new centroid example — append-only, grows the
|
|
// classifier as used. same shape as nudges.outcome tuning cooldowns.
|
|
//
|
|
// The Embedder is the one impure seam (the ONNX int8 model is a later module).
|
|
// Given a deterministic embedder, the classifier + cascade are pure and unit-
|
|
// testable with zero infra — same instinct as the loop's gather/pure split.
|
|
package router
|
|
|
|
import "time"
|
|
|
|
// Intent — the seven save-where labels from DESIGN.md's routing table. The
|
|
// discriminator is "does the loop evaluate a predicate against it?":
|
|
//
|
|
// - act: command now, not stored (function call into the allowlist)
|
|
// - reminder: has a fire-time → reminders table (sqlite). bypasses the gate
|
|
// - fact: structured state the loop reasons over → facts (sqlite)
|
|
// - note: recall/preference, no predicate touches it → semantic store
|
|
// - query: answer, don't store → the resident model reads sqlite or the
|
|
// semantic store (RAG)
|
|
// - chat: conversational, no store side-effect — LLM replies from
|
|
// dialogue history + general knowledge
|
|
//
|
|
// fact-vs-note is the whole line: predicate will read it → structured facts
|
|
// row; just "recall when relevant" → semantic store. reminder splits off by
|
|
// future timestamp; act splits off by being imperative-now.
|
|
type Intent string
|
|
|
|
const (
|
|
IntentAct Intent = "act"
|
|
IntentReminder Intent = "reminder"
|
|
IntentFact Intent = "fact"
|
|
IntentNote Intent = "note"
|
|
IntentQuery Intent = "query"
|
|
IntentChat Intent = "chat"
|
|
IntentSystem Intent = "system"
|
|
)
|
|
|
|
// Slots — per-intent extracted arguments (stage 2). Not every field is set for
|
|
// every intent; the Intent decides which matter. A slot that doesn't parse
|
|
// leaves its Has* flag false — the daemon's SLM last-resort lane picks it up
|
|
// for free-form notes the parsers choke on. Never an error to be missing.
|
|
type Slots struct {
|
|
// Reminder: absolute fire time. The router resolves relative→absolute AT
|
|
// CAPTURE ("in 4h" → now+4h, never the string) per spec — the reminders
|
|
// table stores only absolute fire_ts. HasTime=false ⇒ no datetime parsed.
|
|
Time time.Time
|
|
HasTime bool
|
|
|
|
// Act: the function name from the allowlist + positional args. The router
|
|
// fuzzy-matches the verb against the allowlist; not on the list → refuse,
|
|
// don't improvise. Fn empty ⇒ no allowlist match. Destructive acts still
|
|
// gate behind confirm at the daemon layer, not here.
|
|
Fn string
|
|
Args []string
|
|
HasFn bool
|
|
|
|
// Fact: structured (key,value) the loop will evaluate predicates against.
|
|
// "drank water" → key=water; "slept 6h" → key=sleep, value=6h. The value
|
|
// is the raw string the daemon json-encodes before WriteFact.
|
|
Key string
|
|
Value string
|
|
HasKey bool
|
|
|
|
// Note/Query: free-form payload (chroma-bound for note, RAG input for query).
|
|
// Always set to the utterance for those intents.
|
|
Text string
|
|
}
|
|
|
|
// Decision — the router's output. The cascade is deterministic: stage 0 wins
|
|
// outright; otherwise classify (1) → extract (2) → gate (3). Clarify=true ⇒
|
|
// the daemon asks instead of guessing — "shuts up when uncertain" for routing,
|
|
// same shape as since(key)==null → don't fire for the loop.
|
|
type Decision struct {
|
|
Utterance string
|
|
Stage int // 0 exact-match, 1 classified, 2 slots-extracted, 3 clarify-gated
|
|
Intent Intent
|
|
Confidence float64 // 1.0 for stage-0; classifier cosine similarity for 1+
|
|
Slots Slots
|
|
Clarify bool // stage 3: below threshold — ask, don't guess
|
|
}
|