11import asyncio
22import logging
3- from typing import TYPE_CHECKING , Awaitable , Callable
3+ import weakref
4+ from typing import TYPE_CHECKING , Awaitable , Callable , Optional
45
5- from vision_agents .core .utils .utils import await_or_run
6- from vision_agents .core .warmup import WarmupCache , Warmable
6+ from vision_agents .core .utils .utils import await_or_run , cancel_and_wait
7+ from vision_agents .core .warmup import Warmable , WarmupCache
78
89if TYPE_CHECKING :
910 from .agents import Agent
@@ -23,33 +24,72 @@ def __init__(
2324 self ,
2425 create_agent : Callable [..., "Agent" | Awaitable ["Agent" ]],
2526 join_call : Callable [..., None | Awaitable [None ]] | None = None ,
27+ agent_idle_timeout : float = 10.0 ,
28+ agent_idle_cleanup_interval : float = 5.0 ,
2629 ):
2730 """
2831 Initialize the agent launcher.
2932
3033 Args:
3134 create_agent: A function that creates and returns an Agent instance
3235 join_call: Optional function that handles joining a call with the agent
36+ agent_idle_timeout: Optional timeout in seconds for agent to stay alone on the call. Default - `30.0`.
37+ `0` means idle agents won't leave the call until it's ended.
38+
3339 """
3440 self .create_agent = create_agent
3541 self .join_call = join_call
3642 self ._warmup_lock = asyncio .Lock ()
3743 self ._warmup_cache = WarmupCache ()
3844
45+ if agent_idle_timeout < 0 :
46+ raise ValueError ("agent_idle_timeout must be >= 0" )
47+ self ._agent_idle_timeout = agent_idle_timeout
48+
49+ if agent_idle_cleanup_interval <= 0 :
50+ raise ValueError ("agent_idle_cleanup_interval must be > 0" )
51+ self ._agent_idle_cleanup_interval = agent_idle_cleanup_interval
52+
53+ self ._active_agents : weakref .WeakSet [Agent ] = weakref .WeakSet ()
54+
55+ self ._running = False
56+ self ._cleanup_task : Optional [asyncio .Task ] = None
57+ self ._warmed_up : bool = False
58+
59+ async def start (self ):
60+ if self ._running :
61+ raise RuntimeError ("AgentLauncher is already running" )
62+ logger .debug ("Starting AgentLauncher" )
63+ self ._running = True
64+ await self .warmup ()
65+ self ._cleanup_task = asyncio .create_task (self ._cleanup_idle_agents ())
66+ logger .debug ("AgentLauncher started" )
67+
68+ async def stop (self ):
69+ logger .debug ("Stopping AgentLauncher" )
70+ self ._running = False
71+ if self ._cleanup_task :
72+ await cancel_and_wait (self ._cleanup_task )
73+ logger .debug ("AgentLauncher stopped" )
74+
3975 async def warmup (self ) -> None :
4076 """
4177 Warm up all agent components.
4278
4379 This method creates the agent and calls warmup() on LLM, TTS, STT,
4480 and turn detection components if they exist.
4581 """
82+ if self ._warmed_up or self ._warmup_lock .locked ():
83+ return
84+
4685 async with self ._warmup_lock :
4786 logger .info ("Creating agent..." )
4887
4988 # Create a dry-run Agent instance and warmup its components for the first time.
5089 agent : "Agent" = await await_or_run (self .create_agent )
5190 logger .info ("Warming up agent components..." )
5291 await self ._warmup_agent (agent )
92+ self ._warmed_up = True
5393
5494 logger .info ("Agent warmup completed" )
5595
@@ -65,6 +105,7 @@ async def launch(self, **kwargs) -> "Agent":
65105 """
66106 agent : "Agent" = await await_or_run (self .create_agent , ** kwargs )
67107 await self ._warmup_agent (agent )
108+ self ._active_agents .add (agent )
68109 return agent
69110
70111 async def _warmup_agent (self , agent : "Agent" ) -> None :
@@ -99,10 +140,46 @@ async def _warmup_agent(self, agent: "Agent") -> None:
99140 warmup_tasks .append (agent .turn_detection .warmup (self ._warmup_cache ))
100141
101142 # Warmup processors
102- if agent .processors :
103- for processor in agent .processors :
104- if isinstance (processor , Warmable ):
105- warmup_tasks .append (processor .warmup (self ._warmup_cache ))
143+ for processor in agent .processors :
144+ if isinstance (processor , Warmable ):
145+ warmup_tasks .append (processor .warmup (self ._warmup_cache ))
106146
107147 if warmup_tasks :
108148 await asyncio .gather (* warmup_tasks )
149+
150+ async def _cleanup_idle_agents (self ) -> None :
151+ if not self ._agent_idle_timeout :
152+ return
153+
154+ while self ._running :
155+ # Collect idle agents first to close them all at once
156+ idle_agents = []
157+ for agent in self ._active_agents :
158+ agent_idle_for = agent .idle_for ()
159+ if agent_idle_for >= self ._agent_idle_timeout :
160+ logger .info (
161+ f'Agent with user_id "{ agent .agent_user .id } " is idle for { round (agent_idle_for , 2 )} s, '
162+ f"closing it after { self ._agent_idle_timeout } s timeout"
163+ )
164+ idle_agents .append (agent )
165+
166+ if idle_agents :
167+ coros = [asyncio .shield (a .close ()) for a in idle_agents ]
168+ result = await asyncio .shield (
169+ asyncio .gather (* coros , return_exceptions = True )
170+ )
171+ for agent , r in zip (idle_agents , result ):
172+ if isinstance (r , Exception ):
173+ logger .error (
174+ f"Failed to close idle agent with user_id { agent .agent_user .id } " ,
175+ exc_info = r ,
176+ )
177+
178+ await asyncio .sleep (self ._agent_idle_cleanup_interval )
179+
180+ async def __aenter__ (self ):
181+ await self .start ()
182+ return self
183+
184+ async def __aexit__ (self , exc_type , exc_val , exc_tb ):
185+ await self .stop ()
0 commit comments