Select Deferred Tools and Gate Memory With a Classifier - #16181
Open
aligulzar729 wants to merge 11 commits into
Open
aligulzar729 wants to merge 11 commits into
aligulzar729 wants to merge 11 commits into
Conversation
aligulzar729
force-pushed
the
feat/classification-tool-selection
branch
3 times, most recently
from
September 22, 2026 04:01
fd92ec2 to
c032c3c
Compare
aligulzar729
force-pushed
the
feat/classification-tool-selection
branch
from
September 22, 2026 04:08
c032c3c to
d85e83f
Compare
This was referenced Sep 22, 2026
aligulzar729
force-pushed
the
feat/classification-tool-selection
branch
from
September 22, 2026 05:43
d85e83f to
e19fad3
Compare
aligulzar729
force-pushed
the
feat/classification-tool-selection
branch
from
September 22, 2026 12:27
e19fad3 to
c3e2e91
Compare
aligulzar729
force-pushed
the
feat/classification-tool-selection
branch
from
September 24, 2026 03:24
e9b3586 to
4f84f3e
Compare
aligulzar729
force-pushed
the
feat/classification-tool-selection
branch
from
September 24, 2026 05:14
0aa7785 to
48fc047
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What this changes: two things, both off by default. Before a turn, the deferred tools most likely to be needed have their schemas shipped with the first model call instead of costing a
tool_searchround trip. After a turn, one yes/no question asks whether the user's newest message asks to remember, update or forget something, and the memory model only runs when it does.An agent holding a large MCP catalog pays for every tool's argument schema on every turn. Deferring those schemas fixes the cost, but the model then spends a
tool_searchround trip discovering the tool it needs, on the turn it needs it.This ranks the deferred tools against the request before the model runs and surfaces the few it is likely to need, so their schemas arrive with the first call. It also puts one yes/no question in front of the memory model, which otherwise runs after every turn even though the default memory instructions only store what the user asks to keep.
Measured on my own instance, with a stand-in model endpoint that reports the tool block it was handed rather than answering:
And for memory: across 120 conversations, 713 user turns produced 2 stored memories holding 101 tokens between them. With memory on, the memory model runs once per completed turn, so that is 713 model calls to produce two durable facts.
Selection only ever adds. A tool it passes over stays listed by name in the
tool_searchdescription and remains one search away, so a wrong prediction costs a round trip rather than an answer.Depends on #16180. The diff below includes its commits: GitHub cannot show a delta between two cross-fork branches. The first commit and
fix: bound each judgment by one deadline...belong to #16180; the rest is this pull request. The memory gate also needs #16276, which keeps the newest message in the memory window after a turn with tool calls.How it works
Predictions take their own parameter rather than reusing
discoveredToolNames, which is persisted into a paused job and replayed on resume: a guess must not be recorded as a real discovery. The step resolves ahead of the checkpoint setup, because an await between the checkpoint prune andcreateRunwould serialize two things that are deliberately overlapped.The memory gate asks its extra questions in the request it already makes, so
categorizeanddetectUpdatescost no additional round trip. The result reaches the memory model as a suggestion it may ignore, not as an override:Each capability can set its own
timeoutMs, because one ceiling does not fit both: a ranking request over 202 tools is about 9,600 input tokens and takes roughly 4s, while the gate's yes/no answers in under a second.Both questions ship with default wording an operator can replace from
librechat.yaml.The default question matches the default memory instructions, which only store what the user asks to keep ("Remember...", "Forget..."). Asking instead whether a turn holds something durable let through preferences the memory model then refused, and it scored "Forget where I live" at 0.21, below the threshold, so an explicit forget request could be skipped. Measured against the classifier, requests score 0.88 to 0.98 ("keep in mind that I am vegetarian", "call me Ali from now on", "yes, remember that", "forget where I live", one in Japanese) and everything else 0.39 or lower, so the default
thresholdis 0.5. A deployment whosememory.instructionskeep other facts replaces the question inlibrechat.yaml.The gate judges only the user's newest message, sent as
latest, with the few messages before it asconversationfor context. "yes, remember that" is read against the offer it answers, and a request is not judged again on the next turn.The
classificationblock is parsed when the app config loads (loadClassificationConfig). The loaded yaml is otherwise the raw object, so none of the schema defaults existed at runtime: tool selection ranked an empty batch becausemaxCatalogToolswas undefined, andcategoryThresholdwas never met.Every judgment is billed. Tool selection and the gate report their usage through an
onUsagecallback, and the client records it withcontext: 'classification', priced from the shared table the way activity labels are, without touching the response's stream usage. Ajevrow prices TypeSafe's model at $0.042 per million input tokens with free output; a host whose model has no row falls back to the default rate like any other unknown model.The prediction starts where it did but is awaited after the memory run starts, so the two overlap.
createRunstill starts in the same tick as the checkpoint prune.Name matching checks every occurrence of a tool name, not only the first, and stops at
shortlistso a common word cannot surface a tool from every server. Tools atool_searchalready loaded earlier in the conversation are left out of the ranking.Type of change
Testing
How the numbers were produced: a stand-in model endpoint that replies with the tool block it was handed rather than answering, so the count is what the provider would have received. The agent held 202 tools from 8 servers, all deferred,
shortlist: 5. The baseline row is the same agent with every tool pinned open. Tool definitions were measured asJSON.stringify(tools).length / 3.6. The memory numbers come from a read-only script overGET /api/convosandGET /api/messages/{id}.predict.spec.tsandgate.spec.ts: batching at the option ceiling, the needs-a-tool gate both ways, widening at the confidence boundary and when confidence is unmeasured, name matching that will not match inside a longer word, a named tool surviving both a conversational gate and an outright failure, categorization skipped withoutvalidKeys, a low-confidence key dropped rather than suggested, the update phrasing, and three gate failure modes that all resolve to processing the turn.Also: the gate inside
createMemoryProcessorwith the real one-message buffer (the newest message is judged, and a skip never starts the memory model), the newest message sent aslatestwith earlier turns only as context, a window with no user message skipped without a request, schema defaults filled byloadClassificationConfig(including the 0.5 threshold), and name matching past a partial match and at the cap.Also: the gate and tool selection report usage (and report nothing when a judgment fails), the client bills it as a
classificationtransaction without replacing the stream usage, thejevrow prices every name the model is served under, and the memory run starts before the prediction resolves (this test fails with the prediction awaited first).cd packages/api && npx jest src/memory/gate src/tools/predict src/agents/memory src/classificationpasses 154,cd packages/data-schemas && npx jest src/app/service.spec.ts src/methods/tx.spec.tspasses, andcd api && npx jest server/controllers/agents/client.test.jspasses 258.Live, a six-turn conversation wrote 12
classificationtransactions asjev-latestat a rate of 0.042 with free completion: 1,140 input tokens cost 47.88 credits.npx tsc --noEmitclean for all four workspace projects, plusopenapi:check,openapi:testandcircular-deps.The gate question against the live classifier:
Ranking a 61-option subset of the same catalog returned
dataforseo_labs_google_ranked_keywordsat 0.39 withdataforseo_labs_google_competitors_domainat 0.38, and a confidence of 0.37. That is whatlowConfidenceBelowis for: two genuinely close candidates produce a low confidence, and widening the shortlist bylowConfidenceExtrasurfaces both rather than betting on a 0.01 margin. Surfacing the wrong one alone costs atool_searchround trip; surfacing both costs one extra schema.A conversational turn on the 202-tool catalog logged
needs_tool 0.09 below 0.15: surfacing 0 named tool(s) only.An A/B run on a 25-tool Playwright agent, averaged over 3 runs of the same six prompts (open a page, find links, "thanks", a stated preference, "please remember" that preference, a screenshot), with everything off against
deferSchemaChars: 300, tool selection and the memory gate on, andendpoints.all.maxToolResultChars: 8000:tool_searchround tripsThe ranges do not overlap: the most expensive run with everything on used 234,049 prompt tokens, the cheapest with everything off 324,746. Tool selection surfaced
browser_take_screenshotfor the screenshot turn, so the model called it with no search. The gate ran the memory model only on "Please remember..." (0.94 each run, withpreferencessuggested) and the memory was saved every time.Pointed at an unreachable endpoint, the turn completed normally with a warning logged and no user-visible error. Under a real timeout the gate logged
judgment failed, processing memory as usualand the memory was still written, which is the intended direction.Risk / compatibility
The gate threshold has a thin margin on one side: in the A/B runs the stated preference without a request scored 0.45 to 0.47 against the 0.5 threshold, while requests scored 0.94. A missed request costs a memory, and a false pass costs one memory model call that the default instructions then decline, so an operator who prefers to err toward running can lower
threshold.Both capabilities default to false; off, the turn is byte-identical to
dev. Categorization is skipped whenmemory.validKeysis not configured, since there would be nothing to choose between. No database change.Latency:
createRunneeds the names to build the model binding, so the step is serial and lands at the start of the turn, around 740ms on a 202-tool catalog.Prompt caching: tool definitions sit at the front of a request, so a changing tool set invalidates a cached prefix. The saving is unambiguous on uncached turns; operators running a warm cache should measure first.
Checklist