Skip to content

Python: [Bug]: Multi-selection edge group runs a target twice when the selection function names it twice #8738

Closed

Description

Description

FanOutEdgeRunner.send_message checks that every ID a selection function returns is a valid target, but not that each ID appears once. It then delivers the message once per entry, so a target named twice runs twice on the same message.

A selection function that maps tags or categories to executors hits this as soon as two tags share an executor. With an AgentExecutor target that is a second model call and a duplicated response. The add_multi_selection_edge_group docstring describes the return value as the IDs of the target executors that "should receive the message".

Code Sample

import asyncio
from dataclasses import dataclass

from agent_framework import WorkflowBuilder, WorkflowContext, executor

calls = {"billing_team": 0, "tech_team": 0}


@dataclass
class Ticket:
    text: str
    tags: list[str]


@executor(id="triage")
async def triage(ticket: Ticket, ctx: WorkflowContext[Ticket]) -> None:
    await ctx.send_message(ticket)


@executor(id="billing_team")
async def billing_team(ticket: Ticket, ctx: WorkflowContext[None, str]) -> None:
    calls["billing_team"] += 1
    await ctx.yield_output(f"billing handled: {ticket.text}")


@executor(id="tech_team")
async def tech_team(ticket: Ticket, ctx: WorkflowContext[None, str]) -> None:
    calls["tech_team"] += 1
    await ctx.yield_output(f"tech handled: {ticket.text}")


ROUTES = {"billing": "billing_team", "refund": "billing_team", "bug": "tech_team"}


def route_by_tags(ticket: Ticket, target_ids: list[str]) -> list[str]:
    return [ROUTES[tag] for tag in ticket.tags]


async def main() -> None:
    workflow = (
        WorkflowBuilder(start_executor=triage)
        .add_multi_selection_edge_group(triage, [billing_team, tech_team], selection_func=route_by_tags)
        .build()
    )
    result = await workflow.run(Ticket("charged twice, want my money back", ["billing", "refund"]))
    print("calls:", calls)
    print("outputs:", result.get_outputs())


asyncio.run(main())

Output on main (2c46deb):

calls: {'billing_team': 2, 'tech_team': 0}
outputs: ['billing handled: charged twice, want my money back', 'billing handled: charged twice, want my money back']

Expected: billing_team runs once and yields one output.

Package Versions

agent-framework-core 1.19.0 (main at 2c46deb)

Python Version

3.13.13

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 24, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Sep 24, 2026
  3. github-actions commented on Sep 24, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: python/packages/core/agent_framework/_workflows/_edge_runner.py::FanOutEdgeRunner.send_message preserves duplicate IDs returned by a selection function and executes the corresponding edge twice at lines 328-371. Trigger it with an untargeted message and a selection function returning [target.id, target.id]; the regression test constructs a two-target FanOutEdgeGroup and observes call_count == 2.

    • Failing test: python/packages/core/tests/workflow/test_runner.py::test_fanout_edge_runner_delivers_once_when_selection_contains_duplicate_target_ids
    • Files examined: python/packages/core/agent_framework/_workflows/_edge_runner.py, python/packages/core/agent_framework/_workflows/_workflow_builder.py, python/packages/core/tests/workflow/test_runner.py, python/packages/core/pyproject.toml
    • Tests run: existing fanout test (passed), duplicate-selection regression test (failed)
    • Reported version: 1.19.0
    • Current version: 1.19.0
  4. added
    workflowsUsage: [Issues, PRs], Target: Workflows
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowworkflowsUsage: [Issues, PRs], Target: Workflows

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions