fix: dedupe before_tool_call in embedded runtime (#15635) (thanks @lailoo)

This commit is contained in:
Peter Steinberger
2026-02-14 02:48:51 +01:00
parent 534e4213a1
commit 8c3cc793b7
6 changed files with 108 additions and 31 deletions

View File

@@ -8,7 +8,11 @@ import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js";
import { logDebug, logError } from "../logger.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { isPlainObject } from "../utils.js";
import { runBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
import {
consumeAdjustedParamsForToolCall,
isToolWrappedWithBeforeToolCallHook,
runBeforeToolCallHook,
} from "./pi-tools.before-tool-call.js";
import { normalizeToolName } from "./tool-policy.js";
import { jsonResult } from "./tools/common.js";
@@ -83,6 +87,7 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
return tools.map((tool) => {
const name = tool.name || "tool";
const normalizedName = normalizeToolName(name);
const beforeHookWrapped = isToolWrappedWithBeforeToolCallHook(tool);
return {
name,
label: tool.label ?? name,
@@ -90,12 +95,23 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
parameters: tool.parameters,
execute: async (...args: ToolExecuteArgs): Promise<AgentToolResult<unknown>> => {
const { toolCallId, params, onUpdate, signal } = splitToolExecuteArgs(args);
let executeParams = params;
try {
// NOTE: before_tool_call hook is NOT called here — it is already
// invoked by wrapToolWithBeforeToolCallHook (applied in pi-tools.ts)
// before the tool reaches toToolDefinitions. Calling it again would
// fire the hook twice per invocation (#15502).
const result = await tool.execute(toolCallId, params, signal, onUpdate);
if (!beforeHookWrapped) {
const hookOutcome = await runBeforeToolCallHook({
toolName: name,
params,
toolCallId,
});
if (hookOutcome.blocked) {
throw new Error(hookOutcome.reason);
}
executeParams = hookOutcome.params;
}
const result = await tool.execute(toolCallId, executeParams, signal, onUpdate);
const afterParams = beforeHookWrapped
? (consumeAdjustedParamsForToolCall(toolCallId) ?? executeParams)
: executeParams;
// Call after_tool_call hook
const hookRunner = getGlobalHookRunner();
@@ -104,7 +120,7 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
await hookRunner.runAfterToolCall(
{
toolName: name,
params: isPlainObject(params) ? params : {},
params: isPlainObject(afterParams) ? afterParams : {},
result,
},
{ toolName: name },
@@ -128,6 +144,9 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
if (name === "AbortError") {
throw err;
}
if (beforeHookWrapped) {
consumeAdjustedParamsForToolCall(toolCallId);
}
const described = describeToolExecutionError(err);
if (described.stack && described.stack !== described.message) {
logDebug(`tools: ${normalizedName} failed stack:\n${described.stack}`);