agentrun-sdk 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agentrun-sdk might be problematic. Click here for more details.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- agentrun_wrapper/tools/code_interpreter_client.py +186 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""Swarm Multi-Agent Pattern Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides a collaborative agent orchestration system where
|
|
4
|
+
agents work together as a team to solve complex tasks, with shared context
|
|
5
|
+
and autonomous coordination.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Self-organizing agent teams with shared working memory
|
|
9
|
+
- Tool-based coordination
|
|
10
|
+
- Autonomous agent collaboration without central control
|
|
11
|
+
- Dynamic task distribution based on agent capabilities
|
|
12
|
+
- Collective intelligence through shared context
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import copy
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Callable, Tuple
|
|
23
|
+
|
|
24
|
+
from opentelemetry import trace as trace_api
|
|
25
|
+
|
|
26
|
+
from ..agent import Agent, AgentResult
|
|
27
|
+
from ..agent.state import AgentState
|
|
28
|
+
from ..telemetry import get_tracer
|
|
29
|
+
from ..tools.decorator import tool
|
|
30
|
+
from ..types.content import ContentBlock, Messages
|
|
31
|
+
from ..types.event_loop import Metrics, Usage
|
|
32
|
+
from .base import MultiAgentBase, MultiAgentResult, NodeResult, Status
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class SwarmNode:
|
|
39
|
+
"""Represents a node (e.g. Agent) in the swarm."""
|
|
40
|
+
|
|
41
|
+
node_id: str
|
|
42
|
+
executor: Agent
|
|
43
|
+
_initial_messages: Messages = field(default_factory=list, init=False)
|
|
44
|
+
_initial_state: AgentState = field(default_factory=AgentState, init=False)
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
"""Capture initial executor state after initialization."""
|
|
48
|
+
# Deep copy the initial messages and state to preserve them
|
|
49
|
+
self._initial_messages = copy.deepcopy(self.executor.messages)
|
|
50
|
+
self._initial_state = AgentState(self.executor.state.get())
|
|
51
|
+
|
|
52
|
+
def __hash__(self) -> int:
|
|
53
|
+
"""Return hash for SwarmNode based on node_id."""
|
|
54
|
+
return hash(self.node_id)
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other: Any) -> bool:
|
|
57
|
+
"""Return equality for SwarmNode based on node_id."""
|
|
58
|
+
if not isinstance(other, SwarmNode):
|
|
59
|
+
return False
|
|
60
|
+
return self.node_id == other.node_id
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
"""Return string representation of SwarmNode."""
|
|
64
|
+
return self.node_id
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
"""Return detailed representation of SwarmNode."""
|
|
68
|
+
return f"SwarmNode(node_id='{self.node_id}')"
|
|
69
|
+
|
|
70
|
+
def reset_executor_state(self) -> None:
|
|
71
|
+
"""Reset SwarmNode executor state to initial state when swarm was created."""
|
|
72
|
+
self.executor.messages = copy.deepcopy(self._initial_messages)
|
|
73
|
+
self.executor.state = AgentState(self._initial_state.get())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class SharedContext:
|
|
78
|
+
"""Shared context between swarm nodes."""
|
|
79
|
+
|
|
80
|
+
context: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
def add_context(self, node: SwarmNode, key: str, value: Any) -> None:
|
|
83
|
+
"""Add context."""
|
|
84
|
+
self._validate_key(key)
|
|
85
|
+
self._validate_json_serializable(value)
|
|
86
|
+
|
|
87
|
+
if node.node_id not in self.context:
|
|
88
|
+
self.context[node.node_id] = {}
|
|
89
|
+
self.context[node.node_id][key] = value
|
|
90
|
+
|
|
91
|
+
def _validate_key(self, key: str) -> None:
|
|
92
|
+
"""Validate that a key is valid.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
key: The key to validate
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If key is invalid
|
|
99
|
+
"""
|
|
100
|
+
if key is None:
|
|
101
|
+
raise ValueError("Key cannot be None")
|
|
102
|
+
if not isinstance(key, str):
|
|
103
|
+
raise ValueError("Key must be a string")
|
|
104
|
+
if not key.strip():
|
|
105
|
+
raise ValueError("Key cannot be empty")
|
|
106
|
+
|
|
107
|
+
def _validate_json_serializable(self, value: Any) -> None:
|
|
108
|
+
"""Validate that a value is JSON serializable.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
value: The value to validate
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If value is not JSON serializable
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
json.dumps(value)
|
|
118
|
+
except (TypeError, ValueError) as e:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Value is not JSON serializable: {type(value).__name__}. "
|
|
121
|
+
f"Only JSON-compatible types (str, int, float, bool, list, dict, None) are allowed."
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class SwarmState:
|
|
127
|
+
"""Current state of swarm execution."""
|
|
128
|
+
|
|
129
|
+
current_node: SwarmNode # The agent currently executing
|
|
130
|
+
task: str | list[ContentBlock] # The original task from the user that is being executed
|
|
131
|
+
completion_status: Status = Status.PENDING # Current swarm execution status
|
|
132
|
+
shared_context: SharedContext = field(default_factory=SharedContext) # Context shared between agents
|
|
133
|
+
node_history: list[SwarmNode] = field(default_factory=list) # Complete history of agents that have executed
|
|
134
|
+
start_time: float = field(default_factory=time.time) # When swarm execution began
|
|
135
|
+
results: dict[str, NodeResult] = field(default_factory=dict) # Results from each agent execution
|
|
136
|
+
# Total token usage across all agents
|
|
137
|
+
accumulated_usage: Usage = field(default_factory=lambda: Usage(inputTokens=0, outputTokens=0, totalTokens=0))
|
|
138
|
+
# Total metrics across all agents
|
|
139
|
+
accumulated_metrics: Metrics = field(default_factory=lambda: Metrics(latencyMs=0))
|
|
140
|
+
execution_time: int = 0 # Total execution time in milliseconds
|
|
141
|
+
handoff_message: str | None = None # Message passed during agent handoff
|
|
142
|
+
|
|
143
|
+
def should_continue(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
max_handoffs: int,
|
|
147
|
+
max_iterations: int,
|
|
148
|
+
execution_timeout: float,
|
|
149
|
+
repetitive_handoff_detection_window: int,
|
|
150
|
+
repetitive_handoff_min_unique_agents: int,
|
|
151
|
+
) -> Tuple[bool, str]:
|
|
152
|
+
"""Check if the swarm should continue.
|
|
153
|
+
|
|
154
|
+
Returns: (should_continue, reason)
|
|
155
|
+
"""
|
|
156
|
+
# Check handoff limit
|
|
157
|
+
if len(self.node_history) >= max_handoffs:
|
|
158
|
+
return False, f"Max handoffs reached: {max_handoffs}"
|
|
159
|
+
|
|
160
|
+
# Check iteration limit
|
|
161
|
+
if len(self.node_history) >= max_iterations:
|
|
162
|
+
return False, f"Max iterations reached: {max_iterations}"
|
|
163
|
+
|
|
164
|
+
# Check timeout
|
|
165
|
+
elapsed = time.time() - self.start_time
|
|
166
|
+
if elapsed > execution_timeout:
|
|
167
|
+
return False, f"Execution timed out: {execution_timeout}s"
|
|
168
|
+
|
|
169
|
+
# Check for repetitive handoffs (agents passing back and forth)
|
|
170
|
+
if repetitive_handoff_detection_window > 0 and len(self.node_history) >= repetitive_handoff_detection_window:
|
|
171
|
+
recent = self.node_history[-repetitive_handoff_detection_window:]
|
|
172
|
+
unique_nodes = len(set(recent))
|
|
173
|
+
if unique_nodes < repetitive_handoff_min_unique_agents:
|
|
174
|
+
return (
|
|
175
|
+
False,
|
|
176
|
+
(
|
|
177
|
+
f"Repetitive handoff: {unique_nodes} unique nodes "
|
|
178
|
+
f"out of {repetitive_handoff_detection_window} recent iterations"
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return True, "Continuing"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class SwarmResult(MultiAgentResult):
|
|
187
|
+
"""Result from swarm execution - extends MultiAgentResult with swarm-specific details."""
|
|
188
|
+
|
|
189
|
+
node_history: list[SwarmNode] = field(default_factory=list)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Swarm(MultiAgentBase):
|
|
193
|
+
"""Self-organizing collaborative agent teams with shared working memory."""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
nodes: list[Agent],
|
|
198
|
+
*,
|
|
199
|
+
max_handoffs: int = 20,
|
|
200
|
+
max_iterations: int = 20,
|
|
201
|
+
execution_timeout: float = 900.0,
|
|
202
|
+
node_timeout: float = 300.0,
|
|
203
|
+
repetitive_handoff_detection_window: int = 0,
|
|
204
|
+
repetitive_handoff_min_unique_agents: int = 0,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Initialize Swarm with agents and configuration.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
nodes: List of nodes (e.g. Agent) to include in the swarm
|
|
210
|
+
max_handoffs: Maximum handoffs to agents and users (default: 20)
|
|
211
|
+
max_iterations: Maximum node executions within the swarm (default: 20)
|
|
212
|
+
execution_timeout: Total execution timeout in seconds (default: 900.0)
|
|
213
|
+
node_timeout: Individual node timeout in seconds (default: 300.0)
|
|
214
|
+
repetitive_handoff_detection_window: Number of recent nodes to check for repetitive handoffs
|
|
215
|
+
Disabled by default (default: 0)
|
|
216
|
+
repetitive_handoff_min_unique_agents: Minimum unique agents required in recent sequence
|
|
217
|
+
Disabled by default (default: 0)
|
|
218
|
+
"""
|
|
219
|
+
super().__init__()
|
|
220
|
+
|
|
221
|
+
self.max_handoffs = max_handoffs
|
|
222
|
+
self.max_iterations = max_iterations
|
|
223
|
+
self.execution_timeout = execution_timeout
|
|
224
|
+
self.node_timeout = node_timeout
|
|
225
|
+
self.repetitive_handoff_detection_window = repetitive_handoff_detection_window
|
|
226
|
+
self.repetitive_handoff_min_unique_agents = repetitive_handoff_min_unique_agents
|
|
227
|
+
|
|
228
|
+
self.shared_context = SharedContext()
|
|
229
|
+
self.nodes: dict[str, SwarmNode] = {}
|
|
230
|
+
self.state = SwarmState(
|
|
231
|
+
current_node=SwarmNode("", Agent()), # Placeholder, will be set properly
|
|
232
|
+
task="",
|
|
233
|
+
completion_status=Status.PENDING,
|
|
234
|
+
)
|
|
235
|
+
self.tracer = get_tracer()
|
|
236
|
+
|
|
237
|
+
self._setup_swarm(nodes)
|
|
238
|
+
self._inject_swarm_tools()
|
|
239
|
+
|
|
240
|
+
def __call__(self, task: str | list[ContentBlock], **kwargs: Any) -> SwarmResult:
|
|
241
|
+
"""Invoke the swarm synchronously."""
|
|
242
|
+
|
|
243
|
+
def execute() -> SwarmResult:
|
|
244
|
+
return asyncio.run(self.invoke_async(task))
|
|
245
|
+
|
|
246
|
+
with ThreadPoolExecutor() as executor:
|
|
247
|
+
future = executor.submit(execute)
|
|
248
|
+
return future.result()
|
|
249
|
+
|
|
250
|
+
async def invoke_async(self, task: str | list[ContentBlock], **kwargs: Any) -> SwarmResult:
|
|
251
|
+
"""Invoke the swarm asynchronously."""
|
|
252
|
+
logger.debug("starting swarm execution")
|
|
253
|
+
|
|
254
|
+
# Initialize swarm state with configuration
|
|
255
|
+
initial_node = next(iter(self.nodes.values())) # First SwarmNode
|
|
256
|
+
self.state = SwarmState(
|
|
257
|
+
current_node=initial_node,
|
|
258
|
+
task=task,
|
|
259
|
+
completion_status=Status.EXECUTING,
|
|
260
|
+
shared_context=self.shared_context,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
start_time = time.time()
|
|
264
|
+
span = self.tracer.start_multiagent_span(task, "swarm")
|
|
265
|
+
with trace_api.use_span(span, end_on_exit=True):
|
|
266
|
+
try:
|
|
267
|
+
logger.debug("current_node=<%s> | starting swarm execution with node", self.state.current_node.node_id)
|
|
268
|
+
logger.debug(
|
|
269
|
+
"max_handoffs=<%d>, max_iterations=<%d>, timeout=<%s>s | swarm execution config",
|
|
270
|
+
self.max_handoffs,
|
|
271
|
+
self.max_iterations,
|
|
272
|
+
self.execution_timeout,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
await self._execute_swarm()
|
|
276
|
+
except Exception:
|
|
277
|
+
logger.exception("swarm execution failed")
|
|
278
|
+
self.state.completion_status = Status.FAILED
|
|
279
|
+
raise
|
|
280
|
+
finally:
|
|
281
|
+
self.state.execution_time = round((time.time() - start_time) * 1000)
|
|
282
|
+
|
|
283
|
+
return self._build_result()
|
|
284
|
+
|
|
285
|
+
def _setup_swarm(self, nodes: list[Agent]) -> None:
|
|
286
|
+
"""Initialize swarm configuration."""
|
|
287
|
+
# Validate nodes before setup
|
|
288
|
+
self._validate_swarm(nodes)
|
|
289
|
+
|
|
290
|
+
# Validate agents have names and create SwarmNode objects
|
|
291
|
+
for i, node in enumerate(nodes):
|
|
292
|
+
if not node.name:
|
|
293
|
+
node_id = f"node_{i}"
|
|
294
|
+
node.name = node_id
|
|
295
|
+
logger.debug("node_id=<%s> | agent has no name, dynamically generating one", node_id)
|
|
296
|
+
|
|
297
|
+
node_id = str(node.name)
|
|
298
|
+
|
|
299
|
+
# Ensure node IDs are unique
|
|
300
|
+
if node_id in self.nodes:
|
|
301
|
+
raise ValueError(f"Node ID '{node_id}' is not unique. Each agent must have a unique name.")
|
|
302
|
+
|
|
303
|
+
self.nodes[node_id] = SwarmNode(node_id=node_id, executor=node)
|
|
304
|
+
|
|
305
|
+
swarm_nodes = list(self.nodes.values())
|
|
306
|
+
logger.debug("nodes=<%s> | initialized swarm with nodes", [node.node_id for node in swarm_nodes])
|
|
307
|
+
|
|
308
|
+
def _validate_swarm(self, nodes: list[Agent]) -> None:
|
|
309
|
+
"""Validate swarm structure and nodes."""
|
|
310
|
+
# Check for duplicate object instances
|
|
311
|
+
seen_instances = set()
|
|
312
|
+
for node in nodes:
|
|
313
|
+
if id(node) in seen_instances:
|
|
314
|
+
raise ValueError("Duplicate node instance detected. Each node must have a unique object instance.")
|
|
315
|
+
seen_instances.add(id(node))
|
|
316
|
+
|
|
317
|
+
# Check for session persistence
|
|
318
|
+
if node._session_manager is not None:
|
|
319
|
+
raise ValueError("Session persistence is not supported for Swarm agents yet.")
|
|
320
|
+
|
|
321
|
+
# Check for callbacks
|
|
322
|
+
if node.hooks.has_callbacks():
|
|
323
|
+
raise ValueError("Agent callbacks are not supported for Swarm agents yet.")
|
|
324
|
+
|
|
325
|
+
def _inject_swarm_tools(self) -> None:
|
|
326
|
+
"""Add swarm coordination tools to each agent."""
|
|
327
|
+
# Create tool functions with proper closures
|
|
328
|
+
swarm_tools = [
|
|
329
|
+
self._create_handoff_tool(),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
for node in self.nodes.values():
|
|
333
|
+
# Check for existing tools with conflicting names
|
|
334
|
+
existing_tools = node.executor.tool_registry.registry
|
|
335
|
+
conflicting_tools = []
|
|
336
|
+
|
|
337
|
+
if "handoff_to_agent" in existing_tools:
|
|
338
|
+
conflicting_tools.append("handoff_to_agent")
|
|
339
|
+
|
|
340
|
+
if conflicting_tools:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"Agent '{node.node_id}' already has tools with names that conflict with swarm coordination tools: "
|
|
343
|
+
f"{', '.join(conflicting_tools)}. Please rename these tools to avoid conflicts."
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Use the agent's tool registry to process and register the tools
|
|
347
|
+
node.executor.tool_registry.process_tools(swarm_tools)
|
|
348
|
+
|
|
349
|
+
logger.debug(
|
|
350
|
+
"tool_count=<%d>, node_count=<%d> | injected coordination tools into agents",
|
|
351
|
+
len(swarm_tools),
|
|
352
|
+
len(self.nodes),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def _create_handoff_tool(self) -> Callable[..., Any]:
|
|
356
|
+
"""Create handoff tool for agent coordination."""
|
|
357
|
+
swarm_ref = self # Capture swarm reference
|
|
358
|
+
|
|
359
|
+
@tool
|
|
360
|
+
def handoff_to_agent(agent_name: str, message: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
361
|
+
"""Transfer control to another agent in the swarm for specialized help.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
agent_name: Name of the agent to hand off to
|
|
365
|
+
message: Message explaining what needs to be done and why you're handing off
|
|
366
|
+
context: Additional context to share with the next agent
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Confirmation of handoff initiation
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
context = context or {}
|
|
373
|
+
|
|
374
|
+
# Validate target agent exists
|
|
375
|
+
target_node = swarm_ref.nodes.get(agent_name)
|
|
376
|
+
if not target_node:
|
|
377
|
+
return {"status": "error", "content": [{"text": f"Error: Agent '{agent_name}' not found in swarm"}]}
|
|
378
|
+
|
|
379
|
+
# Execute handoff
|
|
380
|
+
swarm_ref._handle_handoff(target_node, message, context)
|
|
381
|
+
|
|
382
|
+
return {"status": "success", "content": [{"text": f"Handed off to {agent_name}: {message}"}]}
|
|
383
|
+
except Exception as e:
|
|
384
|
+
return {"status": "error", "content": [{"text": f"Error in handoff: {str(e)}"}]}
|
|
385
|
+
|
|
386
|
+
return handoff_to_agent
|
|
387
|
+
|
|
388
|
+
def _handle_handoff(self, target_node: SwarmNode, message: str, context: dict[str, Any]) -> None:
|
|
389
|
+
"""Handle handoff to another agent."""
|
|
390
|
+
# If task is already completed, don't allow further handoffs
|
|
391
|
+
if self.state.completion_status != Status.EXECUTING:
|
|
392
|
+
logger.debug(
|
|
393
|
+
"task_status=<%s> | ignoring handoff request - task already completed",
|
|
394
|
+
self.state.completion_status,
|
|
395
|
+
)
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# Update swarm state
|
|
399
|
+
previous_agent = self.state.current_node
|
|
400
|
+
self.state.current_node = target_node
|
|
401
|
+
|
|
402
|
+
# Store handoff message for the target agent
|
|
403
|
+
self.state.handoff_message = message
|
|
404
|
+
|
|
405
|
+
# Store handoff context as shared context
|
|
406
|
+
if context:
|
|
407
|
+
for key, value in context.items():
|
|
408
|
+
self.shared_context.add_context(previous_agent, key, value)
|
|
409
|
+
|
|
410
|
+
logger.debug(
|
|
411
|
+
"from_node=<%s>, to_node=<%s> | handed off from agent to agent",
|
|
412
|
+
previous_agent.node_id,
|
|
413
|
+
target_node.node_id,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def _build_node_input(self, target_node: SwarmNode) -> str:
|
|
417
|
+
"""Build input text for a node based on shared context and handoffs.
|
|
418
|
+
|
|
419
|
+
Example formatted output:
|
|
420
|
+
```
|
|
421
|
+
Handoff Message: The user needs help with Python debugging - I've identified the issue but need someone with more expertise to fix it.
|
|
422
|
+
|
|
423
|
+
User Request: My Python script is throwing a KeyError when processing JSON data from an API
|
|
424
|
+
|
|
425
|
+
Previous agents who worked on this: data_analyst → code_reviewer
|
|
426
|
+
|
|
427
|
+
Shared knowledge from previous agents:
|
|
428
|
+
• data_analyst: {"issue_location": "line 42", "error_type": "missing key validation", "suggested_fix": "add key existence check"}
|
|
429
|
+
• code_reviewer: {"code_quality": "good overall structure", "security_notes": "API key should be in environment variable"}
|
|
430
|
+
|
|
431
|
+
Other agents available for collaboration:
|
|
432
|
+
Agent name: data_analyst. Agent description: Analyzes data and provides deeper insights
|
|
433
|
+
Agent name: code_reviewer.
|
|
434
|
+
Agent name: security_specialist. Agent description: Focuses on secure coding practices and vulnerability assessment
|
|
435
|
+
|
|
436
|
+
You have access to swarm coordination tools if you need help from other agents. If you don't hand off to another agent, the swarm will consider the task complete.
|
|
437
|
+
```
|
|
438
|
+
""" # noqa: E501
|
|
439
|
+
context_info: dict[str, Any] = {
|
|
440
|
+
"task": self.state.task,
|
|
441
|
+
"node_history": [node.node_id for node in self.state.node_history],
|
|
442
|
+
"shared_context": {k: v for k, v in self.shared_context.context.items()},
|
|
443
|
+
}
|
|
444
|
+
context_text = ""
|
|
445
|
+
|
|
446
|
+
# Include handoff message prominently at the top if present
|
|
447
|
+
if self.state.handoff_message:
|
|
448
|
+
context_text += f"Handoff Message: {self.state.handoff_message}\n\n"
|
|
449
|
+
|
|
450
|
+
# Include task information if available
|
|
451
|
+
if "task" in context_info:
|
|
452
|
+
task = context_info.get("task")
|
|
453
|
+
if isinstance(task, str):
|
|
454
|
+
context_text += f"User Request: {task}\n\n"
|
|
455
|
+
elif isinstance(task, list):
|
|
456
|
+
context_text += "User Request: Multi-modal task\n\n"
|
|
457
|
+
|
|
458
|
+
# Include detailed node history
|
|
459
|
+
if context_info.get("node_history"):
|
|
460
|
+
context_text += f"Previous agents who worked on this: {' → '.join(context_info['node_history'])}\n\n"
|
|
461
|
+
|
|
462
|
+
# Include actual shared context, not just a mention
|
|
463
|
+
shared_context = context_info.get("shared_context", {})
|
|
464
|
+
if shared_context:
|
|
465
|
+
context_text += "Shared knowledge from previous agents:\n"
|
|
466
|
+
for node_name, context in shared_context.items():
|
|
467
|
+
if context: # Only include if node has contributed context
|
|
468
|
+
context_text += f"• {node_name}: {context}\n"
|
|
469
|
+
context_text += "\n"
|
|
470
|
+
|
|
471
|
+
# Include available nodes with descriptions if available
|
|
472
|
+
other_nodes = [node_id for node_id in self.nodes.keys() if node_id != target_node.node_id]
|
|
473
|
+
if other_nodes:
|
|
474
|
+
context_text += "Other agents available for collaboration:\n"
|
|
475
|
+
for node_id in other_nodes:
|
|
476
|
+
node = self.nodes.get(node_id)
|
|
477
|
+
context_text += f"Agent name: {node_id}."
|
|
478
|
+
if node and hasattr(node.executor, "description") and node.executor.description:
|
|
479
|
+
context_text += f" Agent description: {node.executor.description}"
|
|
480
|
+
context_text += "\n"
|
|
481
|
+
context_text += "\n"
|
|
482
|
+
|
|
483
|
+
context_text += (
|
|
484
|
+
"You have access to swarm coordination tools if you need help from other agents. "
|
|
485
|
+
"If you don't hand off to another agent, the swarm will consider the task complete."
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
return context_text
|
|
489
|
+
|
|
490
|
+
async def _execute_swarm(self) -> None:
|
|
491
|
+
"""Shared execution logic used by execute_async."""
|
|
492
|
+
try:
|
|
493
|
+
# Main execution loop
|
|
494
|
+
while True:
|
|
495
|
+
if self.state.completion_status != Status.EXECUTING:
|
|
496
|
+
reason = f"Completion status is: {self.state.completion_status}"
|
|
497
|
+
logger.debug("reason=<%s> | stopping execution", reason)
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
should_continue, reason = self.state.should_continue(
|
|
501
|
+
max_handoffs=self.max_handoffs,
|
|
502
|
+
max_iterations=self.max_iterations,
|
|
503
|
+
execution_timeout=self.execution_timeout,
|
|
504
|
+
repetitive_handoff_detection_window=self.repetitive_handoff_detection_window,
|
|
505
|
+
repetitive_handoff_min_unique_agents=self.repetitive_handoff_min_unique_agents,
|
|
506
|
+
)
|
|
507
|
+
if not should_continue:
|
|
508
|
+
self.state.completion_status = Status.FAILED
|
|
509
|
+
logger.debug("reason=<%s> | stopping execution", reason)
|
|
510
|
+
break
|
|
511
|
+
|
|
512
|
+
# Get current node
|
|
513
|
+
current_node = self.state.current_node
|
|
514
|
+
if not current_node or current_node.node_id not in self.nodes:
|
|
515
|
+
logger.error("node=<%s> | node not found", current_node.node_id if current_node else "None")
|
|
516
|
+
self.state.completion_status = Status.FAILED
|
|
517
|
+
break
|
|
518
|
+
|
|
519
|
+
logger.debug(
|
|
520
|
+
"current_node=<%s>, iteration=<%d> | executing node",
|
|
521
|
+
current_node.node_id,
|
|
522
|
+
len(self.state.node_history) + 1,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Execute node with timeout protection
|
|
526
|
+
# TODO: Implement cancellation token to stop _execute_node from continuing
|
|
527
|
+
try:
|
|
528
|
+
await asyncio.wait_for(
|
|
529
|
+
self._execute_node(current_node, self.state.task),
|
|
530
|
+
timeout=self.node_timeout,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
self.state.node_history.append(current_node)
|
|
534
|
+
|
|
535
|
+
logger.debug("node=<%s> | node execution completed", current_node.node_id)
|
|
536
|
+
|
|
537
|
+
# Check if the current node is still the same after execution
|
|
538
|
+
# If it is, then no handoff occurred and we consider the swarm complete
|
|
539
|
+
if self.state.current_node == current_node:
|
|
540
|
+
logger.debug("node=<%s> | no handoff occurred, marking swarm as complete", current_node.node_id)
|
|
541
|
+
self.state.completion_status = Status.COMPLETED
|
|
542
|
+
break
|
|
543
|
+
|
|
544
|
+
except asyncio.TimeoutError:
|
|
545
|
+
logger.exception(
|
|
546
|
+
"node=<%s>, timeout=<%s>s | node execution timed out after timeout",
|
|
547
|
+
current_node.node_id,
|
|
548
|
+
self.node_timeout,
|
|
549
|
+
)
|
|
550
|
+
self.state.completion_status = Status.FAILED
|
|
551
|
+
break
|
|
552
|
+
|
|
553
|
+
except Exception:
|
|
554
|
+
logger.exception("node=<%s> | node execution failed", current_node.node_id)
|
|
555
|
+
self.state.completion_status = Status.FAILED
|
|
556
|
+
break
|
|
557
|
+
|
|
558
|
+
except Exception:
|
|
559
|
+
logger.exception("swarm execution failed")
|
|
560
|
+
self.state.completion_status = Status.FAILED
|
|
561
|
+
|
|
562
|
+
elapsed_time = time.time() - self.state.start_time
|
|
563
|
+
logger.debug("status=<%s> | swarm execution completed", self.state.completion_status)
|
|
564
|
+
logger.debug(
|
|
565
|
+
"node_history_length=<%d>, time=<%s>s | metrics",
|
|
566
|
+
len(self.state.node_history),
|
|
567
|
+
f"{elapsed_time:.2f}",
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
async def _execute_node(self, node: SwarmNode, task: str | list[ContentBlock]) -> AgentResult:
|
|
571
|
+
"""Execute swarm node."""
|
|
572
|
+
start_time = time.time()
|
|
573
|
+
node_name = node.node_id
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
# Prepare context for node
|
|
577
|
+
context_text = self._build_node_input(node)
|
|
578
|
+
node_input = [ContentBlock(text=f"Context:\n{context_text}\n\n")]
|
|
579
|
+
|
|
580
|
+
# Clear handoff message after it's been included in context
|
|
581
|
+
self.state.handoff_message = None
|
|
582
|
+
|
|
583
|
+
if not isinstance(task, str):
|
|
584
|
+
# Include additional ContentBlocks in node input
|
|
585
|
+
node_input = node_input + task
|
|
586
|
+
|
|
587
|
+
# Execute node
|
|
588
|
+
result = None
|
|
589
|
+
node.reset_executor_state()
|
|
590
|
+
result = await node.executor.invoke_async(node_input)
|
|
591
|
+
|
|
592
|
+
execution_time = round((time.time() - start_time) * 1000)
|
|
593
|
+
|
|
594
|
+
# Create NodeResult
|
|
595
|
+
usage = Usage(inputTokens=0, outputTokens=0, totalTokens=0)
|
|
596
|
+
metrics = Metrics(latencyMs=execution_time)
|
|
597
|
+
if hasattr(result, "metrics") and result.metrics:
|
|
598
|
+
if hasattr(result.metrics, "accumulated_usage"):
|
|
599
|
+
usage = result.metrics.accumulated_usage
|
|
600
|
+
if hasattr(result.metrics, "accumulated_metrics"):
|
|
601
|
+
metrics = result.metrics.accumulated_metrics
|
|
602
|
+
|
|
603
|
+
node_result = NodeResult(
|
|
604
|
+
result=result,
|
|
605
|
+
execution_time=execution_time,
|
|
606
|
+
status=Status.COMPLETED,
|
|
607
|
+
accumulated_usage=usage,
|
|
608
|
+
accumulated_metrics=metrics,
|
|
609
|
+
execution_count=1,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Store result in state
|
|
613
|
+
self.state.results[node_name] = node_result
|
|
614
|
+
|
|
615
|
+
# Accumulate metrics
|
|
616
|
+
self._accumulate_metrics(node_result)
|
|
617
|
+
|
|
618
|
+
return result
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
execution_time = round((time.time() - start_time) * 1000)
|
|
622
|
+
logger.exception("node=<%s> | node execution failed", node_name)
|
|
623
|
+
|
|
624
|
+
# Create a NodeResult for the failed node
|
|
625
|
+
node_result = NodeResult(
|
|
626
|
+
result=e, # Store exception as result
|
|
627
|
+
execution_time=execution_time,
|
|
628
|
+
status=Status.FAILED,
|
|
629
|
+
accumulated_usage=Usage(inputTokens=0, outputTokens=0, totalTokens=0),
|
|
630
|
+
accumulated_metrics=Metrics(latencyMs=execution_time),
|
|
631
|
+
execution_count=1,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
# Store result in state
|
|
635
|
+
self.state.results[node_name] = node_result
|
|
636
|
+
|
|
637
|
+
raise
|
|
638
|
+
|
|
639
|
+
def _accumulate_metrics(self, node_result: NodeResult) -> None:
|
|
640
|
+
"""Accumulate metrics from a node result."""
|
|
641
|
+
self.state.accumulated_usage["inputTokens"] += node_result.accumulated_usage.get("inputTokens", 0)
|
|
642
|
+
self.state.accumulated_usage["outputTokens"] += node_result.accumulated_usage.get("outputTokens", 0)
|
|
643
|
+
self.state.accumulated_usage["totalTokens"] += node_result.accumulated_usage.get("totalTokens", 0)
|
|
644
|
+
self.state.accumulated_metrics["latencyMs"] += node_result.accumulated_metrics.get("latencyMs", 0)
|
|
645
|
+
|
|
646
|
+
def _build_result(self) -> SwarmResult:
|
|
647
|
+
"""Build swarm result from current state."""
|
|
648
|
+
return SwarmResult(
|
|
649
|
+
status=self.state.completion_status,
|
|
650
|
+
results=self.state.results,
|
|
651
|
+
accumulated_usage=self.state.accumulated_usage,
|
|
652
|
+
accumulated_metrics=self.state.accumulated_metrics,
|
|
653
|
+
execution_count=len(self.state.node_history),
|
|
654
|
+
execution_time=self.state.execution_time,
|
|
655
|
+
node_history=self.state.node_history,
|
|
656
|
+
)
|
agentrun_sdk/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file that indicates this package supports typing
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Session module.
|
|
2
|
+
|
|
3
|
+
This module provides session management functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .file_session_manager import FileSessionManager
|
|
7
|
+
from .repository_session_manager import RepositorySessionManager
|
|
8
|
+
from .s3_session_manager import S3SessionManager
|
|
9
|
+
from .session_manager import SessionManager
|
|
10
|
+
from .session_repository import SessionRepository
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FileSessionManager",
|
|
14
|
+
"RepositorySessionManager",
|
|
15
|
+
"S3SessionManager",
|
|
16
|
+
"SessionManager",
|
|
17
|
+
"SessionRepository",
|
|
18
|
+
]
|