Skip to content

[AAASM-4474] 🐛 (langgraph): Add missing idempotency guard for sync subgraph invoke - #237

Merged
Chisanan232 merged 1 commit into
masterfrom
v0.1.0/AAASM-4474/fix/langgraph_sync_invoke_idempotency
Jul 11, 2026
Merged

[AAASM-4474] 🐛 (langgraph): Add missing idempotency guard for sync subgraph invoke#237
Chisanan232 merged 1 commit into
masterfrom
v0.1.0/AAASM-4474/fix/langgraph_sync_invoke_idempotency

Conversation

@Chisanan232

Copy link
Copy Markdown
Contributor

What changed

_wrap_subgraph_invoke_methods_in_place() in agent_assembly/adapters/langgraph/patch.py (added by AAASM-4472) now guards the sync .invoke wrap path with an _agent_assembly_invoke_spawned marker, mirroring the existing _agent_assembly_ainvoke_spawned guard already used for the async .ainvoke path.

Why

AAASM-4472 fixed lineage tracking for a compiled subgraph reused as a node (parent_graph.add_node("sub", compiled_subgraph)) by mutating the subgraph's own invoke/ainvoke in place. The async path correctly no-ops on a second wrap via its marker check. The sync path had no equivalent guard.

Root cause: if the same compiled subgraph instance is mounted as a node in two (or more) different parent graphs — a legitimate reusable-subgraph pattern — each compile() call re-wraps .invoke around whatever .invoke currently is. On the second compile, original_invoke captured inside _wrap_subgraph_invoke_methods_in_place is already the first wrapper (not the true original), so invoking the first parent graph synchronously nests two spawn-context scopes: the outer sets depth 1, the inner then computes depth _current_spawn_ctx.depth + 1 = 2. The subgraph's own node observes lineage depth 2 instead of 1.

Fix: add _agent_assembly_invoke_spawned, set on first wrap and checked before wrapping .invoke again — same naming convention and same check logic as the existing async guard, so the two guards read as a matched pair.

Test evidence

Two new tests added to test/integration/test_langgraph_real_package_smoke.py, alongside the existing (unchanged) AAASM-4472 test test_real_stategraph_subgraph_as_node_propagates_spawn_lineage:

  • test_real_stategraph_reused_subgraph_across_parents_sync_invoke_lineage_depth_stable — mounts the same compiled subgraph as a node in two different parent graphs, invokes the first synchronously, asserts lineage depth is 1.
  • test_real_stategraph_reused_subgraph_across_parents_async_ainvoke_lineage_depth_stable — symmetric async version (.ainvoke), asserting the already-correct async path stays correct.

Before fix (TDD — wrote the sync test first against the unpatched code):

FAILED test/integration/test_langgraph_real_package_smoke.py::test_real_stategraph_reused_subgraph_across_parents_sync_invoke_lineage_depth_stable
AssertionError: expected lineage depth 1 for a sync invoke of a subgraph reused across two parent graphs, not an accumulated double-wrap depth
assert [2] == [1]
1 failed, 1 passed (async test passed as expected — confirms the bug is sync-specific)

After fix:

test_real_stategraph_sync_invoke_fires_node_hooks PASSED
test_real_stategraph_async_ainvoke_fires_node_hooks PASSED
test_real_stategraph_subgraph_as_node_propagates_spawn_lineage PASSED   (AAASM-4472 test, unchanged)
test_real_stategraph_reused_subgraph_across_parents_sync_invoke_lineage_depth_stable PASSED
test_real_stategraph_reused_subgraph_across_parents_async_ainvoke_lineage_depth_stable PASSED
test_revert_restores_the_real_stategraph_compile PASSED
6 passed

Full check suite

  • pytest test/ — 782 passed, 16 skipped (pre-existing skips: native _core module not built, optional frameworks not installed — unrelated to this change), 0 failed.
  • ruff check . — all checks passed.
  • ruff format --check . — no formatting issues in the changed files (6 unrelated pre-existing files elsewhere in the repo would reformat; not touched here).
  • mypy agent_assembly — same 4 pre-existing errors present on master baseline (native _core stub / grpc stubs, unrelated to patch.py); no new errors.
  • pre-commit run (scoped to changed files) — all hooks passed (isort, autoflake, black, mypy, etc).

How to verify

.venv/bin/python -m pytest test/integration/test_langgraph_real_package_smoke.py -v --no-cov

Closes AAASM-4474

AAASM-4472 added _wrap_subgraph_invoke_methods_in_place() to inject
lineage tracking into a compiled subgraph's own invoke/ainvoke when it
is reused as a node. The async path guards against double-wrapping via
an _agent_assembly_ainvoke_spawned marker, but the sync path had no
equivalent guard: reusing the same compiled subgraph as a node in two
different parent graphs re-wrapped .invoke on the second compile,
stacking spawn-context depth (2 instead of 1) on a single sync invoke.

Add a matching _agent_assembly_invoke_spawned marker checked before
wrapping .invoke, mirroring the existing async guard exactly.

New regression tests: a sync test reproducing the double-wrap (mount
the same compiled subgraph in two parent graphs, invoke synchronously,
assert lineage depth stays 1) and a symmetric async test confirming
the already-correct async path stays correct.

Closes AAASM-4474
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Chisanan232
Chisanan232 merged commit fd82a8c into master Jul 11, 2026
26 checks passed
@Chisanan232
Chisanan232 deleted the v0.1.0/AAASM-4474/fix/langgraph_sync_invoke_idempotency branch July 11, 2026 15:05
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.

1 participant