fix(agent): recover the front-loaded answer over a summary closer #25
+14
-4
@@ -52,7 +52,7 @@ func finalOutput(msgs []llm.Message, terminal string) string {
|
|||||||
case isCitationsOnly(terminal):
|
case isCitationsOnly(terminal):
|
||||||
mode = modeCitations
|
mode = modeCitations
|
||||||
case isWeakFinal(terminal):
|
case isWeakFinal(terminal):
|
||||||
|
|
|||||||
// modeBackRef
|
mode = modeBackRef
|
||||||
case isSummaryCloser(terminal):
|
case isSummaryCloser(terminal):
|
||||||
mode = modeSummary
|
mode = modeSummary
|
||||||
default:
|
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
|
// behavior (fail closed). Assembled from named fragments so the alternations
|
||||||
// stay legible and extendable.
|
// stay legible and extendable.
|
||||||
const (
|
const (
|
||||||
|
gitea-actions
commented
🟡 Leading-marker char class maintainability · flagged by 3 models
🪰 Gadfly · advisory 🟡 **Leading-marker char class `[\s>#*_+-]*` duplicated between summaryLead and citationLabelRe (comment admits the copy); hoist a shared const**
_maintainability · flagged by 3 models_
- **`agent/finalize.go:131` / `:156` — duplicated leading-marker character class.** `summaryLead = `[\s>#*_+-]*`` is a byte-for-byte copy of the leading class in `citationLabelRe`, and the comment (`// … as in citationLabelRe`) explicitly acknowledges the copy. Two independent literals for "the same set of leading markdown/list markers" will drift if one is ever extended. Low-churn fix: hoist a shared `const leadMarkers = `[\s>#*_+-]*`` and reference it from both `summaryLead` and `citationLabel…
<sub>🪰 Gadfly · advisory</sub>
|
|||||||
summaryLead = `[\s>#*_+-]*` // leading markdown/list markers, as in citationLabelRe
|
|
||||||
summaryPreface = `((done|all set|ok(ay)?)[\s,.!:—-]+)?` // optional "Done —" style opener
|
summaryPreface = `((done|all set|ok(ay)?)[\s,.!:—-]+)?` // optional "Done —" style opener
|
||||||
summaryNouns = `(citations?|sources?|references?|claims?)`
|
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+)*`
|
summaryCopulas = `((are|were|have\s+been|all)\s+)*`
|
||||||
summaryVerbs = `(logged|recorded|cited|saved|noted|captured|filed)`
|
summaryVerbs = `(logged|recorded|cited|saved|noted|captured|filed)`
|
||||||
summaryArticle = `((all|the)\s+)*` // star, not ?: "Logged all the citations."
|
summaryArticle = `((all|the)\s+)*` // star, not ?: "Logged all the citations."
|
||||||
)
|
)
|
||||||
|
gitea-actions
commented
🟠 summaryCloserRe regex can false-positive on prose where [.] is not end-of-sentence error-handling · flagged by 1 model
🪰 Gadfly · advisory 🟠 **summaryCloserRe regex can false-positive on prose where [.] is not end-of-sentence**
_error-handling · flagged by 1 model_
- `agent/finalize.go:139-141` — The `summaryCloserRe` regex matches a prefix ending in `[.!]` but does not enforce that the sentence actually terminates there (whitespace or end-of-string). Because `MatchString` returns true for any prefix match, a string like `"Citations are logged...and then more text"` matches even though the model is continuing prose about citations rather than producing a bookkeeping ack. This false positive could cause legitimate terminal text to be misclassified as a summ…
<sub>🪰 Gadfly · advisory</sub>
|
|||||||
|
|
||||||
var summaryCloserRe = regexp.MustCompile(`(?i)^` + summaryLead + summaryPreface +
|
var summaryCloserRe = regexp.MustCompile(`(?i)^` + leadingMarkers + summaryPreface +
|
||||||
`(` + summaryArticle + summaryNouns + `\s+` + summaryCopulas + summaryVerbs +
|
`(` + summaryArticle + summaryNouns + `\s+` + summaryCopulas + summaryVerbs +
|
||||||
`|logged\s+` + summaryArticle + summaryNouns + `)[.!]`)
|
`|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
|
// the colon/dash separator. Anchored at ^ so a normal answer that merely
|
||||||
// mentions "sources" mid-sentence, or ends with a "Sources:" section AFTER its
|
// mentions "sources" mid-sentence, or ends with a "Sources:" section AFTER its
|
||||||
// prose, is never matched.
|
// 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
|
// 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
|
// to require that a citations terminal carries at least one link and to strip
|
||||||
|
|||||||
+10
-10
@@ -354,11 +354,11 @@ func TestFinalOutput(t *testing.T) {
|
|||||||
want: b3cb9ee9Closer,
|
want: b3cb9ee9Closer,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Gadfly (opus, correctness): the modeSummary scan must stop at the
|
// The modeSummary scan must stop at the most recent user message.
|
||||||
|
gitea-actions
commented
🟡 External review reference embedded in permanent test comment maintainability · flagged by 1 model
🪰 Gadfly · advisory 🟡 **External review reference embedded in permanent test comment**
_maintainability · flagged by 1 model_
- `agent/finalize_test.go:357,391` — Test-case comments embed external review references (`// Gadfly (opus, correctness):`, `// Gadfly (opus, error-handling):`). These will be meaningless noise to future maintainers who don't have access to that review context. Replace them with plain prose describing the invariant being guarded.
<sub>🪰 Gadfly · advisory</sub>
|
|||||||
// most recent user message. Here the current turn's answer sits in
|
// Here the current turn's answer sits in the 1x-3x band (rejected
|
||||||
// the 1x-3x band (rejected by the ratio) while a dwarfing answer to
|
// by the ratio) while a dwarfing answer to a DIFFERENT question
|
||||||
// a DIFFERENT question sits in history — resurrecting it would be
|
// sits in history — resurrecting it would be strictly worse than
|
||||||
// strictly worse than keeping the closer.
|
// keeping the closer.
|
||||||
name: "summary closer never resurrects a stale answer across the user boundary",
|
name: "summary closer never resurrects a stale answer across the user boundary",
|
||||||
msgs: []llm.Message{
|
msgs: []llm.Message{
|
||||||
llm.UserText("earlier, unrelated question?"),
|
llm.UserText("earlier, unrelated question?"),
|
||||||
@@ -388,11 +388,11 @@ func TestFinalOutput(t *testing.T) {
|
|||||||
want: hugeAnswer,
|
want: hugeAnswer,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Gadfly (opus, error-handling): a closer matching BOTH the ack
|
// A closer matching BOTH the ack shape and a back-reference
|
||||||
// shape and a back-reference carries no answer content, so the
|
// carries no answer content, so the back-ref test must win and the
|
||||||
// back-ref test must win and the ordinary recovery bar apply —
|
// ordinary recovery bar apply — under the summary bar this
|
||||||
// under the summary bar this ~106-byte terminal would demand a
|
// ~106-byte terminal would demand a ~318-byte prior and wrongly
|
||||||
// ~318-byte prior and wrongly keep the closer over longAnswer.
|
// keep the closer over longAnswer.
|
||||||
name: "back-reference wins over the summary ack when both match",
|
name: "back-reference wins over the summary ack when both match",
|
||||||
msgs: []llm.Message{
|
msgs: []llm.Message{
|
||||||
llm.UserText("q?"),
|
llm.UserText("q?"),
|
||||||
|
|||||||
Reference in New Issue
Block a user
⚪ Empty isWeakFinal case relies on pre-initialized mode; explicit
mode = modeBackRefwould be more self-containedmaintainability · flagged by 2 models
agent/finalize.go:54-55— emptycasebody relying on the pre-initializedmode.go case isWeakFinal(terminal): // modeBackRefThis depends onmode := modeBackRefset two lines up; the comment documents it, but an explicitmode = modeBackRefin the case body is more self-contained and survives a future refactor that changes the initializer. Trivial, optional.🪰 Gadfly · advisory