From 69aec10852345b400932d1afd77a2e4eedc0d206 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 18 May 2026 08:00:11 +0100 Subject: [PATCH] fix(agents): preserve code mode hook context (#83481) --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/compact.ts | 9 +++++ src/agents/pi-embedded-runner/run/attempt.ts | 1 + src/agents/pi-embedded-runner/tool-split.ts | 11 +++-- src/agents/pi-tool-definition-adapter.ts | 6 ++- ...s.before-tool-call.integration.e2e.test.ts | 40 +++++++++++++++++++ 6 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3f851ae653..8785d00b3685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/code mode: preserve agent, session, run, and channel context in `before_tool_call` hooks for top-level `exec`/`wait` dispatches. Fixes #83387. - Replies: keep final payload delivery after live preview updates so channels can finalize or send the completed answer instead of losing preview-only drafts. (#83468) - Providers/Xiaomi: replay MiMo Anthropic-compatible `reasoning_content` as provider-required thinking blocks even when OpenClaw thinking is disabled, fixing follow-up tool turns for `mimo-v2-flash`. Fixes #83407. Thanks @Xgenious7. - Agents/exec approvals: forward approval-runtime credentials on agent-owned Gateway approval calls so approved async commands complete through the existing runtime path instead of stalling on unauthenticated follow-up calls. Thanks @IWhatsskill, @Patrick-Erichsen, and @jesse-merhi. diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index a403e5994386..4c843eda710c 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -1061,6 +1061,15 @@ async function compactEmbeddedPiSessionDirectOnce( const { customTools } = splitSdkTools({ tools: effectiveTools, sandboxEnabled: !!sandbox?.enabled, + toolHookContext: { + agentId: sessionAgentId, + config: params.config, + cwd: effectiveWorkspace, + sessionKey: sandboxSessionKey, + sessionId: params.sessionId, + runId: params.runId, + channelId: params.currentChannelId, + }, }); // Pi treats `tools` as a name allowlist during session creation. Pass the // exact OpenClaw-managed registrations so custom tools survive startup. diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 9e9321320219..bb288024839c 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -2179,6 +2179,7 @@ export async function runEmbeddedAttempt( const { customTools } = splitSdkTools({ tools: effectiveTools, sandboxEnabled: !!sandbox?.enabled, + toolHookContext: catalogToolHookContext, }); // Add client tools (OpenResponses hosted tools) to customTools. diff --git a/src/agents/pi-embedded-runner/tool-split.ts b/src/agents/pi-embedded-runner/tool-split.ts index 59a82babfc2e..3233e4b8d1c0 100644 --- a/src/agents/pi-embedded-runner/tool-split.ts +++ b/src/agents/pi-embedded-runner/tool-split.ts @@ -1,15 +1,20 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; import { toToolDefinitions } from "../pi-tool-definition-adapter.js"; +import type { HookContext } from "../pi-tools.before-tool-call.js"; // We always pass tools via `customTools` so our policy filtering, sandbox integration, // and extended toolset remain consistent across providers. type AnyAgentTool = AgentTool; -export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }): { +export function splitSdkTools(options: { + tools: AnyAgentTool[]; + sandboxEnabled: boolean; + toolHookContext?: HookContext; +}): { customTools: ReturnType; } { - const { tools } = options; + const { tools, toolHookContext } = options; return { - customTools: toToolDefinitions(tools), + customTools: toToolDefinitions(tools, toolHookContext), }; } diff --git a/src/agents/pi-tool-definition-adapter.ts b/src/agents/pi-tool-definition-adapter.ts index 2a1a7859b6c6..f5eac551f350 100644 --- a/src/agents/pi-tool-definition-adapter.ts +++ b/src/agents/pi-tool-definition-adapter.ts @@ -223,7 +223,10 @@ export function isClientToolNameConflictError(err: unknown): err is Error { return err instanceof Error && err.message.startsWith(CLIENT_TOOL_NAME_CONFLICT_PREFIX); } -export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { +export function toToolDefinitions( + tools: AnyAgentTool[], + hookContext?: HookContext, +): ToolDefinition[] { return tools.map((tool) => { const name = tool.name || "tool"; const normalizedName = normalizeToolName(name); @@ -242,6 +245,7 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { toolName: name, params, toolCallId, + ctx: hookContext, }); if (hookOutcome.blocked) { if (hookOutcome.kind === "veto") { diff --git a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts b/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts index db278b78d79e..c0c0b7452743 100644 --- a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts +++ b/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts @@ -313,6 +313,46 @@ describe("before_tool_call hook deduplication (#15502)", () => { expect(beforeToolCallHook).toHaveBeenCalledTimes(1); }); + it("passes hook context for unwrapped tool definitions", async () => { + const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } }); + const baseTool = { name: "exec", execute, description: "exec", parameters: {} } as any; + const [def] = toToolDefinitions([baseTool], { + agentId: "code-agent", + sessionKey: "agent:code-agent:main", + sessionId: "session-code", + runId: "run-code", + channelId: "channel-code", + }); + const extensionContext = {} as Parameters[4]; + + await def.execute( + "call-code-exec", + { code: "echo hi" }, + undefined, + undefined, + extensionContext, + ); + + expect(beforeToolCallHook).toHaveBeenCalledTimes(1); + expect(beforeToolCallHook).toHaveBeenCalledWith( + { + toolName: "exec", + params: { code: "echo hi" }, + runId: "run-code", + toolCallId: "call-code-exec", + }, + { + toolName: "exec", + agentId: "code-agent", + sessionKey: "agent:code-agent:main", + sessionId: "session-code", + runId: "run-code", + toolCallId: "call-code-exec", + channelId: "channel-code", + }, + ); + }); + it("preserves the hook marker when abort wrapping a hooked tool", () => { const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } }); const baseTool = { name: "Bash", execute, description: "bash", parameters: {} } as any;