Skip to content

feat(plugin): add tool.execute.error hook for failed tool calls#32542

Draft
haabe wants to merge 1 commit into
anomalyco:devfrom
haabe:feat/tool-execute-error
Draft

feat(plugin): add tool.execute.error hook for failed tool calls#32542
haabe wants to merge 1 commit into
anomalyco:devfrom
haabe:feat/tool-execute-error

Conversation

@haabe

@haabe haabe commented Jun 16, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #27900

Type of change

  • New feature

What does this PR do?

tool.execute.after only fires on success. When a tool's execute fails, Tool.execute converts the error to a defect via Effect.orDie, so the after trigger site is never reached and the error just goes to the message stream. Plugins that need to react to failures (retry, fallback, circuit-breaking) have no hook to listen on.

This adds tool.execute.error, fired on failure in both the built-in and MCP tool paths in session/tools.ts, before the error propagates. Because failures are defects, I catch at the cause level with Effect.tapCause (not tapError) and skip interruptions with Cause.hasInterruptsOnly, since user aborts are handled by the abort path. The callID matches the tool.execute.before for the same call, so plugins don't have to reconcile callIDs against the message stream.

Payload is { error, retryable, authority }. Authority is runtime for permission/question rejections, tool for invalid-args and other tool errors, and plugin when a before/after hook throws. I used a separate event rather than adding a status field to after so existing after ⇒ success assumptions don't silently break.

Happy to drop retryable/authority to just { error } if you'd prefer a smaller surface — that's the part I'm least sure about.

How did you verify your code works?

bun run typecheck passes. Added test/session/tool-execute-error.test.ts, which drives the real SessionTools.resolve with a recording plugin and asserts: success fires before+after (not error); failure fires before+error (not after) with the matching callID; a before-veto and an after-crash both report authority: plugin; both the built-in and MCP paths; plus a unit matrix for the classifier. The existing plugin/trigger test still passes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

`tool.execute.after` only fires on success — when a tool's execute Effect
fails, the error is routed to the message stream and no `after`-side plugin
hook fires. Plugins doing admission control (reflexion/retry, fallback
routing, circuit-breaking, policy gates) had no failure signal short of
reconciling callIDs from `tool.execute.before` against the message stream.

Add `tool.execute.error`, fired on failure of the built-in and MCP tool
paths before the error propagates. The `callID` correlates deterministically
with the matching `tool.execute.before`. The payload carries `error`,
`retryable`, and `authority` (`tool` | `runtime` | `plugin`) so frameworks
can make cross-runtime decisions without parsing error text. Interruptions
(user aborts) are excluded — those are handled by the abort path.

Additive and non-breaking: `after` still fires only on success.

Closes anomalyco#27900

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Jun 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: tool.execute.error for failed tool calls (tool.execute.after is currently success-only)

1 participant