feat(coding-agent): enable tools newly added to defaultTools on reload

When the session's initial tools come from defaultTools, /reload activates
names added to the resolved selection. Removals do not deactivate tools, and
explicit --tools/--no-tools/--no-builtin-tools keep overriding the setting.

closes #10245
This commit is contained in:
Armin Ronacher
2026-09-30 17:55:25 +02:00
parent 028c0ec56e
commit db6cc71dc7
5 changed files with 102 additions and 3 deletions
+1
View File
@@ -6,6 +6,7 @@
- Added a `description` field for MCP servers (`pi mcp add --description`), shown with the server in the system prompt and used to rank its tools in tool search, and a `describeNamespace(name)` codemode helper that returns a namespace's instructions and tool names. `describeNamespace()` and `searchTools()` accept a namespace as `mcp__dev-radius`, `mcp__dev_radius`, `dev-radius`, or `dev_radius`.
- Added an `oauth.clientName` setting for MCP servers (`pi mcp add --oauth-client-name`) to change the client name sent during OAuth client registration, for servers such as Figma that only accept known clients ([#10226](https://github.com/earendil-works/pi/issues/10226)).
- `/reload` now enables tools newly added to the `defaultTools` setting. Tools removed from it stay enabled, tools turned off during the session stay off unless newly added, and `--tools`, `--no-tools`, and `--no-builtin-tools` still override the setting ([#10245](https://github.com/earendil-works/pi/issues/10245)).
### Changed
+2
View File
@@ -53,6 +53,8 @@ A list of only `+name` and `-name` entries changes the inherited selection inste
This replaces `bash` with `powershell` and enables `grep`: `["-bash", "+powershell", "+grep"]`. Project settings apply on top of user settings: a project list with only `+name` and `-name` entries changes the user's selection, and a project list with a plain name replaces it. In one list, plain names form the selection, and `+name` and `-name` then apply in order.
`/reload` enables tools newly added to `defaultTools`. It does not disable tools removed from it or re-enable unchanged tools you turned off. `--tools`, `--no-tools`, and `--no-builtin-tools` override `defaultTools`, also on reload.
CLI tool options override this setting for one invocation; `--tools` does not accept `+name` or `-name`. See [Command Line](cli.md#tools).
## Sessions and context
@@ -129,7 +129,7 @@ import {
SessionManager,
type SessionProjection,
} from "./session-manager.ts";
import type { CacheWarmingMode, SettingsManager } from "./settings-manager.ts";
import { type CacheWarmingMode, DEFAULT_TOOL_NAMES, type SettingsManager } from "./settings-manager.ts";
import type { SlashCommandInfo } from "./slash-commands.ts";
import { BUILTIN_PATH_PREFIX, createSyntheticSourceInfo, isSyntheticPath, type SourceInfo } from "./source-info.ts";
import {
@@ -260,6 +260,11 @@ export interface AgentSessionConfig {
cacheWarmer?: Pick<CacheWarmer, "cancel" | "status" | "onAgentSettled" | "onModeChanged" | "onWarmed">;
/** Initial active built-in tool names. Default: [read, bash, edit, write] */
initialActiveToolNames?: string[];
/**
* Whether the initial tools come from the `defaultTools` setting. When true, reload activates
* tools newly added to the setting. Tools removed from it stay active.
*/
usesDefaultTools?: boolean;
/** Optional allowlist of tool names. When provided, only these tool names are exposed. */
allowedToolNames?: string[];
/** Optional denylist of tool names. When provided, these tool names are not exposed. */
@@ -418,6 +423,7 @@ export class AgentSession {
private _cwd: string;
private _extensionRunnerRef?: { current?: ExtensionRunner };
private _initialActiveToolNames?: string[];
private _usesDefaultTools: boolean;
private _allowedToolNames?: Set<string>;
private _excludedToolNames?: Set<string>;
private _baseToolsOverride?: Record<string, AgentTool>;
@@ -462,6 +468,7 @@ export class AgentSession {
}
this._extensionRunnerRef = config.extensionRunnerRef;
this._initialActiveToolNames = config.initialActiveToolNames;
this._usesDefaultTools = config.usesDefaultTools ?? false;
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
this._baseToolsOverride = config.baseToolsOverride;
@@ -3582,12 +3589,22 @@ export class AgentSession {
const previousFlagValues = oldRunner.getFlagValues();
await emitSessionShutdownEvent(oldRunner, { type: "session_shutdown", reason: "reload" });
oldRunner.invalidate();
const previousDefaultTools = new Set(
this._usesDefaultTools ? (this.settingsManager.getDefaultTools() ?? DEFAULT_TOOL_NAMES) : [],
);
await this.settingsManager.reload();
this.syncQueueModesFromSettings();
resetApiProviders();
await this._resourceLoader.reload();
// Activate tools newly added to defaultTools. Removed ones stay active, and tools disabled
// during the session stay disabled unless the setting newly adds them.
const addedDefaultTools = this._usesDefaultTools
? (this.settingsManager.getDefaultTools() ?? DEFAULT_TOOL_NAMES).filter(
(name) => !previousDefaultTools.has(name),
)
: [];
this._buildRuntime({
activeToolNames: this.getActiveToolNames(),
activeToolNames: [...this.getActiveToolNames(), ...addedDefaultTools],
flagValues: previousFlagValues,
includeAllExtensionTools: true,
});
+1
View File
@@ -445,6 +445,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
modelRuntime,
cacheWarmer,
initialActiveToolNames,
usesDefaultTools: options.tools === undefined && !options.noTools,
allowedToolNames,
excludedToolNames,
extensionRunnerRef,
@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@earendil-works/pi-ai/compat";
@@ -156,6 +156,84 @@ describe("defaultTools setting", () => {
toolLessSession.dispose();
});
describe("reload", () => {
const inactiveTool: InlineExtension = (pi) => {
pi.registerTool({
name: "inactive_tool",
label: "Inactive Tool",
description: "Extension tool registered inactive",
parameters: Type.Object({}),
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
defaultActive: false,
});
};
const writeSettings = (settings: object) =>
writeFileSync(join(agentDir, "settings.json"), JSON.stringify(settings));
async function createFileSession(options: ToolOptions = {}) {
const settingsManager = SettingsManager.create(tempDir, agentDir);
const resourceLoader = new DefaultResourceLoader({
cwd: tempDir,
agentDir,
settingsManager,
extensionFactories: [inactiveTool],
});
await resourceLoader.reload();
return (
await createAgentSession({
cwd: tempDir,
agentDir,
model: getModel("anthropic", "claude-sonnet-4-5")!,
settingsManager,
sessionManager: SessionManager.inMemory(tempDir),
resourceLoader,
...options,
})
).session;
}
// #10245
it("activates only tools newly added to defaultTools", async () => {
const session = await createFileSession();
expect(session.getActiveToolNames()).toEqual(["read", "bash", "edit", "write"]);
session.setActiveToolsByName(["read", "edit", "write"]);
writeSettings({ defaultTools: ["+inactive_tool", "+grep"] });
await session.reload();
// bash was disabled during the session and is not newly added, so it stays off.
expect(session.getActiveToolNames().sort()).toEqual(["edit", "grep", "inactive_tool", "read", "write"]);
// Removing tools from the setting does not disable them.
writeSettings({ defaultTools: ["-read"] });
await session.reload();
expect(session.getActiveToolNames().sort()).toEqual(["edit", "grep", "inactive_tool", "read", "write"]);
session.dispose();
});
it("keeps explicit tool options on reload", async () => {
const allowlisted = await createFileSession({ tools: ["read"] });
writeSettings({ defaultTools: ["+grep"] });
await allowlisted.reload();
expect(allowlisted.getActiveToolNames()).toEqual(["read"]);
allowlisted.dispose();
writeSettings({});
const builtinless = await createFileSession({ noTools: "builtin" });
writeSettings({ defaultTools: ["+grep"] });
await builtinless.reload();
expect(builtinless.getActiveToolNames()).toEqual([]);
builtinless.dispose();
writeSettings({});
const excluded = await createFileSession({ excludeTools: ["grep"] });
writeSettings({ defaultTools: ["+grep", "+inactive_tool"] });
await excluded.reload();
expect(excluded.getActiveToolNames().sort()).toEqual(["bash", "edit", "inactive_tool", "read", "write"]);
excluded.dispose();
});
});
it("applies through service-based session creation", async () => {
const settingsManager = SettingsManager.inMemory({ defaultTools: ["ls"] });
const services = await createAgentSessionServices({ cwd: tempDir, agentDir, settingsManager });