Pure-graph parallel dispatch runs all next-nodes concurrently whenever len > 1, ignoring each node's parallel flag #97

Open
opened 2026-08-03 21:50:26 +00:00 by CoreRasurae · 0 comments
Member

Metadata

  • Commit Message: fix(langgraph): gate pure graph concurrent node dispatch on per-node parallel flag
  • Branch: bugfix/m1-pure-graph-parallel-node-gate

Background and context

§6.7 (Parallel Execution) of the Actor Configuration Standard states: "The
graph-level parallel_execution: true flag declares that the graph may
execute multiple ready nodes concurrently. The flag does not by itself cause
parallelism; each individual node MUST additionally declare parallel: true
in its node configuration to participate in parallel execution... Non-
parallel-marked nodes are executed sequentially." §12.3 (Concurrency)
reiterates: nodes MAY execute in parallel only when (a) parallel_execution: true, AND (b) the next-node set has more than one element, AND (c) the
nodes have parallel: true in their config.

PureLangGraph parses each node's parallel flag (NodeConfig.parallel,
default False, nodes.py:106) and even ships a correct helper
can_execute_parallel() (nodes.py:922-924) — used correctly by the older,
unused LangGraph-dependent engine at langgraph/graph.py:296-299. However,
the three concurrent-dispatch sites in the actively-used pure-graph engine
(src/cleveractors/langgraph/pure_graph.py, in execute() at line 1078, and
both execute_stream() branches at lines 1860 and 1965) never call it:

if self.config.parallel_execution and len(next_nodes) > 1:
    tasks = [
        asyncio.create_task(self._execute_from_node(n, output_message, depth + 1))
        for n in next_nodes
    ]
    results = await asyncio.gather(*tasks)

This fires every candidate next-node concurrently whenever the
graph-level flag is on (the default) and there is more than one candidate —
regardless of whether any individual node opted in via parallel: true.

No node in packages/porting-actor-tools-graph-no_prune-single_thread-skills_b.yaml
declares parallel: true anywhere, and its main route sets
parallel_execution: true. Whenever _get_next_nodes() returns more than
one candidate for that graph (see companion issue #95, whose
content_not_contains defect is one way this happens), both candidates run
concurrently via asyncio.gather/asyncio.create_task, even though the
graph draws a strict sequential chain. This was observed in a real run: the
tool calls for code-review-fixes1 and ui-coder-designer interleaved in
the log instead of running strictly one after another.

This is a distinct, independent contract violation from #95 — it would
surface for any graph that legitimately produces 2+ next-nodes (e.g. a
genuine fan-out with only some nodes marked parallel: true), not only the
specific content_not_contains scenario.

Current behavior

  • Any time _get_next_nodes() returns 2+ candidates and the graph-level
    parallel_execution is true (the default), all candidates are
    dispatched concurrently via asyncio.gather, with no check of each node's
    own parallel config flag.
  • NodeConfig.parallel / can_execute_parallel() are parsed and available
    but never consulted by the active pure-graph engine.

Expected behavior

  • Per §6.7/§12.3: of the computed next-node set, only the subset with
    parallel: true on the node itself should run concurrently (joined via
    asyncio.gather); the remaining non-parallel-marked nodes should run
    sequentially.
  • When parallel_execution: false at the graph level, all nodes run
    sequentially regardless of their individual parallel flags (this outer
    gate is already correct — only the per-node gate is missing).

Acceptance criteria

  • Given 2+ next-nodes where none declare parallel: true, all execute
    sequentially (one completes before the next starts) even when
    parallel_execution: true.
  • Given 2+ next-nodes where some declare parallel: true and others
    don't, only the parallel: true subset runs concurrently via
    asyncio.gather; the remaining nodes run sequentially, per §6.7 steps
    1-4.
  • Given parallel_execution: false, all nodes run sequentially
    regardless of individual parallel flags (no regression).
  • The fix is applied consistently at all three dispatch sites in
    pure_graph.py (execute() and both execute_stream() branches).

