[BUG] `supervisor_tools` treats any child `ConductResearch` exception as successful research completion
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:
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:
uv syncCreate a small probe that imports the real supervisor_tools function and patches the real child researcher subgraph to fail:
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:
uv run python supervisor_child_error_probe.pyObserved output:
goto = __end__
update = {'notes': [], 'research_brief': 'brief'}Why this reaches final report generation
The top-level graph wires:
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:
if is_token_limit_exceeded(e, configurable.research_model) or True:This should not treat all exceptions as a clean END.
Possible fix directions:
if is_token_limit_exceeded(e, configurable.research_model):
...
else:
raiseor, 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