grammar: the last two model calls that were still free text

The replier and the meeting summariser were the two call sites without a
GBNF. Both are exactly the shape that makes a Thinking variant answer with
its reasoning as prose, and neither had anything downstream that could
remove it.

The replier already parses {"response","mood"}, so it now sends the phraser's
grammar for that contract, exported once as phraser.ResponseGrammar so the
two definitions cannot drift.

The summariser stays text-in/text-out. The JSON wrapper is attached and
unwrapped in the daemon's Completer, so internal/capture is unchanged and a
Completer without a grammar still works.

The simulator told routing from phrasing by "has a grammar", which stopped
being true here; it now looks for the intent enum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kami
2026-08-02 02:28:53 +04:00
parent 14e98334ad
commit 612ca8cf1b
7 changed files with 118 additions and 4 deletions
+11 -2
View File
@@ -71,6 +71,13 @@ func NewSummarizer(llm Completer, chunkRunes, maxChunks int, contextBlock func()
return &Summarizer{llm: llm, chunkRunes: chunkRunes, maxChunks: maxChunks, context: contextBlock}
}
// Both prompts ask for a JSON wrapper because the daemon's Completer attaches a
// grammar of that shape (summaryGrammar in cmd/mavend/capture.go) and unwraps it
// again before the text reaches this package. The wrapper is what keeps a
// Thinking-variant model from answering a summarisation prompt with its
// reasoning. Nothing here parses it: the map and reduce steps see plain prose,
// and a Completer without the grammar still works.
//
// chunkPrompt — the map step. Deliberately plain: this is not Maven speaking to
// him, it is a model condensing text, so there is no first person in it at all
// and therefore nothing for the persona's gender rules to get wrong. The reply
@@ -79,12 +86,14 @@ func NewSummarizer(llm Completer, chunkRunes, maxChunks int, contextBlock func()
const chunkPrompt = `Ты обрабатываешь фрагмент расшифровки разговора.
Сожми его до 2-4 пунктов: о чём говорили, какие решения приняли, какие задачи назвали.
Без вступлений и выводов. Только по тексту — не придумывай того, чего в нём нет.
Если во фрагменте нет ничего содержательного, ответь одним словом: пусто.`
Если во фрагменте нет ничего содержательного, напиши одно слово: пусто.
Отвечай ТОЛЬКО объектом JSON с одним полем: {"summary": "..."}.`
// reducePrompt — the reduce step. Same rules, over the chunk summaries.
const reducePrompt = `Ниже — конспекты фрагментов одной встречи, по порядку.
Собери из них один короткий итог: о чём была встреча, какие решения приняли, что кому делать.
Не повторяйся, не придумывай, не добавляй вступлений.`
Не повторяйся, не придумывай, не добавляй вступлений.
Отвечай ТОЛЬКО объектом JSON с одним полем: {"summary": "..."}.`
// emptyMarker — what the map step answers for a chunk with nothing in it. Such
// chunks are dropped before the reduce step rather than padding it with noise.
+5
View File
@@ -603,6 +603,11 @@ string ::= "\"" ([^"\\] | "\\" ["\\/bfnrt]){0,1000} "\""
ws ::= [ \t\n]*
`
// ResponseGrammar exposes responseGrammar to the other callers that emit the
// same {"response","mood"} contract — cmd/mavend's reactive replier, which is
// parsed by the same two fields. One definition, so the two cannot drift.
const ResponseGrammar = responseGrammar
// grammar returns the GBNF to attach to a phrasing request, or "" when the
// operator turned it off.
func (p *LLMPhraser) grammar() string {