Skip to content

Python: Surface switch-case condition errors instead of routing to the default branch - #8490

Open
Ruiqi Wang (RachelWanggg) wants to merge 2 commits into
microsoft:mainfrom
RachelWanggg:issue-8489
Open

Ruiqi Wang (RachelWanggg) wants to merge 2 commits into
microsoft:mainfrom
RachelWanggg:issue-8489

Conversation

@RachelWanggg

Copy link
Copy Markdown

Motivation & Context

SwitchCaseEdgeGroup wrapped every case predicate in a bare except Exception that only logged a
warning. A predicate that raised was treated exactly like one that returned False, so the message
fell through to the default branch and the workflow completed successfully. A typo or a missing
attribute in a routing predicate became silently wrong routing instead of a visible failure.

This contradicted the rest of the routing layer:

  • Edge.should_route documents the opposite policy for the same concept — "Any exception raised by
    the callable is deliberately allowed to surface to the caller to avoid masking logic bugs."
  • FanOutEdgeRunner.send_message already handles a selection function that raises: it marks the
    edge-group span EDGE_GROUP_DELIVERY_STATUS = EXCEPTION and re-raises. The inner catch made that
    path unreachable for switch-case groups, so these failures were absent from telemetry as well.
  • The .NET predicate path (SwitchBuilder.cs / WorkflowBuilder.CreateConditionFunc) has no
    equivalent catch.

The block was marked # pragma: no cover, so nothing exercised it.

Description & Review Guide

  • What are the major changes?

    1. _edge.pySwitchCaseEdgeGroup.selection_func no longer catches predicate errors; they
      propagate to the caller, matching Edge.should_route.
    2. _edge_runner.pyFanOutEdgeRunner.send_message drops a message that no target can handle
      before running the selection function, marking the span DROPPED_TYPE_MISMATCH and
      returning False. This mirrors SingleEdgeRunner, which checks _can_handle before
      evaluating Edge.should_route.
    3. Three tests in test_edge.py covering both behaviours.
  • Why change 2 is part of this PR. SingleEdgeRunner only ever evaluates a predicate for a
    message the target could actually receive; FanOutEdgeRunner evaluated the selection function
    first. The existing test_switch_case_edge_group_send_message_with_invalid_data passes a str
    into a lambda x: x.data < 0 predicate and depended on the swallow to reach its
    success is False assertion. Without the ordering fix, change 1 alone would turn that
    undeliverable-message case into a raised AttributeError. With it, that test passes unchanged and
    genuine predicate errors still surface. I'm happy to split this into a separate PR if you'd
    rather keep the two concerns apart.

  • What is the impact of these changes?

    • A broken switch-case predicate now fails loudly instead of misrouting to the default branch, and
      the failure is recorded on the edge-group span.
    • A selection function is no longer invoked for messages that no target can handle. Delivery
      outcome for those messages is unchanged (False); they are just dropped earlier.
    • No public API change.
  • What do you want reviewers to focus on? Whether bundling the FanOutEdgeRunner ordering
    change with the swallow removal is the right call, or whether you'd prefer the existing
    ..._with_invalid_data test to be updated instead.

Verification

  • uv run poe test -P core — full core + workflow suite passes (0 failures).
  • uv run poe syntax -P core, uv run poe pyright -P core, uv run poe test-typing -P core
    (mypy, pyrefly, ty, zuban, pyright) — all pass.
  • Each new test was mutation-checked: reverting change 1 fails the two condition-propagation tests;
    reverting change 2 fails the fan-out ordering test and the pre-existing
    ..._with_invalid_data test.

Related Issue

Fixes #8489

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change.

…fault

SwitchCaseEdgeGroup wrapped every case predicate in a bare `except Exception`
that only logged a warning, so a predicate that raised was treated exactly like
one that returned False: the message fell through to the default branch and the
workflow completed successfully. A broken routing predicate became silently
wrong routing rather than a visible failure.

This contradicted `Edge.should_route`, which documents that predicate errors are
deliberately allowed to surface "to avoid masking logic bugs", and made the
existing `FanOutEdgeRunner` error path (span status EXCEPTION, re-raise)
unreachable for switch-case groups, so the failures were missing from telemetry
too. The block was marked `# pragma: no cover`.

Also align `FanOutEdgeRunner` with `SingleEdgeRunner`, which checks
`_can_handle` before evaluating a condition: drop messages no target can handle
before running the selection function. Without this, removing the catch would
turn an undeliverable message into a raised AttributeError instead of a
type-mismatch drop.

Fixes microsoft#8489

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Sep 18, 2026
@RachelWanggg

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Comment thread python/packages/core/agent_framework/_workflows/_edge.py
…function

The runner fans every message out to all of the source's edge runners and gathers
them with `gather_cancelling_siblings_on_error`. `FanOutEdgeRunner` checked
`message.target_id` only after running the selection function, so a message
addressed to an executor reached through a different edge still had this group's
selection function evaluated against it - and a raise there cancelled the sibling
runner that was actually delivering the message.

Check `message.target_id` against `_target_map` first and return
DROPPED_TARGET_MISMATCH, ahead of the `_can_handle` check, matching
`SingleEdgeRunner`, which checks the target and then `_can_handle` before
evaluating `Edge.should_route`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: Python: [Bug]: SwitchCaseEdgeGroup swallows case-condition errors and silently routes the message to the default branch

3 participants