[BUG] `supervisor_tools` treats any child `ConductResearch` exception as successful research completion

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

Bug Description

The main Deep Researcher graph currently treats any exception raised by a child ConductResearch run as a normal end of the research phase.

The problem is in src/open_deep_research/deep_researcher.py inside supervisor_tools:

python
except Exception as e:
    # Handle research execution errors
    if is_token_limit_exceeded(e, configurable.research_model) or True:
        # Token limit exceeded or other error - end research phase
        return Command(
            goto=END,
            update={
                "notes": get_notes_from_tool_calls(supervisor_messages),
                "research_brief": state.get("research_brief", "")
            }
        )

Because of or True, this branch always executes.

That means a child researcher failure is not surfaced as an error and is not converted into a failed tool result. Instead, the supervisor exits the research phase as if research had completed normally.

This is especially risky because the top-level graph then continues from research_supervisor to final_report_generation, so a run can still produce a final report after part of the research fan-out has failed.

Deployment Type

  • Self-hosted / local development
  • Source-level bug in the shipped production graph implementation

Steps to Reproduce

Minimal real-source PoC

From a clean checkout:

bash
uv sync

Create a small probe that imports the real supervisor_tools function and patches the real child researcher subgraph to fail:

python
import asyncio
from unittest.mock import patch

from langchain_core.messages import AIMessage
from open_deep_research.deep_researcher import supervisor_tools, researcher_subgraph

state = {
    "supervisor_messages": [
        AIMessage(
            content="",
            tool_calls=[
                {
                    "name": "ConductResearch",
                    "args": {"research_topic": "test topic"},
                    "id": "call-1",
                    "type": "tool_call",
                }
            ],
        )
    ],
    "research_iterations": 1,
    "research_brief": "brief",
}

config = {"configurable": {"research_model": "openai:gpt-4.1"}}

async def boom(*args, **kwargs):
    raise RuntimeError("boom from child researcher")

async def main():
    with patch.object(researcher_subgraph, "ainvoke", side_effect=boom):
        cmd = await supervisor_tools(state, config)
        print("goto =", cmd.goto)
        print("update =", cmd.update)

asyncio.run(main())

Run it with:

bash
uv run python supervisor_child_error_probe.py

Observed output:

goto = __end__
update = {'notes': [], 'research_brief': 'brief'}

Why this reaches final report generation

The top-level graph wires:

python
deep_researcher_builder.add_edge("research_supervisor", "final_report_generation")

So once supervisor_tools returns goto=END from the supervisor subgraph, the outer graph proceeds to final report generation instead of failing the run.

Expected Behavior

If a child ConductResearch run raises an arbitrary runtime exception, the failure should not be treated as successful research completion.

At minimum, the error should either:

  • fail the current run explicitly, or
  • be turned into an explicit failed research result that the supervisor can reason about

Actual Behavior

Any child researcher exception is swallowed by the unconditional or True branch.

In practice, the supervisor exits the research phase early and the outer graph can continue to final report generation with empty or partial findings.

Suggested Fix

The immediate issue is the unconditional branch:

python
if is_token_limit_exceeded(e, configurable.research_model) or True:

This should not treat all exceptions as a clean END.

Possible fix directions:

python
if is_token_limit_exceeded(e, configurable.research_model):
    ...
else:
    raise

or, if graceful degradation is desired:

  • use asyncio.gather(..., return_exceptions=True)
  • preserve successful sibling results
  • convert failed child runs into explicit tool/error messages instead of silently ending the supervisor phase

Environment Information

  • Browser: n/a
  • Operating System: reproduced on local Linux dev environment
  • open_deep_research Version: commit 1f24f1142db24f81e29eb88751985a00ec8ed580

Additional Environment Details (Self-hosted)

  • Python environment created with uv sync
  • Package version: 0.0.16

Impact

This can silently truncate research in normal production conditions, for example if one child researcher hits:

  • a provider error
  • a transient network failure
  • an MCP/tool execution error
  • an unexpected runtime exception inside the child research flow

When that happens:

  • the supervisor can stop the entire research phase early even though the user request may have been split into multiple research branches
  • successful sibling branches may already have been launched, but their findings may never be incorporated into the current run output
  • the outer graph can still continue to final_report_generation
  • the user can receive a normal-looking report that is materially incomplete, without an explicit indication that one branch of the research failed

This also makes failures harder to detect operationally:

  • callers that only look at run completion can interpret the run as successful
  • partial research can be mistaken for a fully supported answer
  • model/tool/search cost may already have been spent on sibling branches whose output is then discarded from the final report

So this is not just an internal error-handling quirk. It converts a real runtime failure into a silent quality failure that can surface as an incomplete but apparently successful research result.

Source: langchain-ai/open_deep_research