diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index bca73c40e..3321e78ac 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1525,10 +1525,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // entries capture it at fetch time and are dropped at injection if a newer // mutation (any tool) bumped it in the meantime. const fileMutationVersions = new Map(); + const activeToolNames = new Set(); + const setActiveToolNames = (names: Iterable): void => { + activeToolNames.clear(); + for (const name of names) { + activeToolNames.add(name); + } + }; const toolSession: ToolSession = { get cwd() { return sessionManager.getCwd(); }, + isToolActive: name => activeToolNames.has(name), + setActiveToolNames, hasUI: options.hasUI ?? false, enableLsp, get hasEditTool() { @@ -2540,6 +2549,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} }); hasRegistered = true; + setActiveToolNames(initialToolNames); const { systemPrompt } = await logger.time( "buildSystemPrompt", rebuildSystemPrompt, @@ -2834,6 +2844,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} rebuildSystemPrompt, reloadSshTool, requestedToolNames: requestedToolNameSet, + setActiveToolNames, getMcpServerInstructions: mcpManager ? () => { const raw = mcpManager.getServerInstructions(); diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 5b36e37a6..8be2067d4 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -686,6 +686,8 @@ export interface AgentSessionConfig { toolRegistry?: Map; /** Tool names whose current registry entry is still the built-in implementation. */ builtInToolNames?: Iterable; + /** Update tool-session predicates that render guidance from the live active tool set. */ + setActiveToolNames?: (names: Iterable) => void; /** Current session pre-LLM message transform pipeline */ transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise; /** @@ -1717,6 +1719,7 @@ export class AgentSession { | undefined; #getMcpServerInstructions: (() => Map | undefined) | undefined; #reloadSshTool: (() => Promise) | undefined; + #setActiveToolNames: ((names: Iterable) => void) | undefined; #disconnectOwnedMcpManager: (() => Promise) | undefined; #requestedToolNames: ReadonlySet | undefined; #baseSystemPrompt: string[]; @@ -2155,6 +2158,7 @@ export class AgentSession { this.#rebuildSystemPrompt = config.rebuildSystemPrompt; this.#getMcpServerInstructions = config.getMcpServerInstructions; this.#reloadSshTool = config.reloadSshTool; + this.#setActiveToolNames = config.setActiveToolNames; this.#disconnectOwnedMcpManager = config.disconnectOwnedMcpManager; this.#baseSystemPrompt = this.agent.state.systemPrompt; this.#promptModelKey = this.#currentPromptModelKey(); @@ -6290,6 +6294,7 @@ export class AgentSession { ), ); } + this.#setActiveToolNames?.(validToolNames); const activeNameSet = new Set(validToolNames); for (const name of Array.from(this.#selectedDiscoveredToolNames)) { if (!activeNameSet.has(name) || isMCPToolName(name) || !this.#toolRegistry.has(name)) { @@ -6395,6 +6400,7 @@ export class AgentSession { async refreshBaseSystemPrompt(): Promise { if (!this.#rebuildSystemPrompt) return; const activeToolNames = this.getActiveToolNames(); + this.#setActiveToolNames?.(activeToolNames); const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry); this.#baseSystemPrompt = built.systemPrompt; this.#baseSystemPromptBeforeMemoryPromotion = undefined; diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 9feef6a76..ea9f4a031 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -376,7 +376,21 @@ export class BashTool implements AgentTool this.session.isToolActive?.(name) ?? fallback; + return prompt.render(bashDescription, { + asyncEnabled: this.#asyncEnabled, + autoBackgroundEnabled: this.#autoBackgroundEnabled, + autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)), + hasAstGrep: isToolActive("ast_grep", this.session.settings.get("astGrep.enabled")), + hasAstEdit: isToolActive("ast_edit", this.session.settings.get("astEdit.enabled")), + hasGrep: isToolActive("grep", this.session.settings.get("grep.enabled")), + hasGlob: isToolActive("glob", this.session.settings.get("glob.enabled")), + hasRead: isToolActive("read", true), + hasEval: isToolActive("eval", evalBackends.python || evalBackends.js || evalBackends.ruby || evalBackends.julia), + }); + } readonly parameters: BashToolSchema; // Non-pty calls run alongside each other (the executor isolates overlapping // runs on the same shell session); pty takes over the terminal UI and must @@ -398,19 +412,6 @@ export class BashTool implements AgentTool this.session.isToolActive?.(name) ?? fallback; - this.description = prompt.render(bashDescription, { - asyncEnabled: this.#asyncEnabled, - autoBackgroundEnabled: this.#autoBackgroundEnabled, - autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)), - hasAstGrep: isToolActive("ast_grep", this.session.settings.get("astGrep.enabled")), - hasAstEdit: isToolActive("ast_edit", this.session.settings.get("astEdit.enabled")), - hasGrep: isToolActive("grep", this.session.settings.get("grep.enabled")), - hasGlob: isToolActive("glob", this.session.settings.get("glob.enabled")), - hasRead: isToolActive("read", true), - hasEval: isToolActive("eval", evalBackends.python || evalBackends.js || evalBackends.ruby || evalBackends.julia), - }); } #formatResultOutput(result: BashResult | BashInteractiveResult): string { diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 83e39a517..8ea30c85d 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -226,6 +226,8 @@ export interface ToolSession { getToolByName?: (name: string) => AgentTool | undefined; /** Return whether a built-in tool is active in this turn's tool set. */ isToolActive?: (name: string) => boolean; + /** Update the active built-in tool predicate when a session changes tools mid-run. */ + setActiveToolNames?: (names: Iterable) => void; /** Agent registry for IRC routing across live sessions. */ agentRegistry?: AgentRegistry; /** Get artifacts directory for artifact:// URLs */ @@ -650,7 +652,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P ]; const activeToolNames = new Set(baseEntries.map(([name]) => name)); - session.isToolActive = name => activeToolNames.has(name); + if (session.setActiveToolNames) { + session.setActiveToolNames(activeToolNames); + } else { + session.isToolActive = name => activeToolNames.has(name); + } const baseResults = await Promise.all( baseEntries.map(async ([name, factory]) => { diff --git a/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts b/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts index 6cefd9c5b..221720e06 100644 --- a/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts +++ b/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts @@ -176,6 +176,52 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => { expect(rebuildCount).toBe(baseline + 2); }); + it("updates live active-tool predicates before rebuilding the prompt", async () => { + const activeToolNames = new Set(["read", "bash", "grep"]); + const readTool = createBasicTool("read", "Read"); + const bashTool = createBasicTool("bash", "Bash"); + const grepTool = createBasicTool("grep", "Grep"); + Object.defineProperty(bashTool, "description", { + get: () => (activeToolNames.has("grep") ? "bash sees grep" : "bash hides grep"), + enumerable: true, + configurable: true, + }); + const toolRegistry = new Map([ + [readTool.name, readTool], + [bashTool.name, bashTool], + [grepTool.name, grepTool], + ]); + const agent = new Agent({ + initialState: { + model: createModel(), + systemPrompt: ["initial"], + tools: [readTool, bashTool, grepTool], + messages: [], + }, + }); + const session = new AgentSession({ + agent, + sessionManager: SessionManager.inMemory(), + settings: Settings.isolated({ "compaction.enabled": false }), + modelRegistry: {} as never, + toolRegistry, + setActiveToolNames: names => { + activeToolNames.clear(); + for (const name of names) { + activeToolNames.add(name); + } + }, + rebuildSystemPrompt: async (_toolNames, tools) => ({ + systemPrompt: [tools.get("bash")?.description ?? "missing bash"], + }), + }); + sessions.push(session); + + await session.setActiveToolsByName(["read", "bash"]); + + expect(agent.state.systemPrompt).toEqual(["bash hides grep"]); + }); + it("does not skip when refreshBaseSystemPrompt is called explicitly", async () => { let rebuildCount = 0; const { session } = newSession(async toolNames => { diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index 8978dbfb3..845a285b1 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -273,6 +273,28 @@ describe("createTools", () => { expect(session.isToolActive?.("read")).toBe(false); }); + it("renders bash guidance from the live active tool predicate", async () => { + const activeToolNames = new Set(); + const session = createTestSession({ + isToolActive: name => activeToolNames.has(name), + setActiveToolNames: names => { + activeToolNames.clear(); + for (const name of names) { + activeToolNames.add(name); + } + }, + }); + + const tools = await createTools(session, ["bash", "grep", "read", "glob"]); + const bash = tools.find(tool => tool.name === "bash"); + + expect(bash?.description).toContain("`grep` tool"); + session.setActiveToolNames?.(["bash"]); + expect(bash?.description).not.toContain("`grep` tool"); + expect(bash?.description).not.toContain("`ls` → `read`"); + expect(bash?.description).not.toContain("`find` → the `glob` tool"); + }); + it("includes search_tool_bm25 when MCP tool discovery is enabled and executable", async () => { const session = createTestSession({ settings: createSettingsWithOverrides({