Skip to content

Make ContextSelector grammar cache compatible#2686

Merged
GeorgeNgMsft merged 39 commits into
mainfrom
dev/georgeng/contextselector_cache
Jul 17, 2026
Merged

Make ContextSelector grammar cache compatible#2686
GeorgeNgMsft merged 39 commits into
mainfrom
dev/georgeng/contextselector_cache

Conversation

@GeorgeNgMsft

@GeorgeNgMsft GeorgeNgMsft commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Make  contextSelector  work with the construction cache (registry-first Tier 1.5)

Summary

Today, when a phrase could belong to two agents but the construction cache has already learned it for just one, the router sees a single confident answer and stops. The topic-based resolver ( contextSelector ) never gets to weigh in, so it can't fix the routing even when recent conversation makes the right agent obvious. This PR lets it step in for those "cache-hidden" collisions. (Before, you had to turn the cache off with  @const builtin off  just to see the problem.)

Why

The cache returns one match for an ambiguous phrase, but  contextSelector  needs at least two options to choose between. So the cached answer always wins by default. The fix: use the neighborhood registry to notice the hidden ambiguity, then let  contextSelector  pick the right agent from it.

What changed

image

• New step (Tier 1.5) in  resolveGrammarRegistryFirst  ( matchCollision.ts ): when the registry marks a single cache match as ambiguous, it pulls in the related "sibling" agents and lets  contextSelector  choose among them. It runs after an explicit user choice (Tiers 0 and 1), so it never overrides one, and before asking the user to clarify (Tier 2) — so when intent is clear, it just routes automatically instead of prompting.
• Scorer cleanup ( matchContextSelector.ts ): a shared  scoreAndDecide  core, plus a new  resolveContextSelectorMembers  entry point that can score the registry siblings (which don't carry a  MatchResult ) next to the cache matches.
• Routing: if a cache match wins, it routes with no LLM call. If a sibling wins, it saves a one-turn hint ( pendingTopicalRoute , a new field) that  pickInitialSchema  reads to lock in the agent and show the  ↪ routed to … — recent topic  note — only once the route is final, and it works even with embeddings off. This replaces an older hint that lived too long, so the note can't be wrong or carry over into the next turn.
• Safety limits: siblings are limited to agents active this turn (so it never routes to a disabled one), and it scores every real match plus the registry siblings together (so a genuine two-way match never loses a valid option).

Gating

Off by default and does nothing unless you opt in. It only runs with  preference.registryFirst: on  and a loaded  preference.registryPath  (both off/empty by default) and  contextSelector.detect  (on by default). Default config is unchanged.

Accepted trade-off

With  registryFirst  on and a real two-way match, scoring includes the registry siblings too — so a sibling the cache didn't match, but that fits the recent topic well, can win. This can change the outcome compared to scoring only the cache matches. That's on purpose: the cache isn't the final word, and a strong topic signal should be able to correct it. It stays safe because the evidence bar leans toward asking the user — more contenders means it's more likely to clarify, not less.

GeorgeNgMsft and others added 30 commits July 9, 2026 18:10
Comprehensive Tier-1 jest coverage for the raw-turns -> context-vector extraction subsystem: realistic multi-turn accumulation, recency ordering, tokenization robustness (punctuation/NFKC/emoji), protected patterns, plural->singular stemming consistency, window eviction, degenerate all-glue turns, and the documented adversarial limitations (negation, quoted speech).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comprehensive Tier-1 jest coverage for the context-vector + candidates -> resolve/abstain decision subsystem via TfIdfStrategy and TfIdfScorer: clear-winner resolve, shared-token cancellation (candidate-local IDF), margin/minMass/coverage gates at their boundaries, determinism under candidate reordering, and the documented adversarial score patterns (broad-token leak, negated-word pile-up).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Suppress negated content words from the conversation context vector so "not the spreadsheet" no longer deposits its topic and misroutes a collision. New opt-in tokenize option (dropNegatedSpans) enabled by the conversation signal via a negationGuard config lever (default on); keyword extraction stays byte-identical. A negation scope opens at a cue (not/no/never/without/...) and closes at a clause boundary, a reset connector (but/instead/...), or turn end. Clause-boundary detection requires whitespace adjacency so intra-token punctuation (decimals 2.5, times 3:30, versions v3.2) does not reopen the scope.

Measured on the offline benchmark: adversarial wrong-target 13 -> 8, resolution accuracy 7.1 -> 20.0 percent, retrieval share 32.7 -> 56.6 percent, zero safe-tier regressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…report fix

Add a CS_NEGATION_GUARD toggle to the offline runner so the pre-guard baseline can be replayed for before/after measurement. Document the shipped negation guard and the measured-net-negative global-IDF finding in the README, and add a "Methodology & relationship to the static-collision benchmark" section relating this deterministic lexical benchmark to the LLM/embedding @collision corpus probe. Compute the adversarial wrong-target count dynamically in the generated report instead of a stale hard-coded value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ites

metricRunner/measureMetrics: full head-to-head of contextSelector vs every context-blind collision strategy (first-match, score-rank, priority, user-clarify), plus the deployed routing-lift of contextSelector layered on each silent auto-resolver; rendered to console and the markdown report.

reproduce.mts: one-command run-all harness (measureMetrics + cached LLM arm) with a per-child V8 heap-cap safeguard (CS_BENCH_MAX_OLD_SPACE_MB, default 2 GB) so a memory regression OOM-fails fast instead of exhausting system RAM; --out is forwarded to both arms so their reports land in one directory.

Tests: contextSelectorTrigger (150 trigger/abstain collisions incl. multi-turn) and contextSelectorExtractionAccuracy (75 extraction cases) - deterministic, offline, no LLM. README documents the negation-scope guard and strategy comparison.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
All arms now write a single contextSelector-report.md at a fixed local path (next to the scripts; --out overrides), instead of scattering contextSelector-metrics.{md,json} + contextSelector-vs-llm.md to cwd.

New reportFile.mts provides the shared target: path resolution, the base-report write, and an idempotent marker-delimited LLM-section upsert - so any run order or repetition yields the metrics report followed by exactly one LLM section. measureMetrics writes the base; compareLlm upserts its section; reproduce coordinates and prints the path.

Also drops the unused machine-readable JSON sidecar (nothing in the repo consumed it) and fixes the llm-cache.json trailing-newline churn so re-runs keep it prettier-clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
…define retrieval

The per-tier tables (and console) showed 'spurious / wrong-target' as a single '18 / 8' cell that reads like a ratio. Split into two separate percentage rows: Spurious (over should-abstain fixtures, e.g. 54.5% (18/33)) and Wrong-target (over should-resolve fires, e.g. 80.0% (8/10)), each with its denominator.

Metric 1 now defines 'appropriately retrieve the conversation's topic' in plain language (the signal source sorts recent, recency-weighted words into three buckets - intended topic / look-alike distractor / unrelated noise - and checks most weight landed on the topic), with a one-line gloss per measure. Also adds a 'Reading the columns' note defining the combined column (the full realistic calibration corpus, with the adversarial tier excluded).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
…tric goals

Readability pass so the report explains itself to a reader who has never seen it. Adds a plain-language 'What this measures' overview (the grammar-collision problem, resolve/abstain, the wrong-target=0 safety promise, the offline/deterministic nature, the corpus makeup), a 'Key terms' section with a four-outcome table (correct / safe-miss / wrong-target / spurious) and one-line metric definitions, and a 'How to regenerate this report' block with the run command.

Gives Metrics 2, 3, Safety, and the threshold sweep explicit 'Goal:' statements (Metric 1 already had one), and adds the same framing plus a caching note to the LLM-comparison section.

De-jargons row labels (Gate A/Gate D/WRR/B-6/L2/'vice versa' -> plain language), fixes the self-contradictory 'Safety is threshold-robust: N cells produced a wrong-target' line, and drops the now-unused easy-slice metric.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
New '## Summary of results' section near the top (after the overview, before Key terms): a plain-language bottom-line conclusion plus five headline bullets — safety (0 silent misroutes across the realistic corpus), helpfulness (yield + resolution accuracy), net routing gain over first-match, how it compares with every silent strategy, and the adversarial limitation. A reader gets the conclusion up front and can scroll down for the detailed findings.

All figures are interpolated from the live metrics, so the summary stays accurate on every regeneration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
…examples

Adds a 'How the benchmark is built' section to the generated report: an at-a-glance one-paragraph summary of the whole benchmark (offline, deterministic, real pipeline, corpus sizes, no LLM), the three-layer structure (foundation / corpus generators / scoring engine), the Fixture shape, a generators comparison table, and a worked example fixture for each of the four corpus generators (easy / siblings / real-pairs / dialogue).

Restructures the README's flat file list into the same three layers with the Fixture shape and a generator table, so the code layout and the report tell the same story.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
The report is now a tracked artifact at docs/architecture/collision/contextSelector-report.md, but the generator defaulted to the (untracked) source folder, so the committed copy drifted. Make that docs path the default output of resolveReportPath() (still overridable with --out), so a plain 'reproduce.mts' keeps the tracked report current.

reproduce.mts now runs 'prettier --write' on the report as a final step: the generator emits valid markdown but not prettier-normalized table alignment, and the tracked file must pass CI's 'prettier --check .'. This bakes in the manual format pass that previously kept it clean.

Regenerates the report (now including the 'How the benchmark is built' section) into the tracked location; updates README + header comments to state the docs default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
The LLM arm is a proxy, not the production fallback: it asks a default chat model to pick between the two colliding agents (described by one-line blurbs) and is explicitly told to watch for negation/sarcasm/quoting, whereas the real fallback is full schema translation across all agents on the configured model. Document that this makes the LLM look stronger than production would — so the realistic-tier 0 regressions is a robust floor, but the adversarial regression count is a worst-case (not expected) cost — and point at an L3 live replay as the faithful measurement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
Flip the shipped default contextSelector.detect to true. It runs as a deterministic, LLM-free first pass on grammar collisions: a confident topical pick resolves the collision, otherwise it abstains and (via the unchanged defer-to-strategy fallback) hands off to first-match — strictly additive over today's routing, never a silent misroute.

Backed by the offline benchmark (docs/architecture/collision/contextSelector-report.md): 0 wrong-target across the ~1690-collision realistic corpus, +29% routing lift over first-match with no per-class regression. Verified the full dispatcher offline suite still passes (78 suites / 1407 tests) with the default on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 61308b63-2247-4538-ab35-593de5423150
Extract helpers to bring two functions under the CI complexity caps
(cyclomatic 25 / cognitive 30) without changing behavior:

- tokenize (CC 32 / Cog 34): pull option resolution, the vocabulary
  drop check, and per-token classification into resolveTokenizeOptions,
  isDroppedWord, and classifyToken. tokenize is now a thin match loop.
- runMetrics (CC 26 / Cog 58): extract the per-fixture accumulation loop
  and the assembly into a MetricsTally plus createTally, tallyBaselines,
  tallyResolvable, tallyAbstainable, makeAccuracy, buildDeployedLifts,
  and buildStrategyComparison.

Verified behavior-preserving: dispatcher build clean, harness typecheck
clean, 287 contextSelector specs pass, and the generated benchmark report
is byte-identical (apart from its wall-clock timestamp). Ratchet now OK
(0 functions over either cap).
… drop dead var and any-casts

- lintReport.ts: add the contextSelector benchmark/validation harness
  (tsx-run CLI scripts that print reports to stdout) to CONSOLE_ALLOWED_GLOBS,
  matching the existing tools/scripts no-console exemption.
- proveKeywordUse.mts: remove the unused listText read and two superfluous
  \�s any\ casts (b.decision already narrows on .kind, same as line 145).

Ratchet now OK (base 7 -> head 7, +0).
…t schemas to use real schemas instead of authored hardcoded ones
GeorgeNgMsft and others added 7 commits July 15, 2026 15:45
Convert compareLlm's LLM call to a TypeChat createJsonTranslator with a typed CollisionVerdict schema (createTypeScriptJsonValidator), replacing the hand-rolled parseChoice JSON parsing. Add a 'C'/neither verdict so the model can decline both colliding agents (distinct from 'unclear'); both score as no-commit. Rename the module-directory const HERE -> MODULE_DIR across the harness for clarity. Regenerate llm-cache.json and the consolidated report, and refresh README numbers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 37258b23-68ac-410b-9f8c-33ba5d74234d
Resolved 2 conflicts:
- docs/architecture/collision/context-weighted-collision-resolution-design.md
  (section 13.3): kept the implemented registry-first/contextSelector bullets
  (HEAD) over main's stale "open TODO" bullet describing the same gap.
- packages/defaultAgentProvider/README.AUTOGEN.md: took main's newer AI-generated
  output (regenerated by the daily docs pipeline; the feature commit never
  touched this file).

Verified post-merge: pnpm install --frozen-lockfile, full workspace build
(264/264), and the dispatcher spec suite (80 suites / 1449 tests) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 039b2e98-263c-4011-b869-3705d2c12615
@GeorgeNgMsft
GeorgeNgMsft marked this pull request as ready for review July 17, 2026 07:16
@GeorgeNgMsft
GeorgeNgMsft force-pushed the dev/georgeng/contextselector_cache branch from c53a5f5 to 0c594f1 Compare July 17, 2026 18:23
@robgruen

Copy link
Copy Markdown
Collaborator

We are going to have trouble assessing the quality/usefulness of this until we actually have real users with real behaviors and preferences.

@GeorgeNgMsft

GeorgeNgMsft commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

We are going to have trouble assessing the quality/usefulness of this until we actually have real users with real behaviors and preferences.

I agree, I implemented this though so that it could be compatible with grammar caching, which would quickly skip it otherwise

Comment thread ts/packages/dispatcher/dispatcher/src/translation/matchCollision.ts
@GeorgeNgMsft
GeorgeNgMsft enabled auto-merge July 17, 2026 21:14
@GeorgeNgMsft
GeorgeNgMsft added this pull request to the merge queue Jul 17, 2026
Merged via the queue into main with commit af666cd Jul 17, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants