Visitar URL original
Python: continue declarative workflows after empty conditional branches by uragoodman · Pull Request #10184 · microsoft/agent-framework · GitHub
Skip to content

Python: continue declarative workflows after empty conditional branches - #10184

Open
Kip (uragoodman) wants to merge 6 commits into
microsoft:mainfrom
uragoodman:fix/declarative-empty-then-continuation
Open

Kip (uragoodman) wants to merge 6 commits into
microsoft:mainfrom
uragoodman:fix/declarative-empty-then-continuation

Conversation

@uragoodman

@uragoodman Kip (uragoodman) commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Motivation & Context

Declarative conditional actions can have empty branches: an If with no then
actions (then: [], then: null, or then omitted with no actions fallback),
and a ConditionGroup case with no actions. The builder created continuation
passthroughs for empty else/default paths but not for these empty case
branches. A matched branch therefore had no outgoing edge for its
ConditionResult.branch_index, so the action after the conditional was
unreachable from that branch in the graph.

This change adds symmetric passthroughs for empty branches: ThenPassthrough
for an empty If then branch and CasePassthrough for an empty ConditionGroup
case. Both provide a continuation node so the following action is reachable from
the matched branch.

The passthrough is added only when the authored branch is empty. A branch whose
actions are all skipped as unsupported is not treated as an empty branch.

This matters because:

  • Empty then/case branches are valid no-op or guard branches; when matched,
    downstream responses, state updates, and tool or agent calls must remain
    reachable instead of being skipped.
  • Matched branches now behave consistently with the existing empty
    else/default paths.
  • Users do not need to rewrite YAML or insert placeholder actions as a workaround.

Without this change, the builder still succeeds, but the matched branch has no
continuation edge in the graph, so downstream actions are unreachable on that
branch's graph path.

Description & Review Guide

  • What are the major changes?
    • For an If whose authored then list is empty (then: [], then: null,
      or then omitted with no actions fallback), create a ThenPassthrough
      JoinExecutor, wire the evaluator with branch_index == 0, and use it as
      the then-branch exit.
    • For a ConditionGroup case whose authored actions list is empty ([],
      null, or omitted), create a CasePassthrough JoinExecutor, register it
      in both branch_entries and branch_exits, and wire it to the case's
      branch_index.
    • Record the new passthroughs in the relevant structures for symmetry with
      ElsePassthrough/default handling.
    • Base passthrough creation on whether the authored branch action list is
      empty, rather than on whether executor construction returned an entry. A
      non-empty branch containing only unsupported actions therefore keeps its
      existing no-continuation behavior.
    • Add graph-level regression tests for empty then and empty ConditionGroup
      case wiring, condition routing, the non-empty then path, and non-empty
      branches containing only unsupported actions; update the nullable-then
      generated-ID expectation.
    • Include the ConditionGroup fix cherry-picked from Aman (@its-amann) as
      92c901c12, with the original author metadata preserved.
  • What is the impact of these changes?
    • Only declarative If actions whose authored then value is empty
      (then: [], then: null, or then omitted with no actions fallback),
      and ConditionGroup cases whose authored actions list is empty, change.
      Non-empty branches keep their existing routing, including branches whose
      actions are all skipped as unsupported.
    • No YAML schema, public API, or non-declarative workflow changes.
  • What do you want reviewers to focus on?
    • The distinction between authored-list emptiness and executor-construction
      failure, so a branch with only unsupported actions is not silently treated
      as an empty branch.
    • The symmetry with the existing empty else/default passthrough paths.
    • Whether passthrough nodes are preferable to direct evaluator-to-successor
      wiring, since the successor is not known when the structure is built.
    • The condition-routing assertions and the updated generated-ID expectation.
    • The cherry-picked ConditionGroup commit and its attribution.

Related Issue

Fixes #10094

The issue is open, has been reproduced by the triage workflow, and has the
ready-for-implementation label. I am happy to adjust the approach if
maintainers prefer. No other open PR references this issue.

The ConditionGroup case is the same empty-branch continuation problem and is
included here rather than as a parallel fix. If maintainers prefer a separate
issue for that part, I can split the cherry-picked commit out.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: implementation, tests, and PR description were drafted
with an AI coding assistant; I reviewed the diff and ran the focused tests and
static checks locally. The ConditionGroup fix was authored by Aman (@its-amann) and
cherry-picked with the original author metadata preserved.

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 links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change.

Kip (uragoodman) and others added 2 commits November 4, 2026 22:45
Add a ThenPassthrough continuation node for empty then branches, mirroring the existing ElsePassthrough path, and cover graph wiring and condition routing with regression tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
then_actions = action_def.get("then", action_def.get("actions", []))
then_entry = self._create_executors_for_actions(then_actions, builder, branch_context)
then_passthrough = None
if then_entry is None:

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.

