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.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. 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
+ ]