puffinflow 2.dev0__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.
- puffinflow/__init__.py +132 -0
- puffinflow/core/__init__.py +110 -0
- puffinflow/core/agent/__init__.py +320 -0
- puffinflow/core/agent/base.py +1635 -0
- puffinflow/core/agent/checkpoint.py +50 -0
- puffinflow/core/agent/context.py +521 -0
- puffinflow/core/agent/decorators/__init__.py +90 -0
- puffinflow/core/agent/decorators/builder.py +454 -0
- puffinflow/core/agent/decorators/flexible.py +714 -0
- puffinflow/core/agent/decorators/inspection.py +144 -0
- puffinflow/core/agent/dependencies.py +57 -0
- puffinflow/core/agent/scheduling/__init__.py +21 -0
- puffinflow/core/agent/scheduling/builder.py +160 -0
- puffinflow/core/agent/scheduling/exceptions.py +35 -0
- puffinflow/core/agent/scheduling/inputs.py +137 -0
- puffinflow/core/agent/scheduling/parser.py +209 -0
- puffinflow/core/agent/scheduling/scheduler.py +413 -0
- puffinflow/core/agent/state.py +141 -0
- puffinflow/core/config.py +62 -0
- puffinflow/core/coordination/__init__.py +137 -0
- puffinflow/core/coordination/agent_group.py +359 -0
- puffinflow/core/coordination/agent_pool.py +629 -0
- puffinflow/core/coordination/agent_team.py +577 -0
- puffinflow/core/coordination/coordinator.py +720 -0
- puffinflow/core/coordination/deadlock.py +1759 -0
- puffinflow/core/coordination/fluent_api.py +421 -0
- puffinflow/core/coordination/primitives.py +478 -0
- puffinflow/core/coordination/rate_limiter.py +520 -0
- puffinflow/core/observability/__init__.py +47 -0
- puffinflow/core/observability/agent.py +139 -0
- puffinflow/core/observability/alerting.py +73 -0
- puffinflow/core/observability/config.py +127 -0
- puffinflow/core/observability/context.py +88 -0
- puffinflow/core/observability/core.py +147 -0
- puffinflow/core/observability/decorators.py +105 -0
- puffinflow/core/observability/events.py +71 -0
- puffinflow/core/observability/interfaces.py +196 -0
- puffinflow/core/observability/metrics.py +137 -0
- puffinflow/core/observability/tracing.py +209 -0
- puffinflow/core/reliability/__init__.py +27 -0
- puffinflow/core/reliability/bulkhead.py +96 -0
- puffinflow/core/reliability/circuit_breaker.py +149 -0
- puffinflow/core/reliability/leak_detector.py +122 -0
- puffinflow/core/resources/__init__.py +77 -0
- puffinflow/core/resources/allocation.py +790 -0
- puffinflow/core/resources/pool.py +645 -0
- puffinflow/core/resources/quotas.py +567 -0
- puffinflow/core/resources/requirements.py +217 -0
- puffinflow/version.py +21 -0
- puffinflow-2.dev0.dist-info/METADATA +334 -0
- puffinflow-2.dev0.dist-info/RECORD +55 -0
- puffinflow-2.dev0.dist-info/WHEEL +5 -0
- puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
- puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
- puffinflow-2.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"""Agent team coordination with messaging and event systems."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Callable, Optional
|
|
10
|
+
|
|
11
|
+
from ..agent.base import Agent, AgentResult
|
|
12
|
+
from ..agent.state import AgentStatus
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TeamResult:
|
|
19
|
+
"""Result container for team execution."""
|
|
20
|
+
|
|
21
|
+
team_name: str
|
|
22
|
+
status: str
|
|
23
|
+
agent_results: dict[str, AgentResult] = field(default_factory=dict)
|
|
24
|
+
start_time: Optional[float] = None
|
|
25
|
+
end_time: Optional[float] = None
|
|
26
|
+
total_duration: Optional[float] = None
|
|
27
|
+
error: Optional[Exception] = None
|
|
28
|
+
|
|
29
|
+
def get_agent_result(self, agent_name: str) -> Optional[AgentResult]:
|
|
30
|
+
"""Get result for specific agent."""
|
|
31
|
+
return self.agent_results.get(agent_name)
|
|
32
|
+
|
|
33
|
+
def get_all_outputs(self, key: str) -> list[Any]:
|
|
34
|
+
"""Get specific output from all agents."""
|
|
35
|
+
return [
|
|
36
|
+
result.get_output(key)
|
|
37
|
+
for result in self.agent_results.values()
|
|
38
|
+
if result.get_output(key) is not None
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def get_all_variables(self, key: str) -> list[Any]:
|
|
42
|
+
"""Get specific variable from all agents."""
|
|
43
|
+
return [
|
|
44
|
+
result.get_variable(key)
|
|
45
|
+
for result in self.agent_results.values()
|
|
46
|
+
if result.get_variable(key) is not None
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def get_best_by(self, metric: str, maximize: bool = True) -> Optional[AgentResult]:
|
|
50
|
+
"""Get agent with best metric value."""
|
|
51
|
+
valid_results = [
|
|
52
|
+
result
|
|
53
|
+
for result in self.agent_results.values()
|
|
54
|
+
if result.get_output(metric) is not None
|
|
55
|
+
or result.get_metric(metric) is not None
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if not valid_results:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def get_value(result: Any) -> Any:
|
|
62
|
+
return result.get_output(metric) or result.get_metric(metric) or 0
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
max(valid_results, key=get_value)
|
|
66
|
+
if maximize
|
|
67
|
+
else min(valid_results, key=get_value)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def average(self, key: str) -> float:
|
|
71
|
+
"""Get average of a numeric output/metric across agents."""
|
|
72
|
+
values = []
|
|
73
|
+
for result in self.agent_results.values():
|
|
74
|
+
value = result.get_output(key) or result.get_metric(key)
|
|
75
|
+
if isinstance(value, (int, float)):
|
|
76
|
+
values.append(value)
|
|
77
|
+
|
|
78
|
+
return sum(values) / len(values) if values else 0.0
|
|
79
|
+
|
|
80
|
+
def sum(self, key: str) -> float:
|
|
81
|
+
"""Get sum of a numeric output/metric across agents."""
|
|
82
|
+
total = 0.0
|
|
83
|
+
for result in self.agent_results.values():
|
|
84
|
+
value = result.get_output(key) or result.get_metric(key)
|
|
85
|
+
if isinstance(value, (int, float)):
|
|
86
|
+
total += value
|
|
87
|
+
return total
|
|
88
|
+
|
|
89
|
+
def count_successful(self) -> int:
|
|
90
|
+
"""Count successful agent executions."""
|
|
91
|
+
return sum(1 for result in self.agent_results.values() if result.is_success)
|
|
92
|
+
|
|
93
|
+
def count_failed(self) -> int:
|
|
94
|
+
"""Count failed agent executions."""
|
|
95
|
+
return sum(1 for result in self.agent_results.values() if result.is_failed)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def success_rate(self) -> float:
|
|
99
|
+
"""Get success rate as percentage."""
|
|
100
|
+
total = len(self.agent_results)
|
|
101
|
+
return (self.count_successful() / total * 100) if total > 0 else 0.0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Message:
|
|
106
|
+
"""Message between agents."""
|
|
107
|
+
|
|
108
|
+
sender: str
|
|
109
|
+
recipient: str
|
|
110
|
+
message_type: str
|
|
111
|
+
data: dict[str, Any]
|
|
112
|
+
timestamp: float = field(default_factory=time.time)
|
|
113
|
+
correlation_id: Optional[str] = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Event:
|
|
118
|
+
"""Event emitted by agents."""
|
|
119
|
+
|
|
120
|
+
source: str
|
|
121
|
+
event_type: str
|
|
122
|
+
data: dict[str, Any]
|
|
123
|
+
timestamp: float = field(default_factory=time.time)
|
|
124
|
+
event_id: str = field(default_factory=lambda: f"event_{int(time.time() * 1000)}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class EventBus:
|
|
128
|
+
"""Event bus for agent communication."""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self._handlers: dict[str, list[Callable]] = defaultdict(list)
|
|
132
|
+
self._event_history: list[Event] = []
|
|
133
|
+
self._max_history = 1000
|
|
134
|
+
|
|
135
|
+
def subscribe(self, event_type: str, handler: Callable) -> None:
|
|
136
|
+
"""Subscribe to event type."""
|
|
137
|
+
self._handlers[event_type].append(handler)
|
|
138
|
+
|
|
139
|
+
def unsubscribe(self, event_type: str, handler: Callable) -> None:
|
|
140
|
+
"""Unsubscribe from event type."""
|
|
141
|
+
if event_type in self._handlers:
|
|
142
|
+
with contextlib.suppress(ValueError):
|
|
143
|
+
self._handlers[event_type].remove(handler)
|
|
144
|
+
|
|
145
|
+
async def emit(self, event: Event) -> None:
|
|
146
|
+
"""Emit an event to all subscribers."""
|
|
147
|
+
self._event_history.append(event)
|
|
148
|
+
|
|
149
|
+
# Trim history if needed
|
|
150
|
+
if len(self._event_history) > self._max_history:
|
|
151
|
+
self._event_history = self._event_history[-self._max_history :]
|
|
152
|
+
|
|
153
|
+
# Notify handlers
|
|
154
|
+
handlers = self._handlers.get(event.event_type, [])
|
|
155
|
+
for handler in handlers:
|
|
156
|
+
try:
|
|
157
|
+
if asyncio.iscoroutinefunction(handler):
|
|
158
|
+
await handler(event)
|
|
159
|
+
else:
|
|
160
|
+
handler(event)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Error in event handler for {event.event_type}: {e}")
|
|
163
|
+
|
|
164
|
+
def get_events(
|
|
165
|
+
self, event_type: Optional[str] = None, source: Optional[str] = None
|
|
166
|
+
) -> list[Event]:
|
|
167
|
+
"""Get events by type and/or source."""
|
|
168
|
+
events = self._event_history
|
|
169
|
+
|
|
170
|
+
if event_type:
|
|
171
|
+
events = [e for e in events if e.event_type == event_type]
|
|
172
|
+
|
|
173
|
+
if source:
|
|
174
|
+
events = [e for e in events if e.source == source]
|
|
175
|
+
|
|
176
|
+
return events
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AgentTeam:
|
|
180
|
+
"""Enhanced agent team with coordination features."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, name: str):
|
|
183
|
+
self.name = name
|
|
184
|
+
self._agents: dict[str, Agent] = {}
|
|
185
|
+
self._shared_context: dict[str, Any] = {}
|
|
186
|
+
self._message_queue: asyncio.Queue = asyncio.Queue()
|
|
187
|
+
self._event_bus = EventBus()
|
|
188
|
+
self._running = False
|
|
189
|
+
self._results: dict[str, AgentResult] = {}
|
|
190
|
+
self._execution_order: list[str] = []
|
|
191
|
+
self._parallel_groups: list[list[str]] = []
|
|
192
|
+
self._dependencies: dict[str, set[str]] = defaultdict(set)
|
|
193
|
+
|
|
194
|
+
def add_agent(self, agent: Agent) -> "AgentTeam":
|
|
195
|
+
"""Add agent to team."""
|
|
196
|
+
self._agents[agent.name] = agent
|
|
197
|
+
agent.set_team(self)
|
|
198
|
+
|
|
199
|
+
# Share context
|
|
200
|
+
agent.shared_state.update(self._shared_context)
|
|
201
|
+
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def add_agents(self, agents: list[Agent]) -> "AgentTeam":
|
|
205
|
+
"""Add multiple agents to team."""
|
|
206
|
+
for agent in agents:
|
|
207
|
+
self.add_agent(agent)
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def get_agent(self, name: str) -> Optional[Agent]:
|
|
211
|
+
"""Get agent by name."""
|
|
212
|
+
return self._agents.get(name)
|
|
213
|
+
|
|
214
|
+
def remove_agent(self, name: str) -> bool:
|
|
215
|
+
"""Remove agent from team."""
|
|
216
|
+
if name in self._agents:
|
|
217
|
+
del self._agents[name]
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
def with_shared_context(
|
|
222
|
+
self, context: Optional[dict[str, Any]] = None
|
|
223
|
+
) -> "AgentTeam":
|
|
224
|
+
"""Set shared context for all agents."""
|
|
225
|
+
if context:
|
|
226
|
+
self._shared_context.update(context)
|
|
227
|
+
|
|
228
|
+
# Update all agent contexts
|
|
229
|
+
for agent in self._agents.values():
|
|
230
|
+
agent.shared_state.update(context)
|
|
231
|
+
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
def set_global_variable(self, key: str, value: Any) -> None:
|
|
235
|
+
"""Set variable for all agents."""
|
|
236
|
+
self._shared_context[key] = value
|
|
237
|
+
for agent in self._agents.values():
|
|
238
|
+
agent.set_shared_variable(key, value)
|
|
239
|
+
|
|
240
|
+
def get_global_variable(self, key: str, default: Any = None) -> Any:
|
|
241
|
+
"""Get global variable."""
|
|
242
|
+
return self._shared_context.get(key, default)
|
|
243
|
+
|
|
244
|
+
def set_variable_for_all(self, key: str, value: Any) -> "AgentTeam":
|
|
245
|
+
"""Set variable for all agents (fluent)."""
|
|
246
|
+
for agent in self._agents.values():
|
|
247
|
+
agent.set_variable(key, value)
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
# Messaging system
|
|
251
|
+
async def send_message(
|
|
252
|
+
self, sender: str, recipient: str, data: dict[str, Any]
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
"""Send message between agents."""
|
|
255
|
+
message = Message(
|
|
256
|
+
sender=sender,
|
|
257
|
+
recipient=recipient,
|
|
258
|
+
message_type=data.get("message_type", "generic"),
|
|
259
|
+
data=data,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
recipient_agent = self._agents.get(recipient)
|
|
263
|
+
if recipient_agent:
|
|
264
|
+
return await recipient_agent.handle_message(
|
|
265
|
+
message.message_type, message.data, sender
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return {}
|
|
269
|
+
|
|
270
|
+
async def broadcast_message(
|
|
271
|
+
self, sender: str, message_type: str, data: dict[str, Any]
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Broadcast message to all agents except sender."""
|
|
274
|
+
for agent_name, agent in self._agents.items():
|
|
275
|
+
if agent_name != sender:
|
|
276
|
+
try:
|
|
277
|
+
await agent.handle_message(message_type, data, sender)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Error broadcasting to {agent_name}: {e}")
|
|
280
|
+
|
|
281
|
+
# Event system
|
|
282
|
+
async def emit_event(
|
|
283
|
+
self, source: str, event_type: str, data: dict[str, Any]
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Emit event to event bus."""
|
|
286
|
+
event = Event(source=source, event_type=event_type, data=data)
|
|
287
|
+
await self._event_bus.emit(event)
|
|
288
|
+
|
|
289
|
+
def subscribe_to_events(self, event_type: str, handler: Callable) -> None:
|
|
290
|
+
"""Subscribe to events."""
|
|
291
|
+
self._event_bus.subscribe(event_type, handler)
|
|
292
|
+
|
|
293
|
+
# Execution methods
|
|
294
|
+
async def run(
|
|
295
|
+
self, mode: str = "parallel", timeout: Optional[float] = None
|
|
296
|
+
) -> TeamResult:
|
|
297
|
+
"""Run the team with specified mode.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
mode: Execution mode - "parallel", "sequential", or "dependencies"
|
|
301
|
+
timeout: Optional timeout for execution
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
TeamResult with execution results
|
|
305
|
+
"""
|
|
306
|
+
if mode == "parallel":
|
|
307
|
+
return await self.run_parallel(timeout)
|
|
308
|
+
elif mode == "sequential":
|
|
309
|
+
return await self.run_sequential()
|
|
310
|
+
elif mode == "dependencies":
|
|
311
|
+
return await self.run_with_dependencies()
|
|
312
|
+
else:
|
|
313
|
+
raise ValueError(f"Unknown execution mode: {mode}")
|
|
314
|
+
|
|
315
|
+
async def run_parallel(self, timeout: Optional[float] = None) -> TeamResult:
|
|
316
|
+
"""Run all agents in parallel."""
|
|
317
|
+
start_time = time.time()
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
# Create tasks for all agents
|
|
321
|
+
tasks = {
|
|
322
|
+
agent.name: asyncio.create_task(agent.run())
|
|
323
|
+
for agent in self._agents.values()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Wait for completion with optional timeout
|
|
327
|
+
if timeout:
|
|
328
|
+
done, pending = await asyncio.wait(
|
|
329
|
+
tasks.values(), timeout=timeout, return_when=asyncio.ALL_COMPLETED
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Cancel pending tasks
|
|
333
|
+
for task in pending:
|
|
334
|
+
task.cancel()
|
|
335
|
+
else:
|
|
336
|
+
await asyncio.gather(*tasks.values(), return_exceptions=True)
|
|
337
|
+
|
|
338
|
+
# Collect results
|
|
339
|
+
results = {}
|
|
340
|
+
for agent_name, task in tasks.items():
|
|
341
|
+
if task.done():
|
|
342
|
+
try:
|
|
343
|
+
result = task.result()
|
|
344
|
+
results[agent_name] = result
|
|
345
|
+
except Exception as e:
|
|
346
|
+
# Create error result
|
|
347
|
+
results[agent_name] = AgentResult(
|
|
348
|
+
agent_name=agent_name,
|
|
349
|
+
status=AgentStatus.FAILED,
|
|
350
|
+
error=e,
|
|
351
|
+
start_time=start_time,
|
|
352
|
+
end_time=time.time(),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
end_time = time.time()
|
|
356
|
+
|
|
357
|
+
return TeamResult(
|
|
358
|
+
team_name=self.name,
|
|
359
|
+
status="completed",
|
|
360
|
+
agent_results=results,
|
|
361
|
+
start_time=start_time,
|
|
362
|
+
end_time=end_time,
|
|
363
|
+
total_duration=end_time - start_time,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
end_time = time.time()
|
|
368
|
+
return TeamResult(
|
|
369
|
+
team_name=self.name,
|
|
370
|
+
status="failed",
|
|
371
|
+
error=e,
|
|
372
|
+
start_time=start_time,
|
|
373
|
+
end_time=end_time,
|
|
374
|
+
total_duration=end_time - start_time,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
async def run_sequential(
|
|
378
|
+
self, agent_order: Optional[list[str]] = None
|
|
379
|
+
) -> TeamResult:
|
|
380
|
+
"""Run agents sequentially."""
|
|
381
|
+
start_time = time.time()
|
|
382
|
+
results = {}
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
order = agent_order or list(self._agents.keys())
|
|
386
|
+
|
|
387
|
+
for agent_name in order:
|
|
388
|
+
agent = self._agents.get(agent_name)
|
|
389
|
+
if agent:
|
|
390
|
+
result = await agent.run()
|
|
391
|
+
results[agent_name] = result
|
|
392
|
+
|
|
393
|
+
# Stop on first failure if needed
|
|
394
|
+
if not result.is_success:
|
|
395
|
+
logger.warning(f"Agent {agent_name} failed, continuing...")
|
|
396
|
+
|
|
397
|
+
end_time = time.time()
|
|
398
|
+
|
|
399
|
+
return TeamResult(
|
|
400
|
+
team_name=self.name,
|
|
401
|
+
status="completed",
|
|
402
|
+
agent_results=results,
|
|
403
|
+
start_time=start_time,
|
|
404
|
+
end_time=end_time,
|
|
405
|
+
total_duration=end_time - start_time,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
end_time = time.time()
|
|
410
|
+
return TeamResult(
|
|
411
|
+
team_name=self.name,
|
|
412
|
+
status="failed",
|
|
413
|
+
agent_results=results,
|
|
414
|
+
error=e,
|
|
415
|
+
start_time=start_time,
|
|
416
|
+
end_time=end_time,
|
|
417
|
+
total_duration=end_time - start_time,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
async def run_parallel_and_collect(self) -> TeamResult:
|
|
421
|
+
"""Run in parallel and collect all results."""
|
|
422
|
+
return await self.run_parallel()
|
|
423
|
+
|
|
424
|
+
async def run_with_dependencies(self) -> TeamResult:
|
|
425
|
+
"""Run agents respecting dependencies."""
|
|
426
|
+
start_time = time.time()
|
|
427
|
+
results = {}
|
|
428
|
+
completed: set[str] = set()
|
|
429
|
+
running: dict[str, Any] = {}
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
while len(completed) < len(self._agents):
|
|
433
|
+
# Find agents ready to run
|
|
434
|
+
ready = []
|
|
435
|
+
for agent_name, _agent in self._agents.items():
|
|
436
|
+
if (
|
|
437
|
+
agent_name not in completed
|
|
438
|
+
and agent_name not in running
|
|
439
|
+
and self._dependencies[agent_name].issubset(completed)
|
|
440
|
+
):
|
|
441
|
+
ready.append(agent_name)
|
|
442
|
+
|
|
443
|
+
if not ready:
|
|
444
|
+
# Wait for running agents
|
|
445
|
+
if running:
|
|
446
|
+
done_tasks = await asyncio.wait(
|
|
447
|
+
running.values(), return_when=asyncio.FIRST_COMPLETED
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Process completed agents
|
|
451
|
+
for task in done_tasks[0]:
|
|
452
|
+
for agent_name, agent_task in list(running.items()):
|
|
453
|
+
if agent_task == task:
|
|
454
|
+
result = await task
|
|
455
|
+
results[agent_name] = result
|
|
456
|
+
completed.add(agent_name)
|
|
457
|
+
del running[agent_name]
|
|
458
|
+
break
|
|
459
|
+
else:
|
|
460
|
+
break # No agents ready and none running
|
|
461
|
+
else:
|
|
462
|
+
# Start ready agents
|
|
463
|
+
for agent_name in ready:
|
|
464
|
+
agent = self._agents[agent_name]
|
|
465
|
+
task = asyncio.create_task(agent.run())
|
|
466
|
+
running[agent_name] = task
|
|
467
|
+
|
|
468
|
+
# Wait for remaining agents
|
|
469
|
+
if running:
|
|
470
|
+
remaining_results = await asyncio.gather(*running.values())
|
|
471
|
+
for i, (agent_name, _) in enumerate(running.items()):
|
|
472
|
+
results[agent_name] = remaining_results[i]
|
|
473
|
+
completed.add(agent_name)
|
|
474
|
+
|
|
475
|
+
end_time = time.time()
|
|
476
|
+
|
|
477
|
+
return TeamResult(
|
|
478
|
+
team_name=self.name,
|
|
479
|
+
status="completed",
|
|
480
|
+
agent_results=results,
|
|
481
|
+
start_time=start_time,
|
|
482
|
+
end_time=end_time,
|
|
483
|
+
total_duration=end_time - start_time,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
except Exception as e:
|
|
487
|
+
end_time = time.time()
|
|
488
|
+
return TeamResult(
|
|
489
|
+
team_name=self.name,
|
|
490
|
+
status="failed",
|
|
491
|
+
agent_results=results,
|
|
492
|
+
error=e,
|
|
493
|
+
start_time=start_time,
|
|
494
|
+
end_time=end_time,
|
|
495
|
+
total_duration=end_time - start_time,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Dependency management
|
|
499
|
+
def add_dependency(self, agent: str, depends_on: str) -> "AgentTeam":
|
|
500
|
+
"""Add dependency between agents."""
|
|
501
|
+
self._dependencies[agent].add(depends_on)
|
|
502
|
+
return self
|
|
503
|
+
|
|
504
|
+
def set_execution_order(self, order: list[str]) -> "AgentTeam":
|
|
505
|
+
"""Set execution order for sequential runs."""
|
|
506
|
+
self._execution_order = order
|
|
507
|
+
return self
|
|
508
|
+
|
|
509
|
+
# Monitoring and control
|
|
510
|
+
def get_status(self) -> dict[str, Any]:
|
|
511
|
+
"""Get team status."""
|
|
512
|
+
agent_statuses = {
|
|
513
|
+
name: (
|
|
514
|
+
agent.status.value
|
|
515
|
+
if hasattr(agent.status, "value")
|
|
516
|
+
else str(agent.status)
|
|
517
|
+
)
|
|
518
|
+
for name, agent in self._agents.items()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
"team_name": self.name,
|
|
523
|
+
"agent_count": len(self._agents),
|
|
524
|
+
"agent_statuses": agent_statuses,
|
|
525
|
+
"shared_variables": len(self._shared_context),
|
|
526
|
+
"running": self._running,
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async def pause_all(self) -> None:
|
|
530
|
+
"""Pause all agents."""
|
|
531
|
+
for agent in self._agents.values():
|
|
532
|
+
await agent.pause()
|
|
533
|
+
|
|
534
|
+
async def resume_all(self) -> None:
|
|
535
|
+
"""Resume all agents."""
|
|
536
|
+
for agent in self._agents.values():
|
|
537
|
+
await agent.resume()
|
|
538
|
+
|
|
539
|
+
async def cancel_all(self) -> None:
|
|
540
|
+
"""Cancel all agents."""
|
|
541
|
+
for agent in self._agents.values():
|
|
542
|
+
await agent.cancel_all()
|
|
543
|
+
|
|
544
|
+
# Context manager support
|
|
545
|
+
async def __aenter__(self) -> "AgentTeam":
|
|
546
|
+
"""Async context manager entry."""
|
|
547
|
+
self._running = True
|
|
548
|
+
return self
|
|
549
|
+
|
|
550
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
551
|
+
"""Async context manager exit."""
|
|
552
|
+
self._running = False
|
|
553
|
+
await self.cancel_all()
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# Helper functions for easy team creation
|
|
557
|
+
def create_team(name: str, agents: list[Agent]) -> AgentTeam:
|
|
558
|
+
"""Create a team with agents."""
|
|
559
|
+
team = AgentTeam(name)
|
|
560
|
+
team.add_agents(agents)
|
|
561
|
+
return team
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
async def run_agents_parallel(
|
|
565
|
+
agents: list[Agent], timeout: Optional[float] = None
|
|
566
|
+
) -> dict[str, AgentResult]:
|
|
567
|
+
"""Run agents in parallel and return results."""
|
|
568
|
+
team = create_team("parallel_execution", agents)
|
|
569
|
+
result = await team.run_parallel(timeout)
|
|
570
|
+
return result.agent_results
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
async def run_agents_sequential(agents: list[Agent]) -> dict[str, AgentResult]:
|
|
574
|
+
"""Run agents sequentially and return results."""
|
|
575
|
+
team = create_team("sequential_execution", agents)
|
|
576
|
+
result = await team.run_sequential()
|
|
577
|
+
return result.agent_results
|