Skip to content

Inconsistent Event Visibility in Parallel Agent Branching and LLM Context #3470

Description

@dannovikov

Describe the bug
We have encountered a bug when nesting multiple instances of the following architecture:
SequentialAgent[ParallelAgent[A, B], LLMAgent],
where the LLMAgent acts as a reducer of the outputs of A and B.

When we only have one instance of that architecture, as above, it works exactly as expected. However, when nesting multiple of these architectures, the LLMAgent reducers don't see the outputs of Agents A and B in the parallel group before them.

This is because the _is_event_belongs_to_branch function from google.adk.flows.llm_flows.contents seems to be incorrectly filtering out events from the ParallelAgent before it.

In general, nesting parallel agents seem to have event visibility issues where the reducing agents don't always see the context of the agents in the Parallel Agent before them in sequence.

To Reproduce
Please share a minimal code and data to reproduce your problem.
Steps to reproduce the behavior:
We want the following architecture:

    # Callback to demonstrate issue
    def print_llmrequest_contents(callback_context: CallbackContext, llm_request: LlmRequest):
        print("\n\n\n" + "*****"*10)
        print(f"Model called in agent: {callback_context.agent_name}")
        print(f"Agent branch: {getattr(callback_context, 'branch', 'NO BRANCH')}")
        print(f"Total session events: {len(callback_context.session.events)}")
    
        # Print all events with their branches
        print("ALL SESSION EVENTS:")
        for i, event in enumerate(callback_context.session.events):
            print(f"  {i}: {event.author} in branch '{event.branch}' said: \"{''.join([part.text for part in event.content.parts]) if event.content and event.content.parts else 'NO CONTENT'}\"")
    
        print("FILTERED CONTENTS SENT TO MODEL:")
        for content in llm_request.contents:
            print(f"{content.role}: {''.join([part.text for part in content.parts])}")
        print("\n\n\n" + "*****"*10)

    # Group 1
    A = Agent(name='Alice', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=model)
    B = Agent(name='Bob', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=model)
    C = Agent(name='Charlie', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=model)

    # Group 2
    D = Agent(name='David', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=model)
    E = Agent(name='Eve', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=model)
    F = Agent(name='Frank', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=model)

    # Parallel Agents
    P1 = ParallelAgent(
        name='ABC',
        description='Parallel group ABC',
        sub_agents=[A, B, C],
    )

    P2 = ParallelAgent(
        name='DEF',
        description='Parallel group DEF',
        sub_agents=[D, E, F],
    )

    # Reducers
    # ABC -> R1
    R1 = Agent(name="reducer1", description="Reducer for ABC", instruction="Summarize the responses from agents A, B, and C.", model=model, before_model_callback=print_llmrequest_contents)
    S1 = SequentialAgent(name='Group1_Sequential', description="Sequential group for ABC", sub_agents=[P1, R1])

    # DEF -> R2
    R2 = Agent(name="reducer2", description="Reducer for DEF", instruction="Summarize the responses from agents D, E, and F.", model=model, before_model_callback=print_llmrequest_contents)
    S2 = SequentialAgent(name='Group2_Sequential', description="Sequential group for DEF", sub_agents=[P2, R2])

    # Running ABC and DEF in parallel
    P3 = ParallelAgent(
        name='Final_Parallel',
        sub_agents=[S1, S2],
    )

    # Final reduction of previous reducers
    R3 = Agent(name="final_reducer", description="Final Reducer", instruction="Summarize the outputs from both groups.", model=model, before_model_callback=print_llmrequest_contents)
    S3 = SequentialAgent(name='Final_Sequential', description="Sequential group for final outputs", sub_agents=[P3, R3])

So this combines two of those architectures to run in parallel, where each one contains three LLM Agents in Parallel followed by a single LLMAgent reducer in sequence; then both of those reducers go to the final reducer.

Architure looks like:

Sequential1 = [A, B, C] -> Reducer 1
Sequential2 = [D, E, F] -> Reducer 2
[Sequential1, Sequential2] -> Reducer 3

where [] denotes parallel execution.

The bug is that, when you run S3, Reducers R1 and R2 don't see the outputs of the parallel groups that came before them. Although they are present in session.events, they are filtered out and not present in llm_request.contents. However if you just run S1 or S2 (those would each be one instance of the reduce architecture), the reducers DO see the outputs of the parallel groups before them. So its only when nesting these architectures.

