Tool retrieval: HARD drop manage_memory when query is a contact-save pattern

Description-level steering wasn't enough — even with the explicit 'DO
NOT use for info about another person' in manage_memory's description,
models kept choosing memory over manage_contact. They can't if memory
isn't in the toolset.

New logic in ToolIndex.get_tools_for_query: detect three contact-save
patterns and discard manage_memory from the returned set (overriding
ALWAYS_AVAILABLE):

1. 'save [up to 3 words] for/to <name>' where <name> isn't a timing /
   pronoun stopword (later, tomorrow, me, you, future, etc.). Catches
   the canonical 'save this for X' and the wider 'save this address
   for X', 'save it for X'.
2. 'to/in/into (my) contacts' or 'address book'. Catches both 'add X
   to my contacts' and 'put this in my address book for X'.
3. Possessive: 'save (his/her/their) (address/phone/email/...)'.
   Stronger signal — also force-adds manage_contact to the set in
   case the keyword fallback missed it.

Verified: 8 positive contact patterns all drop memory, 10 false-
positive 'save X for later/tomorrow/me/the next thing' all keep it.
This commit is contained in:
pewdiepie-archdaemon
2026-06-11 09:46:34 +09:00
parent 803df21fc2
commit f5ad59317c
+47
View File
@@ -514,6 +514,53 @@ class ToolIndex:
# prompts do not drag web schemas into the agent context.
if self._WEB_RE.search(query):
base.update({"web_search", "web_fetch"})
# Hard steering: when the query is a clear "save info about a specific
# person" pattern (address paste + name, phone next to a name, etc.),
# the model has been observed defaulting to manage_memory even with
# manage_contact in the toolset. Pull memory out for these queries so
# the model literally cannot pick it. ALWAYS_AVAILABLE includes
# manage_memory by default; we override that here.
# The "for/to <word>" check needs to allow lowercase names (users
# don't always capitalize) but filter out timing/pronoun stopwords
# so "save this for later" / "save for tomorrow" don't trigger.
_CONTACT_STOPWORDS_AFTER_FOR = {
"later", "tomorrow", "yesterday", "now", "then", "today",
"tonight", "me", "us", "you", "him", "her", "them", "myself",
"yourself", "next", "this", "that", "the", "a", "an", "future",
"real", "use", "uses", "another", "future", "reference",
}
# Regex catches "save (this|it|the|her|...|<noun>) for <name>" / "to my
# contacts" patterns. More forgiving than literal-keyword matching —
# 'save this address for Alex' uses one extra word between 'save' and
# 'for' that breaks the contiguous 'save this for' phrase.
save_for_match = re.search(
r"\bsave\b(?:\s+\w+){0,3}\s+(?:for|to)\s+([A-Za-z]+)",
ql,
)
# "to my contacts", "into my contacts", "in my address book", etc.
to_contacts = re.search(r"\b(?:to|in|into)\s+(?:my\s+)?(?:contacts|address\s+book)\b", ql)
# Possessive: "save (his|her|their) (address|phone|email|number) ..."
# — strong contact signal even without "for <name>". Force-include
# manage_contact here too since the keyword fallback misses this
# construction.
possessive_contact = re.search(
r"\bsave\b(?:\s+\w+){0,2}\s+(?:his|her|their)\s+(?:address|phone|number|email|contact|details)",
ql,
)
word_after = (
save_for_match.group(1).lower() if save_for_match else None
)
contact_only_signal = (
(save_for_match is not None
and word_after is not None
and word_after not in _CONTACT_STOPWORDS_AFTER_FOR)
or to_contacts is not None
or possessive_contact is not None
)
if possessive_contact is not None:
base.add("manage_contact")
if contact_only_signal and "manage_contact" in base:
base.discard("manage_memory")
return base