From 49c88617ab963dd00be2eb38e36609254887ff4b Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Sun, 1 Mar 2026 22:17:35 +0530 Subject: [PATCH 1/8] feat: Add --dev flag to dk run for watchdog-based hot-reloading Implemented real-time automatic agent reloading during interactive CLI sessions. - Added --dev option to cli_tools_click.py. - Integrated watchdog.events.FileSystemEventHandler in cli.py to monitor .py and .yaml changes. - Refactored un_interactively to multiplex sys.stdin.readline inside a daemon thread alongside syncio.wait(reload_event.wait()) for non-blocking reload. - Used AgentLoader.remove_agent_from_cache to force module cache flushing and instantly reload logic while preserving session_service context. - Added async unit tests in est_cli.py to cover multiplexing agent transitions. --- src/google/adk/cli/cli.py | 124 +++++++++++++++++++++++++- src/google/adk/cli/cli_tools_click.py | 8 ++ tests/unittests/cli/utils/test_cli.py | 65 ++++++++++++++ 3 files changed, 195 insertions(+), 2 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 1d49f50d79..9f7013ce23 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -18,6 +18,11 @@ from pathlib import Path from typing import Optional from typing import Union +import asyncio +import sys +import threading +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer import click from google.genai import types @@ -42,6 +47,27 @@ from .utils.service_factory import create_session_service_from_options +class DevModeChangeHandler(FileSystemEventHandler): + """Watchdog event handler to trigger agent reload upon file changes.""" + + def __init__(self, loop: asyncio.AbstractEventLoop, reload_event: asyncio.Event): + super().__init__() + self.loop = loop + self.reload_event = reload_event + + def _handle_change(self, event): + if event.is_directory: + return + if event.src_path.endswith('.py') or event.src_path.endswith('.yaml'): + self.loop.call_soon_threadsafe(self.reload_event.set) + + def on_modified(self, event): + self._handle_change(event) + + def on_created(self, event): + self._handle_change(event) + + class InputFile(BaseModel): state: dict[str, object] queries: list[str] @@ -98,6 +124,10 @@ async def run_interactively( session_service: BaseSessionService, credential_service: BaseCredentialService, memory_service: Optional[BaseMemoryService] = None, + dev: bool = False, + reload_event: Optional[asyncio.Event] = None, + agent_loader: Optional[AgentLoader] = None, + agent_folder_name: Optional[str] = None, ) -> None: app = ( root_agent_or_app @@ -111,11 +141,72 @@ async def run_interactively( memory_service=memory_service, credential_service=credential_service, ) + + if dev: + loop = asyncio.get_running_loop() + input_queue = asyncio.Queue() + + def _read_input(): + while True: + try: + line = sys.stdin.readline() + if not line: break + loop.call_soon_threadsafe(input_queue.put_nowait, line) + except Exception: + break + + threading.Thread(target=_read_input, daemon=True).start() + sys.stdout.write('[user]: ') + sys.stdout.flush() + while True: - query = input('[user]: ') + if not dev or reload_event is None: + query = input('[user]: ') + else: + input_task = asyncio.create_task(input_queue.get()) + reload_task = asyncio.create_task(reload_event.wait()) + done, pending = await asyncio.wait( + [input_task, reload_task], return_when=asyncio.FIRST_COMPLETED + ) + + if reload_task in done: + input_task.cancel() + reload_event.clear() + click.secho('\nChanges detected, reloading agent...', fg='yellow') + await runner.close() + + if agent_loader and agent_folder_name: + try: + agent_loader.remove_agent_from_cache(agent_folder_name) + new_agent_or_app = agent_loader.load_agent(agent_folder_name) + app = ( + new_agent_or_app + if isinstance(new_agent_or_app, App) + else App(name=session.app_name, root_agent=new_agent_or_app) + ) + runner = Runner( + app=app, + artifact_service=artifact_service, + session_service=session_service, + memory_service=memory_service, + credential_service=credential_service, + ) + except Exception as e: + click.secho(f'Error reloading agent: {e}', fg='red') + + sys.stdout.write('\n[user]: ') + sys.stdout.flush() + continue + else: + reload_task.cancel() + query = input_task.result() + if not query or not query.strip(): + if dev: + sys.stdout.write('[user]: ') + sys.stdout.flush() continue - if query == 'exit': + if query.strip() == 'exit': break async with Aclosing( runner.run_async( @@ -130,6 +221,11 @@ async def run_interactively( if event.content and event.content.parts: if text := ''.join(part.text or '' for part in event.content.parts): click.echo(f'[{event.author}]: {text}') + + if dev: + sys.stdout.write('\n[user]: ') + sys.stdout.flush() + await runner.close() @@ -141,6 +237,7 @@ async def run_cli( saved_session_file: Optional[str] = None, save_session: bool, session_id: Optional[str] = None, + dev: bool = False, session_service_uri: Optional[str] = None, artifact_service_uri: Optional[str] = None, memory_service_uri: Optional[str] = None, @@ -203,6 +300,17 @@ async def run_cli( credential_service = InMemoryCredentialService() + observer = None + reload_event = None + if dev: + loop = asyncio.get_running_loop() + reload_event = asyncio.Event() + event_handler = DevModeChangeHandler(loop, reload_event) + observer = Observer() + observer.schedule(event_handler, path=str(agent_root), recursive=True) + observer.start() + click.secho(f"Auto-reload enabled - watching for file changes in {agent_folder_name}...", fg="green") + # Helper function for printing events def _print_event(event) -> None: content = event.content @@ -250,6 +358,10 @@ def _print_event(event) -> None: session_service, credential_service, memory_service=memory_service, + dev=dev, + reload_event=reload_event, + agent_loader=agent_loader, + agent_folder_name=agent_folder_name, ) else: session = await session_service.create_session( @@ -263,6 +375,10 @@ def _print_event(event) -> None: session_service, credential_service, memory_service=memory_service, + dev=dev, + reload_event=reload_event, + agent_loader=agent_loader, + agent_folder_name=agent_folder_name, ) if save_session: @@ -281,3 +397,7 @@ def _print_event(event) -> None: ) print('Session saved to', session_path) + + if observer: + observer.stop() + observer.join() diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index f55a8f1084..b97c16b2ed 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -618,6 +618,12 @@ def wrapper(*args, **kwargs): ), callback=validate_exclusive, ) +@click.option( + "--dev", + is_flag=True, + default=False, + help="Optional. Enable development mode with automatic agent reloading on file changes.", +) @click.argument( "agent", type=click.Path( @@ -630,6 +636,7 @@ def cli_run( session_id: Optional[str], replay: Optional[str], resume: Optional[str], + dev: bool = False, session_service_uri: Optional[str] = None, artifact_service_uri: Optional[str] = None, memory_service_uri: Optional[str] = None, @@ -656,6 +663,7 @@ def cli_run( saved_session_file=resume, save_session=save_session, session_id=session_id, + dev=dev, session_service_uri=session_service_uri, artifact_service_uri=artifact_service_uri, memory_service_uri=memory_service_uri, diff --git a/tests/unittests/cli/utils/test_cli.py b/tests/unittests/cli/utils/test_cli.py index f7df1bf17f..1306fc9f18 100644 --- a/tests/unittests/cli/utils/test_cli.py +++ b/tests/unittests/cli/utils/test_cli.py @@ -519,3 +519,68 @@ async def test_run_interactively_whitespace_and_exit( # verify: assistant echoed once with 'echo:hello' assert any("echo:hello" in m for m in echoed) + + +@pytest.mark.asyncio +async def test_run_interactively_dev_reload( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """run_interactively should reload the agent when reload_event is set.""" + import asyncio + import sys + import time + + session_service = InMemorySessionService() + sess = await session_service.create_session(app_name="dummy", user_id="u") + artifact_service = InMemoryArtifactService() + credential_service = InMemoryCredentialService() + root_agent = BaseAgent(name="root") + + reload_event = asyncio.Event() + sys_stdin_readline_calls = [] + + def mock_readline(): + sys_stdin_readline_calls.append(True) + if len(sys_stdin_readline_calls) == 1: + # Return a normal query first + return "hello\n" + elif len(sys_stdin_readline_calls) == 2: + # Sleep a bit to allow the loop to run, then trigger the reload + time.sleep(0.1) + # In tests, we need to set the event thread-safely + loop = asyncio.get_event_loop() + loop.call_soon_threadsafe(reload_event.set) + time.sleep(0.1) + return "exit\n" + return "exit\n" + + monkeypatch.setattr(sys.stdin, "readline", mock_readline) + + echoed: list[str] = [] + monkeypatch.setattr(click, "echo", lambda msg, **kw: echoed.append(msg)) + monkeypatch.setattr(click, "secho", lambda msg, **kw: echoed.append(msg)) + + class DummyAgentLoader: + removed = False + reloaded = False + def remove_agent_from_cache(self, name): + self.removed = True + + def load_agent(self, name): + self.reloaded = True + return BaseAgent(name="reloaded_root") + + loader = DummyAgentLoader() + + await cli.run_interactively( + root_agent, artifact_service, sess, session_service, credential_service, + dev=True, reload_event=reload_event, agent_loader=loader, agent_folder_name="dummy_folder" + ) + + # Check that the agent handled the first message + assert any("echo:hello" in m for m in echoed) + # Check that the reload message was printed + assert any("reloading agent..." in m for m in echoed) + # Check that the loader cache was manipulated + assert loader.removed is True + assert loader.reloaded is True From 255d86eea0c4cdab7303544f1ed846c48495c600 Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Sun, 1 Mar 2026 22:54:03 +0530 Subject: [PATCH 2/8] feat: improve hot-reloading robustness and error reporting --- src/google/adk/cli/cli.py | 203 +++++++++++++++++++------------------- 1 file changed, 103 insertions(+), 100 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 9f7013ce23..57b7997e67 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -152,7 +152,8 @@ def _read_input(): line = sys.stdin.readline() if not line: break loop.call_soon_threadsafe(input_queue.put_nowait, line) - except Exception: + except Exception as e: + print(f"[ERROR] Exception in stdin reader thread: {e}", file=sys.stderr) break threading.Thread(target=_read_input, daemon=True).start() @@ -173,26 +174,26 @@ def _read_input(): input_task.cancel() reload_event.clear() click.secho('\nChanges detected, reloading agent...', fg='yellow') - await runner.close() - if agent_loader and agent_folder_name: - try: - agent_loader.remove_agent_from_cache(agent_folder_name) - new_agent_or_app = agent_loader.load_agent(agent_folder_name) - app = ( - new_agent_or_app - if isinstance(new_agent_or_app, App) - else App(name=session.app_name, root_agent=new_agent_or_app) - ) - runner = Runner( - app=app, - artifact_service=artifact_service, - session_service=session_service, - memory_service=memory_service, - credential_service=credential_service, - ) - except Exception as e: - click.secho(f'Error reloading agent: {e}', fg='red') + try: + agent_loader.remove_agent_from_cache(agent_folder_name) + new_agent_or_app = agent_loader.load_agent(agent_folder_name) + reloaded_app = ( + new_agent_or_app + if isinstance(new_agent_or_app, App) + else App(name=session.app_name, root_agent=new_agent_or_app) + ) + new_runner = Runner( + app=reloaded_app, + artifact_service=artifact_service, + session_service=session_service, + memory_service=memory_service, + credential_service=credential_service, + ) + await runner.close() + runner = new_runner + except Exception as e: + click.secho(f'Error reloading agent: {e}', fg='red') sys.stdout.write('\n[user]: ') sys.stdout.flush() @@ -307,7 +308,8 @@ async def run_cli( reload_event = asyncio.Event() event_handler = DevModeChangeHandler(loop, reload_event) observer = Observer() - observer.schedule(event_handler, path=str(agent_root), recursive=True) + watch_path = str(agent_root) if agent_root.is_dir() else str(agent_parent_path) + observer.schedule(event_handler, path=watch_path, recursive=True) observer.start() click.secho(f"Auto-reload enabled - watching for file changes in {agent_folder_name}...", fg="green") @@ -322,82 +324,83 @@ def _print_event(event) -> None: author = event.author or 'system' click.echo(f'[{author}]: {"".join(text_parts)}') - if input_file: - session = await run_input_file( - app_name=session_app_name, - user_id=user_id, - agent_or_app=agent_or_app, - artifact_service=artifact_service, - session_service=session_service, - memory_service=memory_service, - credential_service=credential_service, - input_path=input_file, - ) - elif saved_session_file: - # Load the saved session from file - with open(saved_session_file, 'r', encoding='utf-8') as f: - loaded_session = Session.model_validate_json(f.read()) - - # Create a new session in the service, copying state from the file - session = await session_service.create_session( - app_name=session_app_name, - user_id=user_id, - state=loaded_session.state if loaded_session else None, - ) - - # Append events from the file to the new session and display them - if loaded_session: - for event in loaded_session.events: - await session_service.append_event(session, event) - _print_event(event) - - await run_interactively( - agent_or_app, - artifact_service, - session, - session_service, - credential_service, - memory_service=memory_service, - dev=dev, - reload_event=reload_event, - agent_loader=agent_loader, - agent_folder_name=agent_folder_name, - ) - else: - session = await session_service.create_session( - app_name=session_app_name, user_id=user_id - ) - click.echo(f'Running agent {agent_or_app.name}, type exit to exit.') - await run_interactively( - agent_or_app, - artifact_service, - session, - session_service, - credential_service, - memory_service=memory_service, - dev=dev, - reload_event=reload_event, - agent_loader=agent_loader, - agent_folder_name=agent_folder_name, - ) - - if save_session: - session_id = session_id or input('Session ID to save: ') - session_path = agent_root / f'{session_id}.session.json' - - # Fetch the session again to get all the details. - session = await session_service.get_session( - app_name=session.app_name, - user_id=session.user_id, - session_id=session.id, - ) - session_path.write_text( - session.model_dump_json(indent=2, exclude_none=True, by_alias=True), - encoding='utf-8', - ) - - print('Session saved to', session_path) - - if observer: - observer.stop() - observer.join() + try: + if input_file: + session = await run_input_file( + app_name=session_app_name, + user_id=user_id, + agent_or_app=agent_or_app, + artifact_service=artifact_service, + session_service=session_service, + memory_service=memory_service, + credential_service=credential_service, + input_path=input_file, + ) + elif saved_session_file: + # Load the saved session from file + with open(saved_session_file, 'r', encoding='utf-8') as f: + loaded_session = Session.model_validate_json(f.read()) + + # Create a new session in the service, copying state from the file + session = await session_service.create_session( + app_name=session_app_name, + user_id=user_id, + state=loaded_session.state if loaded_session else None, + ) + + # Append events from the file to the new session and display them + if loaded_session: + for event in loaded_session.events: + await session_service.append_event(session, event) + _print_event(event) + + await run_interactively( + agent_or_app, + artifact_service, + session, + session_service, + credential_service, + memory_service=memory_service, + dev=dev, + reload_event=reload_event, + agent_loader=agent_loader, + agent_folder_name=agent_folder_name, + ) + else: + session = await session_service.create_session( + app_name=session_app_name, user_id=user_id + ) + click.echo(f'Running agent {agent_or_app.name}, type exit to exit.') + await run_interactively( + agent_or_app, + artifact_service, + session, + session_service, + credential_service, + memory_service=memory_service, + dev=dev, + reload_event=reload_event, + agent_loader=agent_loader, + agent_folder_name=agent_folder_name, + ) + + if save_session: + session_id = session_id or input('Session ID to save: ') + session_path = agent_root / f'{session_id}.session.json' + + # Fetch the session again to get all the details. + session = await session_service.get_session( + app_name=session.app_name, + user_id=session.user_id, + session_id=session.id, + ) + session_path.write_text( + session.model_dump_json(indent=2, exclude_none=True, by_alias=True), + encoding='utf-8', + ) + + print('Session saved to', session_path) + finally: + if observer: + observer.stop() + observer.join() From 2e6b160677af18bd2d9bf598a1f90e4a82be9676 Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Sun, 1 Mar 2026 23:18:19 +0530 Subject: [PATCH 3/8] refactor: enhance dev mode hot-reloading and follow PEP 8 --- src/google/adk/cli/cli.py | 11 ++--------- tests/unittests/cli/utils/test_cli.py | 6 +++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 57b7997e67..2a5a4d7708 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -55,19 +55,12 @@ def __init__(self, loop: asyncio.AbstractEventLoop, reload_event: asyncio.Event) self.loop = loop self.reload_event = reload_event - def _handle_change(self, event): + def on_any_event(self, event): if event.is_directory: return - if event.src_path.endswith('.py') or event.src_path.endswith('.yaml'): + if event.src_path.endswith(('.py', '.yaml')) or getattr(event, 'dest_path', '').endswith(('.py', '.yaml')): self.loop.call_soon_threadsafe(self.reload_event.set) - def on_modified(self, event): - self._handle_change(event) - - def on_created(self, event): - self._handle_change(event) - - class InputFile(BaseModel): state: dict[str, object] queries: list[str] diff --git a/tests/unittests/cli/utils/test_cli.py b/tests/unittests/cli/utils/test_cli.py index 1306fc9f18..a5165348b4 100644 --- a/tests/unittests/cli/utils/test_cli.py +++ b/tests/unittests/cli/utils/test_cli.py @@ -16,9 +16,12 @@ from __future__ import annotations +import asyncio import json from pathlib import Path +import sys from textwrap import dedent +import time import types from typing import Any from typing import Dict @@ -526,9 +529,6 @@ async def test_run_interactively_dev_reload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """run_interactively should reload the agent when reload_event is set.""" - import asyncio - import sys - import time session_service = InMemorySessionService() sess = await session_service.create_session(app_name="dummy", user_id="u") From 0b2b3dce55ec4dc43982d5e0ecceeb931358604d Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Sun, 1 Mar 2026 23:37:12 +0530 Subject: [PATCH 4/8] refactor: enhance hot-reloading robustness and fix input thread deadlocks --- src/google/adk/cli/cli.py | 17 +++++++++++------ tests/unittests/cli/utils/test_cli.py | 5 +++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 2a5a4d7708..b7761c54ed 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -49,6 +49,7 @@ class DevModeChangeHandler(FileSystemEventHandler): """Watchdog event handler to trigger agent reload upon file changes.""" + WATCHED_EXTENSIONS = ('.py', '.yaml') def __init__(self, loop: asyncio.AbstractEventLoop, reload_event: asyncio.Event): super().__init__() @@ -58,7 +59,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop, reload_event: asyncio.Event) def on_any_event(self, event): if event.is_directory: return - if event.src_path.endswith(('.py', '.yaml')) or getattr(event, 'dest_path', '').endswith(('.py', '.yaml')): + if event.src_path.endswith(self.WATCHED_EXTENSIONS) or getattr(event, 'dest_path', '').endswith(self.WATCHED_EXTENSIONS): self.loop.call_soon_threadsafe(self.reload_event.set) class InputFile(BaseModel): @@ -138,16 +139,18 @@ async def run_interactively( if dev: loop = asyncio.get_running_loop() input_queue = asyncio.Queue() + _EOF_SENTINEL = object() def _read_input(): - while True: - try: + try: + while True: line = sys.stdin.readline() if not line: break loop.call_soon_threadsafe(input_queue.put_nowait, line) - except Exception as e: - print(f"[ERROR] Exception in stdin reader thread: {e}", file=sys.stderr) - break + except Exception as e: + print(f"[ERROR] Exception in stdin reader thread: {e}", file=sys.stderr) + finally: + loop.call_soon_threadsafe(input_queue.put_nowait, _EOF_SENTINEL) threading.Thread(target=_read_input, daemon=True).start() sys.stdout.write('[user]: ') @@ -194,6 +197,8 @@ def _read_input(): else: reload_task.cancel() query = input_task.result() + if query is _EOF_SENTINEL: + break if not query or not query.strip(): if dev: diff --git a/tests/unittests/cli/utils/test_cli.py b/tests/unittests/cli/utils/test_cli.py index a5165348b4..893c674251 100644 --- a/tests/unittests/cli/utils/test_cli.py +++ b/tests/unittests/cli/utils/test_cli.py @@ -539,6 +539,8 @@ async def test_run_interactively_dev_reload( reload_event = asyncio.Event() sys_stdin_readline_calls = [] + main_loop = asyncio.get_running_loop() + def mock_readline(): sys_stdin_readline_calls.append(True) if len(sys_stdin_readline_calls) == 1: @@ -548,8 +550,7 @@ def mock_readline(): # Sleep a bit to allow the loop to run, then trigger the reload time.sleep(0.1) # In tests, we need to set the event thread-safely - loop = asyncio.get_event_loop() - loop.call_soon_threadsafe(reload_event.set) + main_loop.call_soon_threadsafe(reload_event.set) time.sleep(0.1) return "exit\n" return "exit\n" From 26554b6dfcc333a495579bc912a138e72c844383 Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Sun, 1 Mar 2026 23:51:02 +0530 Subject: [PATCH 5/8] refactor(cli): enforce strict dev path validation and robust thread shutdown --- src/google/adk/cli/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index b7761c54ed..8d5a5e528b 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -150,7 +150,8 @@ def _read_input(): except Exception as e: print(f"[ERROR] Exception in stdin reader thread: {e}", file=sys.stderr) finally: - loop.call_soon_threadsafe(input_queue.put_nowait, _EOF_SENTINEL) + if not loop.is_closed(): + loop.call_soon_threadsafe(input_queue.put_nowait, _EOF_SENTINEL) threading.Thread(target=_read_input, daemon=True).start() sys.stdout.write('[user]: ') @@ -306,7 +307,9 @@ async def run_cli( reload_event = asyncio.Event() event_handler = DevModeChangeHandler(loop, reload_event) observer = Observer() - watch_path = str(agent_root) if agent_root.is_dir() else str(agent_parent_path) + if not agent_root.is_dir(): + raise RuntimeError(f"Agent root directory not found or is not a directory: {agent_root}") + watch_path = str(agent_root) observer.schedule(event_handler, path=watch_path, recursive=True) observer.start() click.secho(f"Auto-reload enabled - watching for file changes in {agent_folder_name}...", fg="green") From ba34e462950ed628fd6536b94eb0ba678d23d4a6 Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Mon, 2 Mar 2026 00:03:44 +0530 Subject: [PATCH 6/8] refactor(cli): extract prompt formatting and reload logic to helper functions --- src/google/adk/cli/cli.py | 69 +++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 8d5a5e528b..76e5e57203 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -135,12 +135,42 @@ async def run_interactively( memory_service=memory_service, credential_service=credential_service, ) + runner_ref = [runner] if dev: loop = asyncio.get_running_loop() input_queue = asyncio.Queue() _EOF_SENTINEL = object() + def _prompt_user(new_line: bool = False): + prompt = '\n[user]: ' if new_line else '[user]: ' + sys.stdout.write(prompt) + sys.stdout.flush() + + async def _handle_reload(): + click.secho('\nChanges detected, reloading agent...', fg='yellow') + if not (agent_loader and agent_folder_name): + return + try: + agent_loader.remove_agent_from_cache(agent_folder_name) + new_agent_or_app = agent_loader.load_agent(agent_folder_name) + reloaded_app = ( + new_agent_or_app + if isinstance(new_agent_or_app, App) + else App(name=session.app_name, root_agent=new_agent_or_app) + ) + new_runner = Runner( + app=reloaded_app, + artifact_service=artifact_service, + session_service=session_service, + memory_service=memory_service, + credential_service=credential_service, + ) + await runner_ref[0].close() + runner_ref[0] = new_runner + except Exception as e: + click.secho(f'Error reloading agent: {e}', fg='red') + def _read_input(): try: while True: @@ -154,8 +184,7 @@ def _read_input(): loop.call_soon_threadsafe(input_queue.put_nowait, _EOF_SENTINEL) threading.Thread(target=_read_input, daemon=True).start() - sys.stdout.write('[user]: ') - sys.stdout.flush() + _prompt_user() while True: if not dev or reload_event is None: @@ -170,30 +199,10 @@ def _read_input(): if reload_task in done: input_task.cancel() reload_event.clear() - click.secho('\nChanges detected, reloading agent...', fg='yellow') - if agent_loader and agent_folder_name: - try: - agent_loader.remove_agent_from_cache(agent_folder_name) - new_agent_or_app = agent_loader.load_agent(agent_folder_name) - reloaded_app = ( - new_agent_or_app - if isinstance(new_agent_or_app, App) - else App(name=session.app_name, root_agent=new_agent_or_app) - ) - new_runner = Runner( - app=reloaded_app, - artifact_service=artifact_service, - session_service=session_service, - memory_service=memory_service, - credential_service=credential_service, - ) - await runner.close() - runner = new_runner - except Exception as e: - click.secho(f'Error reloading agent: {e}', fg='red') - sys.stdout.write('\n[user]: ') - sys.stdout.flush() + await _handle_reload() + + _prompt_user(new_line=True) continue else: reload_task.cancel() @@ -203,13 +212,12 @@ def _read_input(): if not query or not query.strip(): if dev: - sys.stdout.write('[user]: ') - sys.stdout.flush() + _prompt_user() continue if query.strip() == 'exit': break async with Aclosing( - runner.run_async( + runner_ref[0].run_async( user_id=session.user_id, session_id=session.id, new_message=types.Content( @@ -223,10 +231,9 @@ def _read_input(): click.echo(f'[{event.author}]: {text}') if dev: - sys.stdout.write('\n[user]: ') - sys.stdout.flush() + _prompt_user(new_line=True) - await runner.close() + await runner_ref[0].close() async def run_cli( From 94dd45a1805a4a59a8e91f43b288f4679b746e8a Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Mon, 2 Mar 2026 00:09:49 +0530 Subject: [PATCH 7/8] refactor(cli): use nonlocal for runner state rebinding --- src/google/adk/cli/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 76e5e57203..eea4b5be65 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -135,7 +135,6 @@ async def run_interactively( memory_service=memory_service, credential_service=credential_service, ) - runner_ref = [runner] if dev: loop = asyncio.get_running_loop() @@ -148,6 +147,7 @@ def _prompt_user(new_line: bool = False): sys.stdout.flush() async def _handle_reload(): + nonlocal runner click.secho('\nChanges detected, reloading agent...', fg='yellow') if not (agent_loader and agent_folder_name): return @@ -166,8 +166,8 @@ async def _handle_reload(): memory_service=memory_service, credential_service=credential_service, ) - await runner_ref[0].close() - runner_ref[0] = new_runner + await runner.close() + runner = new_runner except Exception as e: click.secho(f'Error reloading agent: {e}', fg='red') @@ -217,7 +217,7 @@ def _read_input(): if query.strip() == 'exit': break async with Aclosing( - runner_ref[0].run_async( + runner.run_async( user_id=session.user_id, session_id=session.id, new_message=types.Content( @@ -233,7 +233,7 @@ def _read_input(): if dev: _prompt_user(new_line=True) - await runner_ref[0].close() + await runner.close() async def run_cli( From 4143f63ef2532b01daaf82affb013d4731b4c3a6 Mon Sep 17 00:00:00 2001 From: Reddy Durgeshwant Date: Mon, 2 Mar 2026 00:15:49 +0530 Subject: [PATCH 8/8] feat(cli): enhance error diagnostics in stdin reader thread --- src/google/adk/cli/cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index eea4b5be65..e46d435bc8 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -177,8 +177,10 @@ def _read_input(): line = sys.stdin.readline() if not line: break loop.call_soon_threadsafe(input_queue.put_nowait, line) - except Exception as e: - print(f"[ERROR] Exception in stdin reader thread: {e}", file=sys.stderr) + except Exception: + import traceback + print("[ERROR] Exception in stdin reader thread:", file=sys.stderr) + traceback.print_exc(file=sys.stderr) finally: if not loop.is_closed(): loop.call_soon_threadsafe(input_queue.put_nowait, _EOF_SENTINEL)