Skip to content

Commit e3db2d0

Browse files
GWealecopybara-github
authored andcommitted
fix: Propagate RunConfig custom metadata to all events
Adds a method to merge custom metadata from the RunConfig into each Event. This metadata is applied to events generated by the agent, early exit events, and the initial user message event. Close #3953 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 852433171
1 parent 4ddb2cb commit e3db2d0

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

src/google/adk/runners.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ def _has_non_empty_transcription_text(transcription) -> bool:
8484
)
8585

8686

87+
def _apply_run_config_custom_metadata(
88+
event: Event, run_config: RunConfig | None
89+
) -> None:
90+
"""Merges run-level custom metadata into the event, if present."""
91+
if not run_config or not run_config.custom_metadata:
92+
return
93+
94+
event.custom_metadata = {
95+
**run_config.custom_metadata,
96+
**(event.custom_metadata or {}),
97+
}
98+
99+
87100
class Runner:
88101
"""The Runner class is used to run agents.
89102
@@ -695,6 +708,9 @@ async def _exec_with_plugin(
695708
author='model',
696709
content=early_exit_result,
697710
)
711+
_apply_run_config_custom_metadata(
712+
early_exit_event, invocation_context.run_config
713+
)
698714
if self._should_append_event(early_exit_event, is_live_call):
699715
await self.session_service.append_event(
700716
session=session,
@@ -721,6 +737,9 @@ async def _exec_with_plugin(
721737

722738
async with Aclosing(execute_fn(invocation_context)) as agen:
723739
async for event in agen:
740+
_apply_run_config_custom_metadata(
741+
event, invocation_context.run_config
742+
)
724743
if is_live_call:
725744
if event.partial and _is_transcription(event):
726745
is_transcribing = True
@@ -775,7 +794,13 @@ async def _exec_with_plugin(
775794
modified_event = await plugin_manager.run_on_event_callback(
776795
invocation_context=invocation_context, event=event
777796
)
778-
yield (modified_event if modified_event else event)
797+
if modified_event:
798+
_apply_run_config_custom_metadata(
799+
modified_event, invocation_context.run_config
800+
)
801+
yield modified_event
802+
else:
803+
yield event
779804

780805
# Step 4: Run the after_run callbacks to perform global cleanup tasks or
781806
# finalizing logs and metrics data.
@@ -846,6 +871,7 @@ async def _append_new_message_to_session(
846871
author='user',
847872
content=new_message,
848873
)
874+
_apply_run_config_custom_metadata(event, invocation_context.run_config)
849875
# If new_message is a function response, find the matching function call
850876
# and use its branch as the new event's branch.
851877
if function_call := invocation_context._find_matching_function_call(event):

tests/unittests/test_runners.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
from pathlib import Path
1717
import sys
1818
import textwrap
19+
from typing import AsyncGenerator
1920
from typing import Optional
2021
from unittest.mock import AsyncMock
2122

2223
from google.adk.agents.base_agent import BaseAgent
2324
from google.adk.agents.context_cache_config import ContextCacheConfig
2425
from google.adk.agents.invocation_context import InvocationContext
2526
from google.adk.agents.llm_agent import LlmAgent
27+
from google.adk.agents.run_config import RunConfig
2628
from google.adk.apps.app import App
2729
from google.adk.apps.app import ResumabilityConfig
2830
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
@@ -54,7 +56,9 @@ def __init__(
5456
if parent_agent:
5557
self.parent_agent = parent_agent
5658

57-
async def _run_async_impl(self, invocation_context):
59+
async def _run_async_impl(
60+
self, invocation_context: InvocationContext
61+
) -> AsyncGenerator[Event, None]:
5862
yield Event(
5963
invocation_id=invocation_context.invocation_id,
6064
author=self.name,
@@ -78,7 +82,9 @@ def __init__(
7882
self.disallow_transfer_to_parent = disallow_transfer_to_parent
7983
self.parent_agent = parent_agent
8084

81-
async def _run_async_impl(self, invocation_context):
85+
async def _run_async_impl(
86+
self, invocation_context: InvocationContext
87+
) -> AsyncGenerator[Event, None]:
8288
yield Event(
8389
invocation_id=invocation_context.invocation_id,
8490
author=self.name,
@@ -88,6 +94,25 @@ async def _run_async_impl(self, invocation_context):
8894
)
8995

9096

97+
class MockAgentWithMetadata(BaseAgent):
98+
"""Mock agent that returns event-level custom metadata."""
99+
100+
def __init__(self, name: str):
101+
super().__init__(name=name, sub_agents=[])
102+
103+
async def _run_async_impl(
104+
self, invocation_context: InvocationContext
105+
) -> AsyncGenerator[Event, None]:
106+
yield Event(
107+
invocation_id=invocation_context.invocation_id,
108+
author=self.name,
109+
content=types.Content(
110+
role="model", parts=[types.Part(text="Test response")]
111+
),
112+
custom_metadata={"event_key": "event_value"},
113+
)
114+
115+
91116
class MockPlugin(BasePlugin):
92117
"""Mock plugin for unit testing."""
93118

@@ -495,6 +520,41 @@ def test_is_transferable_across_agent_tree_with_non_llm_agent(self):
495520
assert result is False
496521

497522

523+
@pytest.mark.asyncio
524+
async def test_run_config_custom_metadata_propagates_to_events():
525+
session_service = InMemorySessionService()
526+
runner = Runner(
527+
app_name=TEST_APP_ID,
528+
agent=MockAgentWithMetadata("metadata_agent"),
529+
session_service=session_service,
530+
artifact_service=InMemoryArtifactService(),
531+
)
532+
await session_service.create_session(
533+
app_name=TEST_APP_ID, user_id=TEST_USER_ID, session_id=TEST_SESSION_ID
534+
)
535+
536+
run_config = RunConfig(custom_metadata={"request_id": "req-1"})
537+
events = [
538+
event
539+
async for event in runner.run_async(
540+
user_id=TEST_USER_ID,
541+
session_id=TEST_SESSION_ID,
542+
new_message=types.Content(role="user", parts=[types.Part(text="hi")]),
543+
run_config=run_config,
544+
)
545+
]
546+
547+
assert events[0].custom_metadata is not None
548+
assert events[0].custom_metadata["request_id"] == "req-1"
549+
assert events[0].custom_metadata["event_key"] == "event_value"
550+
551+
session = await session_service.get_session(
552+
app_name=TEST_APP_ID, user_id=TEST_USER_ID, session_id=TEST_SESSION_ID
553+
)
554+
user_event = next(event for event in session.events if event.author == "user")
555+
assert user_event.custom_metadata == {"request_id": "req-1"}
556+
557+
498558
class TestRunnerWithPlugins:
499559
"""Tests for Runner with plugins."""
500560

0 commit comments

Comments
 (0)