| 18 | |
| 19 | |
| 20 | class Scheduler: |
| 21 | def __init__(self, max_concurrent: int | None = None) -> None: |
| 22 | self._max_concurrent = max_concurrent if max_concurrent is not None else max_concurrent_agents() |
| 23 | self._sem = asyncio.Semaphore(self._max_concurrent) |
| 24 | self._launched = 0 |
| 25 | self._peak = 0 |
| 26 | self._active = 0 |
| 27 | |
| 28 | @property |
| 29 | def max_concurrent(self) -> int: |
| 30 | return self._max_concurrent |
| 31 | |
| 32 | @property |
| 33 | def launched(self) -> int: |
| 34 | return self._launched |
| 35 | |
| 36 | @property |
| 37 | def peak_concurrency(self) -> int: |
| 38 | return self._peak |
| 39 | |
| 40 | def reserve(self) -> int: |
| 41 | """Claim a lifetime slot and return this call's 0-based index. |
| 42 | |
| 43 | Raises once the per-run agent cap is hit. Called synchronously at |
| 44 | ``agent()`` entry (before any ``await``) so indices are deterministic. |
| 45 | """ |
| 46 | if self._launched >= MAX_AGENTS_PER_RUN: |
| 47 | raise WorkflowLimitError( |
| 48 | f"workflow exceeded the per-run agent cap of {MAX_AGENTS_PER_RUN}" |
| 49 | ) |
| 50 | index = self._launched |
| 51 | self._launched += 1 |
| 52 | return index |
| 53 | |
| 54 | @asynccontextmanager |
| 55 | async def slot(self): |
| 56 | """Hold a concurrency slot for the duration of one subagent run.""" |
| 57 | async with self._sem: |
| 58 | self._active += 1 |
| 59 | self._peak = max(self._peak, self._active) |
| 60 | try: |
| 61 | yield |
| 62 | finally: |
| 63 | self._active -= 1 |
no outgoing calls