Deep Research Agent
A deep research agent: give it a question, and it researches the web and produces a structured, cited Markdown report.
| Mandatory run variables | messages: A list of ChatMessages |
| Output variables | report: The final cited Markdown reportbrief, notes: The intermediate research brief and collected summaries |
| API reference | Agent Pack |
| GitHub link | https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/agent_pack/src/haystack_integrations/agent_pack/deep_research |
| Package name | agent-pack-haystack |
Part of Agent Pack, which is experimental for the moment. Its APIs and agent architectures can change in any release, without following the usual deprecation policy.
When to use this agent
Use the deep research agent when you need more than a quick answer. It's designed for questions that require gathering information from many web sources, evaluating them, and producing a structured report with citations.
Typical use cases include:
- Researching a broad topic across many sources.
- Comparing products, companies, technologies, or scientific findings.
- Preparing a literature review or market overview.
- Answering complex questions that benefit from investigating several sub-topics in parallel.
It's less useful when:
- A single web search or RAG lookup is enough. The multi-agent workflow adds latency and cost.
- The information lives in a private knowledge base rather than on the public web. For that, use the Advanced RAG Agent.
Installation
trafilatura, pypdf, and arrow are separate installs the deep research agent needs at runtime (HTML and PDF parsing, and date rendering). tavily-haystack is only needed for the default search_tool.
Set OPENAI_API_KEY and TAVILY_API_KEY in the environment.
Usage
from haystack.dataclasses import ChatMessage
from haystack_integrations.agent_pack import create_deep_research_agent
agent = create_deep_research_agent()
result = agent.run(messages=[ChatMessage.from_user("your research question")])
print(result["report"])
agent.run(...) returns a dictionary whose main output is report, the final Markdown report. The dictionary also carries the intermediate brief (a str) and notes (a list[str]), plus the standard Agent outputs messages, last_message, step_count, token_usage, and tool_call_counts.
Configuration
Everything is configured through keyword arguments to create_deep_research_agent. All parameters are keyword-only and optional.
Main agent
The main agent is the orchestrator: it plans the investigation and delegates the sub-questions.
llmis the LLM that drives the orchestrator's loop. Defaults toOpenAIResponsesChatGenerator("gpt-5.4").system_promptoverrides the pre-made orchestrator prompt. The placeholder{{ max_subtopics }}is replaced with the value ofmax_subtopics.max_agent_stepsis the maximum number of steps for the orchestrator's agent loop (reflect and delegate rounds). Defaults to8.max_subtopicsis the maximum number of sub-questions the orchestrator may delegate (breadth). Defaults to5.max_concurrent_researchersis the maximum number of sub-researchers that run at the same time. Defaults to5.
Sub-researchers
researcher_llmis the LLM that drives each sub-researcher's search, read, and think loop. Defaults toOpenAIResponsesChatGenerator("gpt-5.4-mini").search_toolis the web search tool each sub-researcher uses. Defaults toTavilyWebSearchTool(top_k=10), which requirestavily-haystack. The pre-made researcher prompt refers to this tool asweb_search, so name a custom tool the same way or adapt the prompt.page_summary_llmis the LLM used inside theread_urltool to summarize a fetched page toward the question. Defaults toOpenAIResponsesChatGenerator("gpt-5.4-mini").max_researcher_stepsis the maximum number of steps for each sub-researcher's agent loop. Defaults to20.max_page_charsis the maximum number of raw page characters fed topage_summary_llm, before summarization. Defaults to50000.
Brief and report
The Scope and Write phases are single LLM calls, each with its own ChatGenerator, so you can mix models by cost and capability or swap in a different provider.
brief_llmis the LLM that rewrites the user query into a focused research brief. Defaults toOpenAIResponsesChatGenerator("gpt-5.4").report_llmis the LLM that turns the brief plus collected notes into the final report. Defaults toOpenAIResponsesChatGenerator("gpt-5.4").
Further customization
To change anything else, such as adding tools or hooks, use clone() on the returned agent. Unpack the existing values to keep the built-in tools, state entries, and the Scope and Write hooks:
agent = create_deep_research_agent()
customized = agent.clone(
tools=[*agent.tools, my_tool],
hooks={**agent.hooks, "before_llm": [my_hook]},
)
How it works
The architecture is built around a single top-level Haystack Agent, which acts as the orchestrator. Two hooks run before and after its loop, creating three logical phases: Scope, Research, and Write. During the Research phase, the orchestrator invokes isolated sub-researcher agents, each its own Agent, through a tool:
- Scope. The user question is rewritten into a focused research brief.
- Research. The orchestrator splits the brief into focused sub-questions, delegates each to a sub-researcher, and collects their summaries.
- Write. The brief and the collected summaries become the final report: Markdown with inline
[text](url)citations.
Scope and Write are plain LLM calls (a ChatPromptBuilder and an OpenAIResponsesChatGenerator), wrapped as serializable hook classes (ScopeHook, WriteHook):
- Scope runs as a
before_runhook: before the orchestrator's loop starts, it turns the user query into a brief, stored on the agent'sState. - Write runs as an
after_runhook: when the orchestrator's loop finishes, it turns the brief plus collectednotesinto the final report.
brief, notes, and report are declared in the agent's state_schema, so they come back as outputs of a single agent.run(...) call.
The agents
The Research phase uses two nested agents. Each one is a Haystack Agent: an LLM that loops, calling tools, until it decides to answer.
Orchestrator
The orchestrator is the lead agent: it receives the research brief and coordinates the whole investigation.
- Job: split the brief into a few focused, non-overlapping sub-questions, delegate each one, check coverage, and stop when there's enough.
- Parallelism: it emits several delegation calls in a single turn, and they run concurrently (bounded by
max_concurrent_researchers). - Memory: the summaries returned by sub-researchers are appended to a shared
noteslist (the agent'sState), which the writer later turns into the report. - Stops when: it replies with plain text (research complete) or hits
max_agent_steps.
The orchestrator's tools:
| Tool | What it is | What it does |
|---|---|---|
research_subtopic | The sub-researcher agent, exposed as an AgentTool | Researches a single sub-question in an isolated context and returns a compressed, cited summary. Only that summary is shown to the orchestrator; the summary is also appended to notes. |
think_tool | A no-op reflection tool | Lets the orchestrator pause to plan sub-questions and assess coverage between rounds. |
Sub-researcher
The sub-researcher is a reusable agent that answers a single sub-question. The orchestrator runs it many times in parallel, each in its own isolated context. This is the key idea: each sub-researcher processes the raw search results privately and returns only a concise summary, so the orchestrator's context stays small and the final report stays coherent.
- Job: search the web, optionally read promising pages, reflect, then write a compressed summary with inline citations to the exact source URLs.
- Returns: its final text message is the summary (it exits as soon as it writes plain text).
- Bounded by:
max_researcher_steps.
The sub-researcher's tools:
| Tool | What it is | What it does |
|---|---|---|
web_search | TavilyWebSearchTool from the Tavily integration by default, or the search_tool you pass | Runs a web search and returns the top results as title, exact URL, and snippet. |
read_url | PipelineTool over a fetch, route, convert-to-text, and summarize pipeline | Fetches a page (LinkContentFetcher), routes by MIME type (FileTypeRouter) to HTMLToDocument (Trafilatura) or PyPDFToDocument so PDFs are parsed too, and summarizes the page toward a question the agent passes, so only the relevant text enters the agent's context, not the full page. Used only when a search snippet is too shallow. |
think_tool | A no-op reflection tool | "What did I learn? What's missing? Stop or continue?" between searches. |
Context management
The core challenge in a deep research agent is keeping each context window small and focused. Raw web content (search results, full pages, PDFs) is large and noisy. If it all accumulated in a single context, the model's output quality would degrade. We avoid that with isolation and compression:
- Each sub-researcher runs as its own agent with its own
State, so all the messy intermediate content (every search result, every fetched page) stays in its private context. - It finishes by writing one short summary (its final message). Only that summary leaves the sub-researcher: the raw content never reaches the orchestrator or the writer.
The AgentTool default output handling and one setting on research_subtopic decide where that summary goes:
| Behavior or setting | Controls | Effect |
|---|---|---|
AgentTool default output handling | What the orchestrator's LLM sees as the tool result | The text of the sub-researcher's final reply comes back by default, not its full message history. Keeps the orchestrator's context clean. |
outputs_to_state={"notes": {...}} | What gets saved for the writer | The same summary is appended (as text) to the shared notes list, which becomes the writer's input. |
So each summary travels two ways, into the orchestrator's reasoning (so it can decide whether to dig further) and into the notes accumulator (so the writer can use it), while the bulky raw research stays isolated and is not propagated beyond the sub-researcher:
sub-researcher (private context: searches, pages, reflections)
│ writes one short summary
├─ AgentTool default output → orchestrator's LLM (decide: done, or dig more?)
└─ outputs_to_state → notes → writer (final report)