Skip to content

feat(agent_sdk): add A2UIOutputMode enum for unified prompt generation#1466

Open
jswortz wants to merge 1 commit into
google:mainfrom
jswortz:feat/output-mode
Open

feat(agent_sdk): add A2UIOutputMode enum for unified prompt generation#1466
jswortz wants to merge 1 commit into
google:mainfrom
jswortz:feat/output-mode

Conversation

@jswortz
Copy link
Copy Markdown

@jswortz jswortz commented May 20, 2026

Summary

  • Add A2UIOutputMode enum (TEXT, TOOL) to unify system instruction generation
  • Add output_mode parameter to A2uiSchemaManager.generate_system_prompt()
  • Add TOOL_WORKFLOW_RULES constant with tool-calling oriented workflow rules
  • Add 14 test cases validating mode behavior and backward compatibility

Addresses #1289

Motivation

Developers using SendA2uiToClientToolset (tool-based output) often accidentally double-inject the A2UI schema by also passing include_schema=True to generate_system_prompt(). The toolset's process_llm_request() already injects schema and examples — having them in the system prompt too causes:

  • Longer prompts and higher token costs
  • Confused output where the LLM mixes <a2ui-json> text with send_a2ui_json_to_client tool calls
  • Debugging difficulty — developers don't know what's actually being sent to the LLM

There's currently no API-level way to express "I'm using tool-based output, handle the schema for me."

Approach

A2UIOutputMode makes the output path explicit:

Mode Schema injection Workflow rules LLM output format
TEXT (default) In system prompt DEFAULT_WORKFLOW_RULES <a2ui-json> tags in text
TOOL By SendA2uiToClientToolset TOOL_WORKFLOW_RULES send_a2ui_json_to_client calls

When output_mode=TOOL:

  1. include_schema and include_examples are automatically set to False
  2. A warning is logged if the developer explicitly passed True for these
  3. TOOL_WORKFLOW_RULES instruct the LLM to use the tool function

All existing behavior is unchanged when output_mode is not specified (defaults to TEXT).

Changes

File Change
agent_sdks/python/src/a2ui/schema/output_mode.py NewA2UIOutputMode enum
agent_sdks/python/src/a2ui/schema/constants.py Add TOOL_WORKFLOW_RULES constant
agent_sdks/python/src/a2ui/schema/manager.py Add output_mode parameter to generate_system_prompt()
agent_sdks/python/tests/schema/test_schema_manager.py Add 14 test cases for output mode

Usage

from a2ui.schema.manager import A2uiSchemaManager
from a2ui.schema.output_mode import A2UIOutputMode
from a2ui.basic_catalog.provider import BasicCatalog

manager = A2uiSchemaManager(
    version='0.8',
    catalogs=[BasicCatalog.get_config('0.8')],
)

# TEXT mode — schema in system prompt (default, backward compatible)
text_prompt = manager.generate_system_prompt(
    role_description="You are a restaurant finder.",
    include_schema=True,
    output_mode=A2UIOutputMode.TEXT,
)

# TOOL mode — schema handled by SendA2uiToClientToolset
tool_prompt = manager.generate_system_prompt(
    role_description="You are a restaurant finder.",
    output_mode=A2UIOutputMode.TOOL,
)
# Schema and examples are automatically excluded.
# Use with SendA2uiToClientToolset which injects them via process_llm_request().

Test plan

  • All existing test_schema_manager.py tests pass (backward compatibility)
  • Default (no output_mode) produces identical output to before
  • TEXT mode includes schema and examples when requested
  • TOOL mode excludes schema and examples even when requested
  • TOOL mode uses TOOL_WORKFLOW_RULES referencing send_a2ui_json_to_client
  • TOOL mode preserves role_description, ui_description, workflow_description
  • Works with both v0.8 and v0.9 catalogs
  • TOOL_WORKFLOW_RULES includes top-down ordering requirement
  • ./scripts/fix_format.sh passes (Pyink formatting)

Relationship to #1465

This PR is complementary to #1465 (strict_output mode). They control orthogonal concerns:

  • output_mode controls where the schema goes (system prompt vs tool)
  • strict_output controls how aggressively to enforce A2UI-first output

