import asyncio import time from datetime import datetime, timezone from typing import Any, Dict, Optional from .base import AgentContext, AgentResult, AgentStatus, BaseAgent from src.config import settings from src.telemetry import collector class AgentExecutor: def __init__(self): self._semaphore = asyncio.Semaphore(settings.max_concurrent_agents) self._active_runs: Dict[str, asyncio.Task] = {} async def run( self, agent: BaseAgent, context: Optional[AgentContext] = None, timeout: Optional[int] = None, **kwargs, ) -> AgentResult: if context is None: context = AgentContext(agent_id=agent.agent_id) timeout = timeout or settings.agent_timeout started_at = datetime.now(timezone.utc) t0 = time.monotonic() result = AgentResult( run_id=context.run_id, agent_id=agent.agent_id, status=AgentStatus.PENDING, started_at=started_at, ) async with self._semaphore: result.status = AgentStatus.RUNNING collector.agent_start(agent.agent_id, context.run_id, agent.agent_type) try: await agent.on_start(context) output = await asyncio.wait_for( agent.execute(context, **kwargs), timeout=timeout, ) result.output = output result.status = AgentStatus.COMPLETED except asyncio.TimeoutError: result.status = AgentStatus.TIMEOUT result.error = f"Agent timed out after {timeout}s" collector.agent_failed(agent.agent_id, context.run_id, result.error) await agent.on_error(context, TimeoutError(result.error)) except asyncio.CancelledError: result.status = AgentStatus.CANCELLED result.error = "Agent execution was cancelled" raise except Exception as exc: result.status = AgentStatus.FAILED result.error = str(exc) collector.agent_failed(agent.agent_id, context.run_id, result.error) await agent.on_error(context, exc) finally: result.completed_at = datetime.now(timezone.utc) result.duration_ms = (time.monotonic() - t0) * 1000 if result.status == AgentStatus.COMPLETED: collector.agent_complete(agent.agent_id, context.run_id, result.duration_ms) await agent.on_complete(context, result) return result async def run_with_retry( self, agent: BaseAgent, context: Optional[AgentContext] = None, retries: Optional[int] = None, **kwargs, ) -> AgentResult: max_retries = retries if retries is not None else settings.retry_limit last_result: Optional[AgentResult] = None for attempt in range(max_retries + 1): ctx = context or AgentContext(agent_id=agent.agent_id) if attempt > 0: ctx.metadata["retry_attempt"] = attempt last_result = await self.run(agent, ctx, **kwargs) if last_result.status == AgentStatus.COMPLETED: return last_result if last_result.status == AgentStatus.CANCELLED: break return async def cancel(self, run_id: str) -> bool: task = self._active_runs.get(run_id) if task and not task.done(): task.cancel() return True return False executor = AgentExecutor()