From cf622442320cfb79248a1e44adcb1867293ee1fc Mon Sep 17 00:00:00 2001 From: Jah-yee Date: Mon, 13 Apr 2026 21:33:16 +0800 Subject: [PATCH 1/2] fix: clarify agent directory formats in adk web/api_server docs Updates documentation for 'adk web' and 'adk api_server' commands to clarify the multiple supported agent directory formats: - {agent_name}/agent.py (module with root_agent) - {agent_name}.py (module with root_agent) - {agent_name}/__init__.py (package with root_agent or app) - {agent_name}/root_agent.yaml (YAML config folder) The implementation already supports all these formats via AgentLoader, but the docstring only mentioned the YAML case, causing user confusion per issue #4425. Signed-off-by: RoomWithOutRoof --- src/google/adk/cli/cli_tools_click.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 07ccc15892..a41de4ef72 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -1632,8 +1632,12 @@ def cli_web( ): """Starts a FastAPI server with Web UI for agents. - AGENTS_DIR: The directory of agents, where each subdirectory is a single - agent, containing at least `__init__.py` and `agent.py` files. + AGENTS_DIR: The directory of agents. Each subdirectory (or file) can contain an agent + loaded via multiple formats: + - `{agent_name}/agent.py` (module with `root_agent`) + - `{agent_name}.py` (module with `root_agent`) + - `{agent_name}/__init__.py` (package with `root_agent` or `app`) + - `{agent_name}/root_agent.yaml` (YAML config folder) Example: @@ -1745,8 +1749,12 @@ def cli_api_server( ): """Starts a FastAPI server for agents. - AGENTS_DIR: The directory of agents, where each subdirectory is a single - agent, containing at least `__init__.py` and `agent.py` files. + AGENTS_DIR: The directory of agents. Each subdirectory (or file) can contain an agent + loaded via multiple formats: + - `{agent_name}/agent.py` (module with `root_agent`) + - `{agent_name}.py` (module with `root_agent`) + - `{agent_name}/__init__.py` (package with `root_agent` or `app`) + - `{agent_name}/root_agent.yaml` (YAML config folder) Example: From 7781b67a58b15a51fd405a0beeffabc84470087a Mon Sep 17 00:00:00 2001 From: Jah-yee Date: Tue, 14 Apr 2026 01:13:30 +0800 Subject: [PATCH 2/2] fix: run on_event_callback before append_event to persist plugin modifications Previously, on_event_callback was executed AFTER append_event, causing plugin modifications to events (e.g., custom_metadata) to only be visible in the real-time event stream but not persisted to the database. This fix reorders the execution so that: 1. on_event_callback runs first to modify the event 2. append_event stores the (possibly modified) event to DB 3. The event is then yielded to the client This ensures plugin modifications are persisted and visible when fetching conversation history. Fixes: google/adk-python#3990 --- src/google/adk/runners.py | 95 +++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 3a8e49c3f2..75833f83c5 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -791,34 +791,6 @@ def _should_append_event(self, event: Event, is_live_call: bool) -> bool: return False return True - def _get_output_event( - self, - *, - original_event: Event, - modified_event: Event | None, - run_config: RunConfig | None, - ) -> Event: - """Returns the event that should be persisted and yielded. - - Plugins may return a replacement event that only overrides a subset of - fields. Merge those changes onto the original event so the streamed event - and the persisted event stay aligned without losing the original event - identity. - """ - if modified_event is None: - return original_event - - _apply_run_config_custom_metadata(modified_event, run_config) - update = {} - for field_name in modified_event.model_fields_set: - if field_name in {'id', 'invocation_id', 'timestamp'}: - continue - update[field_name] = modified_event.__dict__[field_name] - output_event = original_event.model_copy(update=update) - if not output_event.author: - output_event.author = original_event.author - return output_event - async def _exec_with_plugin( self, invocation_context: InvocationContext, @@ -853,12 +825,26 @@ async def _exec_with_plugin( _apply_run_config_custom_metadata( early_exit_event, invocation_context.run_config ) - if self._should_append_event(early_exit_event, is_live_call): + # Step 3: Run the on_event callbacks to optionally modify the event. + # This MUST run before append_event so modifications are persisted. + modified_early_exit_event = await plugin_manager.run_on_event_callback( + invocation_context=invocation_context, event=early_exit_event + ) + if modified_early_exit_event: + _apply_run_config_custom_metadata( + modified_early_exit_event, invocation_context.run_config + ) + event_to_append = modified_early_exit_event + else: + event_to_append = early_exit_event + # Step 4: Append the (possibly modified) event to the database. + if self._should_append_event(event_to_append, is_live_call): await self.session_service.append_event( session=session, - event=early_exit_event, + event=event_to_append, ) - yield early_exit_event + # Step 5: Yield the modified event to the client. + yield modified_early_exit_event if modified_early_exit_event else early_exit_event else: # Step 2: Otherwise continue with normal execution # Note for live/bidi: @@ -882,24 +868,13 @@ async def _exec_with_plugin( _apply_run_config_custom_metadata( event, invocation_context.run_config ) - # Step 3: Run the on_event callbacks before persisting so callback - # changes are stored in the session and match the streamed event. - modified_event = await plugin_manager.run_on_event_callback( - invocation_context=invocation_context, event=event - ) - output_event = self._get_output_event( - original_event=event, - modified_event=modified_event, - run_config=invocation_context.run_config, - ) - if is_live_call: if event.partial and _is_transcription(event): is_transcribing = True if is_transcribing and _is_tool_call_or_response(event): # only buffer function call and function response event which is # non-partial - buffered_events.append(output_event) + buffered_events.append(event) continue # Note for live/bidi: for audio response, it's considered as # non-partial event(event.partial=None) @@ -907,6 +882,17 @@ async def _exec_with_plugin( # non-partial event; event.partial=True is considered as partial # event. if event.partial is not True: + # Step 3: Run the on_event callbacks to optionally modify the event. + # This MUST run before append_event so modifications are persisted. + modified_event = await plugin_manager.run_on_event_callback( + invocation_context=invocation_context, event=event + ) + if modified_event: + _apply_run_config_custom_metadata( + modified_event, invocation_context.run_config + ) + event = modified_event # Use modified event for appending + if _is_transcription(event) and ( _has_non_empty_transcription_text(event.input_transcription) or _has_non_empty_transcription_text( @@ -920,7 +906,7 @@ async def _exec_with_plugin( ) if self._should_append_event(event, is_live_call): await self.session_service.append_event( - session=session, event=output_event + session=session, event=event ) for buffered_event in buffered_events: @@ -936,15 +922,28 @@ async def _exec_with_plugin( if self._should_append_event(event, is_live_call): logger.debug('Appending non-buffered event: %s', event) await self.session_service.append_event( - session=session, event=output_event + session=session, event=event ) + # Step 5: Yield the event to the caller. + yield event else: + # Step 3: Run the on_event callbacks to optionally modify the event. + # This MUST run before append_event so modifications are persisted. + modified_event = await plugin_manager.run_on_event_callback( + invocation_context=invocation_context, event=event + ) + if modified_event: + _apply_run_config_custom_metadata( + modified_event, invocation_context.run_config + ) + event = modified_event # Use modified event for appending and yielding + # Step 4: Append the event to the database (after on_event_callback). if event.partial is not True: await self.session_service.append_event( - session=session, event=output_event + session=session, event=event ) - - yield output_event + # Step 5: Yield the event to the caller. + yield event # Step 4: Run the after_run callbacks to perform global cleanup tasks or # finalizing logs and metrics data.