Skip to content

Commit 5df0b9c

Browse files
committed
feat(multi-agent): introduce Graph orchestrator
1 parent f20a405 commit 5df0b9c

File tree

12 files changed

+1378
-36
lines changed

12 files changed

+1378
-36
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ __pycache__*
88
.ruff_cache
99
*.bak
1010
.vscode
11-
dist
11+
dist
12+
repl_state

pyproject.toml

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ dev = [
5757
"pre-commit>=3.2.0,<4.2.0",
5858
"pytest>=8.0.0,<9.0.0",
5959
"pytest-asyncio>=0.26.0,<0.27.0",
60+
"pytest-cov>=4.1.0,<5.0.0",
61+
"pytest-xdist>=3.0.0,<4.0.0",
6062
"ruff>=0.4.4,<0.5.0",
6163
]
6264
docs = [
@@ -89,13 +91,59 @@ a2a = [
8991
"fastapi>=0.115.12",
9092
"starlette>=0.46.2",
9193
]
94+
all = [
95+
# anthropic
96+
"anthropic>=0.21.0,<1.0.0",
97+
98+
# dev
99+
"commitizen>=4.4.0,<5.0.0",
100+
"hatch>=1.0.0,<2.0.0",
101+
"moto>=5.1.0,<6.0.0",
102+
"mypy>=1.15.0,<2.0.0",
103+
"pre-commit>=3.2.0,<4.2.0",
104+
"pytest>=8.0.0,<9.0.0",
105+
"pytest-asyncio>=0.26.0,<0.27.0",
106+
"pytest-cov>=4.1.0,<5.0.0",
107+
"pytest-xdist>=3.0.0,<4.0.0",
108+
"ruff>=0.4.4,<0.5.0",
109+
110+
# docs
111+
"sphinx>=5.0.0,<6.0.0",
112+
"sphinx-rtd-theme>=1.0.0,<2.0.0",
113+
"sphinx-autodoc-typehints>=1.12.0,<2.0.0",
114+
115+
# litellm
116+
"litellm>=1.72.6,<1.73.0",
117+
118+
# llama
119+
"llama-api-client>=0.1.0,<1.0.0",
120+
121+
# mistral
122+
"mistralai>=1.8.2",
123+
124+
# ollama
125+
"ollama>=0.4.8,<1.0.0",
126+
127+
# openai
128+
"openai>=1.68.0,<2.0.0",
129+
130+
# otel
131+
"opentelemetry-exporter-otlp-proto-http>=1.30.0,<2.0.0",
132+
133+
# a2a
134+
"a2a-sdk>=0.2.6",
135+
"uvicorn>=0.34.2",
136+
"httpx>=0.28.1",
137+
"fastapi>=0.115.12",
138+
"starlette>=0.46.2",
139+
]
92140

93141
[tool.hatch.version]
94142
# Tells Hatch to use your version control system (git) to determine the version.
95143
source = "vcs"
96144

97145
[tool.hatch.envs.hatch-static-analysis]
98-
features = ["anthropic", "litellm", "llamaapi", "ollama", "openai", "otel","mistral"]
146+
features = ["anthropic", "litellm", "llamaapi", "ollama", "openai", "otel", "mistral", "a2a"]
99147
dependencies = [
100148
"mypy>=1.15.0,<2.0.0",
101149
"ruff>=0.11.6,<0.12.0",
@@ -111,15 +159,14 @@ format-fix = [
111159
]
112160
lint-check = [
113161
"ruff check",
114-
# excluding due to A2A and OTEL http exporter dependency conflict
115-
"mypy -p src --exclude src/strands/multiagent"
162+
"mypy -p src"
116163
]
117164
lint-fix = [
118165
"ruff check --fix"
119166
]
120167

121168
[tool.hatch.envs.hatch-test]
122-
features = ["anthropic", "litellm", "llamaapi", "ollama", "openai", "otel","mistral"]
169+
features = ["anthropic", "litellm", "llamaapi", "ollama", "openai", "otel", "mistral", "a2a"]
123170
extra-dependencies = [
124171
"moto>=5.1.0,<6.0.0",
125172
"pytest>=8.0.0,<9.0.0",
@@ -135,35 +182,17 @@ extra-args = [
135182

136183
[tool.hatch.envs.dev]
137184
dev-mode = true
138-
features = ["dev", "docs", "anthropic", "litellm", "llamaapi", "ollama", "otel","mistral"]
139-
140-
[tool.hatch.envs.a2a]
141-
dev-mode = true
142-
features = ["dev", "docs", "anthropic", "litellm", "llamaapi", "ollama", "a2a"]
143-
144-
[tool.hatch.envs.a2a.scripts]
145-
run = [
146-
"pytest{env:HATCH_TEST_ARGS:} tests/multiagent/a2a {args}"
147-
]
148-
run-cov = [
149-
"pytest{env:HATCH_TEST_ARGS:} tests/multiagent/a2a --cov --cov-config=pyproject.toml {args}"
150-
]
151-
lint-check = [
152-
"ruff check",
153-
"mypy -p src/strands/multiagent/a2a"
154-
]
185+
features = ["dev", "docs", "anthropic", "litellm", "llamaapi", "ollama", "otel", "mistral"]
155186

156187
[[tool.hatch.envs.hatch-test.matrix]]
157188
python = ["3.13", "3.12", "3.11", "3.10"]
158189

159190
[tool.hatch.envs.hatch-test.scripts]
160191
run = [
161-
# excluding due to A2A and OTEL http exporter dependency conflict
162-
"pytest{env:HATCH_TEST_ARGS:} {args} --ignore=tests/multiagent/a2a"
192+
"pytest{env:HATCH_TEST_ARGS:} {args}"
163193
]
164194
run-cov = [
165-
# excluding due to A2A and OTEL http exporter dependency conflict
166-
"pytest{env:HATCH_TEST_ARGS:} --cov --cov-config=pyproject.toml {args} --ignore=tests/multiagent/a2a"
195+
"pytest{env:HATCH_TEST_ARGS:} --cov --cov-config=pyproject.toml {args}"
167196
]
168197

169198
cov-combine = []
@@ -198,10 +227,6 @@ prepare = [
198227
"hatch run test-lint",
199228
"hatch test --all"
200229
]
201-
test-a2a = [
202-
# required to run manually due to A2A and OTEL http exporter dependency conflict
203-
"hatch -e a2a run run {args}"
204-
]
205230

206231
[tool.mypy]
207232
python_version = "3.10"

src/strands/agent/agent.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import random
1616
from concurrent.futures import ThreadPoolExecutor
1717
from typing import Any, AsyncIterator, Callable, Generator, List, Mapping, Optional, Type, TypeVar, Union, cast
18+
from uuid import uuid4
1819

1920
from opentelemetry import trace
2021
from pydantic import BaseModel
@@ -191,6 +192,7 @@ def __init__(
191192
load_tools_from_directory: bool = True,
192193
trace_attributes: Optional[Mapping[str, AttributeValue]] = None,
193194
*,
195+
agent_id: Optional[str] = None,
194196
name: Optional[str] = None,
195197
description: Optional[str] = None,
196198
state: Optional[Union[AgentState, dict]] = None,
@@ -226,6 +228,8 @@ def __init__(
226228
load_tools_from_directory: Whether to load and automatically reload tools in the `./tools/` directory.
227229
Defaults to True.
228230
trace_attributes: Custom trace attributes to apply to the agent's trace span.
231+
agent_id: Optional ID for the agent, useful for multi-agent scenarios.
232+
If None, a UUID is generated.
229233
name: name of the Agent
230234
Defaults to None.
231235
description: description of what the Agent does
@@ -240,6 +244,9 @@ def __init__(
240244
self.messages = messages if messages is not None else []
241245

242246
self.system_prompt = system_prompt
247+
self.agent_id = agent_id or str(uuid4())
248+
self.name = name
249+
self.description = description
243250

244251
# If not provided, create a new PrintingCallbackHandler instance
245252
# If explicitly set to None, use null_callback_handler
@@ -305,8 +312,6 @@ def __init__(
305312
self.state = AgentState()
306313

307314
self.tool_caller = Agent.ToolCaller(self)
308-
self.name = name
309-
self.description = description
310315

311316
@property
312317
def tool(self) -> ToolCaller:

src/strands/agent/conversation_manager/sliding_window_conversation_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def apply_management(self, agent: "Agent") -> None:
7575

7676
if len(messages) <= self.window_size:
7777
logger.debug(
78-
"window_size=<%s>, message_count=<%s> | skipping context reduction", len(messages), self.window_size
78+
"message_count=<%s>, window_size=<%s> | skipping context reduction", len(messages), self.window_size
7979
)
8080
return
8181
self.reduce_context(agent)

src/strands/multiagent/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,13 @@
99
"""
1010

1111
from . import a2a
12+
from .base import MultiAgentBase, MultiAgentResult
13+
from .graph import GraphBuilder, GraphResult
1214

13-
__all__ = ["a2a"]
15+
__all__ = [
16+
"a2a",
17+
"GraphBuilder",
18+
"GraphResult",
19+
"MultiAgentBase",
20+
"MultiAgentResult",
21+
]

src/strands/multiagent/base.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Multi-Agent Base Class.
2+
3+
Provides minimal foundation for multi-agent patterns (Swarm, Graph).
4+
"""
5+
6+
from abc import ABC, abstractmethod
7+
from dataclasses import dataclass, field
8+
from typing import Any, Union
9+
10+
from ..agent import AgentResult
11+
from ..types.event_loop import Metrics, Usage
12+
13+
14+
@dataclass
15+
class NodeResult:
16+
"""Unified result from node execution - handles both Agent and nested MultiAgentBase results."""
17+
18+
# Core result data - single AgentResult or nested MultiAgentResult
19+
result: Union[AgentResult, "MultiAgentResult"]
20+
21+
# Execution metadata
22+
execution_time: int = 0
23+
status: Any = None
24+
25+
# Accumulated metrics from this node and all children
26+
accumulated_usage: Usage = field(default_factory=lambda: Usage(inputTokens=0, outputTokens=0, totalTokens=0))
27+
accumulated_metrics: Metrics = field(default_factory=lambda: Metrics(latencyMs=0))
28+
execution_count: int = 0
29+
30+
def get_agent_results(self) -> list[AgentResult]:
31+
"""Get all AgentResult objects from this node, flattened if nested."""
32+
if isinstance(self.result, AgentResult):
33+
return [self.result]
34+
else:
35+
# Flatten nested results from MultiAgentResult
36+
flattened = []
37+
for nested_node_result in self.result.results.values():
38+
flattened.extend(nested_node_result.get_agent_results())
39+
return flattened
40+
41+
42+
@dataclass
43+
class MultiAgentResult:
44+
"""Result from multi-agent execution with accumulated metrics."""
45+
46+
results: dict[str, NodeResult]
47+
accumulated_usage: Usage = field(default_factory=lambda: Usage(inputTokens=0, outputTokens=0, totalTokens=0))
48+
accumulated_metrics: Metrics = field(default_factory=lambda: Metrics(latencyMs=0))
49+
execution_count: int = 0
50+
execution_time: int = 0
51+
52+
53+
class MultiAgentBase(ABC):
54+
"""Base class for multi-agent helpers.
55+
56+
This class integrates with existing Strands Agent instances and provides
57+
multi-agent orchestration capabilities.
58+
"""
59+
60+
@abstractmethod
61+
# TODO: for task - multi-modal input (Message), list of messages
62+
async def execute(self, task: str) -> MultiAgentResult:
63+
"""Execute task."""
64+
raise NotImplementedError("execute not implemented")
65+
66+
@abstractmethod
67+
# TODO: for task - multi-modal input (Message), list of messages
68+
async def resume(self, task: str, state: Any) -> MultiAgentResult:
69+
"""Resume task from previous state."""
70+
raise NotImplementedError("resume not implemented")

0 commit comments

Comments
 (0)