[BUG] `legacy/multi_agent.py` can treat partial section completion as full research completion

Author: bossjoker1Created Jun 25, 2026Updated Jul 23, 2026

Bug Description

The packaged legacy/multi_agent.py path currently has a premature join in the supervisor/research-team wiring.

The multi-agent graph fans out section research through Send(...), but each research_team completion routes directly back into supervisor:

python
supervisor_builder.add_node("research_team", research_builder.compile())
...
supervisor_builder.add_edge("research_team", "supervisor")

At the same time, supervisor(...) uses this condition:

python
if state.get("completed_sections") and not state.get("final_report"):
    research_complete_message = {
        "role": "user",
        "content": "Research is complete. Now write the introduction and conclusion ..."
    }

So the presence of any completed section is enough to switch the supervisor into completion mode.

That means the graph can move on to introduction/conclusion writing from partial section state instead of waiting for all parallel section researchers to finish.

Steps to Reproduce

From a clean checkout:

bash
uv sync

Create repro_partial_join.py in the repo root:

python
import asyncio
import sys
import types

sys.path.insert(0, "src")


# Stub just enough imports to exercise the real supervisor logic without
# requiring live models, MCP servers, or external search providers.
langchain = types.ModuleType("langchain")
chat_models = types.ModuleType("langchain.chat_models")


class FakeLLM:
    def __init__(self):
        self.captured = None

    def bind_tools(self, tools, **kwargs):
        return self

    async def ainvoke(self, messages):
        self.captured = messages
        return types.SimpleNamespace(tool_calls=[])


fake_llm = FakeLLM()
chat_models.init_chat_model = lambda *args, **kwargs: fake_llm
langchain.chat_models = chat_models
sys.modules["langchain"] = langchain
sys.modules["langchain.chat_models"] = chat_models

mcp_client_mod = types.ModuleType("langchain_mcp_adapters.client")
mcp_client_mod.MultiServerMCPClient = type(
    "FakeMCP",
    (),
    {"__init__": lambda self, *a, **k: None, "get_tools": lambda self: []},
)
sys.modules["langchain_mcp_adapters"] = types.ModuleType("langchain_mcp_adapters")
sys.modules["langchain_mcp_adapters.client"] = mcp_client_mod

legacy_utils = types.ModuleType("legacy.utils")
legacy_utils.get_config_value = lambda v: v
legacy_utils.tavily_search = None
legacy_utils.duckduckgo_search = None
legacy_utils.get_today_str = lambda: "2026-06-25"
sys.modules["legacy.utils"] = legacy_utils

from legacy.multi_agent import supervisor
from legacy.state import Section

state = {
    "messages": [{"role": "user", "content": "start report"}],
    "completed_sections": [
        Section(
            name="Body A",
            description="A",
            research=True,
            content="SECTION A COMPLETE",
        )
    ],
    "final_report": "",
}
config = {"configurable": {"supervisor_model": "fake", "search_api": "none"}}


async def main():
    out = await supervisor(state, config)
    captured = fake_llm.captured
    print("OUT_KEYS", sorted(out.keys()))
    print("CAPTURED_LEN", len(captured))
    print("LAST_MSG_ROLE", captured[-1]["role"])
    print("LAST_MSG_CONTENT", captured[-1]["content"])


asyncio.run(main())

Run it with:

bash
uv run python repro_partial_join.py

Observed output:

OUT_KEYS ['messages']
CAPTURED_LEN 3
LAST_MSG_ROLE user
LAST_MSG_CONTENT Research is complete. Now write the introduction and conclusion for the report. Here are the completed main body sections: 

SECTION A COMPLETE

Expected Behavior

The supervisor should only move into the “research complete / write intro+conclusion” phase after all parallel section researchers have completed.

One completed section should not be enough to trigger convergence.

Actual Behavior

The supervisor switches into completion mode as soon as completed_sections is non-empty.

Because each research_team child flows directly back into supervisor, a single finished child can trigger intro/conclusion writing while other section researchers are still pending.

Suggested Fix

This looks like a missing wait-for-all barrier on the research_team join.

Possible fix directions:

  • add an explicit aggregation/join node that waits for all section researchers to finish before re-entering supervisor
  • or track the planned section count and only emit the “Research is complete” prompt when len(completed_sections) reaches that full expected count

Environment Information

  • Operating System: reproduced on Linux
  • open_deep_research commit: 1f24f1142db24f81e29eb88751985a00ec8ed580
  • Python environment created with uv sync

Impact

This is a graph-level correctness bug in the packaged legacy implementation.

If the graph converges after only one child completes, the run can:

  • start writing introduction/conclusion from incomplete body coverage
  • omit sections that were still in progress
  • produce a final report that looks complete even though not all planned section researchers finished

That makes it a silent partial-result bug rather than a clean failure, which is harder for users and maintainers to detect.

Source: langchain-ai/open_deep_research