Skip to content

feat(hooks): wire after_tool_call hook into tool execution pipeline#10678

Open
yassinebkr wants to merge 3 commits intoopenclaw:mainfrom
yassinebkr:feat/after-tool-call
Open

feat(hooks): wire after_tool_call hook into tool execution pipeline#10678
yassinebkr wants to merge 3 commits intoopenclaw:mainfrom
yassinebkr:feat/after-tool-call

Conversation

@yassinebkr
Copy link

@yassinebkr yassinebkr commented Feb 6, 2026

feat(plugins): add after_tool_call hook

Summary

Adds the after_tool_call plugin hook — the complement to before_tool_call (merged in #5513). Together they form the complete tool lifecycle pair for plugin observability.

What this PR does

  • New file: pi-tools.after-tool-call.ts — exports runAfterToolCallHook() and wrapToolWithAfterToolCallHook()
  • New file: pi-tools.after-tool-call.test.ts — 7 tests covering the full contract
  • Wired into pi-tools.ts — every tool now gets both before_tool_call and after_tool_call hooks applied

Design

Follows the exact same pattern as before_tool_call with key differences:

before_tool_call after_tool_call
Timing Before tool execution After tool execution (in finally)
Blocking Yes — can block/modify params No — fire-and-forget
Receives toolName, params toolName, params, result, error, durationMs
Hook errors Logged, tool proceeds Logged, never crash tool execution
Zero-overhead hasHooks fast-path hasHooks fast-path

Fire-and-forget semantics

The hook promise is deliberately not awaitedrunAfterToolCallHook returns void (not Promise<void>). This ensures:

  • Tool results are returned immediately to the model
  • Slow hooks don't degrade agent responsiveness
  • Async hook errors are caught via .catch() on the promise

Error resilience

Three layers of protection:

  1. hasHooks check — zero overhead when no plugins register after_tool_call
  2. Synchronous try/catch around hookRunner.runAfterToolCall() call
  3. .catch() on the returned promise for async failures

Tests

✓ executes tool normally when no hook is registered
✓ fires with correct event data after tool execution
✓ is fire-and-forget (does not block tool result)
✓ receives error info when tool execution fails
✓ continues execution when hook throws synchronously
✓ continues execution when hook rejects asynchronously
✓ normalizes non-object params for hook contract

References

Greptile Overview

Greptile Summary

This PR introduces an after_tool_call plugin 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 updates createOpenClawCodingTools to apply both before/after tool-call hooks to every tool before abort-signal wrapping.

Confidence Score: 3/5

  • This PR is close to mergeable but has a hook semantics issue that can mislead plugins.
  • Core implementation and tests look consistent with existing hook runner patterns, but the current wrapper order will emit after_tool_call events even when before_tool_call blocks a tool call, which is likely not the intended meaning of “after tool call/execution” for observability hooks.
  • src/agents/pi-tools.ts (hook wrapping order/semantics), src/agents/pi-tools.after-tool-call.ts (event semantics)

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
@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 6, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +1 to +3
import type { AnyAgentTool } from "./tools/common.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.
@yassinebkr yassinebkr closed this Feb 7, 2026
@yassinebkr yassinebkr reopened this Feb 7, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 444 to 446
const withBeforeHooks = normalized.map((tool) => wrapToolWithBeforeToolCallHook(tool, hookCtx));
const withHooks = withBeforeHooks.map((tool) => wrapToolWithAfterToolCallHook(tool, hookCtx));
const withAbort = options?.abortSignal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@yassinebkr yassinebkr force-pushed the feat/after-tool-call branch from 96174d7 to dba545a Compare February 7, 2026 20:50
@TheKnightCoder
Copy link

would be great to get this merged in

@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Feb 21, 2026
@vincentkoc vincentkoc self-assigned this Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Wire up after_tool_call hook + exec auto-retry on failure

3 participants