From bee2e0f38f5632d53651b8aed95198d3f0d799a8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 08:43:10 +0100 Subject: [PATCH] fix: keep custom pi tools executable --- src/agents/pi-embedded-runner/compact.ts | 10 +++++-- .../pi-embedded-runner/run/attempt-session.ts | 2 +- src/agents/pi-embedded-runner/run/attempt.ts | 9 ++++-- .../tool-name-allowlist.test.ts | 29 +++++++++++++++++++ .../pi-embedded-runner/tool-name-allowlist.ts | 4 +++ 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/agents/pi-embedded-runner/tool-name-allowlist.test.ts diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 132469e5ca4..53118156961 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -132,7 +132,7 @@ import { buildEmbeddedSystemPrompt, createSystemPromptOverride, } from "./system-prompt.js"; -import { collectAllowedToolNames } from "./tool-name-allowlist.js"; +import { collectAllowedToolNames, toSessionToolAllowlist } from "./tool-name-allowlist.js"; import { logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas, @@ -839,10 +839,14 @@ export async function compactEmbeddedPiSessionDirect( contextTokenBudget: ctxInfo.tokens, }); - const { builtInTools, customTools } = splitSdkTools({ + const { customTools } = splitSdkTools({ tools: effectiveTools, sandboxEnabled: !!sandbox?.enabled, }); + // Pi treats `tools` as a name allowlist. Compaction uses the same custom + // tool path as normal turns, so pass names here to keep those tools active + // across compaction retries. + const sessionToolAllowlist = toSessionToolAllowlist(allowedToolNames); const providerStreamFn = resolveCompactionProviderStream({ effectiveModel, @@ -880,7 +884,7 @@ export async function compactEmbeddedPiSessionDirect( modelRegistry, model: effectiveModel, thinkingLevel: mapThinkingLevel(thinkLevel), - tools: builtInTools, + tools: sessionToolAllowlist, customTools, sessionManager, settingsManager, diff --git a/src/agents/pi-embedded-runner/run/attempt-session.ts b/src/agents/pi-embedded-runner/run/attempt-session.ts index 35854735ce7..0a51c79ebf7 100644 --- a/src/agents/pi-embedded-runner/run/attempt-session.ts +++ b/src/agents/pi-embedded-runner/run/attempt-session.ts @@ -5,7 +5,7 @@ export type EmbeddedAgentSessionOptions = { modelRegistry: unknown; model: unknown; thinkingLevel: unknown; - tools: readonly unknown[]; + tools: readonly string[]; customTools: readonly unknown[]; sessionManager: unknown; settingsManager: unknown; diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index e01b4979767..a0500b238e0 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -167,7 +167,7 @@ import { createSystemPromptOverride, } from "../system-prompt.js"; import { dropThinkingBlocks } from "../thinking.js"; -import { collectAllowedToolNames } from "../tool-name-allowlist.js"; +import { collectAllowedToolNames, toSessionToolAllowlist } from "../tool-name-allowlist.js"; import { installContextEngineLoopHook, installToolResultContextGuard, @@ -1121,6 +1121,11 @@ export async function runEmbeddedAttempt( : []; const allCustomTools = [...customTools, ...clientToolDefs]; + // Pi's `tools` option is a name allowlist, not the tool definitions. + // OpenClaw registers local tools through `customTools`, so passing the + // same names here keeps custom tools executable instead of silently + // filtering them out with an empty allowlist. + const sessionToolAllowlist = toSessionToolAllowlist(allowedToolNames); ({ session } = await createEmbeddedAgentSessionWithResourceLoader({ createAgentSession: async (options) => @@ -1132,7 +1137,7 @@ export async function runEmbeddedAttempt( modelRegistry: params.modelRegistry, model: params.model, thinkingLevel: mapThinkingLevel(params.thinkLevel), - tools: builtInTools, + tools: sessionToolAllowlist, customTools: allCustomTools, sessionManager, settingsManager, diff --git a/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts b/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts new file mode 100644 index 00000000000..d88e97fdffb --- /dev/null +++ b/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { createStubTool } from "../test-helpers/pi-tool-stubs.js"; +import { collectAllowedToolNames, toSessionToolAllowlist } from "./tool-name-allowlist.js"; + +describe("tool name allowlists", () => { + it("collects local and client tool names", () => { + const names = collectAllowedToolNames({ + tools: [createStubTool("read"), createStubTool("memory_search")], + clientTools: [ + { + type: "function", + function: { + name: "image_generate", + description: "Generate an image", + parameters: { type: "object", properties: {} }, + }, + }, + ], + }); + + expect([...names]).toEqual(["read", "memory_search", "image_generate"]); + }); + + it("builds a stable Pi session allowlist from custom tool names", () => { + const allowlist = toSessionToolAllowlist(new Set(["write", "read", "read", "edit"])); + + expect(allowlist).toEqual(["edit", "read", "write"]); + }); +}); diff --git a/src/agents/pi-embedded-runner/tool-name-allowlist.ts b/src/agents/pi-embedded-runner/tool-name-allowlist.ts index ca3b122342f..eded5ac1968 100644 --- a/src/agents/pi-embedded-runner/tool-name-allowlist.ts +++ b/src/agents/pi-embedded-runner/tool-name-allowlist.ts @@ -24,3 +24,7 @@ export function collectAllowedToolNames(params: { } return names; } + +export function toSessionToolAllowlist(allowedToolNames: Iterable): string[] { + return [...new Set(allowedToolNames)].toSorted((a, b) => a.localeCompare(b)); +}