This PR uses unicode-aware segmentation and normalization to handle tool
search for multilingual text.
Fixes#10816
---------
Co-authored-by: Petr Houška <houskape@gmail.com>
Follow-up to #11625 (#11331). Same change in durable's copy: #11631.
Problem: #11625 made a query term in a tool's name outweigh any number
of description mentions. That regressed queries where another tool
matches more of the query in its description:
- `screenshot window`: `peekaboo__window` (name has `window`) beat
`peekaboo__image` ("Take a screenshot of the screen, an app, or a
window.").
- `describe app`: `peekaboo__app` (whole name `app`) beat
`peekaboo__see` ("... describe the UI elements of the app.").
Fix: `Bm25Ranker` sorts by coverage first: a tool matching more of the
query's distinct terms, in any field (name, description and schema,
namespace), ranks above one matching fewer, whatever the scores. Within
the same coverage, the score decides as before. A query that is exactly
a tool's name still ranks that tool first.
On the 16-tool set from #11631 (5 servers, 16 queries), this fixes both
regressions and changes nothing else; `search github issues` stays at
rank 5, since `list_issues` lacks "search" and ties on coverage with
`search_code` and `linear__search_issues`.
Changelog: updates the unreleased #11331 entry from #11625.
Follow-up to #11440 for the latest comment on #11331.
Problem: the ranker scored a tool as one bag of words. A query term in
the tool's name counted like a mention in its description, and the
server's instructions were copied into every tool's text, which
lengthened it and lowered its score. Models often add words to a tool
name, so `peekaboo app` ranked `mcp__peekaboo__app` 14th: other peekaboo
tools mention "app" in shorter descriptions, and its own description
says "applications".
Fix: `Bm25Ranker` scores three fields separately:
- name: a query term in the name counts 3x its IDF; the description part
of BM25 is at most 2.2x, so a name match beats any number of description
mentions;
- description and schema text: BM25 as before, length-normalized on this
field only;
- namespace name, description, and instructions: 0.5x IDF per matching
term, without length normalization.
A tool whose whole local name (last `__` segment) is in the query gets a
bonus worth one more match of every query term, so it mostly wins
(`peekaboo app`, `describe app`, `read thread_read state`). A query that
is exactly a tool's name still ranks it first (#11440). Applies to
`tool_search` and codemode `searchTools()`.
Not included: the comment's second point (loading a deferred tool on a
direct call instead of `Tool X not found`). That error comes from
`prepareToolCall` in `packages/agent`, which has no hook for unknown
tools; it needs an agent-loop API change.
A query that equals a tool's full name or the part after a __ separator
(thread_read for mcp__app__thread_read) now ranks that tool first in
tool_search and codemode searchTools(). BM25 length normalization could
otherwise rank a shorter description sharing the name's words above it.
closes#11331
Move the codemode script reference, including the models API, into
docs/codemode.md and keep only one line per global in the tool
description. Declared tools get a one-line note on how scripts call them,
and the codemode system prompt guidance and MCP server section are shorter.
With the default tools a GPT-5.6 request drops from about 5,300 to 3,300
tokens.
Errors now point the way back: unknown tools and models members name close
matches, models.classify() and models.generateImages() validate their
arguments, unknown or mistyped models point to getAvailableOfType(),
store() overflow explains the store, and unshown generated images get a note.
The first prompt waits only for servers with direct tools. Other servers
connect in the background; codemode scripts, tool_search, and the
resource tools wait for the servers they need. codemode and tool_search
are activated from the config.
Tool declarations no longer depend on connected servers: the codemode
description leaves out deferred tools, and the tool_search description
no longer lists sources. Servers are listed in an mcp_servers system
prompt section instead, set in before_agent_start from the config and
connected state, so changes are appended to the conversation.
describeNamespace() and searchTools() accept mcp__dev-radius,
mcp__dev_radius, dev-radius, and dev_radius.
closes#10212
MCP codemode exposure no longer lists tools, tool counts, or server
instructions in the codemode description. Servers are listed by name and
an optional mcp.json description; scripts find tools with searchTools()
and read instructions with describeNamespace(). codemode-deferred is an
alias for codemode.
refs #10212
This adds codemode and MCP support to pi. Codemode (packages/codemode) runs
model-written JavaScript in a QuickJS wasm VM inside a worker. From there the
script calls pi's tools as async functions, uses a per-session store, and can
read the model catalog and run classifiers. The codemode tool in coding-agent
is compatible with Codex's exec tool, and codemode.mode works like Codex's
tool modes (on or only). Large results are written to a JSON temp file. MCP
(packages/mcp) is a standalone client for stdio and streamable HTTP servers,
with OAuth.
The core only gets general mechanisms; codemode, tool_search and MCP are
built-in extensions that use them. Other extensions can replace these
built-ins. Tools declare an exposure: direct, model-only, codemode, deferred
or hidden. prepareLoadout() lets an orchestrating tool change which tool
declarations the model sees. ctx.executeTool() runs nested calls through
validation and the tool_call/tool_result hooks. Those calls emit events with
parentToolCallId and are recorded as bounded nestedCalls on the parent
result, which compaction and HTML export use. Tools can also return
structuredContent with an outputSchema, and can report errors with isError
instead of throwing. MCP servers come from mcp.json (global, or per project
after the project is trusted) or from pi.registerMcpServer(). They are
managed with /mcp and pi mcp list|login|logout, and their runtimes load on
first use.
Closes#10040