diff --git a/src/google/adk/cli/adk_web_server.py b/src/google/adk/cli/adk_web_server.py index 654a9e2f9f..d4c3c26416 100644 --- a/src/google/adk/cli/adk_web_server.py +++ b/src/google/adk/cli/adk_web_server.py @@ -1713,6 +1713,34 @@ async def run_agent_live( async def forward_events(): runner = await self.get_runner_async(app_name) + + # Reload the agent to pick up any configuration changes + # This ensures dynamic updates (e.g., instructions from config manager) + # are reflected in the live session without restarting the server + if self.reload_agents: + try: + # Force reload the agent from disk + self.agent_loader.remove_agent_from_cache(app_name) + agent_or_app = self.agent_loader.load_agent(app_name) + + if isinstance(agent_or_app, App): + reloaded_agent = agent_or_app.root_agent + else: + reloaded_agent = agent_or_app + + logger.info( + 'Reloaded agent %s for live session (instructions may have updated)', + app_name + ) + except Exception as e: + logger.warning( + 'Failed to reload agent %s, using cached version: %s', + app_name, e + ) + reloaded_agent = None + else: + reloaded_agent = None + run_config = RunConfig( response_modalities=modalities, proactivity=( @@ -1734,6 +1762,7 @@ async def forward_events(): session=session, live_request_queue=live_request_queue, run_config=run_config, + agent=reloaded_agent, # Pass the reloaded agent ) ) as agen: async for event in agen: diff --git a/src/google/adk/cli/utils/agent_loader.py b/src/google/adk/cli/utils/agent_loader.py index 8b5805c5a9..d02f325e7e 100644 --- a/src/google/adk/cli/utils/agent_loader.py +++ b/src/google/adk/cli/utils/agent_loader.py @@ -395,6 +395,14 @@ def _determine_agent_language( raise ValueError(f"Could not determine agent type for '{agent_name}'.") def remove_agent_from_cache(self, agent_name: str): + """Removes an agent from the cache to force reload on next access. + + This method is useful for hot-reload scenarios where agent configuration + has changed and needs to be reloaded from disk. + + Args: + agent_name: The name of the agent to remove from cache. + """ # Clear module cache for the agent and its submodules keys_to_delete = [ module_name @@ -405,3 +413,27 @@ def remove_agent_from_cache(self, agent_name: str): logger.debug("Deleting module %s", key) del sys.modules[key] self._agent_cache.pop(agent_name, None) + logger.debug("Removed agent %s from cache", agent_name) + + def reload_agent(self, agent_name: str) -> Union[BaseAgent, App]: + """Forces a reload of the agent from disk, bypassing the cache. + + This method is useful for hot-reload scenarios where agent configuration + (e.g., instructions loaded from a config manager) has changed at runtime + and needs to be refreshed without restarting the application. + + Args: + agent_name: The name of the agent to reload. + + Returns: + The freshly loaded agent or app. + + Example: + # Force reload to pick up instruction changes + agent_loader.reload_agent("voice_agent") + + # Then get the runner which will use the reloaded agent + runner = await web_server.get_runner_async("voice_agent") + """ + self.remove_agent_from_cache(agent_name) + return self.load_agent(agent_name) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 921afee693..893bc2460c 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -352,6 +352,38 @@ def _format_session_not_found_message(self, session_id: str) -> str: 'auto_create_session=True when constructing the runner.' ) + def update_agent(self, agent: BaseAgent) -> None: + """Updates the runner's agent reference. + + This method allows updating the agent used by the runner without creating + a new Runner instance. Useful for hot-reload scenarios where agent + configuration (e.g., instructions, tools, model) changes at runtime. + + Note: This updates the runner's default agent. For one-time overrides + in run_live(), you can also pass the agent parameter directly to run_live(). + + Args: + agent: The new agent to use. + + Example: + # Initial setup + runner = Runner(app_name="voice_app", agent=create_agent(), ...) + + # Later, when instructions change + updated_agent = create_agent() # Loads latest config + runner.update_agent(updated_agent) + + # Or use directly in run_live + runner.run_live(..., agent=create_agent()) + """ + self.agent = agent + # Re-infer agent origin for the new agent + self._agent_origin_app_name, self._agent_origin_dir = ( + self._infer_agent_origin(agent) + ) + self._enforce_app_name_alignment() + logger.info('Updated runner agent to: %s', agent.name) + async def _get_or_create_session( self, *, user_id: str, session_id: str ) -> Session: @@ -927,6 +959,7 @@ async def run_live( live_request_queue: LiveRequestQueue, run_config: Optional[RunConfig] = None, session: Optional[Session] = None, + agent: Optional[BaseAgent] = None, ) -> AsyncGenerator[Event, None]: """Runs the agent in live mode (experimental feature). @@ -968,6 +1001,11 @@ async def run_live( run_config: The run config for the agent. session: The session to use. This parameter is deprecated, please use `user_id` and `session_id` instead. + agent: Optional agent to use for this invocation. If provided, this + agent will be used instead of the runner's default agent. This allows + for dynamic agent updates without creating a new Runner instance. + Useful for hot-reload scenarios where agent configuration (e.g., + instructions) changes at runtime. Yields: AsyncGenerator[Event, None]: An asynchronous generator that yields @@ -1003,13 +1041,18 @@ async def run_live( session = await self._get_or_create_session( user_id=user_id, session_id=session_id ) + + # Use the provided agent or fall back to the runner's agent + # This allows for dynamic agent updates without creating a new Runner + active_agent = agent if agent is not None else self.agent + invocation_context = self._new_invocation_context_for_live( session, live_request_queue=live_request_queue, run_config=run_config, ) - root_agent = self.agent + root_agent = active_agent invocation_context.agent = self._find_agent_to_run(session, root_agent) # Pre-processing for live streaming tools