Skip to content

fix(agents): keep Literal and Annotated output schemas - #7928

Open
sumedh-aerram wants to merge 3 commits into
google:mainfrom
sumedh-aerram:fix/output-schema-annotated-literal
Open

sumedh-aerram wants to merge 3 commits into
google:mainfrom
sumedh-aerram:fix/output-schema-annotated-literal

Conversation

@sumedh-aerram

@sumedh-aerram sumedh-aerram commented Nov 1, 2026 •

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:
SchemaType is types.SchemaUnion. A typing.Literal[...] or typing.Annotated[...] alias matches none of its alias members, and types.Schema sets from_attributes=True. So pydantic coerces the alias into an empty types.Schema(), with no error. This silently drops the schema for LlmAgent.output_schema, BaseNode.input_schema, BaseNode.output_schema and RequestInput.response_schema.

Solution:

  • utils/_schema_utils.py: SchemaType is now Annotated[types.SchemaUnion, WrapValidator(...)]. The validator returns Literal/Annotated aliases unchanged and passes every other value to the existing union validation. All four fields are fixed in one place, and the generated JSON schema of the fields is unchanged.
  • models/llm_request.py: google-genai raises Unsupported schema type for these aliases in response_schema. So set_output_schema now sends them as response_json_schema, built with the existing schema_to_json_schema (for example Literal["yes", "no"] becomes {"type": "string", "enum": ["yes", "no"]}). Every other schema type is handled as before.
  • agents/config_agent_utils.py: _is_schema_type unwraps Annotated, so that YAML agent configs still resolve output_schema code references now that the field annotation is wrapped.
  • models/anthropic_llm.py: the native Anthropic adapter falls back to response_json_schema when response_schema is unset, so these schemas still get its structured-output tool. Scalar schemas are wrapped as {"response": ...} as before. response_schema keeps precedence.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally. (See below: pre-existing environment-dependent failures only.)

New tests:

  • the alias is preserved on LlmAgent, BaseNode and RequestInput;
  • set_output_schema sends the aliases as responseJsonSchema through google-genai's real request converter;
  • every previously accepted input (BaseModel classes, list[M], dict, types.Schema, unions, Optional) validates and produces the same JSON schema as raw types.SchemaUnion;
  • YAML config resolution of an output_schema reference works;
  • the Anthropic adapter builds the structured-output tool from response_json_schema for Literal and Annotated[Model, Field(...)].

The new regression cases fail on main and pass with the change. The YAML cases also fail if only the config_agent_utils.py change is reverted.

Python 3.11, macOS:

  • Affected suites (tests/unittests/agents tests/unittests/models tests/unittests/utils tests/unittests/workflow plus tests/unittests/events/test_request_input.py): 4895 passed, 1 skipped, 2 xfailed.
  • The same focused suites also pass on Python 3.12, 3.13 and 3.14.
  • Full pytest tests/unittests -n auto (first commit) (in a sandboxed runner): 22543 passed, 47 failed, 3 errors. All failures are environment-related and reproduce on unchanged main: telemetry goldens, A2A, macOS VMaaS, and sandbox restrictions on sockets/file writes (Teams runner, Apigee streaming, samples, dotenv, CrewAI collection). Two tests were flaky under parallel load: a CLI reload test and an eval stdio test. Both pass when run individually, with and without this change.
  • pre-commit on the changed files: clean.

Manual End-to-End (E2E) Tests:

Not run against a live model. Reproduction without a model call:

from typing import Literal
from google.adk.agents import LlmAgent

agent = LlmAgent(name="a", model="gemini-2.5-flash", output_schema=Literal["yes", "no"])
print(agent.output_schema)  # main: Schema() with every field None; this PR: typing.Literal['yes', 'no']

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes. (New and affected tests pass; see the pre-existing failures above.)
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Known limitations, kept out of scope to keep this change small:

  • LiteLLM reads only response_schema, so for these aliases it now sends no response_format. That matches what it already does for int and list[Model] output schemas, which it treats as unsupported for structured output. On main it sent a strict json_schema with an empty schema {}. Supporting them properly needs root $ref inlining and non-object roots in the strict OpenAI conversion, which is a separate change.
  • Optional[Literal[...]] and Literal[...] | None were already preserved, and google-genai accepts them as response_schema, so they are unchanged.
  • Like agents whose output_schema is a BaseModel class, model_dump_json() on an agent with a Literal/Annotated output schema raises a serialization error. On main it returned the empty Schema().
  • validate_schema still does not enforce Literal values or Field constraints (validate_schema skips validation for non-BaseModel schemas (list[str], dict[…], raw JSON Schema) #7280 / fix: validate non-BaseModel schemas in validate_schema #7285).

…rcing to empty Schema

SchemaType is types.SchemaUnion, and types.Schema validates from attributes,
so typing.Literal and typing.Annotated aliases were silently coerced into an
empty Schema() on LlmAgent.output_schema, BaseNode.input_schema/output_schema
and RequestInput.response_schema.

Wrap SchemaType with a validator that keeps these aliases unchanged. Send
them to Gemini as response_json_schema, since google-genai rejects them as
response_schema. Unwrap Annotated in _is_schema_type so YAML agent configs
still resolve output_schema references.

Fixes google#7927
@sumedh-aerram sumedh-aerram changed the title fix(agents): keep Literal and Annotated output schemas instead of coercing to empty Schema fix(agents): keep Literal and Annotated output schemas Nov 1, 2026
Literal and Annotated output schemas now reach the model request as
response_json_schema. Make the native Anthropic adapter fall back to it when
response_schema is unset, so these schemas still get the structured-output
tool. response_schema keeps precedence.

Fixes google#7927
…nnotated-literal-v3

# Conflicts:
#	src/google/adk/models/anthropic_llm.py
#	src/google/adk/models/llm_request.py
#	src/google/adk/utils/_schema_utils.py
#	tests/unittests/agents/test_llm_agent_fields.py
#	tests/unittests/models/test_llm_request.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LlmAgent output_schema silently becomes an empty Schema() for Literal and Annotated types

2 participants