Repository navigation
Closed
Description
🔴 Required Information
Describe the Bug:
Since 460715b, a workflow graph may have several edges between the same two nodes as long as their routes are disjoint, for example a routing map (triage, {"billing": handle, "refund": handle}). When a node emits a list of routes that matches more than one of those edges, Graph.get_next_pending_nodes returns the target once per matched edge, so the target runs once per edge:
- A plain node runs twice (
handle@1andhandle@2, each in its own sub-branch, because_buffer_downstream_triggerstreats the two entries as a fan-out). - A
JoinNodewhose only predecessor is that node fires twice, and so does everything after it.
A single edge with route=["billing", "refund"] to the same node runs it once.
Steps to Reproduce:
- Install google-adk.
- Run the code below.
Expected Behavior:
Each target runs once.
-- fan_in
handle ran with ticket
-- join
notify ran
Observed Behavior:
-- fan_in
handle ran with ticket
handle ran with ticket
-- join
notify ran
notify ran
Environment Details:
- ADK Library Version (pip show google-adk):
mainat 1973796 (2.14.0) - Desktop OS: macOS
- Python Version (python -V): 3.14.7
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A (no model call is needed to reproduce)
🟡 Optional Information
Regression:
Yes. Before 460715b, graph validation rejected a second edge between the same two nodes.
Minimal Reproduction Code:
import asyncio
from google.adk.events.event import Event
from google.adk.runners import InMemoryRunner
from google.adk.workflow import JoinNode
from google.adk.workflow import START
from google.adk.workflow import Workflow
from google.genai import types
def triage():
return Event(output="ticket", route=["billing", "refund"])
def handle(node_input: str):
print("handle ran with", node_input)
def notify():
print("notify ran")
async def run(name, edges):
runner = InMemoryRunner(app_name=name, agent=Workflow(name=name, edges=edges))
session = await runner.session_service.create_session(
app_name=name, user_id="u"
)
print(f"-- {name}")
async for _ in runner.run_async(
user_id="u",
session_id=session.id,
new_message=types.Content(role="user", parts=[types.Part(text="hi")]),
):
pass
async def main():
await run(
"fan_in", [(START, triage), (triage, {"billing": handle, "refund": handle})]
)
gate = JoinNode(name="gate")
await run(
"join",
[
(START, triage),
(triage, {"billing": gate, "refund": gate}),
(gate, notify),
],
)
asyncio.run(main())How often has this issue occurred?:
- Always (100%)
Metadata
Metadata
Assignees
Labels
No labels
Activity
fix: run a node once when a list of routes matches several edges to it