Expected behavior

We expect that the parallel sub-agents' outputs are visible to the reducing agent.

Screenshots

Desktop (please complete the following information):

  • OS: [e.g. macOS, Linux, Windows] - MacOS
  • Python version(python -V): 3.12
  • ADK version(pip show google-adk): 1.16

Model Information:

  • Are you using LiteLLM: Yes/No -- Yes
  • Which model is being used(e.g. gemini-2.5-pro) -- Azure OpenAI 4o

Exploration into the cause and fix
The cause of this situation is happening in this function:

def _is_event_belongs_to_branch(
    invocation_branch: Optional[str], event: Event
) -> bool:
  """Check if an event belongs to the current branch.

  This is for event context segration between agents. E.g. agent A shouldn't
  see output of agent B.
  """
  if not invocation_branch or not event.branch:
    return True
  return invocation_branch.startswith(event.branch)

The nested parallel agents have branches named like ParallelAgent.A and ParallelAgent.B; and if A and B are parallel agents themselves, you might see sub branches like ParallelAgent.A.sub1 and ParallelAgent.A.sub2.

When filtering events to decide what contents go to a particular LLM invocation, the above function checks if the invocation branch either equals the event branch; or starts with the event branch. But in the case of the architecture we had, the invocation of the reducer agents would have branch ParallelAgent.A, while the events from the parallel agents before it would have sub branches like ParallelAgent.A.sub1; and since ParallelAgent.A **does not start with ** ParallelAgent.A.sub1, indeed, this is only true in reverse, the reducer on branch ParallelAgent.A doesn't see the events of ParallelAgent.A.sub1.

One proposed solution is to patch this function to perform this check in the other direction as well. If the event's branch starts with the invocation's branch, then accept it too.

def _is_event_belongs_to_branch(invocation_branch, event) -> bool:
    """Check if an event belongs to the current branch.

    This is for event context segration between agents. E.g. agent A shouldn't
    see output of agent B.
    """
    if not invocation_branch or not event.branch:
        return True
    # Original logic: can see parent branch events
    if invocation_branch.startswith(event.branch):
        return True
    # New logic: can also see child branch events
    if event.branch.startswith(invocation_branch):
        return True
    return False

In our testing, this fixes the architecture, allowing the reducers to see the parallel agents' outputs before them. But I'm not sure why this check in the reverse direction wasn't present before, and what this breaks, so I'm hoping to have the discussion about it.