Both can be used independently and will compose cleanly when merged together.

Introduces A2UIOutputMode.TEXT and A2UIOutputMode.TOOL to
generate_system_prompt(), preventing accidental double-injection
of schemas when using SendA2uiToClientToolset.

Addresses google#1289
@google-cla
Copy link
Copy Markdown

google-cla Bot commented May 20, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an output_mode parameter to A2uiSchemaManager.generate_system_prompt, enabling a TOOL mode that uses specialized workflow rules and avoids redundant schema injection. The changes include a new A2UIOutputMode enum and comprehensive unit tests. Review feedback recommends defining the tool name as a constant for maintainability, addressing an API signature mismatch with the InferenceStrategy base class, and reducing package coupling by removing specific class references from docstrings and log messages.

Comment on lines +68 to +78
TOOL_WORKFLOW_RULES = """
The generated response MUST follow these rules:
- To send UI to the user, call the `send_a2ui_json_to_client` tool with valid A2UI JSON.
- Each response SHOULD include at least one tool call to render UI components.
- You may include brief conversational text alongside tool calls for context.
- The JSON payload passed to the tool MUST be a single, raw JSON object (usually a list of A2UI messages) and MUST validate against the provided A2UI JSON SCHEMA.
- Top-Down Component Ordering: Within the `components` list of a message:
- The 'root' component MUST be the FIRST element.
- Parent components MUST appear before their child components.
This specific ordering allows the streaming parser to yield and render the UI incrementally as it arrives.
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tool name send_a2ui_json_to_client is hardcoded within the workflow rules string. Defining this as a constant improves maintainability and ensures consistency with DEFAULT_WORKFLOW_RULES, which already uses constants for its tags. This also makes it easier for other components to reference the tool name without re-typing the string.

Suggested change
TOOL_WORKFLOW_RULES = """
The generated response MUST follow these rules:
- To send UI to the user, call the `send_a2ui_json_to_client` tool with valid A2UI JSON.
- Each response SHOULD include at least one tool call to render UI components.
- You may include brief conversational text alongside tool calls for context.
- The JSON payload passed to the tool MUST be a single, raw JSON object (usually a list of A2UI messages) and MUST validate against the provided A2UI JSON SCHEMA.
- Top-Down Component Ordering: Within the `components` list of a message:
- The 'root' component MUST be the FIRST element.
- Parent components MUST appear before their child components.
This specific ordering allows the streaming parser to yield and render the UI incrementally as it arrives.
"""
A2UI_TOOL_NAME = "send_a2ui_json_to_client"
TOOL_WORKFLOW_RULES = f"""
The generated response MUST follow these rules:
- To send UI to the user, call the `{A2UI_TOOL_NAME}` tool with valid A2UI JSON.
- Each response SHOULD include at least one tool call to render UI components.
- You may include brief conversational text alongside tool calls for context.
- The JSON payload passed to the tool MUST be a single, raw JSON object (usually a list of A2UI messages) and MUST validate against the provided A2UI JSON SCHEMA.
- Top-Down Component Ordering: Within the `components` list of a message:
- The 'root' component MUST be the FIRST element.
- Parent components MUST appear before their child components.
This specific ordering allows the streaming parser to yield and render the UI incrementally as it arrives.
"""

include_schema: bool = False,
include_examples: bool = False,
validate_examples: bool = False,
output_mode: A2UIOutputMode = A2UIOutputMode.TEXT,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The addition of the output_mode parameter to generate_system_prompt causes a signature mismatch with the InferenceStrategy abstract base class. While Python's default arguments prevent immediate runtime breakage, it creates an inconsistency in the abstract interface. It is recommended to update the InferenceStrategy base class to include this parameter (or a generic **kwargs) to maintain a consistent API across all strategy implementations.

Comment on lines +233 to +236
output_mode: Controls how A2UI instructions are delivered. TEXT (default)
injects the schema into the system prompt. TOOL skips schema and
example injection (handled by SendA2uiToClientToolset) and uses
tool-oriented workflow rules.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring for output_mode explicitly references SendA2uiToClientToolset, which creates a conceptual dependency on the adk package. To maintain better modularity, consider a more generic description of the mode's behavior.

Suggested change
output_mode: Controls how A2UI instructions are delivered. TEXT (default)
injects the schema into the system prompt. TOOL skips schema and
example injection (handled by SendA2uiToClientToolset) and uses
tool-oriented workflow rules.
output_mode: Controls how A2UI instructions are delivered. TEXT (default)
injects the schema into the system prompt. TOOL skips schema and
example injection (as they are expected to be handled by the toolset)
and uses tool-oriented workflow rules.

Comment on lines +245 to +249
logger.warning(
"output_mode=TOOL overrides include_schema and include_examples "
"to False. Schema and examples are injected by "
"SendA2uiToClientToolset.process_llm_request() instead."
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This warning message explicitly references SendA2uiToClientToolset.process_llm_request(), creating a tight coupling between the a2ui.schema package and the a2ui.adk package. The schema package should ideally remain independent of specific ADK implementations. Consider using a more generic message.

Suggested change
logger.warning(
"output_mode=TOOL overrides include_schema and include_examples "
"to False. Schema and examples are injected by "
"SendA2uiToClientToolset.process_llm_request() instead."
)
logger.warning(
"output_mode=TOOL overrides include_schema and include_examples "
"to False. In this mode, schema and examples are expected to be "
"provided by the toolset implementation (e.g., via tool-specific "
"instructions)."
)

Comment on lines +27 to +31
TOOL: Schema is provided via SendA2uiToClientToolset's
process_llm_request(). The LLM calls the send_a2ui_json_to_client
tool to deliver A2UI JSON. When using this mode,
generate_system_prompt() automatically skips schema and example
injection to prevent double-injection.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring for TOOL mode references specific classes in the a2ui.adk package. To maintain package independence and follow clean architecture principles, consider describing the behavior more generically in terms of how the prompt is constructed and how the LLM is expected to respond.

Suggested change
TOOL: Schema is provided via SendA2uiToClientToolset's
process_llm_request(). The LLM calls the send_a2ui_json_to_client
tool to deliver A2UI JSON. When using this mode,
generate_system_prompt() automatically skips schema and example
injection to prevent double-injection.
TOOL: Schema and examples are omitted from the system prompt, as they are
expected to be provided by the toolset (e.g., via tool-specific
instructions). The LLM is instructed to use a tool to deliver A2UI JSON.
When using this mode, generate_system_prompt() automatically skips
schema and example injection to prevent double-injection.

@ditman

This comment was marked as resolved.

@nan-yu
Copy link
Copy Markdown
Collaborator

nan-yu commented May 26, 2026

@jswortz Thats for the PR! The implementation is solid and the test coverage is comprehensive.

After reviewing both #1466 and #1465 together, I want to propose a design refinement that addresses the parameter naming issue and consolidates both concerns cleanly.

Current State

These solve two orthogonal problems:

  1. Where the schema comes from (system prompt vs. toolset)
  2. How the output should be structured (flexible vs. A2UI-first)

Issue with output_mode Name

The name output_mode is misleading because:

  • What the name suggests: "How should the LLM output be formatted?" (TEXT format vs. TOOL calls)
  • What it actually does: "Where should the schema be injected?" (system prompt vs. tool call per-request)

This conflation caused the confusion around include_schema overlapping semantics.

Proposed Solution

  • Repurpose output_mode enum for the output strictness level.
  • Introduce a new schema_injection_mode enum for where and when the schema should be injected.
class A2UISchemaInjectionMode(Enum):
    """Where/when the schema is injected."""
    EMBEDDED = "embedded"   # Schema in system prompt (upfront, one-time)
    TOOL = "tool"           # Schema injected per-request by toolset
    NONE = "none"           # No schema injection (default)

class A2UIOutputMode(Enum):
    """How the LLM should prioritize A2UI vs. text."""
    A2UI_FIRST = "a2ui_first"  # A2UI blocks first, minimal text after
    TEXT_FIRST = "text_first"  # Text and A2UI can be interleaved (default)
    # Future: STRICT_A2UI, A2UI_ONLY for even stricter modes

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

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants