add the greeting msg
Some checks failed
CI / test (push) Failing after 4m41s
CI / fixture (push) Failing after 9s

This commit is contained in:
2026-10-05 16:20:17 +05:30
parent 939598a187
commit a372340281
8 changed files with 1043 additions and 32 deletions

View File

@@ -0,0 +1,126 @@
package runtime
import "strings"
// isSmalltalk reports a message that cannot be answered any better by looking
// something up.
//
// "Hi" used to cost a full operational turn. Retrieval is gated on
// configuration and never on the question (see retrieve), so a greeting arrived
// at the model wrapped in eight policy chunks, with the whole tool catalogue
// attached and the evidence placed BEFORE the question. A model handed that
// reasonably concludes it was asked for an operational briefing, and answers
// with one — screening backlog, uncovered shifts, citations and all. Measured
// against production: 6,174 tokens over two model calls, for the word "hi".
//
// The cost is the smaller half. The real damage is that the product appears not
// to understand being greeted, which is the first thing anybody tries.
//
// This is deliberately NOT a per-agent rule, and not an `if agent_key == ...`
// — §13 lists that as the anti-pattern it is. It is a property of the MESSAGE,
// applied identically to every spec, so adding an agent still requires no
// runtime change (I6).
//
// Conservative by construction: the normalised message must match a phrase in
// the set EXACTLY. Nothing substring-matches, so "hi, which shifts are
// uncovered?" is an operational question and keeps its tools and its evidence.
// A false negative costs a few thousand tokens; a false positive answers a real
// question with a greeting, so the set only holds phrases that carry no request
// at all.
func isSmalltalk(q string) bool {
n := normaliseSmalltalk(q)
if n == "" {
return false
}
_, ok := smalltalkPhrases[n]
return ok
}
// normaliseSmalltalk reduces a message to lowercase letters and single spaces.
//
// Punctuation and emoji are dropped rather than enumerated, so "Hi!", "hi :)"
// and "HI 👋" all arrive as "hi" without the set needing a row for each. Digits
// are NOT letters and so are dropped too, which is harmless here: no phrase in
// the set contains one, and a message that does — "shift 12?" — fails the exact
// match either way.
func normaliseSmalltalk(q string) string {
var b strings.Builder
b.Grow(len(q))
space := false
for _, r := range strings.ToLower(strings.TrimSpace(q)) {
switch {
case r >= 'a' && r <= 'z':
if space && b.Len() > 0 {
b.WriteByte(' ')
}
space = false
b.WriteRune(r)
case r == '\'' || r == '’':
// Dropped outright rather than treated as a separator, so "how's"
// stays one word. Both the ASCII quote and the curly one a phone
// keyboard substitutes — the same character to whoever typed it,
// and not to the first version of this function.
default:
// Any other run of non-letters is one separator, so "thank-you"
// and "thank you" normalise alike.
space = true
}
}
return b.String()
}
// smalltalkPhrases is the whole rule, as data.
//
// Greetings, thanks and farewells only. Each is a complete message that asks
// for nothing, which is what makes skipping retrieval and tools safe rather
// than merely cheap. Acknowledgements like "ok" and "cool" are deliberately
// absent: they are plausible smalltalk but also plausible answers to a
// question the agent just asked, and the cost of being wrong is higher than
// the tokens being saved.
var smalltalkPhrases = map[string]struct{}{
"hi": {}, "hii": {}, "hiya": {}, "hello": {}, "helo": {}, "hey": {},
"yo": {}, "howdy": {}, "greetings": {}, "hi there": {},
"hello there": {}, "hey there": {}, "hi owliver": {},
"hello owliver": {}, "hey owliver": {},
"good morning": {}, "good afternoon": {}, "good evening": {},
"good day": {}, "morning": {}, "afternoon": {}, "evening": {},
"gm": {}, "ge": {},
"how are you": {}, "how are you doing": {}, "hows it going": {},
"how is it going": {}, "you there": {}, "are you there": {},
"thanks": {}, "thank you": {}, "thanks a lot": {},
"thank you very much": {}, "thanks very much": {}, "many thanks": {},
"ty": {}, "cheers": {}, "thank u": {},
"bye": {}, "goodbye": {}, "good bye": {}, "see you": {},
"see ya": {}, "good night": {}, "goodnight": {}, "later": {},
}
// smalltalkDirective is appended to the system prompt for a smalltalk turn.
//
// Needed because the agent's own instructions describe an operational analyst,
// and an operational analyst greeted with "hi" and given no tools will still
// reach for the longest answer it can justify. Removing the evidence removes
// the citations; it does not by itself shorten the reply.
//
// Appended to the SYSTEM prompt rather than wrapped around the user's message:
// it is a standing instruction from the platform, not something the person
// said, and putting words in their mouth is how a transcript stops matching
// what was typed. I7 is untouched — this is the runtime's own text, not
// retrieved content, and nothing retrieved can reach here because retrieval did
// not run.
const smalltalkDirective = "\n\nThe person has greeted you or said something " +
"conversational. Reply in one or two short sentences: greet them back and " +
"offer to help. Do not summarise data, do not list findings or next steps, " +
"and do not cite sources — you have not looked anything up."
// smalltalkMaxOutputTokens caps a greeting's reply.
//
// A ceiling the model is not told about truncates mid-sentence rather than
// winding down, so this sits well above any sane greeting (a sentence or two is
// well under 100 tokens) and acts only as a backstop for a model that ignores
// the directive above. The directive does the shortening; this bounds the bill
// when it does not.
const smalltalkMaxOutputTokens = 256