same dead end is still in _create_condition_group_structure: a condition with empty actions matches, the evaluator sends branch_index=i, but nothing listens for it so the next action never runs

             if branch_entry:
                 branch_entries.append((i, branch_entry))
                 # Track exit for later wiring
                 branch_exit = self._get_branch_exit(branch_entry)
                 if branch_exit:
                     branch_exits.append(branch_exit)
+            else:
+                case_passthrough = JoinExecutor(
+                    {"kind": "CasePassthrough"}, id=self._allocate_generated_id(f"{action_id}_case{i}_pass")
+                )
+                self._executors[case_passthrough.id] = case_passthrough
+                branch_entries.append((i, case_passthrough))
+                branch_exits.append(case_passthrough)

fixed this plus a test in this commit, you can just cherry pick it instead of redoing it: its-amann@be2f39d

Copilot AI balanced review requested due to automatic review settings November 9, 2026 10:14
@uragoodman

Copy link
Copy Markdown
Contributor Author

Cherry-picked Aman (@its-amann)'s fix as 92c901c, with author metadata preserved and the test included. The empty ConditionGroup case now gets a CasePassthrough registered in both branch_entries and branch_exits, so the evaluator's branch_index has a listener and the following action runs. Ran the declarative unit suite (pytest -m 'not integration'): passed. Thanks for the fix!

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 insufficient remaining quota. Try requesting a review at a lower effort level.

@uragoodman Kip (uragoodman) changed the title Python: continue declarative workflows after an If with an empty then Python: continue declarative workflows after empty conditional branches Nov 10, 2026
@eavanvalkenburg

Copy link
Copy Markdown
Member

/review

@github-actions github-actions Bot 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.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (3 commit(s)): d8a3eca37429, c1c4fcb13579, 92c901c12c63
Model: gpt-5.6-sol

Overview

The change restores continuation for genuinely empty If and ConditionGroup branches through generated, collision-safe passthrough executors, with focused graph-routing tests and existing generic successor wiring providing strong guardrails. However, the new emptiness checks use executor-construction failure as a proxy for an empty authored branch, so a non-empty branch containing only unsupported actions now falls through to downstream work instead of stopping. This can silently skip a misspelled or unavailable guard action and execute subsequent side effects.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py

then_actions = action_def.get("then", action_def.get("actions", []))
then_entry = self._create_executors_for_actions(then_actions, builder, branch_context)
then_passthrough = None
if then_entry is None:

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.

_create_executors_for_actions() also returns None when a non-empty branch contains only unknown action kinds, because those actions are warned about and skipped. This check (and the equivalent new ConditionGroup fallback) therefore treats a misspelled or unavailable guard action as an empty no-op branch and continues into downstream actions, potentially executing side effects that the branch was meant to gate. Please base both passthrough decisions on whether the authored action list is actually empty, or reject unsupported nested actions instead of continuing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 50f7854: passthrough creation now keys off whether the authored action list is empty, so a non-empty branch containing only unsupported actions keeps its existing no-continuation behavior. Added regression tests covering both the If and ConditionGroup cases; the declarative suite now reports 1611 passed.

Base the passthrough decision on the authored action list rather than on whether an executor was created. Non-empty branches containing only unsupported actions no longer continue as empty branches. Preserve the existing empty-branch passthroughs and the ConditionGroup fix from @its-amann.

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

Copy link
Copy Markdown
Contributor Author

Hi Eduard van Valkenburg (@eavanvalkenburg) — thanks for approving and rerunning this head. The rerun still failed with an xdist worker crash rather than a declarative assertion:

[gw3] node down: Not properly terminated → worker 'gw3' crashed while running packages/openai/tests/openai/test_openai_chat_client.py::test_get_response_streaming_with_response_format

The PR touches 3 files under packages/declarative; packages/openai is untouched, and the failing test file is byte-identical to the last passing head (92c901c12) and to main. That same 3.14.10 Windows job passed at 92c901c12 earlier today, and every other Python Tests matrix job passed. I also saw the same xdist signature locally on a clean upstream/main checkout (beba9219f) with Python 3.14.10, across unrelated packages (happy to share the log).

The merge-gatekeeper failure is downstream.

Could you rerun the failed job once more? I'm also happy to merge latest main for a fresh head if that's preferred.

This branch was successfully deployed

1 active deployment
github-app-auth — 224b6ae0 Deployed Nov 13, 2026 by uragoodman via add_label #27818
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.

Python: [Bug]: Declarative If with empty then has no true-branch continuation edge

4 participants