fix(hooks): consolidate after_tool_call context + single-fire behavior (#32201)

* fix(hooks): deduplicate after_tool_call hook in embedded runs

(cherry picked from commit c129a1a74b)

* fix(hooks): propagate sessionKey in after_tool_call context

The after_tool_call hook in handleToolExecutionEnd was passing
`sessionKey: undefined` in the ToolContext, even though the value is
available on ctx.params. This broke plugins that need session context
in after_tool_call handlers (e.g., for per-session audit trails or
security logging).

- Add `sessionKey` to the `ToolHandlerParams` Pick type
- Pass `ctx.params.sessionKey` through to the hook context
- Add test assertion to prevent regression

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b7117384fc)

* fix(hooks): thread agentId through to after_tool_call hook context

Follow-up to #30511 — the after_tool_call hook context was passing
`agentId: undefined` because SubscribeEmbeddedPiSessionParams did not
carry the agent identity. This threads sessionAgentId (resolved in
attempt.ts) through the session params into the tool handler context,
giving plugins accurate agent-scoped context for both before_tool_call
and after_tool_call hooks.

Changes:
- Add `agentId?: string` to SubscribeEmbeddedPiSessionParams
- Add "agentId" to ToolHandlerParams Pick type
- Pass `agentId: sessionAgentId` at the subscribeEmbeddedPiSession()
  call site in attempt.ts
- Wire ctx.params.agentId into the after_tool_call hook context
- Update tests to assert agentId propagation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit aad01edd3e)

* changelog: credit after_tool_call hook contributors

* Update CHANGELOG.md

* agents: preserve adjusted params until tool end

* agents: emit after_tool_call with adjusted args

* tests: cover adjusted after_tool_call params

* tests: align adapter after_tool_call expectation

---------

Co-authored-by: jbeno <jim@jimbeno.net>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vincent Koc
2026-03-02 14:33:37 -08:00
committed by GitHub
parent f9cbcfca0d
commit 44183c6eb1
9 changed files with 338 additions and 127 deletions

View File

@@ -5,12 +5,10 @@ import type {
} from "@mariozechner/pi-agent-core";
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
import { logDebug, logError } from "../logger.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { isPlainObject } from "../utils.js";
import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js";
import type { HookContext } from "./pi-tools.before-tool-call.js";
import {
consumeAdjustedParamsForToolCall,
isToolWrappedWithBeforeToolCallHook,
runBeforeToolCallHook,
} from "./pi-tools.before-tool-call.js";
@@ -166,29 +164,6 @@ export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
toolName: normalizedName,
result: rawResult,
});
const afterParams = beforeHookWrapped
? (consumeAdjustedParamsForToolCall(toolCallId) ?? executeParams)
: executeParams;
// Call after_tool_call hook
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("after_tool_call")) {
try {
await hookRunner.runAfterToolCall(
{
toolName: name,
params: isPlainObject(afterParams) ? afterParams : {},
result,
},
{ toolName: name },
);
} catch (hookErr) {
logDebug(
`after_tool_call hook failed: tool=${normalizedName} error=${String(hookErr)}`,
);
}
}
return result;
} catch (err) {
if (signal?.aborted) {
@@ -201,41 +176,17 @@ 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}`);
}
logError(`[tools] ${normalizedName} failed: ${described.message}`);
const errorResult = jsonResult({
return jsonResult({
status: "error",
tool: normalizedName,
error: described.message,
});
// Call after_tool_call hook for errors too
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("after_tool_call")) {
try {
await hookRunner.runAfterToolCall(
{
toolName: normalizedName,
params: isPlainObject(params) ? params : {},
error: described.message,
},
{ toolName: normalizedName },
);
} catch (hookErr) {
logDebug(
`after_tool_call hook failed: tool=${normalizedName} error=${String(hookErr)}`,
);
}
}
return errorResult;
}
},
} satisfies ToolDefinition;