fix(agent): recover the answer behind a bare "above" pointer
mort issue #1611: run 8eea3e82 front-loaded a 2,245-char analysis into its cite-call turn and closed with 220 bytes — "Done — that's the full chain above. Short version: …". The user got the 220 bytes and a pointer at a chain that was never posted. finalOutput already had three shapes for this pathology, and the closer matched none of them: too long for the weak-final cap (220 > 120), no citations heading, and no "Citations are logged." ack to open the summary-closer class. So it was delivered verbatim. The three shapes were each a separate vocabulary of ack phrases, which is why a fourth phrasing walked straight through. Two of them are really one signal — the terminal DEFERS, telling us the answer is somewhere the user cannot see — differing only in whether the terminal also carries content of its own. That signal is now isBackRef, shared by both, so a new phrasing is added once and covered in the bare and the "+ compression" variant at the same time. Its open-ended half is aboveRefRe: a DEICTIC "above", separated from the preposition by what follows the word. The deictic use ends its clause ("that's the full chain above.", "as shown above,"); the preposition always continues into a noun phrase ("above 100°C", "above the fold", "above all, …"). pointsAbove additionally requires the reference in the terminal's first 120 bytes — with almost no text before it in THIS message, it cannot be pointing at the message's own content. isSummaryCloser now opens on either the citations ack or pointsAbove. Recovery is unchanged: the mandatory dwarf ratio and the user-message scan boundary still gate it, so a closer only loses to a prior turn in the same user turn that is clearly the fuller original. Break-checked: reverting either classifier, dropping the clause-final rule, or dropping the offset bound each kills a named test; the unmutated control survives.
This commit is contained in:
+73
-19
@@ -25,14 +25,21 @@ import (
|
||||
// glm-5.2 "cite" pattern behind mort issue #1418). The citations are real,
|
||||
// useful content — unlike a back-reference — so recover the prior answer and
|
||||
// KEEP the citations, appended below it.
|
||||
// - a bookkeeping closer ("Citations are logged. Short version: …"): the
|
||||
// model acknowledged the citation round and compressed the answer it had
|
||||
// already written into a one-liner (mort run b3cb9ee9 — a 2,089-char answer
|
||||
// shrank to a 153-byte closer at delivery). The compression is strictly
|
||||
// poorer than the front-loaded answer, so recover the prior turn and
|
||||
// DISCARD the closer — but only when the prior turn clearly dwarfs it,
|
||||
// because unlike a back-reference this closer DOES carry answer content
|
||||
// (see modeSummary).
|
||||
// - a deferring closer ("Citations are logged. Short version: …", "Done —
|
||||
// that's the full chain above. Short version: …"): the model points at the
|
||||
// answer it had already written and compresses it into a one-liner (mort
|
||||
// run b3cb9ee9 — a 2,089-char answer shrank to a 153-byte closer; mort
|
||||
// issue #1611 — a 2,245-char analysis shrank to 220 bytes). The
|
||||
// compression is strictly poorer than the front-loaded answer, so recover
|
||||
// the prior turn and DISCARD the closer — but only when the prior turn
|
||||
// clearly dwarfs it, because unlike a bare back-reference this closer DOES
|
||||
// carry answer content (see modeSummary).
|
||||
//
|
||||
// The last two shapes share one signal — the terminal DEFERS: it tells us the
|
||||
// answer is somewhere the user cannot see (isBackRef, or the bookkeeping ack).
|
||||
// They differ only in whether the terminal also carries content of its own, so
|
||||
// a new deferral phrase added to that shared signal is covered in both the
|
||||
// bare and the "+ compression" variant at once.
|
||||
//
|
||||
// A citations addendum is tested first and wins over the other two (a short
|
||||
// terminal can match more than one shape), so its links are never discarded.
|
||||
@@ -110,9 +117,39 @@ const (
|
||||
|
||||
// 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", ...).
|
||||
// "see above", "as I said", ...). It is a list of fixed phrasings; aboveRefRe
|
||||
// covers the open-ended half of the same family.
|
||||
var backRefRe = regexp.MustCompile(`(?i)(already answered|see above|as (i )?(said|mentioned|stated|noted)|answered (that )?above|per my (previous|earlier))`)
|
||||
|
||||
// aboveRefRe matches a DEICTIC "above" — one pointing at earlier text rather
|
||||
// than serving as a preposition. What follows the word separates the two uses:
|
||||
// the deictic use ends its clause ("that's the full chain above.", "as shown
|
||||
// above,", a line that simply ends in "above"), while the preposition always
|
||||
// continues into a noun phrase ("above 100°C", "above the fold", "above all,
|
||||
// the ..."). Only the clause-final form matches, so the open-ended half of the
|
||||
// back-reference family is covered without enumerating every phrasing a model
|
||||
// might invent — backRefRe's fixed list kept missing new ones (mort issue
|
||||
// #1611: "Done — that's the full chain above.").
|
||||
var aboveRefRe = regexp.MustCompile(`(?i)\babove\b[ \t]*([.,;:!?)\]"'’”—-]|\n|$)`)
|
||||
|
||||
// pointsAbove reports whether a deictic "above" appears in the terminal's
|
||||
// OPENING. The offset bound is what makes a bare "above" safe to key on: with
|
||||
// almost no text before it in THIS message, the reference cannot be pointing
|
||||
// at the message's own content, so it must point at a turn the user never saw
|
||||
// (the harness delivers only the final turn).
|
||||
func pointsAbove(t string) bool {
|
||||
loc := aboveRefRe.FindStringIndex(t)
|
||||
return loc != nil && loc[0] <= backRefHeadChars
|
||||
}
|
||||
|
||||
// isBackRef reports whether a terminal turn defers to earlier content instead
|
||||
// of stating the answer — the signal shared by the back-reference and
|
||||
// summary-closer shapes (see finalOutput). Extend the class here, once, rather
|
||||
// than in either caller.
|
||||
func isBackRef(t string) bool {
|
||||
return backRefRe.MatchString(t) || pointsAbove(t)
|
||||
}
|
||||
|
||||
// summaryCloserRe matches a terminal turn that OPENS with a bookkeeping
|
||||
// acknowledgment of the citation round — "Citations are logged.", "Sources
|
||||
// cited.", "Logged the citations." — the shape a model produces when it
|
||||
@@ -180,6 +217,13 @@ const (
|
||||
// genuine final answer that merely contains "as I said" mid-sentence is
|
||||
// longer than this, so it is never treated as weak.
|
||||
weakFinalMaxChars = 120
|
||||
// backRefHeadChars bounds how far into the terminal a deictic "above" may
|
||||
// sit and still read as pointing OUTSIDE this message (see pointsAbove).
|
||||
// Same guard as weakFinalMaxChars — "there is not enough text before the
|
||||
// reference for it to be pointing at content inside this turn" — but
|
||||
// expressed as an offset, because a summary closer carries a compression
|
||||
// AFTER the pointer and so is not itself short.
|
||||
backRefHeadChars = 120
|
||||
// recoverMinChars: a prior assistant turn this long is treated as a real
|
||||
// answer regardless of how it opens (the preamble filter is not applied at
|
||||
// this length — see isSubstantiveAnswer).
|
||||
@@ -203,13 +247,16 @@ const (
|
||||
)
|
||||
|
||||
// isWeakFinal reports whether a terminal turn's text fails to stand on its own
|
||||
// as the answer: empty/whitespace, or a short pure back-reference.
|
||||
// as the answer: empty/whitespace, or a short pure back-reference. The length
|
||||
// cap is what keeps a genuine answer that merely contains a back-reference
|
||||
// phrase mid-sentence out of the class; past the cap a deferring terminal is
|
||||
// isSummaryCloser's business, under the stricter dwarf bar.
|
||||
func isWeakFinal(s string) bool {
|
||||
t := strings.TrimSpace(s)
|
||||
if t == "" {
|
||||
return true
|
||||
}
|
||||
return len(t) <= weakFinalMaxChars && backRefRe.MatchString(t)
|
||||
return len(t) <= weakFinalMaxChars && isBackRef(t)
|
||||
}
|
||||
|
||||
// isCitationsOnly reports whether a terminal turn is essentially just a
|
||||
@@ -238,19 +285,26 @@ func isCitationsOnly(s string) bool {
|
||||
return len(residue) <= len(t)/citationDominatedDivisor
|
||||
}
|
||||
|
||||
// isSummaryCloser reports whether a terminal turn is a bookkeeping closer: it
|
||||
// opens with a complete "citations are logged"-style ack sentence (see
|
||||
// summaryCloserRe) and is short enough that whatever follows the ack can only
|
||||
// be a compression of an earlier, fuller answer. Whether that fuller answer
|
||||
// actually exists is modeSummary's job — the dwarf ratio in
|
||||
// isSubstantiveAnswer keeps a matching closer in place when nothing earlier
|
||||
// clearly outweighs it.
|
||||
// isSummaryCloser reports whether a terminal turn defers to an earlier answer
|
||||
// and is short enough that whatever follows the deferral can only be a
|
||||
// compression of it. Two openers qualify: a complete "citations are logged"
|
||||
// -style ack sentence (summaryCloserRe), and a deictic back-reference in the
|
||||
// terminal's opening (pointsAbove — mort issue #1611's "Done — that's the full
|
||||
// chain above. Short version: …", which the ack shape alone did not cover).
|
||||
// Whether the fuller answer actually exists is modeSummary's job — the dwarf
|
||||
// ratio in isSubstantiveAnswer keeps a matching closer in place when nothing
|
||||
// earlier clearly outweighs it.
|
||||
//
|
||||
// Only pointsAbove is used here, not the whole isBackRef class: backRefRe's
|
||||
// fixed phrases can appear anywhere in the text, and a 300-byte closer has
|
||||
// room for a real answer that merely mentions "as I said" mid-sentence.
|
||||
// pointsAbove is offset-bounded, so it stays anchored to the opening.
|
||||
func isSummaryCloser(s string) bool {
|
||||
t := strings.TrimSpace(s)
|
||||
if t == "" || len(t) > summaryCloserMaxChars {
|
||||
return false
|
||||
}
|
||||
return summaryCloserRe.MatchString(t)
|
||||
return summaryCloserRe.MatchString(t) || pointsAbove(t)
|
||||
}
|
||||
|
||||
// lastSubstantiveAssistantText scans msgs newest→oldest (skipping the terminal
|
||||
|
||||
Reference in New Issue
Block a user