// Package router is maven's reactive path — the cascade that turns a free-form // utterance into a deterministic Decision. // // Spec contract (from maven.md § reactive path — router): // // - routing is a DECISION, and every decision in maven stays deterministic. // a classifier owns the route; the SLM stays in its phrasing lane. same // boundary as "rules decide, llm phrases," extended to the reactive path. // - a CASCADE, not classifier-vs-deterministic — 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 vosk command path. // stage 1 — intent classifier. embed utterance, nearest-centroid over // labeled intents. one forward pass, ~30ms cpu, similarity score. // 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 six save-where labels from the spec'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 → chroma // - query: answer, don't store → slm reads sqlite or chroma (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 }