From 6f2f164ab7e7f1516c4530da396b151ab2f84779 Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Tue, 13 Oct 2026 15:55:12 +0200 Subject: [PATCH] fix(coding-agent): restore active tools on session resume (#10887) The SDK always passed initial tools, so the constructor never restored the loadout recorded in the transcript, and resumed sessions dropped tools activated during the session. Sessions that use the default tools now restore the recorded loadout, with --tools +name/-name entries applied to it. fixes #10785 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 15 ++- .../test/default-tools-setting.test.ts | 98 +++++++++++++++++++ packages/coding-agent/test/suite/harness.ts | 1 + 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 357b58478..30200c8e4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Pinned internal Pi dependencies to exact release versions to prevent fresh installs from mixing incompatible packages ([#10865](https://github.com/earendil-works/pi/issues/10865)) +- Fixed resumed sessions dropping tools activated during the session. Resume now restores the session's active tools, with `--tools +name`/`-name` entries applied to them, unless `--tools` with tool names, `--no-tools`, or `--no-builtin-tools` selects tools explicitly ([#10785](https://github.com/earendil-works/pi/issues/10785)) ## [1.2.0] - 2026-10-12 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index ca21a5473..8b330d555 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -267,8 +267,9 @@ export interface AgentSessionConfig { /** 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. + * Whether the initial tools come from the `defaultTools` setting. When true, a resumed session + * replaces them with the loadout its transcript records, with `defaultToolModifiers` applied, and + * reload activates tools newly added to the setting. Tools removed from it stay active. */ usesDefaultTools?: boolean; /** @@ -522,7 +523,7 @@ export class AgentSession { activeToolNames: this._initialActiveToolNames, includeAllExtensionTools: true, }); - if (this._initialActiveToolNames === undefined) this._restoreToolsFromTranscript(); + if (this._usesDefaultTools) this._restoreToolsFromTranscript(this._defaultToolModifiers); } get modelRuntime(): ModelRuntime { @@ -1809,12 +1810,16 @@ export class AgentSession { * Restore the active tool loadout declared by the session transcript, if it declares one. * Tools reachable only from other tools are never declared, but they do not depend on the active * set, so the transcript's declarations are the whole loadout. + * @param modifiers `+name`/`-name` entries applied to the restored loadout. */ - private _restoreToolsFromTranscript(): void { + private _restoreToolsFromTranscript(modifiers: readonly string[] = []): void { this._pendingToolNames.clear(); const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages); if (!current) return; - const names = (current.toolsAdded ?? []).map((tool) => tool.name); + const names = applyToolModifiers( + (current.toolsAdded ?? []).map((tool) => tool.name), + modifiers, + ); this._pendingToolNames = new Set(names.filter((name) => this._isAllowedTool(name))); this._setActiveTools(names); } diff --git a/packages/coding-agent/test/default-tools-setting.test.ts b/packages/coding-agent/test/default-tools-setting.test.ts index 91f7361ec..3ab70eaa9 100644 --- a/packages/coding-agent/test/default-tools-setting.test.ts +++ b/packages/coding-agent/test/default-tools-setting.test.ts @@ -289,6 +289,104 @@ describe("defaultTools setting", () => { }); }); + // #10785 + describe("resume", () => { + const extensionTools: 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, + }); + pi.registerTool({ + name: "active_tool", + label: "Active Tool", + description: "Extension tool registered active", + parameters: Type.Object({}), + execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }), + }); + }; + + /** A session whose transcript records the loadout `toolNames`, as a run records it. */ + function createRecordedSession(toolNames: string[]): SessionManager { + const sessionManager = SessionManager.inMemory(tempDir); + sessionManager.appendMessage({ + role: "system", + content: "", + toolsAdded: toolNames.map((name) => ({ name, description: name, parameters: Type.Object({}) })), + timestamp: Date.now(), + }); + return sessionManager; + } + + async function resume(sessionManager: SessionManager, options: ToolOptions = {}) { + const settingsManager = SettingsManager.inMemory(); + const resourceLoader = new DefaultResourceLoader({ + cwd: tempDir, + agentDir, + settingsManager, + extensionFactories: [extensionTools], + }); + await resourceLoader.reload(); + return ( + await createAgentSession({ + cwd: tempDir, + agentDir, + model: getModel("anthropic", "claude-sonnet-4-5")!, + settingsManager, + sessionManager, + resourceLoader, + ...options, + }) + ).session; + } + + it("restores the loadout the transcript records", async () => { + // active_tool was deactivated during the session, inactive_tool activated. + const session = await resume(createRecordedSession(["read", "grep", "inactive_tool"])); + expect(session.getActiveToolNames().sort()).toEqual(["grep", "inactive_tool", "read"]); + session.dispose(); + }); + + it("does not restore excluded tools", async () => { + const session = await resume(createRecordedSession(["read", "grep", "inactive_tool"]), { + excludeTools: ["inactive_tool"], + }); + expect(session.getActiveToolNames().sort()).toEqual(["grep", "read"]); + session.dispose(); + }); + + it("uses explicit tool options instead of the recorded loadout", async () => { + const recorded = ["read", "grep", "inactive_tool"]; + + const allowlisted = await resume(createRecordedSession(recorded), { tools: ["read", "write"] }); + expect(allowlisted.getActiveToolNames()).toEqual(["read", "write"]); + allowlisted.dispose(); + + const builtinless = await resume(createRecordedSession(recorded), { noTools: "builtin" }); + expect(builtinless.getActiveToolNames()).toEqual(["active_tool"]); + builtinless.dispose(); + }); + + it("applies +name and -name tool options to the recorded loadout", async () => { + const session = await resume(createRecordedSession(["read", "grep", "inactive_tool"]), { + tools: ["-grep", "+write"], + }); + expect(session.getActiveToolNames().sort()).toEqual(["inactive_tool", "read", "write"]); + session.dispose(); + }); + + it("uses the default tools when the transcript records no loadout", async () => { + const sessionManager = SessionManager.inMemory(tempDir); + sessionManager.appendMessage({ role: "user", content: "hi", timestamp: Date.now() }); + const session = await resume(sessionManager); + expect(session.getActiveToolNames().sort()).toEqual(["active_tool", "bash", "edit", "read", "write"]); + session.dispose(); + }); + }); + it("applies through service-based session creation", async () => { const settingsManager = SettingsManager.inMemory({ defaultTools: ["ls"] }); const services = await createAgentSessionServices({ cwd: tempDir, agentDir, settingsManager }); diff --git a/packages/coding-agent/test/suite/harness.ts b/packages/coding-agent/test/suite/harness.ts index 220a1558f..bdad9e97a 100644 --- a/packages/coding-agent/test/suite/harness.ts +++ b/packages/coding-agent/test/suite/harness.ts @@ -238,6 +238,7 @@ export async function createHarness(options: HarnessOptions = {}): Promise