Extension tool with the same name as a built-in tool never replaces it in the final tool list
Summary
On pi 0.84.4, when an extension registers a tool via pi.registerTool() using a name that collides with a built-in tool (grep, find), the registration is accepted (the definition lands in the extension's tools map), but the built-in definition stays in effect: runtime.getAllTools() and session.agent.state.tools keep returning the built-in definition, and the LLM's system prompt therefore contains the built-in tool. A tool registered by the same extension under a distinct name works fine, so the failure condition is specifically the name collision with a built-in tool.
Environment
- pi 0.84.4 (
@earendil-works/pi-coding-agent), Windows 11, Node 24.18.0
- Extension used for reproduction:
@ff-labs/pi-fff 0.10.6 (an extension that registers FFF-powered grep/find replacements)
- Reproduced with both the interactive TUI and the SDK (
createAgentSession)
Repro steps
pi install npm:@ff-labs/pi-fff
- Set the extension to its override mode, which registers tools under the built-in names — create
~/.pi/agent/pi-fff.json:
(In this mode the extension calls pi.registerTool() with name: "grep" and name: "find", plus pi.setActiveTools().)
- Start a session and inspect the tool list the LLM receives.
Observed
- The LLM's tool list contains the built-in grep (
description: "Search file contents for a pattern...", params pattern,path,glob,ignoreCase,literal,context,limit) — the extension's definition never reaches it.
- The extension's own registration did succeed: inspecting the loaded extension instance shows its
tools map contains "grep" and "find" with the correct extension definitions (description: "Grep file contents. Smart-case...", params pattern,path,exclude,caseSensitive,context,limit,cursor).
runtime.getAllTools() (which reads the merged _toolDefinitions registry) returns the built-in grep definition.
- Switching the same extension to its
tools-and-ui mode (registers the identical tool definitions under distinct names ffgrep/fffind) makes them appear in state.tools immediately — the registration code path is otherwise identical, only the tool names differ.
SDK probe that shows the mismatch:
import { createAgentSession } from "@earendil-works/pi-coding-agent";
const result = await createAgentSession();
const { session, extensionsResult } = result;
// Trigger the extension's registration path (its session_start/before_agent_start handler)
try { await session.prompt("ok"); } catch {}
await new Promise((r) => setTimeout(r, 3000));
const fffExt = extensionsResult.extensions.find((e) => String(e.path).includes("pi-fff"));
for (const [name, entry] of fffExt.tools) {
console.log(`extension registered [${name}]: ${entry.definition.description.slice(0, 60)}`);
}
const g = session.agent.state.tools.find((x) => x.name === "grep");
console.log("state.tools grep:", g.description.slice(0, 60));
Output with mode: "override":
extension registered [grep]: Grep file contents. Smart-case, auto-detects regex vs litera
extension registered [find]: Fuzzy path search and glob search. Matches against the whole
state.tools grep: Search file contents for a pattern. Returns matching lines with file p
The extension's definitions are present in the extension's tools map, but the merged registry and state.tools keep the built-in versions.
Expected
Based on the merge logic in _refreshToolRegistry — toolRegistry is built from built-in definitions first, then extension tools via toolRegistry.set(tool.name, tool) — an extension tool registered later under the same name should replace the built-in one in state.tools. That is also what this extension (and its documented "override" mode) relies on.
Notes
- No other extension on this machine registers tools named
grep/find, so this is not a first-registered-wins collision between extensions.
- The extension registers its tools from
session_start / before_agent_start handlers (deferred registration), which per the extensions docs should be a supported registration point.
- Happy to provide more traces (e.g. of
_refreshToolRegistry / setActiveToolsByName invocations) if useful.
Extension tool with the same name as a built-in tool never replaces it in the final tool list
Summary
On pi 0.84.4, when an extension registers a tool via
pi.registerTool()using a name that collides with a built-in tool (grep,find), the registration is accepted (the definition lands in the extension'stoolsmap), but the built-in definition stays in effect:runtime.getAllTools()andsession.agent.state.toolskeep returning the built-in definition, and the LLM's system prompt therefore contains the built-in tool. A tool registered by the same extension under a distinct name works fine, so the failure condition is specifically the name collision with a built-in tool.Environment
@earendil-works/pi-coding-agent), Windows 11, Node 24.18.0@ff-labs/pi-fff0.10.6 (an extension that registers FFF-poweredgrep/findreplacements)createAgentSession)Repro steps
pi install npm:@ff-labs/pi-fff~/.pi/agent/pi-fff.json:{ "mode": "override" }pi.registerTool()withname: "grep"andname: "find", pluspi.setActiveTools().)Observed
description: "Search file contents for a pattern...", paramspattern,path,glob,ignoreCase,literal,context,limit) — the extension's definition never reaches it.toolsmap contains"grep"and"find"with the correct extension definitions (description: "Grep file contents. Smart-case...", paramspattern,path,exclude,caseSensitive,context,limit,cursor).runtime.getAllTools()(which reads the merged_toolDefinitionsregistry) returns the built-in grep definition.tools-and-uimode (registers the identical tool definitions under distinct namesffgrep/fffind) makes them appear instate.toolsimmediately — the registration code path is otherwise identical, only the tool names differ.SDK probe that shows the mismatch:
Output with
mode: "override":The extension's definitions are present in the extension's
toolsmap, but the merged registry andstate.toolskeep the built-in versions.Expected
Based on the merge logic in
_refreshToolRegistry—toolRegistryis built from built-in definitions first, then extension tools viatoolRegistry.set(tool.name, tool)— an extension tool registered later under the same name should replace the built-in one instate.tools. That is also what this extension (and its documented "override" mode) relies on.Notes
grep/find, so this is not a first-registered-wins collision between extensions.session_start/before_agent_starthandlers (deferred registration), which per the extensions docs should be a supported registration point._refreshToolRegistry/setActiveToolsByNameinvocations) if useful.