Supporting information

  • Spec: docs/index.md §6.7 (Parallel Execution), §12.3 (Concurrency).
  • Code: src/cleveractors/langgraph/pure_graph.py lines 1078, 1860, 1965
    (dispatch sites); NodeConfig.parallel (default False);
    can_execute_parallel() helper (correctly used by the unused
    langgraph/graph.py engine, unused in pure_graph.py).
  • Reproduction: packages/porting-actor-tools-graph-no_prune-single_thread-skills_b.yaml
    — no node declares parallel: true; the route sets parallel_execution: true.
  • Log evidence from a client/test_app.py run: interleaved tool calls from
    code-review-fixes1, ui-coder-designer, and code-adjustments-classifier1
    where the graph places them strictly sequentially.
  • Related: this issue depends on its companion TDD issue, and is compounded
    by the separately-filed content_not_contains condition bug (#95), which
    is what produces the 2+ candidate set in this particular graph — but this
    dispatch-gating defect is independent and would surface with any 2+
    candidate set regardless of cause.

Subtasks

  • Filter next_nodes / content_next_nodes to the subset where
    node.can_execute_parallel() is true at all three dispatch sites in
    src/cleveractors/langgraph/pure_graph.py (lines 1078, 1860, 1965);
    gather only that subset concurrently, execute the remainder
    sequentially.
  • Tests (Behave): scenarios for all-parallel, mixed parallel/
    non-parallel, no-node-marked-parallel, and parallel_execution: false
    cases.
  • Tests (Robot): integration scenario asserting sequential nodes
    complete in declaration order (via shared state ordering) when none
    are marked parallel: true.
  • Update CHANGELOG with a user-facing entry.
  • Verify coverage >= 97% via nox -s coverage_report.
  • Run nox (all default sessions), fix any errors.

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line matches the Commit Message in
    Metadata exactly.
  • The commit is pushed to the branch matching the Branch in Metadata exactly.
  • The companion TDD regression test (@tdd_issue / @tdd_issue_N) passes
    with @tdd_expected_fail removed.
  • The commit is submitted as a PR to master, reviewed, and merged.
## Metadata - **Commit Message:** `fix(langgraph): gate pure graph concurrent node dispatch on per-node parallel flag` - **Branch:** `bugfix/m1-pure-graph-parallel-node-gate` ## Background and context §6.7 (Parallel Execution) of the Actor Configuration Standard states: "The graph-level `parallel_execution: true` flag declares that the graph **may** execute multiple ready nodes concurrently. The flag does not by itself cause parallelism; each individual node MUST additionally declare `parallel: true` in its node configuration to participate in parallel execution... Non- parallel-marked nodes are executed sequentially." §12.3 (Concurrency) reiterates: nodes MAY execute in parallel only when (a) `parallel_execution: true`, AND (b) the next-node set has more than one element, AND (c) the nodes have `parallel: true` in their config. `PureLangGraph` parses each node's `parallel` flag (`NodeConfig.parallel`, default `False`, `nodes.py:106`) and even ships a correct helper `can_execute_parallel()` (`nodes.py:922-924`) — used correctly by the older, unused LangGraph-dependent engine at `langgraph/graph.py:296-299`. However, the three concurrent-dispatch sites in the actively-used pure-graph engine (`src/cleveractors/langgraph/pure_graph.py`, in `execute()` at line 1078, and both `execute_stream()` branches at lines 1860 and 1965) never call it: ```python if self.config.parallel_execution and len(next_nodes) > 1: tasks = [ asyncio.create_task(self._execute_from_node(n, output_message, depth + 1)) for n in next_nodes ] results = await asyncio.gather(*tasks) ``` This fires **every** candidate next-node concurrently whenever the graph-level flag is on (the default) and there is more than one candidate — regardless of whether any individual node opted in via `parallel: true`. No node in `packages/porting-actor-tools-graph-no_prune-single_thread-skills_b.yaml` declares `parallel: true` anywhere, and its `main` route sets `parallel_execution: true`. Whenever `_get_next_nodes()` returns more than one candidate for that graph (see companion issue #95, whose `content_not_contains` defect is one way this happens), both candidates run concurrently via `asyncio.gather`/`asyncio.create_task`, even though the graph draws a strict sequential chain. This was observed in a real run: the tool calls for `code-review-fixes1` and `ui-coder-designer` interleaved in the log instead of running strictly one after another. This is a distinct, independent contract violation from #95 — it would surface for *any* graph that legitimately produces 2+ next-nodes (e.g. a genuine fan-out with only some nodes marked `parallel: true`), not only the specific `content_not_contains` scenario. ## Current behavior - Any time `_get_next_nodes()` returns 2+ candidates and the graph-level `parallel_execution` is `true` (the default), **all** candidates are dispatched concurrently via `asyncio.gather`, with no check of each node's own `parallel` config flag. - `NodeConfig.parallel` / `can_execute_parallel()` are parsed and available but never consulted by the active pure-graph engine. ## Expected behavior - Per §6.7/§12.3: of the computed next-node set, only the subset with `parallel: true` on the node itself should run concurrently (joined via `asyncio.gather`); the remaining non-parallel-marked nodes should run sequentially. - When `parallel_execution: false` at the graph level, all nodes run sequentially regardless of their individual `parallel` flags (this outer gate is already correct — only the per-node gate is missing). ## Acceptance criteria - [ ] Given 2+ next-nodes where none declare `parallel: true`, all execute sequentially (one completes before the next starts) even when `parallel_execution: true`. - [ ] Given 2+ next-nodes where some declare `parallel: true` and others don't, only the `parallel: true` subset runs concurrently via `asyncio.gather`; the remaining nodes run sequentially, per §6.7 steps 1-4. - [ ] Given `parallel_execution: false`, all nodes run sequentially regardless of individual `parallel` flags (no regression). - [ ] The fix is applied consistently at all three dispatch sites in `pure_graph.py` (`execute()` and both `execute_stream()` branches). ## Supporting information - Spec: `docs/index.md` §6.7 (Parallel Execution), §12.3 (Concurrency). - Code: `src/cleveractors/langgraph/pure_graph.py` lines 1078, 1860, 1965 (dispatch sites); `NodeConfig.parallel` (default `False`); `can_execute_parallel()` helper (correctly used by the unused `langgraph/graph.py` engine, unused in `pure_graph.py`). - Reproduction: `packages/porting-actor-tools-graph-no_prune-single_thread-skills_b.yaml` — no node declares `parallel: true`; the route sets `parallel_execution: true`. - Log evidence from a `client/test_app.py` run: interleaved tool calls from `code-review-fixes1`, `ui-coder-designer`, and `code-adjustments-classifier1` where the graph places them strictly sequentially. - Related: this issue depends on its companion TDD issue, and is compounded by the separately-filed `content_not_contains` condition bug (#95), which is what produces the 2+ candidate set in this particular graph — but this dispatch-gating defect is independent and would surface with any 2+ candidate set regardless of cause. ## Subtasks - [ ] Filter `next_nodes` / `content_next_nodes` to the subset where `node.can_execute_parallel()` is true at all three dispatch sites in `src/cleveractors/langgraph/pure_graph.py` (lines 1078, 1860, 1965); gather only that subset concurrently, execute the remainder sequentially. - [ ] Tests (Behave): scenarios for all-parallel, mixed parallel/ non-parallel, no-node-marked-parallel, and `parallel_execution: false` cases. - [ ] Tests (Robot): integration scenario asserting sequential nodes complete in declaration order (via shared state ordering) when none are marked `parallel: true`. - [ ] Update CHANGELOG with a user-facing entry. - [ ] Verify coverage >= 97% via `nox -s coverage_report`. - [ ] Run `nox` (all default sessions), fix any errors. ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the first line matches the Commit Message in Metadata exactly. - The commit is pushed to the branch matching the Branch in Metadata exactly. - The companion TDD regression test (`@tdd_issue` / `@tdd_issue_N`) passes with `@tdd_expected_fail` removed. - The commit is submitted as a PR to `master`, reviewed, and merged.
CoreRasurae added this to the v2.1.0 milestone 2026-08-03 21:50:26 +00:00
CoreRasurae added the
State
Unverified
Type
Bug
Priority
Critical
labels 2026-08-03 21:50:26 +00:00
CoreRasurae added
State
Verified
and removed
State
Unverified
labels 2026-08-03 21:55:09 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: cleveragents/cleveractors-core#97