Activity

  1. added
    core[Component] This issue is related to the core interface and implementation
    on Nov 10, 2025
  2. added theissue type on Nov 10, 2025
  3. llalitkumarrr commented on Nov 11, 2025

    @llalitkumarrr
    Collaborator

    Hello @dannovikov ,

    Thank you for raising this issue, I tried reproducing your issue and observed the same issue. While running P3 and S3.

    @Jacksunwei sharing output relevant code below for your reference:

    Code:

    from google.adk.agents import ParallelAgent, Agent, SequentialAgent
    from google.adk.sessions import InMemorySessionService
    from google.adk.runners import Runner
    import os
    import asyncio
    from dotenv import load_dotenv
    from google.genai import types
    
    
    load_dotenv()  # Load environment variables from a .env file if present
    
    api = os.environ.get("GEMINI_API_KEY")
    os.environ["api_key"] = api
    
    APP_NAME = "app"
    USER_ID = "user_01"
    SESSION_ID = "session_01"
    GEMINI_MODEL = "gemini-2.0-flash"
    
    
    
    # Sub-agent 1
    A = Agent(name='Alice', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=GEMINI_MODEL)
    # Sub-agent 2
    B = Agent(name='Bob', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=GEMINI_MODEL)
    # Sub-agent 3
    C = Agent(name='Charlie', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=GEMINI_MODEL)
    # Sub-agent 4
    D = Agent(name='David', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=GEMINI_MODEL)
    #Sub-agent 5
    E = Agent(name='Eve', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=GEMINI_MODEL)
    # Sub-agent 6
    F = Agent(name='Frank', description='An obedient agent.', instruction='Please say your name and your favorite sport.', model=GEMINI_MODEL)
    
    
    # Parallel Agent
    #Parallet agent 1
    P1 = ParallelAgent(name='ABC',description='Parallel group ABC',sub_agents=[A, B, C])
    #Parallel agent 2
    P2 = ParallelAgent(name='DEF',description='Parallel group DEF',sub_agents=[D, E, F])
    
    # Reducers
        # ABC -> R1
    R1 = Agent(name="reducer1", description="Reducer for ABC", instruction="Mention your name and then Summarize the responses from agents A, B, and C after running parallel agent ABC.", model=GEMINI_MODEL, #before_model_callback=print_llmrequest_contents
               )
    S1 = SequentialAgent(name='Group1_Sequential', description="Sequential group for ABC", sub_agents=[P1, R1])
    
    
    
    # DEF -> R2
    R2 = Agent(name="reducer2", description="Reducer for DEF", instruction="Mention your name and then Summarize the responses from agents D, E, and F after running parallel agent DEF.", model=GEMINI_MODEL, #before_model_callback=print_llmrequest_contents
               )
    S2 = SequentialAgent(name='Group2_Sequential', description="Sequential group for DEF", sub_agents=[P2, R2])
    
    # Running ABC and DEF in parallel
    P3 = ParallelAgent(name='Final_Parallel',sub_agents=[S1, S2])
    
    # Final reduction of previous reducers
    R3 = Agent(name="final_reducer", description="Final Reducer", instruction="Mention your name and then Summarize the outputs from both groups.", model=GEMINI_MODEL, #before_model_callback=print_llmrequest_contents
               )
    S3 = SequentialAgent(name='Final_Sequential', description="Sequential group for final outputs", sub_agents=[P3, R3])
    
    
    
    
    
    session_service = InMemorySessionService()
    runner = Runner(
        agent=S3, 
        app_name=APP_NAME,  
        session_service=session_service,
        memory_service=None,
        artifact_service=None
    
    )
    
    
    
    async def call_agent(query, USER_ID, SESSION_ID):
        content = types.Content(role='user', parts=[types.Part(text=query)])
        final_response_text = "No final response received."
    
    
    
        async for event in runner.run_async(
            user_id=USER_ID,
            session_id=SESSION_ID,
            new_message=content,
            ):
    
    
    
    
            # Check if this is the final response
            if event.is_final_response():
                final_response_text = (
                    event.content.parts[0].text
                    if event.content and event.content.parts
                    else "No final text found."
                )
    
    
            print(f"==============================\n<<< Each Agent Response >>>\n{final_response_text}\n==============")
    
    
    
    
    
    
    async def run_conversation(USER_ID,SESSION_ID):
       session = await session_service.create_session(app_name="app", user_id=USER_ID, session_id=SESSION_ID)
       await call_agent("Give summary",USER_ID,SESSION_ID)
    
    
    if __name__ == "__main__":
       
       asyncio.run(run_conversation(USER_ID,SESSION_ID))
    
    

    Output:

    ==============================
    <<< Each Agent Response >>>
    My name is Frank, and my favorite sport is soccer.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Bob, and my favorite sport is obeying instructions!
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Charlie, and my favorite sport is basketball.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is David, and my favorite sport is soccer.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    Okay. My name is Alice, and my favorite sport is swimming.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Eve, and my favorite sport is soccer.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    Hello, I am reducer1. After reviewing the responses from agents A, B, and C, here is a summary:
    
    [Insert Summary of A, B, and C's responses here]
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    Okay, my name is reducer2. Here's a summary of the responses from agents D, E, and F:
    
    I need the responses from agents D, E, and F in order to summarize them. Please provide those responses!
    
    ==============
    (.venv) llalitkumarrr-mac:ADK llalitkumarrr$ /Users/llalitkumarrr/projects/ADK/.venv/bin/python /Users/llalitkumarrr/projects/ADK/basic_agent/agentic_system.py
    App name mismatch detected. The runner is configured with app name "app", but the root agent was loaded from "/Users/llalitkumarrr/projects/ADK/.venv/lib/python3.13/site-packages/google/adk/agents", which implies app name "agents".
    ==============================
    <<< Each Agent Response >>>
    My name is Eve, and my favorite sport is soccer.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Alice, and my favorite sport is soccer.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is David, and my favorite sport is soccer. 
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Bob, and my favorite sport is baseball.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Frank, and my favorite sport is obedience... just kidding! As an AI, I don't have personal preferences like favorite sports. How can I help you with a summary? Please provide me with the text you want summarized.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is Charlie, and my favorite sport is soccer.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    Okay, my name is reducer2. I will now provide a summary of the responses from agents D, E, and F after running parallel agent DEF. 
    
    (I need the actual responses from agents D, E, and F to provide the summary. Please provide those responses.)
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    Okay, my name is reducer1. To give you a summary, I need the responses from agents A, B, and C first. Once you provide those, I'll summarize them for you.
    
    ==============
    ==============================
    <<< Each Agent Response >>>
    My name is final_reducer.
    
    *   **reducer1** is waiting for the outputs of agents A, B, and C to provide a summary.
    *   **reducer2** is waiting for the outputs of agents D, E, and F to provide a summary.
    
    ==============
    
    
  4. asheeshgarg commented on Nov 14, 2025

    @asheeshgarg

    @llalitkumarrr is there a update on this. We are really looking for this issue.

  5. ShaharKatz commented on Nov 15, 2025

    @ShaharKatz
    Contributor

    Us to, currently a blocker

  6. removed their assignment
    on Nov 17, 2025
  7. llalitkumarrr commented on Nov 17, 2025

    @llalitkumarrr
    Collaborator

    Hello @asheeshgarg and @ShaharKatz ,

    There is a PR raised by @dannovikov for this and we are also looking into this, we request your patience till we have an update on this for you.

    Thank you for your understanding.

  8. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    and removed
    help wanted[Community] Extra attention is needed
    on Nov 19, 2025
  9. dannovikov commented on Nov 21, 2025

    @dannovikov
    Author

    Hi guys, we found another problem case along the same lines of this issue, which isn't solved by the above proposed solution.

    if you have a sequence of parallel agents, each next parallel agent doesn't see the context from previous parallel agents.

    Example:

    SequentialAgent[
    ParallelAgent1[A, B, C],
    ParallelAgent2[D, E, F],
    ParallelAgent3[G, H, I],
    ]

    Even with the proposed fix, the Agents in Parallel2 cannot see the events of the agents in Parallel1. Likewise the agents in Parallel3 can't see any of the events before them.

    The cause of this has also to do with the branch string and the prefix matching rule, and it is not fixed by the suggested bidirectional prefix check.

    If you look at the branches of the events, you'll see that A, B and C have branch

    parallel_agent1.A
    parallel_agent1.B
    parallel_agent1.C

    meanwhile, the events from agents D, E F have branch

    parallel_agent2.D
    parallel_agent2.E
    parallel_agent2.F

    neither of these branches are prefixes of eachother, which is why downstream parallel subagents cant see the outputs from the group before them.

    I tried to find a way to solve both of these cases using branch naming logic, but its unclear if its even possible. I couldn't find a way to reconcile parallel agent siblings not seeing each other, with sequential agent siblings being able to see each other.

  10. asheeshgarg commented on Nov 22, 2025

    @asheeshgarg

    @Jacksunwei @llalitkumarrr this is important issue for some of our usecase. Appreciate any updates or discussion to move it to closer

  11. 13 remaining items

  12. removed
    stale[Status] Issues which have been marked inactive since there is no user response
    on Mar 12, 2026
  13. adk-bot commented on Apr 5, 2026

    @adk-bot
    Collaborator

    This issue has been automatically marked as stale because it has not had recent activity for 7 days after a maintainer requested clarification. It will be closed if no further activity occurs within 7 days.

  14. added
    stale[Status] Issues which have been marked inactive since there is no user response
    request clarification[Status] The maintainer need clarification or more information from the author
    on Apr 5, 2026
  15. adk-bot commented on Apr 12, 2026

    @adk-bot
    Collaborator

    This has been automatically closed because it has been marked as stale for over 7 days.

  16. adk-bot commented on Apr 13, 2026

    @adk-bot
    Collaborator

    This has been automatically closed because it has been marked as stale for over 7 days.

  17. added 2 commits that reference this issue on Sep 23, 2026
    2cb2c76
    b9a004f
  18. removed
    stale[Status] Issues which have been marked inactive since there is no user response
    on Oct 2, 2026
  19. removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementationneeds review[Status] The PR/issue is awaiting review from the maintainer

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions