Skip to content

GoogleSearchAgentTool parallel responses fail to serialize deferred GroundingMetadata (MockValSer) #6848

Closed

Description

@andrewrfitz

🔴 Required Information

Describe the Bug:

AgentTool(propagate_grounding_metadata=True)—which GoogleSearchAgentTool
enables—stores a raw google.genai.types.GroundingMetadata instance in session
state under temp:_adk_grounding_metadata. That value reaches
EventActions.state_delta, which is typed as dict[str, Any], so event
serialization relies on Pydantic's duck-typed serialization of the value.

google-genai 2.18.0 and newer configure their generated models with
defer_build=True. A GroundingMetadata produced by nested validation (for
example, as part of Candidate validation, as happens for model responses)
leaves GroundingMetadata.__pydantic_serializer__ as a MockValSer.
Serializing the event then raises:

pydantic_core._pydantic_core.PydanticSerializationError: Error calling function `_serialize_state_delta`: TypeError: 'MockValSer' object is not an instance of 'SchemaSerializer'

The _make_json_serializable fallback introduced by #4748 does not recover
from this case. pydantic_core.to_jsonable_python(value, serialize_unknown=True) raises the same TypeError before
serialize_unknown can apply, so the exception escapes
_serialize_state_delta.

This surfaces in merge_parallel_function_response_events in
google/adk/flows/llm_flows/functions.py, which calls
event.actions.model_dump(exclude_none=True, by_alias=True). When a search call
runs in parallel with another function call, the merge raises and aborts the
invocation.

This is related to #4748 and prior grounding-metadata persistence reports such
as #5840, but it is a distinct failure mode: the value is a valid Pydantic
model whose deferred serializer has not been built.

Steps to Reproduce:

  1. Install the affected versions:

    python -m pip install "google-adk==2.7.1" "google-genai==2.19.0" "pydantic==2.13.4"
  2. Run the snippet under Minimal Reproduction Code below. It requires no
    model call or API key.

  3. Observe the PydanticSerializationError above.

Expected Behavior:

Serializing an event whose state_delta contains grounding metadata propagated
by ADK succeeds, and parallel function-response merging preserves both tool
results. ADK-created state should not depend on whether a deferred class
serializer happened to be built earlier in the process.

Observed Behavior:

EventActions.model_dump(...) raises from
merge_parallel_function_response_events, and the invocation terminates
without a final response.

Environment Details:

  • ADK Library Version: 2.7.1 (also reproduced on 2.7.0)
  • Desktop OS: macOS 26.6 (also reproduced on Linux)
  • Python Version: 3.14.6 (also reproduced on 3.12.6)
  • google-genai: 2.19.0 (also reproduced on 2.18.1)
  • pydantic: 2.13.4 / pydantic-core: 2.46.4

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A—the minimal reproduction performs no model
    call and requires no API key.

🟡 Optional Information

Regression:

This is a google-genai 2.18.0 regression:

  • google-genai==2.17.0: nested GroundingMetadata has a built
    SchemaSerializer; the merge succeeds.
  • google-genai==2.18.0: the shared model config includes defer_build=True;
    nested GroundingMetadata retains a MockValSer; the merge fails.

The failure also reproduces with google-adk==2.6.0 plus
google-genai==2.18.1, so it is not specific to ADK 2.7.x. ADK is affected
because it propagates the raw deferred model through state_delta and then
serializes that state while merging parallel tool responses.

Logs:

Failed to serialize `state_delta`; some values are not JSON-serializable and
will be replaced with a string representation in the persisted event.

Traceback (most recent call last):
  File "google/adk/events/event_actions.py", line 106, in _serialize_state_delta
    return cast(dict[str, Any], handler(value))
TypeError: 'MockValSer' object is not an instance of 'SchemaSerializer'

During fallback handling, `to_jsonable_python(...)` raises the same TypeError.
The resulting PydanticSerializationError escapes the parallel-response merge.

Additional Context:

Two details make this easy to miss in tests:

  • Constructing types.GroundingMetadata(...) directly builds the serializer as
    a side effect. Nested validation via types.Candidate.model_validate(...),
    which mirrors the response-validation path, leaves the MockValSer.
  • Once anything else in the process builds this class serializer, the failure
    disappears for subsequent calls.

Possible fixes, in rough order of narrowness:

  1. Build types.GroundingMetadata before ADK propagates or serializes it, for
    example with types.GroundingMetadata.model_rebuild(force=True). Removing
    defer_build for this model in google-genai would also resolve the root
    behavior.
  2. Store grounding metadata in state_delta in serialized form, then
    revalidate it where base_llm_flow attaches it to the response.
  3. Make _make_json_serializable resilient per value so the fallback cannot
    itself abort event serialization. This is defense in depth and would
    degrade the metadata rather than preserve it.

Happy to send a PR for the preferred direction.

Minimal Reproduction Code:

from google.adk.events import Event, EventActions
from google.adk.flows.llm_flows.functions import (
    merge_parallel_function_response_events,
)
from google.genai import types


