fuzzy: generic FuzzyMatcher, fuzzy @agent/@model mention resolution, research archive

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 18:13:20 +01:00
co-authored by Claude Sonnet 5.5
parent c5acc2249f
commit 0ac0584a96
80 changed files with 2615 additions and 81 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ hooks: `docs/wiring/capmcp.md`.
Model protocol (tai-specific extension, absent from the Python app): `[models] list = [...]` and `default` in the manifest, plus an optional probe, `[models] probe = ["bin", "--cap-models"]` or `capabilities.models = true` for `<binary> --cap-models`. Probe output: JSON array of ids or `{id|name|model}` objects, `{"models": [...], "default": "..."}`, or one id per line. Bounds: `CAP_MODEL_PROBE_TIMEOUT` 5 s, `CAP_MODEL_PROBE_MAX_BYTES` 1 MiB, `CAP_MODEL_MAX` 500 per agent. A failed probe is recorded on the entry, never thrown. Tai's own manifest is unchanged and declares no models.
Mention grammar (after bots, providers, backends, known model ids): `@agent`, `@agent:model` (split at the first `:`), or a bare model id found on exactly one ready agent. Model matching: exact, case-insensitive exact, unique prefix, unique substring; ambiguity is an error. An agent that publishes no list passes the model through with a note. With a prompt the turn runs on that agent (`CapMention`, one cached `CapSession` per agent/model/cwd, model sent via `startup.model_arg`); without a prompt the agent is pinned (`Agent::agent_pin`) until `@auto`. Every failure prints the reason plus the full per-agent report (status, binary, manifest, binding, models, default, probe error); an unknown tag appends the same report.
Mention resolution (after bots, providers, backends, known model ids, `@auto`, exact agent, and file paths; so a real file is never shadowed): the typed text is fuzzy-matched with `FuzzyMatcher` ([fuzzy.md](fuzzy.md)). Order: exact agent name or model id; `head:tail` splits at the first `:`, head fuzzy to a ready agent, tail fuzzy to that agent's models (no floor, the agent already disambiguates); otherwise the whole text is ranked against every ready agent name and every model. Cross-namespace picks need a score of `CAP_FUZZY_FLOOR` (0.3); the best score wins and equal scores resolve shorter id then list order, never an error. Unusable agents (no binary, bad manifest) are considered only when no ready candidate reaches the floor and then report why. An agent without a model list passes the model through with a note. When the pick differs from the typed text a note line shows the resolution and score. With a prompt the turn runs on that agent (`CapMention`, one cached `CapSession` per agent/model/cwd, model sent via `startup.model_arg`); without a prompt the agent is pinned (`Agent::agent_pin`) until `@auto`. A miss prints the closest candidate with its score and the full per-agent report (status, binary, manifest, binding, models, default, probe error); an unknown tag appends the same report.
Gaps: no TUI completion for agents yet; `tai --cap-models` is not implemented on tai's own side.
+44
View File
@@ -0,0 +1,44 @@
# FuzzyMatcher (src/util/fuzzy.h)
The one generic "which candidate did the user mean" scorer. Research: `research/fuzzy-matching-selectors/` (conclusions `generic-fuzzy-scorer-design`, `agent-model-mention-resolution`). Used today by `CapModelCatalog` for `@agent` / `@agent:model` mentions.
## Contract
`Score(query, text)` is in [0, 1]. 1 only for a case-insensitive exact match, 0 when nothing relates, never above 0.999 otherwise. It is the max of four signals:
| Signal | Definition | Scale |
|---|---|---|
| `Subsequence` | query bytes in order inside text; DP with D = best ending in a match, M = best prefix (fzy recurrences); bonuses by the byte before the match; normalized by the perfect score `16n + 20 + 4(n-1)` | 1.0 |
| `Typo` | 1 - restricted Damerau-Levenshtein distance / max(len); insert, delete, substitute, adjacent swap cost 1 | 1.0 |
| `TokenTypo` | best `Typo` against each alphanumeric token of the text | 0.95 |
| `Trigram` | pg_trgm: per word two leading and one trailing space, Jaccard over trigram sets | 0.9 |
## Constants (fzf algo.go)
| Name | Value | Meaning |
|---|---|---|
| `kScoreMatch` | 16 | per matched byte |
| `kGap` | -1 | per skipped byte (leading, inner, trailing) |
| `kBonusBoundary` | 8 | match after a non-alphanumeric byte |
| `kBonusWhite` | 10 | match after whitespace or at the start |
| `kBonusCamel` | 7 | lower to Upper, or non-digit to digit |
| `kBonusConsecutive` | 4 | minimum bonus for a run |
| `kFirstMultiplier` | 2 | first query byte bonus factor |
| `kTrigramScale` / `kTokenScale` | 0.9 / 0.95 | signal scales |
## Ranking and ceilings
`Rank` returns candidates with score > 0: score descending, then shorter text, then earlier index (fzf default tie rule). Equal matches never error. `Best(query, texts, floor)` returns the top index above `floor` or -1.
| Ceiling | Value |
|---|---|
| `kFuzzyQueryMax` | 64 bytes |
| `kFuzzyTextMax` | 256 bytes |
| `kFuzzyCandidateMax` | 4096 candidates |
## Gaps
- ASCII case folding only; non-ASCII bytes compare as-is.
- Constants are fzf's (paths), not tuned on model ids; no measurement on a real id corpus yet.
- The CAP floor (`CAP_FUZZY_FLOOR` 0.3) is a judgment, not a measurement.
- Tests: `tests/fuzzy_test.cpp` (`build/fuzzy_test`).