Skip to content

fix: nested parallel agents can't see output from earlier parallel stages (#3470) - #7242

Open
dannovikov wants to merge 1 commit into
google:v1from
dannovikov:branch-visibility-v2
Open

dannovikov wants to merge 1 commit into
google:v1from
dannovikov:branch-visibility-v2

Conversation

@dannovikov

@dannovikov dannovikov commented Sep 23, 2026 •

Copy link
Copy Markdown

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:

Agents cannot see finished output from other branches of a ParallelAgent. #3470 reports two shapes:

Sequential[ Parallel[ Sequential[Parallel[a, b, c], r1],        r1 cannot see a, b, c
                       Sequential[Parallel[d, e, f], r2] ],      r2 cannot see d, e, f
            r3 ]

Sequential[ Parallel[a, b], Parallel[c, d] ]                    c, d cannot see a, b

The events exist in the session. _is_event_belongs_to_branch in flows/llm_flows/contents.py filters them out of the LLM request because it uses prefix matching: an agent on branch X sees an event on branch Y only if Y is a prefix of X. That answers "is the event from above me?", but the question it needs to answer is "are we in isolated lanes?"

Solution:

Change one function. No new types, no new fields, no storage or schema change. Event.branch stays a plain string.

The fix rests on one fact. ParallelAgent is the only agent that writes to branch (agents/parallel_agent.py:35), and it always appends exactly one dot-separated pair: parallel_agent_name.sub_agent_name. SequentialAgent and LoopAgent never touch it. So a branch string is a sequence of these pairs, and the segments alternate: even positions name a ParallelAgent, odd positions name one of its sub-agents.

Walk both branch strings segment by segment to the first mismatch. The index of that mismatch tells you everything:

first mismatch meaning visible?
odd — same ParallelAgent, different sub-agents sibling lanes of one ParallelAgent no
even — different ParallelAgent names not in sibling lanes (see below) yes
none — one branch is a prefix of the other same lane, different depth yes

The odd case is the only thing that creates isolation. Two sub-agents of the same ParallelAgent are in sibling lanes and never see each other's output, regardless of whether one finished before the other started.

The even case is where directionality matters. If the first mismatch is at an even index, the two branches name different ParallelAgents at that position. But every segment before that point matched — same ParallelAgents, same sub-agents, all the way up. That means no ParallelAgent above them forked them into different lanes. Whatever container holds these two ParallelAgents is a SequentialAgent or LoopAgent, which runs children one after another. They are not isolated.

The fourth example in the table below shows why the walk must go left to right. gp.x.p1.a and gp.y.p2.d have different ParallelAgents at index 2 (p1 vs p2), but the walk never reaches index 2 — it stops at index 1 (x vs y), where the two are sub-agents of the same ParallelAgent gp. That odd-index mismatch means they are in sibling lanes of gp, and the event is hidden.

stage1.a    vs  stage1.b        mismatch at 1 (odd)   → hidden
stage2.c    vs  stage1.a        mismatch at 0 (even)  → visible
par.r1      vs  par.r1.abc.a    no mismatch (prefix)  → visible
gp.x.p1.a  vs  gp.y.p2.d      mismatch at 1 (odd)   → hidden
def _is_event_belongs_to_branch(invocation_branch, event):
  """Check if an event belongs to the current branch.

  Sub-agents of a ParallelAgent are isolated lanes: an agent never sees output
  from a sibling lane or from anything nested inside one. Everything else is
  visible.

  Only ParallelAgent writes branches, always as `parallel_agent.sub_agent`
  pairs, so even segments name a ParallelAgent and odd segments name one of
  its sub-agents. Walk both branches to the first mismatch:

    odd    same ParallelAgent, different sub-agents: sibling lanes. Hidden.
    even   different ParallelAgents. Every earlier segment matched, so no
           ParallelAgent above forked them into different lanes. Visible.
    none   one branch is a prefix of the other: same lane. Visible.

    stage1.a    vs  stage1.b        mismatch at 1, odd   hidden
    stage2.c    vs  stage1.a        mismatch at 0, even  visible
    par.r1      vs  par.r1.abc.a    no mismatch, prefix  visible
    gp.x.p1.a  vs  gp.y.p2.d      mismatch at 1, odd   hidden
  """
  if not invocation_branch or not event.branch:
    return True
  current = invocation_branch.split('.')
  other = event.branch.split('.')
  for i, (a, b) in enumerate(zip(current, other)):
    if a != b:
      return i % 2 == 0
  return True

Prior approaches:

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

tests/unittests/flows/llm_flows/test_contents_branch.py

  • test_is_event_belongs_to_branch: nine-row parametrized truth table over the function — one row per case in the table above, plus no-branch, same-branch, and name-prefix-collision rows.
  • test_branch_filtering_parent_sees_finished_children (renamed from test_branch_filtering_parent_cannot_see_child — the old assertion was the reported bug).
  • test_branch_filtering_grandchild_sees_grandparent updated to use parallel.sub pair shapes.

tests/unittests/agents/test_parallel_branch_visibility.py (new)

The new tests reject the alternatives:

rule result
current v1 prefix rule 18 fail
bidirectional prefix (#3470 proposal) 13 fail
this PR 36 pass

Full suite: 6846 passed (v1 baseline: 6815; difference is the new tests).

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.

Additional context

This targets v1. LoopAgent reuses the same branch strings across iterations, so Loop[Parallel[a, b]] has a separate visibility gap — that is a pre-existing issue and will be a separate PR.

@google-cla

google-cla Bot commented Sep 23, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Only ParallelAgent writes branches, always as parallel_agent.sub_agent
pairs. Walk both branches to the first mismatch: an odd index means
sibling lanes of the same ParallelAgent (hidden); an even index or no
mismatch means visible. Event.branch stays a plain string.

Fixes google#3470
@dannovikov

Copy link
Copy Markdown
Author

For reviewers; this PR supersedes my older PR #3889 which offered a far more complicated approach. This one is way simpler and much more elegant.

@dannovikov

Copy link
Copy Markdown
Author

The failing check (test_concurrent_prepare_tables_no_race_condition, 3.11–3.14) is unrelated to this change. It fails on v1 at d509d7c without this PR, because SQLAlchemy 2.1 now resolves for 3.11+ and the sqlite+aiosqlite:///:memory: test loses sessions under it. With sqlalchemy<2.1 the full unit suite passes on this branch (6,846 passed). Happy to rebase once v1 pins SQLAlchemy or picks up a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants