Skip to content

[FEATURE] Allow decorators to selectively present exceptions from being returned to the model #1565

@charles-dyfis-net

Description

@charles-dyfis-net

Problem Statement

Presently, the Strands tool decorator unconditionally wraps tools with an exception handler that returns a response to the model when an exception is thrown.

This is generally desirable behavior for errors that resulted from the model using a tool incorrectly. However, some errors reflect larger systemic issues and should halt an agent's execution entirely; there's currently no clear way to propagate these outward.

Proposed Solution

My initial attempt to work around this was by building an AfterToolCallEvent that would inspect event.exception, and rethrow any exceptions that were of a type that should be handled at outer layers rather than made visible to the agent. This did not work, because the tool call performs error handling before the hook is ever invoked -- but having a hook that was invoked before this would be helpful.

Perhaps more practically, the @tool call could take a predicate that accepts exceptions and returns a boolean indicating whether they should be wrapped; the existing behavior of wrapping all exceptions could be default.

Use Case

Locally, I have a convention that AssertionError always indicates a mistake on the part of operations or development staff. When we have an assertion thrown in our code, it contains internal details about an invalid state we're in, and reflects a condition that the agent performing a tool call is not expected to be able to correct. There's no point in performing another round of agent execution when one of these is seen -- we should always immediately halt the agent's execution and propagate the original exception to an outer stack frame.

There are other classes of exceptions that have similar semantics (meaning something failed outside the agent's control that the agent is not going to be able to correct).

Alternatives Solutions

  1. Invoke AfterToolCallEvent with the exception before wrapping it: The hook callback could then re-raise to prevent wrapping. This matches the existing hook pattern but requires the exception to reach the hook.
  2. Add an unwrapped_exceptions parameter to @tool: A tuple of exception types that should propagate rather than be wrapped:
    @tool(unwrapped_exceptions=(AssertionError, ConfigurationError))
    def my_tool(...):
        ...
    
  3. Pass exceptions through to AfterToolCallEvent.exception: Currently, event.exception is None for decorated tools because the decorator's except block (lines 617-628 in decorator.py) catches and wraps the exception before the executor's exception handler runs. Restructuring so the exception reaches the hook would enable existing hook-based solutions.
  4. Abuse InterruptException: Since InterruptException escapes the catch-all wrapper, tools could raise it with the real exception attached via __cause__. Callers would check for stop_reason="interrupt" and re-raise the underlying exception. This works but misuses the interrupt mechanism (designed for human-in-the-loop, not fatal errors) and requires manual unwrapping at the call site.

Additional Context

The relevant code is in src/strands/tools/decorator.py, DecoratedFunctionTool.stream() method, lines 617-628:

        except Exception as e:
            # Return error result with exception details for any other error
            error_type = type(e).__name__
            error_msg = str(e)
            yield self._wrap_tool_result(
                tool_use_id,
                {
                    "toolUseId": tool_use_id,
                    "status": "error",
                    "content": [{"text": f"Error: {error_type} - {error_msg}"}],
                },
            )

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions