diff --git a/agent/finalize.go b/agent/finalize.go index 88c20ac..72b9e77 100644 --- a/agent/finalize.go +++ b/agent/finalize.go @@ -52,7 +52,7 @@ func finalOutput(msgs []llm.Message, terminal string) string { case isCitationsOnly(terminal): mode = modeCitations case isWeakFinal(terminal): - // modeBackRef + mode = modeBackRef case isSummaryCloser(terminal): mode = modeSummary default: @@ -128,15 +128,17 @@ var backRefRe = regexp.MustCompile(`(?i)(already answered|see above|as (i )?(sai // behavior (fail closed). Assembled from named fragments so the alternations // stay legible and extendable. const ( - summaryLead = `[\s>#*_+-]*` // leading markdown/list markers, as in citationLabelRe summaryPreface = `((done|all set|ok(ay)?)[\s,.!:—-]+)?` // optional "Done —" style opener summaryNouns = `(citations?|sources?|references?|claims?)` + // "all" appears here AND in summaryArticle on purpose: as a quantifier + // between noun and verb ("Citations all logged.") and as a determiner + // before the noun ("All claims cited.", "Logged all the citations."). summaryCopulas = `((are|were|have\s+been|all)\s+)*` summaryVerbs = `(logged|recorded|cited|saved|noted|captured|filed)` summaryArticle = `((all|the)\s+)*` // star, not ?: "Logged all the citations." ) -var summaryCloserRe = regexp.MustCompile(`(?i)^` + summaryLead + summaryPreface + +var summaryCloserRe = regexp.MustCompile(`(?i)^` + leadingMarkers + summaryPreface + `(` + summaryArticle + summaryNouns + `\s+` + summaryCopulas + summaryVerbs + `|logged\s+` + summaryArticle + summaryNouns + `)[.!]`) @@ -153,7 +155,15 @@ var preambleRe = regexp.MustCompile(`(?i)^(let me|let'?s|i'?ll|i will|first[, ]| // the colon/dash separator. Anchored at ^ so a normal answer that merely // mentions "sources" mid-sentence, or ends with a "Sources:" section AFTER its // prose, is never matched. -var citationLabelRe = regexp.MustCompile(`(?i)^[\s>#*_+-]*(sources?|references?|citations?|works cited|further reading)\b[\s*_]*[::\-—]`) +var citationLabelRe = regexp.MustCompile(`(?i)^` + leadingMarkers + + `(sources?|references?|citations?|works cited|further reading)\b[\s*_]*[::\-—]`) + +// leadingMarkers tolerates markdown noise before a label: emphasis (*, _), +// list (-, +, *), block-quote (>), and ATX-heading (#) markers, with their +// whitespace. Shared by citationLabelRe and summaryCloserRe so the two +// classifiers cannot drift apart (the first draft of the summary class +// dropped '+' by hand-copying this set). +const leadingMarkers = `[\s>#*_+-]*` // linkRe matches a whole markdown link "[label](url)" or a bare URL. Used both // to require that a citations terminal carries at least one link and to strip diff --git a/agent/finalize_test.go b/agent/finalize_test.go index 89703fb..61e6586 100644 --- a/agent/finalize_test.go +++ b/agent/finalize_test.go @@ -354,11 +354,11 @@ func TestFinalOutput(t *testing.T) { want: b3cb9ee9Closer, }, { - // Gadfly (opus, correctness): the modeSummary scan must stop at the - // most recent user message. Here the current turn's answer sits in - // the 1x-3x band (rejected by the ratio) while a dwarfing answer to - // a DIFFERENT question sits in history — resurrecting it would be - // strictly worse than keeping the closer. + // The modeSummary scan must stop at the most recent user message. + // Here the current turn's answer sits in the 1x-3x band (rejected + // by the ratio) while a dwarfing answer to a DIFFERENT question + // sits in history — resurrecting it would be strictly worse than + // keeping the closer. name: "summary closer never resurrects a stale answer across the user boundary", msgs: []llm.Message{ llm.UserText("earlier, unrelated question?"), @@ -388,11 +388,11 @@ func TestFinalOutput(t *testing.T) { want: hugeAnswer, }, { - // Gadfly (opus, error-handling): a closer matching BOTH the ack - // shape and a back-reference carries no answer content, so the - // back-ref test must win and the ordinary recovery bar apply — - // under the summary bar this ~106-byte terminal would demand a - // ~318-byte prior and wrongly keep the closer over longAnswer. + // A closer matching BOTH the ack shape and a back-reference + // carries no answer content, so the back-ref test must win and the + // ordinary recovery bar apply — under the summary bar this + // ~106-byte terminal would demand a ~318-byte prior and wrongly + // keep the closer over longAnswer. name: "back-reference wins over the summary ack when both match", msgs: []llm.Message{ llm.UserText("q?"),