fix(agents): preserve code mode hook context (#83481)

This commit is contained in:
Peter Steinberger
2026-05-18 08:00:11 +01:00
committed by GitHub
parent 322f0bb7bc
commit 69aec10852
6 changed files with 64 additions and 4 deletions
+1
View File
@@ -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.
+9
View File
@@ -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.
@@ -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.
+8 -3
View File
@@ -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<typeof toToolDefinitions>;
} {
const { tools } = options;
const { tools, toolHookContext } = options;
return {
customTools: toToolDefinitions(tools),
customTools: toToolDefinitions(tools, toolHookContext),
};
}
+5 -1
View File
@@ -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") {
@@ -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<typeof def.execute>[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;