package main import ( "context" "github.com/kami/maven/internal/phraser" "github.com/kami/maven/internal/router" "github.com/kami/maven/internal/voice" ) // llmReplier is the daemon-side wiring around phraser.Replier: it owns the // deterministic floor, and nothing else. The phrasing itself, the prompt and the // output parsing live in internal/phraser so the eval can score them (#396). type llmReplier struct { p *phraser.Replier stub *voice.StubReplier } func newLLMReplier(c phraser.Completer, block func() string) *llmReplier { return &llmReplier{p: phraser.NewReplier(c, block), stub: voice.NewStubReplier()} } // Reply never fails: a clarify, a model error and an unusable generation all // answer from the stub, which is what keeps a turn from breaking on the model. func (r *llmReplier) Reply(ctx context.Context, d router.Decision) string { if d.Clarify { // The deck, not the stub's single sentence: a clarify she cannot turn // into a question is the line he hears most often when she misses him, // and it used to be the same words every time (Vikunja #457). Still no // model call — this text has to be right every time, and it is not worth // a generation to say something this small. return clarifyMissedLine(d) } if d.Intent == router.IntentFact { // A captured fact is confirmed by echoing him, and the model is not // asked (V-592). It has nothing to phrase FROM: replyContext hands it // "записала факт: water \"drank\"", so every Russian word in the reply // was the model's own invention, and on 2026-08-06 that was "Проверила, // что ты выпел стакан воды" for "я выпил воды". return phraser.FactAck(d.Utterance) } if d.Intent == router.IntentNote { // A successful durable write needs no generation. The resident model // answered one live capture with masculine self-reference ("сохранил") // despite the prompt; the hand-written line is both faster and a hard // persona guarantee on the daemon's reply path (V-721). return phraser.Ack(phraser.AckNote, nil) } out, err := r.p.PhraseReply(ctx, d) if err != nil || out == "" { return r.stub.Reply(ctx, d) } // The persona checks, on the live path (personaguard.go). A reply that // leaks reasoning or calls him "вы" is worse than a flat one. if _, ok := guardSpoken("reply", out); !ok { return r.stub.Reply(ctx, d) } return out }