Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/google/adk/cli/adk_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +1720 to +1742
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The run_agent_live WebSocket endpoint accepts an app_name parameter from the URL which is used to dynamically reload agent configurations. This app_name is passed directly to self.agent_loader.remove_agent_from_cache() and self.agent_loader.load_agent() without proper sanitization, leading to a critical Path Traversal vulnerability and potential Remote Code Execution (RCE). Additionally, the AdkWebServer class lacks a reload_agents attribute, which will cause an AttributeError at runtime. The agent reloading logic could also be simplified.

import os
import re

      # Perform security check for app_name to prevent path traversal.
      if not re.match(r'^[a-zA-Z0-9_]+$', app_name):
          await websocket.close(code=1008, reason='Invalid app_name format')
          return

      # More robust path validation
      agent_path = os.path.abspath(os.path.join(self.agents_dir, app_name))
      if not agent_path.startswith(os.path.abspath(self.agents_dir)):
          await websocket.close(code=1008, reason='Path traversal detected')
          return


run_config = RunConfig(
response_modalities=modalities,
proactivity=(
Expand All @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions src/google/adk/cli/utils/agent_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
45 changes: 44 additions & 1 deletion src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +379 to +385
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When update_agent is called, it updates self.agent but does not update self.app.root_agent if an App instance was provided to the Runner. This can lead to inconsistent behavior, as other parts of the Runner, like event compaction, might still use the old agent from self.app. To ensure consistency, you should also update self.app.root_agent.

    self.agent = agent
    if self.app:
      self.app.root_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:
Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down