candidate = types.Candidate.model_validate(
    {"groundingMetadata": {"webSearchQueries": ["some query"]}}
)
print(type(types.GroundingMetadata.__pydantic_serializer__).__name__)
# MockValSer


def response_event(name: str, state_delta: dict) -> Event:
    return Event(
        invocation_id="inv",
        author="my_agent",
        content=types.Content(
            role="user",
            parts=[
                types.Part.from_function_response(
                    name=name,
                    response={"ok": True},
                )
            ],
        ),
        actions=EventActions(state_delta=state_delta),
    )


events = [
    response_event(
        "search_agent",
        {"temp:_adk_grounding_metadata": candidate.grounding_metadata},
    ),
    response_event("other_tool", {"temp:other": 1}),
]

merge_parallel_function_response_events(events)
# PydanticSerializationError

The fallback also fails directly:

from pydantic_core import to_jsonable_python

to_jsonable_python(
    {"k": candidate.grounding_metadata},
    serialize_unknown=True,
)
# TypeError: 'MockValSer' object is not an instance of 'SchemaSerializer'

How often has this issue occurred?:

  • Always (100%) when a deferred GroundingMetadata is present in one of two
    or more function-response events being merged.

Activity

  1. ALDRIN121 commented on Aug 22, 2026

    @ALDRIN121
    Contributor

    Great analysis — the defer_build/MockValSer failure mode is nasty because it only surfaces in parallel merges.

    I see PR #6849 fixes this case at the source (recursive model_rebuild for deferred Pydantic models), but the issue's own observation is that the _make_json_serializable fallback "does not recover from this case" — to_jsonable_python(value, serialize_unknown=True) raises before serialize_unknown can apply. So any value that trips pydantic-core still aborts _serialize_state_delta and kills the invocation.

    I'd like to contribute the complementary defense-in-depth slice: make _make_json_serializable per-value resilient (walk dicts/lists, convert each leaf under try/except, fall back to repr for values that raise) so the fallback can never crash event serialization, plus a unit test with a deferred-serializer model. Zero overlap with #6849's change — it hardens exactly the fallback path the issue calls out, whatever the next exotic value turns out to be.

    @andrewrfitz — happy to coordinate: if you'd rather keep #6849 self-contained I'll hold and open this as a follow-up once yours lands, or open it alongside now if the maintainers prefer the split.

  2. andrewrfitz commented on Aug 25, 2026

    @andrewrfitz
    Author

    Great analysis — the defer_build/MockValSer failure mode is nasty because it only surfaces in parallel merges.

    I see PR #6849 fixes this case at the source (recursive model_rebuild for deferred Pydantic models), but the issue's own observation is that the _make_json_serializable fallback "does not recover from this case" — to_jsonable_python(value, serialize_unknown=True) raises before serialize_unknown can apply. So any value that trips pydantic-core still aborts _serialize_state_delta and kills the invocation.

    I'd like to contribute the complementary defense-in-depth slice: make _make_json_serializable per-value resilient (walk dicts/lists, convert each leaf under try/except, fall back to repr for values that raise) so the fallback can never crash event serialization, plus a unit test with a deferred-serializer model. Zero overlap with #6849's change — it hardens exactly the fallback path the issue calls out, whatever the next exotic value turns out to be.

    @andrewrfitz — happy to coordinate: if you'd rather keep #6849 self-contained I'll hold and open this as a follow-up once yours lands, or open it alongside now if the maintainers prefer the split.

    Thanks @ALDRIN121! If it's zero overlap, I'm happy to just keep #6849 separate and have your complimentary PR merged as well

  3. llalitkumarrr commented on Aug 25, 2026

    @llalitkumarrr
    Collaborator

    Hello @andrewrfitz,

    Thank you for your contribution. We will review your PR and merge it once all checks pass and the reviewer provides their approval. We will get back to you if any additional information is required, we appreciate your patience in the meantime.

  4. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    and removed
    needs review[Status] The PR/issue is awaiting review from the maintainer
    on Aug 25, 2026
  5. ALDRIN121 commented on Aug 28, 2026

    @ALDRIN121
    Contributor

    Complementary defense-in-depth PR is up: #6932 — makes _make_json_serializable resilient per value (try/except per leaf, repr fallback, warning log with affected paths) so a deferred Pydantic serializer can never abort the invocation. Healthy output is byte-identical to to_jsonable_python (parity-tested). Thanks @andrewrfitz for the green light and @llalitkumarrr for the acknowledgment.

  6. added a commit that references this issue on Sep 1, 2026
    e04d820
  7. DeanChensj commented on Oct 14, 2026

    @DeanChensj
    Collaborator

    Hi @andrewrfitz, thank you for reporting this issue!

    This has been resolved by PR #6932 and merged into the main branch in commit a119dd7.

    We are closing this issue as resolved. If you find that the issue persists on the latest ADK release or if this was closed in error, please feel free to reopen it or leave a comment! Thank you again for your contribution.

  8. github-actions commented on Oct 14, 2026

    @github-actions

    Are you satisfied with the resolution of your issue?
    Yes
    No

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

tools[Component] This issue is related to tools

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions