Skip to content

fix: do not synthesize exception for cancelled tools#2106

Merged
zastrowm merged 1 commit intostrands-agents:mainfrom
Gastly:fix/cancel-tool-exception-propagation
Apr 29, 2026
Merged

fix: do not synthesize exception for cancelled tools#2106
zastrowm merged 1 commit intostrands-agents:mainfrom
Gastly:fix/cancel-tool-exception-propagation

Conversation

@Gastly
Copy link
Copy Markdown
Contributor

@Gastly Gastly commented Apr 10, 2026

Description

After #2046 , setting cancel_tool on a BeforeToolCallEvent now populates AfterToolCallEvent.exception with a synthesized exception which cannot be overwritten, causing tool cancellations to appear as failures to downstream hooks.

Cancellation is a deliberate action, not a failure. AfterToolCallEvent.cancel_message already exists to identify cancelled tools; hooks can use it to distinguish cancellations from genuine exceptions without needing a fake exception object. Additionally, tool result status remains as error for cancellations.

Span observability is preserved: StatusCode.ERROR is now set by inspecting tool_result["status"] directly rather than relying on a synthesized exception.

Public API Changes

AfterToolCallEvent.exception is once again None for cancelled tools. cancel_message remains the correct field to detect cancellations.

Related Issues

#2046

Documentation PR

No documentation changes needed.

Type of Change

Bug fix

Testing

  • [x ] I ran hatch run prepare

Checklist

  • [x ] I have read the CONTRIBUTING document
  • [ x] I have added any necessary tests that prove my fix is effective or my feature works
  • [ x] I have updated the documentation accordingly
  • [ x] I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • [ x] My changes generate no new warnings
  • [ x] Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@gautamsirdeshmukh
Copy link
Copy Markdown
Contributor

/strands review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown

Assessment: Approve

Clean, well-scoped bug fix that correctly addresses the over-eager exception synthesis introduced in #2046. The key insight — that cancellation is an intentional action, not a failure — is sound, and the AfterToolCallEvent.cancel_message field already provides the right mechanism for hooks to detect cancellations without a fake exception.

Review Details
  • Executor change: Removing the synthesized Exception(cancel_message) is correct. _can_write on AfterToolCallEvent only allows result and retry, so the synthesized exception was genuinely immutable from downstream hooks — confirming the bug.
  • Tracer change: The new status-based fallback (error is None and status == "error") in end_tool_call_span preserves StatusCode.ERROR on spans without requiring a fake exception. The elif error_message branch in _end_span sets error status without calling record_exception, which is the right behavior for cancellations.
  • Test coverage: All three key scenarios are covered — ToolResultEvent.exception is None, AfterToolCallEvent.exception is None, and the tracer still marks the span as ERROR.

The approach of deriving span error status from tool_result["status"] rather than a synthesized exception is a better design — it decouples observability from the error-handling contract exposed to hooks.

@github-actions
Copy link
Copy Markdown

Assessment: Approve

Clean, well-scoped bug fix. Cancellation is correctly modeled as a deliberate action rather than a failure — AfterToolCallEvent.cancel_message is the right signal for hooks, not a synthesized exception. The tracer changes preserve StatusCode.ERROR on spans by deriving it from tool_result["status"], which is a better design than threading fake exceptions through the system.

Review Details
  • Executor: Removing the synthesized Exception(cancel_message) is correct. _can_write on AfterToolCallEvent only permits ["result", "retry"], confirming the synthesized exception was immutable from downstream hooks — the bug was real.
  • Tracer: The new elif error_message branch in _end_span and the status == "error" fallback in end_tool_call_span decouple span error signaling from the exception-threading contract. All four flows verified: cancelled (ERROR, no record_exception), failed (ERROR with record_exception), success (OK), interrupted (OK).
  • Tests: Three targeted tests cover ToolResultEvent.exception, AfterToolCallEvent.exception, and the tracer's status-based error path. Codecov confirms full coverage.

Comment thread src/strands/telemetry/tracer.py
Comment thread src/strands/tools/executors/_executor.py
@Gastly Gastly force-pushed the fix/cancel-tool-exception-propagation branch from 806391b to fa87746 Compare April 29, 2026 16:19
@github-actions github-actions Bot added size/s and removed size/s labels Apr 29, 2026
@Gastly Gastly temporarily deployed to manual-approval April 29, 2026 16:19 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown

Assessment: Approve

Well-scoped bug fix that correctly restores the cancellation contract broken by #2046. The separation of concerns is sound: AfterToolCallEvent.exception represents genuine failures, cancel_message represents intentional cancellations, and span observability is preserved independently via tool_result["status"].

Review Details
  • Correctness: All four execution flows verified — cancelled (ERROR span, no record_exception), failed (ERROR span with record_exception), success (OK), interrupted (OK). The _can_write guard on AfterToolCallEvent only permits ["result", "retry"], confirming the synthesized exception was genuinely immutable from hooks.
  • Tracer design: Deriving span error status from tool_result["status"] rather than requiring an exception object is a better separation of concerns — observability should not depend on the hook-facing API contract.
  • Test coverage: Three targeted tests cover the executor's ToolResultEvent, the AfterToolCallEvent hook, and the tracer's status-based error path. Codecov confirms full coverage.

No new issues beyond what zastrowm's review already surfaced.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants