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
+26
View File
@@ -116,3 +116,29 @@ func TestStopReturnsTranscriptAndNotesItWithoutASummary(t *testing.T) {
t.Fatalf("the meeting left no note behind: %+v", notes)
}
}
// The summary path is JSON-wrapped by summaryGrammar, and internal/capture must
// keep seeing plain prose. These cover the wrapper and every way it can be
// absent or broken, because a meeting summary is written once and not retried.
func TestUnwrapSummary(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"grammar output", `{"summary": "решили купить насос"}`, "решили купить насос"},
{"multiline field", `{"summary": "- насос\n- бюджет"}`, "- насос\n- бюджет"},
{"empty marker survives", `{"summary": "пусто"}`, "пусто"},
{"empty field says nothing", `{"summary": ""}`, ""},
{"thinking prefix", "<think>hm</think>\n{\"summary\": \"итог\"}", "итог"},
{"no grammar, plain prose", "решили купить насос", "решили купить насос"},
{"broken json falls back", `{"summary": "обрыв`, `{"summary": "обрыв`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := unwrapSummary(c.in); got != c.want {
t.Errorf("unwrapSummary(%q) = %q, want %q", c.in, got, c.want)
}
})
}
}