fix(agent): address gadfly — don't preamble-veto long answers; dedup ignores <url> wrappers
- isSubstantiveAnswer: restore the original "≥200 chars ⇒ real answer"
unconditional accept (the preamble filter now applies only in the
borderline 80–200 band). Applying preambleRe regardless of length regressed
the recovery path: a long front-loaded answer opening with a conversational
word ("Sure,", "Let me explain:", "First,") was wrongly discarded
(flagged by glm-5.2 + claude-code/opus). The NVD-style planning-narration
case is already handled by the citation-dominance check, not this filter.
- citations dedup: compare with <url> angle-bracket wrappers stripped, so a
front-loaded turn that listed the same sources unwrapped still suppresses the
duplicate block.
- doc: spell out the citationLabelRe leading class members (+ list markers);
drop the stale "gated by preamble" note on recoverMinChars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+30
-18
@@ -45,15 +45,26 @@ func finalOutput(msgs []llm.Message, terminal string) string {
|
||||
}
|
||||
if citations {
|
||||
// Preserve the citations addendum below the recovered answer, unless the
|
||||
// recovered turn already carries it verbatim (guards against a duplicate
|
||||
// sources block when the front-loaded turn included its own citations).
|
||||
if tail := strings.TrimSpace(terminal); !strings.Contains(rec, tail) {
|
||||
// recovered turn already carries it (guards against a duplicate sources
|
||||
// block when the front-loaded turn included its own citations). The
|
||||
// containment test ignores <url> angle-bracket wrappers so a turn that
|
||||
// listed the same sources unwrapped still suppresses the duplicate.
|
||||
if tail := strings.TrimSpace(terminal); !strings.Contains(stripURLAngles(rec), stripURLAngles(tail)) {
|
||||
return rec + "\n\n" + tail
|
||||
}
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// stripURLAngles removes the <…> wrappers Discord uses to suppress link embeds,
|
||||
// so the citations dedup compares URLs regardless of that formatting delta.
|
||||
func stripURLAngles(s string) string {
|
||||
if !strings.ContainsAny(s, "<>") {
|
||||
return s
|
||||
}
|
||||
return strings.NewReplacer("<", "", ">", "").Replace(s)
|
||||
}
|
||||
|
||||
// backRefRe matches a terminal turn that merely points back to an earlier
|
||||
// message instead of stating the answer ("(Already answered above.)",
|
||||
// "see above", "as I said", ...).
|
||||
@@ -66,11 +77,12 @@ var preambleRe = regexp.MustCompile(`(?i)^(let me|let'?s|i'?ll|i will|first[, ]|
|
||||
// citationLabelRe matches a terminal turn that OPENS with a sources/citations
|
||||
// heading — the shape a model produces when it front-loads its answer into an
|
||||
// earlier tool-call turn and closes with only its sources. Leading markdown
|
||||
// emphasis / list / block-quote / ATX-heading markers (with their whitespace,
|
||||
// via \s in the class) are tolerated before the label, and closing emphasis
|
||||
// (** / __) plus whitespace between the label and 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.
|
||||
// emphasis (*, _), list (-, +, *), block-quote (>), and ATX-heading (#) markers
|
||||
// — with their whitespace, since \s is in the class — are tolerated before the
|
||||
// label, as is closing emphasis (** / __) plus whitespace between the label and
|
||||
// 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*_]*[::\-—]`)
|
||||
|
||||
// linkRe matches a whole markdown link "[label](url)" or a bare URL. Used both
|
||||
@@ -89,7 +101,8 @@ const (
|
||||
// longer than this, so it is never treated as weak.
|
||||
weakFinalMaxChars = 120
|
||||
// recoverMinChars: a prior assistant turn this long is treated as a real
|
||||
// answer regardless of how it opens (still gated by the preamble check).
|
||||
// answer regardless of how it opens (the preamble filter is not applied at
|
||||
// this length — see isSubstantiveAnswer).
|
||||
recoverMinChars = 200
|
||||
// recoverFloorChars / recoverRatio gate the borderline band: a shorter
|
||||
// prior turn must clear the floor and — unless the terminal is a citations
|
||||
@@ -164,20 +177,19 @@ func lastSubstantiveAssistantText(msgs []llm.Message, terminal string, citations
|
||||
// isSubstantiveAnswer reports whether txt (a prior assistant turn) reads like a
|
||||
// real answer rather than a preamble, relative to the terminal text.
|
||||
//
|
||||
// A preamble ("Let me look that up…") is never the answer regardless of length,
|
||||
// so that check applies BEFORE the length shortcut — otherwise a long planning
|
||||
// narration would be recovered. A sufficiently long non-preamble turn is
|
||||
// accepted; a shorter one must clear a floor and, unless the terminal is a
|
||||
// citations addendum (not a rival answer, so its length is irrelevant), also
|
||||
// A sufficiently long turn (>= recoverMinChars) is accepted unconditionally: a
|
||||
// multi-hundred-char turn is an answer even when it opens conversationally
|
||||
// ("Sure, here's…", "Let me explain: …"), so the preamble filter is NOT applied
|
||||
// to it — applying it there would drop a legitimate long front-loaded answer.
|
||||
// Only in the borderline band does a turn have to clear a floor, not read like a
|
||||
// short planning preamble ("Let me look that up…"), and — unless the terminal is
|
||||
// a citations addendum (not a rival answer, so its length is irrelevant) — also
|
||||
// clearly dwarf the terminal.
|
||||
func isSubstantiveAnswer(txt, terminal string, citations bool) bool {
|
||||
if preambleRe.MatchString(txt) {
|
||||
return false
|
||||
}
|
||||
if len(txt) >= recoverMinChars {
|
||||
return true
|
||||
}
|
||||
if len(txt) < recoverFloorChars {
|
||||
if len(txt) < recoverFloorChars || preambleRe.MatchString(txt) {
|
||||
return false
|
||||
}
|
||||
return citations || len(txt) >= recoverRatio*len(terminal)
|
||||
|
||||
Reference in New Issue
Block a user