Skip to content
2 changes: 1 addition & 1 deletion docs-website/docs/concepts/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Key capabilities include:
- **State management**: Share typed data between tools, accumulate results across iterations, and surface them in the result dict using `state_schema`. See [State](../pipeline-components/agents-1/state.mdx).
- **Streaming**: Stream token-by-token output with a `streaming_callback`.
- **Human-in-the-loop**: Intercept tool calls for human review before execution. See [Human in the Loop](../pipeline-components/agents-1/human-in-the-loop.mdx).
- **Multi-agent systems**: Wrap an `Agent` as a `ComponentTool` to build coordinator/specialist architectures. See [Multi-Agent Systems](../pipeline-components/agents-1/agent.mdx#multi-agent-systems).
- **Multi-agent systems**: Wrap an `Agent` as a `ComponentTool` to build coordinator/specialist architectures. See [Multi-Agent Systems](./agents/multi-agent-systems.mdx).

Check out the [Agent](../pipeline-components/agents-1/agent.mdx) documentation, or the [example](#tool-calling-agent) below to get started.

Expand Down
219 changes: 219 additions & 0 deletions docs-website/docs/concepts/agents/multi-agent-systems.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
---
title: "Multi-Agent Systems"
id: multi-agent-systems
slug: "/multi-agent-systems"
description: "Learn how to build multi-agent systems in Haystack by spawning agents as tools. Use the @tool decorator or ComponentTool to connect specialist agents to a coordinator."
---

# Multi-Agent Systems

Multi-agent systems let you compose multiple `Agent` instances into larger architectures where a **coordinator** agent delegates to **specialist** agents.
Each specialist focuses on a specific task with its own tools and system prompt - the coordinator plans and routes work without needing to know how each task gets done.

Spawning agents as tools is useful when:

- A task is too broad for a single agent to handle reliably,
- You want to isolate different capabilities into focused, reusable agents,
- You need to keep the coordinator's context lean for better decisions and lower token usage.

In Haystack, you spawn a specialist agent as a tool using either the `@tool` decorator (recommended) or `ComponentTool`.

## Converting an Agent to a Tool

### `@tool` Decorator (Recommended)

Wrapping an agent inside a `@tool` function gives you full control over what the coordinator LLM sees:

- **Simplified parameters**: define explicit `Annotated` arguments instead of exposing `agent.run()`'s full interface
- **Formatted output**: extract and return only what the coordinator needs, rather than the full result dict
- **Error handling**: catch exceptions and return a clean message so the coordinator can recover

This approach works better with smaller LLMs because the tool has a clean, minimal signature.
The coordinator only needs to provide a query string - all the `ChatMessage` construction and result unpacking is hidden inside the function.

```python
from typing import Annotated
from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.generators.utils import print_streaming_chunk
from haystack.dataclasses import ChatMessage
from haystack.tools import ComponentTool, tool
from haystack.components.websearch import SerperDevWebSearch
from haystack.utils import Secret


research_agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[
ComponentTool(
component=SerperDevWebSearch(
api_key=Secret.from_env_var("SERPERDEV_API_KEY"),
top_k=3,
),
name="web_search",
description="Search the web for current information on any topic",
),
],
system_prompt="You are a research specialist. Search the web to find information.",
)


@tool
def research(query: Annotated[str, "The research question to investigate"]) -> str:
"""Research a topic and return a summary of findings."""
try:
result = research_agent.run(messages=[ChatMessage.from_user(query)])
return result["last_message"].text
except Exception as e:
return f"Research failed: {e}"


coordinator = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[research],
system_prompt="You are a coordinator. Delegate research tasks to the research tool.",
streaming_callback=print_streaming_chunk,
)

result = coordinator.run(
messages=[
ChatMessage.from_user("What are the latest developments in Haystack AI?"),
],
)
```

### `ComponentTool`

`ComponentTool` wraps an agent directly without a wrapper function.
Choose it when you want **declarative configuration**: the full specialist setup (model, tools, system prompt) lives in one serializable object alongside the coordinator.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about listing a declarative configuration example here too? A YAML file showing a Multi Agent System configured with ComponentTool? I suggest we add it but maybe you already thought about that and have a good reason against it?
I'd argue that it's particularly useful to show a YAML here because the recommended way differs for Python code (@tool decorator) and YAML (ComponentTool) here. Might be good for LLMs that look into our docs too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a yaml example


Use `outputs_to_string={"source": "last_message"}` to surface only the specialist's final reply to the coordinator rather than the full result dict.

```python
from haystack.tools import ComponentTool

research_tool = ComponentTool(
component=research_agent,
name="research_specialist",
description="A specialist that researches topics on the web",
outputs_to_string={"source": "last_message"},
)

coordinator = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[research_tool],
system_prompt="You are a coordinator. Delegate research tasks to the research specialist.",
streaming_callback=print_streaming_chunk,
)
Comment thread
sjrl marked this conversation as resolved.
```

## Coordinator / Specialist Pattern

The coordinator/specialist pattern cleanly splits responsibilities: the coordinator handles planning and delegation, while each specialist owns a focused toolset and a targeted system prompt.

This is also a form of **context engineering**: deliberately controlling what each agent sees.
A specialist accumulates its own tool call trace, but the coordinator only needs the final answer.
Returning just `result["last_message"].text` (with `@tool`) or using `outputs_to_string` (with `ComponentTool`) surfaces only the specialist's final reply, keeping the coordinator's context lean.

When covering multiple topics, the coordinator can call the same specialist tool several times in a single response.
All tool calls from one LLM response are executed concurrently using a thread pool.
Control the level of parallelism with `max_workers` in `tool_invoker_kwargs` (default: `4`).

The example below asks the coordinator about two topics: it calls `research` twice and both specialists run in parallel.

`HTMLToDocument` uses [Trafilatura](https://trafilatura.readthedocs.io) to extract clean text from HTML pages.
Install it before running:

```shell
pip install trafilatura
```

```python
from typing import Annotated
from haystack.components.agents import Agent
from haystack.components.converters import HTMLToDocument
from haystack.components.fetchers.link_content import LinkContentFetcher
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.generators.utils import print_streaming_chunk
from haystack.components.websearch import SerperDevWebSearch
from haystack.dataclasses import ChatMessage
from haystack.tools import ComponentTool, tool
from haystack.utils import Secret


search_tool = ComponentTool(
component=SerperDevWebSearch(
api_key=Secret.from_env_var("SERPERDEV_API_KEY"),
top_k=3,
),
name="web_search",
description="Search the web for current information on any topic",
)


@tool
def fetch_page(url: Annotated[str, "The URL of the web page to fetch"]) -> str:
"""Fetch the content of a web page given its URL."""
try:
streams = LinkContentFetcher().run(urls=[url])["streams"]
if not streams:
return "No content found."
documents = HTMLToDocument().run(sources=streams)["documents"]
return documents[0].content if documents else "No content extracted."
except Exception as e:
return f"Failed to fetch page: {e}"


research_agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[search_tool, fetch_page],
system_prompt=(
"You are a research specialist. Search the web to find relevant pages, "
"then fetch their full content for detailed information. "
"Return a concise summary of your findings in 3-5 sentences."
),
)


@tool
def research(query: Annotated[str, "The research question to investigate"]) -> str:
"""Research a topic and return a summary of findings."""
try:
result = research_agent.run(messages=[ChatMessage.from_user(query)])
return result["last_message"].text
except Exception as e:
return f"Research failed: {e}"


coordinator = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[research],
system_prompt=(
"You are a coordinator. Delegate research tasks to the research tool. "
"For questions covering multiple topics, research each one independently. "
"Keep your final answer concise."
),
streaming_callback=print_streaming_chunk,
tool_invoker_kwargs={"max_workers": 4}, # run up to 4 specialist calls in parallel
)

result = coordinator.run(
messages=[
ChatMessage.from_user(
"What are the latest developments in large language models and retrieval-augmented generation?",
),
],
)
```

## Additional References

📖 Related docs:

- [Agent](../../pipeline-components/agents-1/agent.mdx)
- [State](../../pipeline-components/agents-1/state.mdx)
- [ComponentTool](../../tools/componenttool.mdx)

📚 Tutorials:

- [Creating a Multi-Agent System](https://haystack.deepset.ai/tutorials/45_creating_a_multi_agent_system)
85 changes: 2 additions & 83 deletions docs-website/docs/pipeline-components/agents-1/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -336,90 +336,9 @@ Write a custom callback only if you need a specific transport (for example, SSE/

## Multi-Agent Systems

You can wrap an `Agent` using [`ComponentTool`](../../tools/componenttool.mdx) to build multi-agent systems where specialized agents act as tools for a coordinator agent.
You can wrap an `Agent` as a tool to build multi-agent systems where specialist agents handle focused subtasks and a coordinator agent plans and delegates.

This pattern is useful when a task is too broad or complex for a single agent to handle well.
Instead of giving one agent a large toolset and hoping it makes good decisions, you can decompose the problem:
a **coordinator** agent handles planning and delegation, while **specialist** agents each own a focused set of tools and a targeted system prompt.

This is also a form of **context engineering** — deliberately controlling what each agent sees.
A specialist accumulates its own tool call trace as it works, but the coordinator only needs the final answer.
By using `outputs_to_string={"source": "last_message"}` when wrapping a specialist as a `ComponentTool`, you surface only its final reply to the coordinator rather than forwarding the full tool call trace.
This keeps the coordinator's context lean and focused, which leads to better decisions and lower token usage as the conversation grows.

```python
from typing import Annotated
from haystack.components.agents import Agent
from haystack.components.fetchers.link_content import LinkContentFetcher
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.generators.utils import print_streaming_chunk
from haystack.components.websearch import SerperDevWebSearch
from haystack.dataclasses import ChatMessage
from haystack.tools import ComponentTool, tool
from haystack.utils import Secret

# Create the specialist agent with web search and page fetching tools

# Option 1: ComponentTool — wrap a component directly (good for straightforward cases)
search_tool = ComponentTool(
component=SerperDevWebSearch(
api_key=Secret.from_env_var("SERPERDEV_API_KEY"),
top_k=3,
),
name="web_search",
description="Search the web for current information on any topic",
)


# Option 2: @tool decorator — wrap a component inside a function for a simpler
# signature, built-in error handling, and custom result formatting
@tool
def fetch_page(url: Annotated[str, "The URL of the web page to fetch"]) -> str:
"""Fetch the full content of a web page given its URL."""
try:
streams = LinkContentFetcher().run(urls=[url])["streams"]
return (
streams[0].data.decode("utf-8", errors="replace")
if streams
else "No content found."
)
except Exception as e:
return f"Failed to fetch page: {e}"


research_agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[search_tool, fetch_page],
system_prompt=(
"You are a research specialist. Search the web to find relevant pages, "
"then fetch their full content for detailed information."
),
)

# Wrap the specialist agent as a tool for the coordinator
research_tool = ComponentTool(
component=research_agent,
name="research_specialist",
description="A specialist that researches topics on the web",
outputs_to_string={"source": "last_message"}, # surface only the final reply
)

# Create the coordinator agent with streaming
coordinator_agent = Agent(
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
tools=[research_tool],
system_prompt="You are a coordinator. Delegate research tasks to the research specialist.",
streaming_callback=print_streaming_chunk,
)

result = coordinator_agent.run(
messages=[
ChatMessage.from_user("What are the latest developments in Haystack AI?"),
],
)

print(result["last_message"].text)
```
See [Multi-Agent Systems](../../concepts/agents/multi-agent-systems.mdx) for a full guide, including the recommended `@tool` decorator approach for full interface control and `ComponentTool` for declarative configuration.

## Additional References

Expand Down
4 changes: 4 additions & 0 deletions docs-website/docs/tools/componenttool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ print(result)

## Additional References

📖 Related docs:

- [Multi-Agent Systems](../concepts/agents/multi-agent-systems.mdx)

📚 Tutorials:

- [Build a Tool-Calling Agent](https://haystack.deepset.ai/tutorials/43_building_a_tool_calling_agent)
Expand Down
12 changes: 11 additions & 1 deletion docs-website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ export default {
label: 'Haystack Concepts',
items: [
'concepts/concepts-overview',
'concepts/agents',
{
type: 'category',
label: 'Agents',
link: {
type: 'doc',
id: 'concepts/agents'
},
items: [
'concepts/agents/multi-agent-systems',
],
},
{
type: 'category',
label: 'Components',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Key capabilities include:
- **State management**: Share typed data between tools, accumulate results across iterations, and surface them in the result dict using `state_schema`. See [State](../pipeline-components/agents-1/state.mdx).
- **Streaming**: Stream token-by-token output with a `streaming_callback`.
- **Human-in-the-loop**: Intercept tool calls for human review before execution. See [Human in the Loop](../pipeline-components/agents-1/human-in-the-loop.mdx).
- **Multi-agent systems**: Wrap an `Agent` as a `ComponentTool` to build coordinator/specialist architectures. See [Multi-Agent Systems](../pipeline-components/agents-1/agent.mdx#multi-agent-systems).
- **Multi-agent systems**: Wrap an `Agent` as a `ComponentTool` to build coordinator/specialist architectures. See [Multi-Agent Systems](./agents/multi-agent-systems.mdx).

Check out the [Agent](../pipeline-components/agents-1/agent.mdx) documentation, or the [example](#tool-calling-agent) below to get started.

Expand Down
Loading