Skip to content

Conversation

@pgrayy
Copy link
Member

@pgrayy pgrayy commented Jan 20, 2026

Motivation

Currently, users can cancel a Graph node execution by setting event.cancel_node = <STR_MSG|True> within a BeforeNodeCallEvent hook. Unlike Swarm, which handles this gracefully by setting status = FAILED and breaking out of the loop, Graph raises a RuntimeError that stops the entire graph execution. This inconsistency forces users to wrap Graph execution in try/catch blocks when intentionally cancelling nodes, which is unnecessary and differs from the Swarm behavior.

This change aligns Graph cancel_node behavior with Swarm to provide a clean exit path without exception handling.

Resolves #1500

Public API Changes

No public API changes. The behavior change is internal - cancel_node still works the same way, but now exits gracefully:

# Before: required try/catch for intentional cancellation
def cancel_callback(event):
    if should_cancel(event.node_id):
        event.cancel_node = "cancelling for business reason"

graph.hooks.add_callback(BeforeNodeCallEvent, cancel_callback)

try:
    result = graph("test task")  # Raised RuntimeError
except RuntimeError:
    # Had to catch the exception to handle cancellation
    pass

# After: clean exit without exception
def cancel_callback(event):
    if should_cancel(event.node_id):
        event.cancel_node = "cancelling for business reason"

graph.hooks.add_callback(BeforeNodeCallEvent, cancel_callback)

result = graph("test task")  # Returns GraphResult with status=FAILED
# No exception to catch - result is available for inspection

Breaking Changes

This is a breaking change for any code that catches RuntimeError during graph node cancellation. The existing behavior was inconsistent with Swarm and is considered a bug.

Migration

# Before: catching RuntimeError
try:
    result = graph("task")
except RuntimeError as e:
    if "cancelled" in str(e):
        handle_cancellation()

# After: check result status
result = graph("task")
if result.status == Status.FAILED:
    # Check if it was a cancellation by inspecting events or node results
    handle_cancellation()

Align Graph cancel_node behavior with Swarm by not raising a RuntimeError
when a node is cancelled. Instead, handle cancellation gracefully by:

- Creating a NodeResult with status=FAILED for the cancelled node
- Adding the node to failed_nodes set
- Yielding MultiAgentNodeStopEvent for the cancelled node
- Returning without exception to allow GraphResult to be yielded

Also add a failed_nodes check in _execute_graph to stop execution
after a cancellation, preventing downstream nodes from executing.

This makes the Graph behavior consistent with Swarm cancel_node handling,
where users can cleanly exit without catching RuntimeError.
@codecov
Copy link

codecov bot commented Jan 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pgrayy pgrayy marked this pull request as ready for review January 20, 2026 21:59
@pgrayy
Copy link
Member Author

pgrayy commented Jan 20, 2026

/strands Please address lint failures

NodeResult.result expects AgentResult | MultiAgentResult | Exception,
not a string. Wrap the cancel message in an Exception to fix the mypy
type error.
@github-actions
Copy link

Fixed the mypy lint error by wrapping cancel_message in Exception() to satisfy the NodeResult.result type signature (AgentResult | MultiAgentResult | Exception). Updated the test assertion accordingly.

Commit: 924a1b3

@pgrayy
Copy link
Member Author

pgrayy commented Jan 20, 2026

/strands Do we need to construct a NodeResult for a cancelled node? Also, do we need to yield MultiAgentNodeStopEvent?

Simplify the cancel_node handling to match Swarm's approach:
- Yield MultiAgentNodeCancelEvent
- Set self.state.status = Status.FAILED
- Return early (no exception)

Remove unnecessary NodeResult construction and MultiAgentNodeStopEvent
for cancelled nodes, as Swarm doesn't emit these either.

Update the _execute_graph check to use Status.FAILED instead of
failed_nodes to stop downstream execution.
@github-actions
Copy link

Simplified the implementation to match Swarm's pattern exactly:

Before (over-engineered):

  • Constructed a NodeResult for the cancelled node
  • Yielded MultiAgentNodeStopEvent
  • Added to failed_nodes set
  • Set various node properties

After (matches Swarm):

yield MultiAgentNodeCancelEvent(node.node_id, cancel_message)
self.state.status = Status.FAILED
return

The key insight is that Swarm (lines 750-759) simply:

  1. Yields the cancel event
  2. Sets completion status to FAILED
  3. Breaks out of the loop

No NodeResult or MultiAgentNodeStopEvent needed for cancelled nodes.

Commit: 21d1fa3

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.

[FEATURE] Graph - Cancel Node - Do Not Raise Exception

2 participants