Skip to content

feat(core): Add AgentCancel exception for control-flow in callbacks#894

Merged
peteski22 merged 6 commits intomainfrom
peteski22/agent-cancel-signals
Jan 16, 2026
Merged

feat(core): Add AgentCancel exception for control-flow in callbacks#894
peteski22 merged 6 commits intomainfrom
peteski22/agent-cancel-signals

Conversation

@peteski22
Copy link
Contributor

@peteski22 peteski22 commented Jan 15, 2026

Summary

  • Add AgentCancel abstract base class for control-flow exceptions in callbacks
  • Modify run_async to preserve AgentCancel subclasses without wrapping in AgentRunError
  • Document the new pattern in callbacks documentation

Rationale

Currently, AnyAgent.run_async unconditionally wraps all exceptions in AgentRunError, breaking user control-flow semantics. When users raise custom exceptions in callbacks to stop agent execution, they cannot catch them by their specific type:

class StopAgent(Exception): pass

class MyCallback(Callback):
    def before_tool_execution(self, context, *args, **kwargs):
        if some_condition:
            raise StopAgent("Limit reached")
        return context

try:
    agent.run(...)
except StopAgent:  # Never matches - wrapped in AgentRunError
    handle_stop()
except AgentRunError as e:
    # Must inspect e.original_exception instead
    if isinstance(e.original_exception, StopAgent):
        handle_stop()

This is particularly problematic for implementing safety guardrails, rate limits, and validation logic where stopping execution is expected behaviour rather than an error.

Solution

Introduce AgentCancel, an abstract base class for control-flow exceptions. Subclasses propagate directly to the caller with the execution trace attached:

from any_agent import AgentCancel

class StopAgent(AgentCancel):
    pass

try:
    agent.run(...)
except StopAgent as e:
    print(f"Stopped: {e}")
    print(f"Trace: {e.trace}")  # Access spans collected before cancellation

This follows Python conventions like asyncio.CancelledError and StopIteration where control-flow exceptions are distinct from error exceptions.

NOTE: This is a non-breaking, opt-in change. Existing code continues to work unchanged, only users who explicitly subclass AgentCancel will see the new behaviour.

Additional Fixes

This PR also includes two unrelated type fixes discovered during CI:

  • fix(tools): Update type: ignore comment in wrappers.py - the isinstance check using a runtime-constructed tuple doesn't allow mypy to narrow the type, requiring [no-any-return] instead of [return-value]
  • fix(openai): Remove type: ignore for reasoning_effort now that any-llm-sdk 1.7.0 supports xhigh

@peteski22 peteski22 added documentation Improvements or additions to documentation callbacks Related to `any_agent/callbacks` labels Jan 15, 2026
@codecov
Copy link

codecov bot commented Jan 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/any_agent/__init__.py 77.77% <100.00%> (ø)
src/any_agent/frameworks/any_agent.py 85.79% <100.00%> (-4.83%) ⬇️
src/any_agent/frameworks/openai.py 62.58% <ø> (-6.46%) ⬇️
src/any_agent/tools/wrappers.py 94.68% <100.00%> (ø)
tests/unit/frameworks/test_agent_cancel.py 100.00% <100.00%> (ø)

... and 44 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@peteski22 peteski22 requested review from daavoo and njbrake January 15, 2026 20:21
Callbacks can now raise exceptions inheriting from AgentCancel to stop
agent execution. These exceptions propagate directly to the caller
without being wrapped in AgentRunError, allowing users to catch them
by their specific type.

- Add AgentCancel ABC with trace property for accessing spans
- Modify run_async to preserve AgentCancel exceptions
- Update AgentRunError docstring for clarity
@peteski22 peteski22 force-pushed the peteski22/agent-cancel-signals branch from ecdb446 to e4ee3fb Compare January 16, 2026 09:11
- Test AgentCancel ABC cannot be instantiated directly
- Test subclasses can be instantiated and caught
- Test trace property and message preservation
- Test run_async preserves AgentCancel without wrapping
- Test regular exceptions are wrapped in AgentRunError
Verify AgentCancel subclasses propagate without being wrapped in
AgentRunError across all agent frameworks.
- Add "Stopping Execution" section to callbacks documentation
- Document AgentCancel vs regular exception patterns
- Add examples for both exception types
- Add AgentCancel to API reference
The isinstance check using a runtime-constructed tuple doesn't allow
mypy to narrow the type, causing a no-any-return error instead of
the original return-value error.
@peteski22 peteski22 force-pushed the peteski22/agent-cancel-signals branch from e4ee3fb to 6def831 Compare January 16, 2026 09:31
@peteski22 peteski22 merged commit d5b7035 into main Jan 16, 2026
11 checks passed
@peteski22 peteski22 deleted the peteski22/agent-cancel-signals branch January 16, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

callbacks Related to `any_agent/callbacks` documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants