Skip to content

Commit c28b499

Browse files
committed
docs: clarify shallow copy behavior in docstring and add isolation test
- Add note in docstring about shallow copy behavior for nested objects - Add test_metadata_shallow_copy_isolation to verify: - Top-level changes are isolated from original dict - Nested object modifications affect original (shallow copy)
1 parent c40af85 commit c28b499

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

src/google/adk/runners.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ async def run_async(
478478
metadata: Optional per-request metadata that will be passed to callbacks.
479479
This allows passing request-specific context such as user_id, trace_id,
480480
or memory context keys to before_model_callback and other callbacks.
481+
Note: A shallow copy is made of this dictionary, so top-level changes
482+
within callbacks won't affect the original. However, modifications to
483+
nested mutable objects (e.g., nested dicts or lists) will affect the
484+
original.
481485
482486
Yields:
483487
The events generated by the agent.

tests/unittests/test_runners.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,67 @@ def test_llm_request_without_metadata(self):
13751375

13761376
assert llm_request.metadata is None
13771377

1378+
@pytest.mark.asyncio
1379+
async def test_metadata_shallow_copy_isolation(self):
1380+
"""Test that shallow copy isolates top-level changes but shares nested objects."""
1381+
# Track modifications made in callback
1382+
callback_received_metadata = None
1383+
1384+
def before_model_callback(callback_context, llm_request):
1385+
nonlocal callback_received_metadata
1386+
callback_received_metadata = llm_request.metadata
1387+
# Modify top-level key (should NOT affect original due to shallow copy)
1388+
llm_request.metadata["top_level_key"] = "modified_in_callback"
1389+
# Modify nested object (WILL affect original due to shallow copy)
1390+
llm_request.metadata["nested"]["inner_key"] = "modified_nested"
1391+
return LlmResponse(
1392+
content=types.Content(
1393+
role="model", parts=[types.Part(text="Test response")]
1394+
)
1395+
)
1396+
1397+
agent_with_callback = LlmAgent(
1398+
name="callback_agent",
1399+
model="gemini-2.0-flash",
1400+
before_model_callback=before_model_callback,
1401+
)
1402+
1403+
runner_with_callback = Runner(
1404+
app_name="test_app",
1405+
agent=agent_with_callback,
1406+
session_service=self.session_service,
1407+
artifact_service=self.artifact_service,
1408+
)
1409+
1410+
await self.session_service.create_session(
1411+
app_name=TEST_APP_ID, user_id=TEST_USER_ID, session_id=TEST_SESSION_ID
1412+
)
1413+
1414+
# Original metadata with nested mutable object
1415+
original_metadata = {
1416+
"top_level_key": "original_value",
1417+
"nested": {"inner_key": "original_nested"},
1418+
}
1419+
1420+
async for event in runner_with_callback.run_async(
1421+
user_id=TEST_USER_ID,
1422+
session_id=TEST_SESSION_ID,
1423+
new_message=types.Content(
1424+
role="user", parts=[types.Part(text="Hello")]
1425+
),
1426+
metadata=original_metadata,
1427+
):
1428+
pass
1429+
1430+
# Verify callback received metadata
1431+
assert callback_received_metadata is not None
1432+
1433+
# Top-level changes in callback should NOT affect original (shallow copy)
1434+
assert original_metadata["top_level_key"] == "original_value"
1435+
1436+
# Nested object changes in callback WILL affect original (shallow copy behavior)
1437+
assert original_metadata["nested"]["inner_key"] == "modified_nested"
1438+
13781439

13791440
if __name__ == "__main__":
13801441
pytest.main([__file__])

0 commit comments

Comments
 (0)