Skip to content

Commit e212afe

Browse files
authored
Merge pull request #1438 from Steve-Dusty/feat/async-subagents
[feat] Async subagent execution with background task registry
2 parents dab0b37 + 5ce5355 commit e212afe

5 files changed

Lines changed: 1191 additions & 0 deletions

File tree

swarms/structs/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from swarms.structs.agent import Agent
2+
from swarms.structs.async_subagent import (
3+
SubagentRegistry,
4+
SubagentTask,
5+
TaskStatus,
6+
)
27
from swarms.structs.agent_loader import AgentLoader
38
from swarms.structs.agent_rearrange import AgentRearrange, rearrange
49
from swarms.structs.aop import AOP
@@ -166,4 +171,7 @@
166171
"AOP",
167172
"SelfMoASeq",
168173
"DebateWithJudge",
174+
"SubagentRegistry",
175+
"SubagentTask",
176+
"TaskStatus",
169177
]

swarms/structs/agent.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ def __init__(
446446
marketplace_prompt_id: Optional[str] = None,
447447
skills_dir: Optional[str] = None,
448448
selected_tools: Optional[Union[str, List[str]]] = "all",
449+
max_subagent_depth: int = 3,
449450
*args,
450451
**kwargs,
451452
):
@@ -586,6 +587,10 @@ def __init__(
586587
2 # Maximum consecutive think calls
587588
)
588589

590+
# Async subagent support
591+
self.max_subagent_depth = max_subagent_depth
592+
self._subagent_registry = None
593+
589594
# Load prompt from marketplace if marketplace_prompt_id is provided
590595
if self.marketplace_prompt_id:
591596
self._load_prompt_from_marketplace()
@@ -3114,6 +3119,83 @@ async def arun(
31143119
error
31153120
) # Ensure this is also async if needed
31163121

