diff --git a/temporalio/contrib/google_adk_agents/_plugin.py b/temporalio/contrib/google_adk_agents/_plugin.py index 03cb78998..9be321398 100644 --- a/temporalio/contrib/google_adk_agents/_plugin.py +++ b/temporalio/contrib/google_adk_agents/_plugin.py @@ -10,7 +10,8 @@ from temporalio.contrib.google_adk_agents._mcp import TemporalMcpToolSetProvider from temporalio.contrib.google_adk_agents._model import invoke_model from temporalio.contrib.pydantic import ( - PydanticPayloadConverter as _DefaultPydanticPayloadConverter, + PydanticPayloadConverter, + ToJsonOptions, ) from temporalio.converter import DataConverter, DefaultPayloadConverter from temporalio.plugin import SimplePlugin @@ -111,11 +112,16 @@ def _configure_data_converter( self, converter: DataConverter | None ) -> DataConverter: if converter is None: - return DataConverter( - payload_converter_class=_DefaultPydanticPayloadConverter - ) + return DataConverter(payload_converter_class=_AdkPayloadConverter) elif converter.payload_converter_class is DefaultPayloadConverter: return dataclasses.replace( - converter, payload_converter_class=_DefaultPydanticPayloadConverter + converter, payload_converter_class=_AdkPayloadConverter ) return converter + + +class _AdkPayloadConverter(PydanticPayloadConverter): + """PayloadConverter for Google ADK that strips unset None fields.""" + + def __init__(self) -> None: + super().__init__(ToJsonOptions(exclude_unset=True)) diff --git a/tests/contrib/google_adk_agents/test_google_adk_agents.py b/tests/contrib/google_adk_agents/test_google_adk_agents.py index a02e98b3f..e35d58ea6 100644 --- a/tests/contrib/google_adk_agents/test_google_adk_agents.py +++ b/tests/contrib/google_adk_agents/test_google_adk_agents.py @@ -14,6 +14,7 @@ """Integration tests for ADK Temporal support.""" +import json import logging import os import uuid @@ -963,3 +964,41 @@ def supported_models(cls) -> list[str]: assert result.content is not None assert result.content.parts is not None assert result.content.parts[0].text == "hello from litellm" + + +def test_unset_none_fields_stripped() -> None: + """ADK plugin converter strips unset None fields from Pydantic payloads.""" + plugin = GoogleAdkPlugin() + converter = plugin._configure_data_converter(None) + request = LlmRequest( + model="gemini-2.0-flash", + contents=[Content(parts=[Part(text="hello")])], + ) + payloads = converter.payload_converter.to_payloads([request]) + serialized = json.loads(payloads[0].data) + + assert serialized["model"] == "gemini-2.0-flash" + assert "contents" in serialized + for field in ( + "cache_config", + "cache_metadata", + "cacheable_contents_token_count", + "previous_interaction_id", + ): + assert field not in serialized, f"Unset field {field!r} should be stripped" + + +def test_explicitly_set_none_preserved() -> None: + """Explicitly-set None is preserved (exclude_unset, not exclude_none).""" + plugin = GoogleAdkPlugin() + converter = plugin._configure_data_converter(None) + request = LlmRequest( + model="gemini-2.0-flash", + contents=[Content(parts=[Part(text="hello")])], + cache_config=None, + ) + payloads = converter.payload_converter.to_payloads([request]) + serialized = json.loads(payloads[0].data) + + assert "cache_config" in serialized, "Explicitly-set None should be preserved" + assert serialized["cache_config"] is None