mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
Broad `find_tools` queries such as "linear issues" saturated the hard
20-match cap on every call: a large server's name token-matches every
one of its tools (+8 name, +1 server), so ranking was decided by a term
carrying no discriminating information and the model got 20 activations
per search regardless of intent.
Three changes, mirroring what makes xum's `tool_catalog_search` behave
well:
- **Lower default with a `limit` argument.** Keyword matches default to
10 per call; the new optional `limit` raises that up to the existing
hard cap of 20 (non-positive values fall back to the default). Exact
`names` are explicit activation requests and bypass the limit up to the
hard cap, so naming 15 tools still activates all 15.
- **Coverage-first ranking.** Keyword matches sort by distinct query
terms matched before raw score, so a query like "linear issues" ranks
tools matching both terms above the dozens matching only the server
name.
- **Model guidance.** `queries` and `names` now carry schema
descriptions (capability keywords and what they are matched against;
exact-name activation), and the tool description opens by explaining
what deferral means, that matches become callable on the next step, and
that a `"server: terms"` prefix scopes a query to one server.
An earlier revision also inferred a server scope from unprefixed query
words ("linear issues" behaving like "linear: issues"). Review kept
surfacing edge cases in that inference, and coverage-first ranking
already resolves the original saturation complaint, so it was dropped in
favor of the explicit prefix.
The hard cap stays at 20 so the persisted result keeps fitting under the
generic tool-result truncation budget that protects activation-recovery
JSON. Budget and reservation accounting are unchanged; the frontend
renderer ignores unknown argument fields, so no `site/` change is
needed.
> [!NOTE]
> Xum acted on @ibetitsmike's behalf in this pull request.
<!-- xum-attribution: model=claude-opus-4-6 thinking=high -->
package chattool
import (
"bufio"
"strings"
"unicode"
"github.com/coder/coder/v2/coderd/render"
coderstrings "github.com/coder/coder/v2/coderd/util/strings"
)
// readmeInputMaxBytes caps how many README bytes are parsed so a giant README
// can't OOM coderd. It sits well above the output rune cap, so it never trims a
// real excerpt.
const readmeInputMaxBytes = 64 * 1024
// readmeText returns the README as bounded, frontmatter-stripped plain text
// truncated to maxRunes, or "" when the README is blank or conversion fails.
func readmeText(readme string, maxRunes int) string {
// Cap the parse input first (see readmeInputMaxBytes); goldmark and the
// tokenizer tolerate a mid-line or mid-rune cut.
bounded := readme[:min(len(readme), readmeInputMaxBytes)]
text, err := render.InnerTextFromMarkdown(stripReadmeFrontmatter(bounded))
if err != nil {
return ""
}
return coderstrings.Truncate(text, maxRunes, coderstrings.TruncateWithEllipsis)
}
// stripReadmeFrontmatter strips a leading frontmatter block from the README if it
// exists. An unterminated frontmatter block is treated as a regular body section.
// UTF-8 BOMs are stripped if present, and CRLF is normalized to LF. Leading
// whitespace is also stripped.
func stripReadmeFrontmatter(readme string) string {
trimmed := strings.TrimLeftFunc(readme, func(r rune) bool {
return unicode.IsSpace(r) || r == '\ufeff'
})
var out strings.Builder
scn := bufio.NewScanner(strings.NewReader(trimmed))
scn.Buffer(nil, readmeInputMaxBytes+1) // headroom
var lineNumber int
var fences int
for scn.Scan() {
line := scn.Text()
lineNumber++
// Only handle fences if we haven't already found an
// opening and closing fence.
if fences < 2 {
isFence := strings.TrimRight(line, " \t\r") == "---"
if isFence {
fences++
continue
} else if lineNumber == 1 {
// No leading fence -> no frontmatter. Return the entire document.
return trimmed
}
// We are still in the frontmatter block. Skip writing this line.
continue
}
_, _ = out.WriteString(line)
_, _ = out.WriteString("\n")
}
// Can err if input is too big. Shouldn't happen normally.
if scn.Err() != nil {
return trimmed
}
// Scanner did not scan any lines. No frontmatter to strip.
if lineNumber == 0 {
return trimmed
}
// If we are still fenced but we reached the end of the document
// we have an unterminated fence.
if fences == 1 {
return trimmed
}
return out.String()
}