3122+
# ── Async Subagent Methods ──────────────────────────────
3123+
3124+
def _get_registry(self):
3125+
"""Lazy-init and return the SubagentRegistry."""
3126+
if self._subagent_registry is None:
3127+
from swarms.structs.async_subagent import (
3128+
SubagentRegistry,
3129+
)
3130+
3131+
self._subagent_registry = SubagentRegistry(
3132+
max_depth=self.max_subagent_depth
3133+
)
3134+
return self._subagent_registry
3135+
3136+
def run_async(self, task: str):
3137+
"""
3138+
Run this agent's task in the background, returning a Future.
3139+
"""
3140+
registry = self._get_registry()
3141+
return registry._executor.submit(self.run, task)
3142+
3143+
def spawn_async(
3144+
self,
3145+
agent,
3146+
task: str,
3147+
max_retries: int = 0,
3148+
retry_on=None,
3149+
fail_fast: bool = True,
3150+
) -> str:
3151+
"""
3152+
Spawn a subagent to run a task in the background.
3153+
3154+
Returns task_id for tracking via get_subagent_results() / gather_results().
3155+
"""
3156+
registry = self._get_registry()
3157+
return registry.spawn(
3158+
agent=agent,
3159+
task=task,
3160+
parent_id=self.id,
3161+
depth=0,
3162+
max_retries=max_retries,
3163+
retry_on=retry_on,
3164+
fail_fast=fail_fast,
3165+
)
3166+
3167+
def run_in_background(self, task: str) -> str:
3168+
"""
3169+
Convenience: spawn self as a background task, return task_id.
3170+
"""
3171+
registry = self._get_registry()
3172+
return registry.spawn(
3173+
agent=self,
3174+
task=task,
3175+
parent_id=None,
3176+
depth=0,
3177+
)
3178+
3179+
def gather_results(
3180+
self,
3181+
strategy: str = "wait_all",
3182+
timeout: float = None,
3183+
):
3184+
"""Wait for spawned subagents and return their results."""
3185+
return self._get_registry().gather(
3186+
strategy=strategy, timeout=timeout
3187+
)
3188+
3189+
def get_subagent_results(self):
3190+
"""Collect results from all completed subagent tasks."""
3191+
return self._get_registry().get_results()
3192+
3193+
def cancel_subagent(self, task_id: str) -> bool:
3194+
"""Cancel a spawned subagent task."""
3195+
return self._get_registry().cancel(task_id)
3196+
3197+
# ── End Async Subagent Methods ──────────────────────────
3198+
31173199
def __call__(
31183200
self,
31193201
task: Optional[str] = None,

swarms/structs/async_subagent.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
"""
2+
Async subagent execution with background task registry,
3+
recursive subagent trees, result aggregation, and fault tolerance.
4+
"""
5+
6+
import uuid
7+
import time
8+
import threading
9+
from concurrent.futures import (
10+
Future,
11+
ThreadPoolExecutor,
12+
wait,
13+
ALL_COMPLETED,
14+
FIRST_COMPLETED,
15+
)
16+
from dataclasses import dataclass, field
17+
from enum import Enum
18+
from typing import Any, Callable, Dict, List, Optional, Type
19+
20+
from loguru import logger
21+
22+
23+
class TaskStatus(str, Enum):
24+
PENDING = "pending"
25+
RUNNING = "running"
26+
COMPLETED = "completed"
27+
FAILED = "failed"
28+
CANCELLED = "cancelled"
29+
30+
31+
@dataclass
32+
class SubagentTask:
33+
"""Tracks a single async subagent task."""
34+
35+
id: str
36+
agent: Any
37+
task_str: str
38+
status: TaskStatus = TaskStatus.PENDING
39+
result: Any = None
40+
error: Optional[Exception] = None
41+
future: Optional[Future] = None
42+
parent_id: Optional[str] = None
43+
depth: int = 0
44+
retries: int = 0
45+
max_retries: int = 0
46+
retry_on: Optional[List[Type[Exception]]] = None
47+
created_at: float = field(default_factory=time.time)
48+
completed_at: Optional[float] = None
49+
50+
51+
class SubagentRegistry:
52+
"""
53+
Manages async subagent tasks with status tracking,
54+
result aggregation, retry policies, and depth-limited recursion.
55+
"""
56+
57+
def __init__(
58+
self,
59+
max_depth: int = 3,
60+
max_workers: Optional[int] = None,
61+
):
62+
self.max_depth = max_depth
63+
self._tasks: Dict[str, SubagentTask] = {}
64+
self._executor = ThreadPoolExecutor(
65+
max_workers=max_workers
66+
)
67+
self._lock = threading.Lock()
68+
69+
def spawn(
70+
self,
71+
agent: Any,
72+
task: str,
73+
parent_id: Optional[str] = None,
74+
depth: int = 0,
75+
max_retries: int = 0,
76+
retry_on: Optional[List[Type[Exception]]] = None,
77+
fail_fast: bool = True,
78+
) -> str:
79+
"""
80+
Spawn an agent task in the background.
81+
82+
Returns the task_id for tracking.
83+
Raises ValueError if depth exceeds max_depth.
84+
"""
85+
if depth > self.max_depth:
86+
raise ValueError(
87+
f"Subagent depth {depth} exceeds max_depth {self.max_depth}"
88+
)
89+
90+
task_id = f"task-{uuid.uuid4().hex[:8]}"
91+
st = SubagentTask(
92+
id=task_id,
93+
agent=agent,
94+
task_str=task,
95+
parent_id=parent_id,
96+
depth=depth,
97+
max_retries=max_retries,
98+
retry_on=retry_on or [],
99+
)
100+
101+
with self._lock:
102+
self._tasks[task_id] = st
103+
104+
agent_name = getattr(agent, "agent_name", str(agent))
105+
logger.info(
106+
f"[SubagentRegistry] Spawned task {task_id} | agent={agent_name} | depth={depth}"
107+
)
108+
109+
st.status = TaskStatus.RUNNING
110+
future = self._executor.submit(
111+
self._execute_task, st, fail_fast
112+
)
113+
st.future = future
114+
115+
return task_id
116+
117+
def _execute_task(
118+
self, st: SubagentTask, fail_fast: bool
119+
) -> Any:
120+
"""Run the agent with retry logic."""
121+
agent_name = getattr(st.agent, "agent_name", str(st.agent))
122+
last_error = None
123+
124+
for attempt in range(st.max_retries + 1):
125+
try:
126+
if attempt > 0:
127+
logger.info(
128+
f"[SubagentRegistry] Retry {attempt}/{st.max_retries} for task {st.id}"
129+
)
130+
st.retries = attempt
131+
132+
result = st.agent.run(st.task_str)
133+
st.result = result
134+
st.status = TaskStatus.COMPLETED
135+
st.completed_at = time.time()
136+
logger.info(
137+
f"[SubagentRegistry] Task {st.id} completed | agent={agent_name} | "
138+
f"duration={st.completed_at - st.created_at:.2f}s"
139+
)
140+
return result
141+
142+
except Exception as e:
143+
last_error = e
144+
should_retry = (
145+
attempt < st.max_retries
146+
and (
147+
not st.retry_on
148+
or any(
149+
isinstance(e, exc_type)
150+
for exc_type in st.retry_on
151+
)
152+
)
153+
)
154+
if should_retry:
155+
continue
156+
157+
st.error = e
158+
st.status = TaskStatus.FAILED
159+
st.completed_at = time.time()
160+
logger.error(
161+
f"[SubagentRegistry] Task {st.id} failed | agent={agent_name} | error={e}"
162+
)
163+
if fail_fast:
164+
raise
165+
return None
166+
167+
# Should not reach here, but handle edge case
168+
st.error = last_error
169+
st.status = TaskStatus.FAILED
170+
st.completed_at = time.time()
171+
if fail_fast:
172+
raise last_error
173+
return None
174+
175+
def get_task(self, task_id: str) -> SubagentTask:
176+
"""Get a task by ID."""
177+
if task_id not in self._tasks:
178+
raise KeyError(f"Task {task_id} not found")
179+
return self._tasks[task_id]
180+
181+
def get_results(self) -> Dict[str, Any]:
182+
"""Collect results from all completed tasks."""
183+
results = {}
184+
for task_id, st in self._tasks.items():
185+
if st.status == TaskStatus.COMPLETED:
186+
results[task_id] = st.result
187+
elif st.status == TaskStatus.FAILED:
188+
results[task_id] = st.error
189+
return results
190+
191+
def cancel(self, task_id: str) -> bool:
192+
"""Cancel a task if it hasn't completed yet."""
193+
st = self.get_task(task_id)
194+
if st.future and st.future.cancel():
195+
st.status = TaskStatus.CANCELLED
196+
st.completed_at = time.time()
197+
logger.info(
198+
f"[SubagentRegistry] Task {task_id} cancelled"
199+
)
200+
return True
201+
return False
202+
203+
def gather(
204+
self,
205+
strategy: str = "wait_all",
206+
timeout: Optional[float] = None,
207+
) -> List[Any]:
208+
"""
209+
Wait for tasks and return results.
210+
211+
Args:
212+
strategy: "wait_all" or "wait_first"
213+
timeout: Max seconds to wait
214+
215+
Returns:
216+
List of results (or exceptions for failed tasks)
217+
"""
218+
# Collect already-completed results
219+
already_done = []
220+
pending_futures = {}
221+
for st in self._tasks.values():
222+
if st.status in (TaskStatus.COMPLETED, TaskStatus.FAILED):
223+
already_done.append(st)
224+
elif st.future is not None:
225+
pending_futures[st.future] = st
226+
227+
if not pending_futures:
228+
return [
229+
st.error if st.status == TaskStatus.FAILED else st.result
230+
for st in already_done
231+
]
232+
233+
return_when = (
234+
FIRST_COMPLETED
235+
if strategy == "wait_first"
236+
else ALL_COMPLETED
237+
)
238+
done, _ = wait(
239+
pending_futures.keys(),
240+
timeout=timeout,
241+
return_when=return_when,
242+
)
243+
244+
results = [
245+
st.error if st.status == TaskStatus.FAILED else st.result
246+
for st in already_done
247+
]
248+
for future in done:
249+
try:
250+
result = future.result(timeout=0)
251+
results.append(result)
252+
except Exception as e:
253+
results.append(e)
254+
255+
return results
256+
257+
def shutdown(self):
258+
"""Shut down the executor."""
259+
self._executor.shutdown(wait=False)
260+
logger.info("[SubagentRegistry] Shut down")
261+
262+
@property
263+
def tasks(self) -> Dict[str, SubagentTask]:
264+
return dict(self._tasks)

0 commit comments

Comments
 (0)