Preserve context across conversation intents (V-542)

Owner explicitly requested direct commits to master; bypass the branch-only hook.
This commit is contained in:
2026-08-13 02:14:46 +04:00
parent a0e6643465
commit da9114b623
15 changed files with 499 additions and 56 deletions
+21 -4
View File
@@ -127,10 +127,14 @@ type toolRow struct {
// Route and Reply are separate because the same model serves both contracts
// (CLAUDE.md, "LLM output contract"): a grammar-constrained call is a routing
// call and gets Route, an unconstrained one is a phrasing call and gets Reply.
// HistoryContains makes a chat reply conditional on the transcript the daemon
// supplied. It prevents a canned answer from making a continuity scenario pass
// while the referent is still absent from the model input.
type scriptEntry struct {
Match string `json:"match"`
Route string `json:"route,omitempty"`
Reply string `json:"reply,omitempty"`
Match string `json:"match"`
Route string `json:"route,omitempty"`
Reply string `json:"reply,omitempty"`
HistoryContains []string `json:"history_contains,omitempty"`
}
// step — one scripted moment. At is "HH:MM" or "HH:MM:SS", interpreted in the
@@ -369,7 +373,7 @@ type scriptedPhraser struct {
// matches scriptedLLM: actionChat logs it and falls back to ChatFallback(), so a
// scenario that never meant to assert on a chat reply behaves exactly as it did
// before, and one that DID means to is told its script has a hole.
func (p *scriptedPhraser) PhraseChat(_ context.Context, utterance string, _ []dialogue.Turn) (string, error) {
func (p *scriptedPhraser) PhraseChat(_ context.Context, utterance string, history []dialogue.Turn) (string, error) {
for _, e := range p.entries {
if e.Reply == "" {
continue
@@ -377,6 +381,19 @@ func (p *scriptedPhraser) PhraseChat(_ context.Context, utterance string, _ []di
if e.Match != "" && !strings.Contains(strings.ToLower(utterance), strings.ToLower(e.Match)) {
continue
}
for _, want := range e.HistoryContains {
found := false
for _, turn := range history {
if containsFold(turn.Text, want) {
found = true
break
}
}
if !found {
return "", fmt.Errorf("simulator: chat history for %q does not contain %q: %+v",
truncateRunes(utterance, 60), want, history)
}
}
return chatReplyText(e.Reply), nil
}
return "", fmt.Errorf("simulator: no scripted chat reply for %q", truncateRunes(utterance, 60))