feat(hooks): wire after_tool_call hook into tool execution pipeline#10678
feat(hooks): wire after_tool_call hook into tool execution pipeline#10678yassinebkr wants to merge 3 commits intoopenclaw:mainfrom
Conversation
Completes the before_tool_call/after_tool_call pair. before_tool_call was merged previously; this adds the after_tool_call counterpart. - after_tool_call fires after every tool execution (fire-and-forget) - Receives toolName, params, result/error, durationMs - Zero overhead when no hooks registered (hasHooks fast-path) - Hook errors caught and logged, never crash tool execution - 7 new tests, all passing Closes openclaw#5513 Ref openclaw#7297
| import type { AnyAgentTool } from "./tools/common.js"; | ||
| import { createSubsystemLogger } from "../logging/subsystem.js"; | ||
| import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; |
There was a problem hiding this comment.
Incorrect AnyAgentTool import
This file imports AnyAgentTool from ./tools/common.js, but the rest of the pi-tools.* wrappers use ./pi-tools.types.js (e.g. src/agents/pi-tools.abort.ts:1, src/agents/pi-tools.before-tool-call.ts:1). This introduces an inconsistent type source and can cause type divergence/TS build issues if those AnyAgentTool aliases ever change independently.
| import type { AnyAgentTool } from "./tools/common.js"; | |
| import { createSubsystemLogger } from "../logging/subsystem.js"; | |
| import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; | |
| import type { AnyAgentTool } from "./pi-tools.types.js"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.after-tool-call.ts
Line: 1:3
Comment:
**Incorrect AnyAgentTool import**
This file imports `AnyAgentTool` from `./tools/common.js`, but the rest of the `pi-tools.*` wrappers use `./pi-tools.types.js` (e.g. `src/agents/pi-tools.abort.ts:1`, `src/agents/pi-tools.before-tool-call.ts:1`). This introduces an inconsistent type source and can cause type divergence/TS build issues if those `AnyAgentTool` aliases ever change independently.
```suggestion
import type { AnyAgentTool } from "./pi-tools.types.js";
```
How can I resolve this? If you propose a fix, please make it concise.Aligns with other pi-tools.* wrappers that import from pi-tools.types.js rather than tools/common.js. Both export the same type but pi-tools.types.js is the canonical source within the pi-tools namespace.
src/agents/pi-tools.ts
Outdated
| const withBeforeHooks = normalized.map((tool) => wrapToolWithBeforeToolCallHook(tool, hookCtx)); | ||
| const withHooks = withBeforeHooks.map((tool) => wrapToolWithAfterToolCallHook(tool, hookCtx)); | ||
| const withAbort = options?.abortSignal |
There was a problem hiding this comment.
after_hook runs on blocks
after_tool_call is currently wrapped outside before_tool_call, so when a plugin blocks a call (before hook throws), the after_tool_call wrapper still runs its finally and emits an after_tool_call event with result: undefined and error: String(err) even though the underlying tool never executed. If after_tool_call is intended to represent “after actual tool execution”, you’ll want to avoid emitting it for before-hook blocks (e.g., by wrapping in the opposite order or by flagging/handling blocked outcomes distinctly).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.ts
Line: 444:446
Comment:
**after_hook runs on blocks**
`after_tool_call` is currently wrapped *outside* `before_tool_call`, so when a plugin blocks a call (before hook throws), the `after_tool_call` wrapper still runs its `finally` and emits an `after_tool_call` event with `result: undefined` and `error: String(err)` even though the underlying tool never executed. If `after_tool_call` is intended to represent “after actual tool execution”, you’ll want to avoid emitting it for before-hook blocks (e.g., by wrapping in the opposite order or by flagging/handling blocked outcomes distinctly).
How can I resolve this? If you propose a fix, please make it concise.- Swap wrap order: after(inner) → before(outer), so after_tool_call only fires for calls that pass the before gate - Add string-match guard as defense-in-depth for blocked error messages - New test: verifies after_tool_call does not fire on blocked calls - Comment in pi-tools.ts explaining wrap order rationale
96174d7 to
dba545a
Compare
|
would be great to get this merged in |
bfc1ccb to
f92900f
Compare
|
This pull request has been automatically marked as stale due to inactivity. |
feat(plugins): add
after_tool_callhookSummary
Adds the
after_tool_callplugin hook — the complement tobefore_tool_call(merged in #5513). Together they form the complete tool lifecycle pair for plugin observability.What this PR does
pi-tools.after-tool-call.ts— exportsrunAfterToolCallHook()andwrapToolWithAfterToolCallHook()pi-tools.after-tool-call.test.ts— 7 tests covering the full contractpi-tools.ts— every tool now gets bothbefore_tool_callandafter_tool_callhooks appliedDesign
Follows the exact same pattern as
before_tool_callwith key differences:before_tool_callafter_tool_callfinally)toolName,paramstoolName,params,result,error,durationMshasHooksfast-pathhasHooksfast-pathFire-and-forget semantics
The hook promise is deliberately not awaited —
runAfterToolCallHookreturnsvoid(notPromise<void>). This ensures:.catch()on the promiseError resilience
Three layers of protection:
hasHookscheck — zero overhead when no plugins registerafter_tool_calltry/catcharoundhookRunner.runAfterToolCall()call.catch()on the returned promise for async failuresTests
References
before_tool_callimplementation)Greptile Overview
Greptile Summary
This PR introduces an
after_tool_callplugin hook and wires it into the tool wrapping pipeline so plugins can observe tool outcomes (result/error and duration) without blocking execution. It adds a new wrapper (wrapToolWithAfterToolCallHook) plus a vitest suite covering the hook contract, and updatescreateOpenClawCodingToolsto apply both before/after tool-call hooks to every tool before abort-signal wrapping.Confidence Score: 3/5
after_tool_callevents even whenbefore_tool_callblocks a tool call, which is likely not the intended meaning of “after tool call/execution” for observability hooks.