Commit Graph

302 Commits

Author SHA1 Message Date
Hangfei Lin 0ab79b9502 fix: Deprecate save_live_audio in favor of save_live_blob and fix audio event saving
This change:
*   Deprecates the `save_live_audio` configuration option in `RunConfig`, introducing `save_live_blob` as its replacement. A warning is issued if `save_live_audio` is used.
*   Updates `base_llm_flow.py` to use the new `save_live_blob` flag.
*   Ensures that audio events generated by `audio_cache_manager.py` are properly appended to the session service.
*   Adds a utility script `pcm_audio_player.py` for playing raw PCM audio files.

Input sample event: 14a5859f-6b6c-46ed-9f28-e5008793b1c6|live_bidi_streaming_multi_agent|user|1e867c7f-dbe1-4268-a7bc-7a9fa5fbd16c|e-7a28a060-29bf-4483-bc5c-17248698a897|1762916981.5932|{"content":{"parts":[{"file_data":{"file_uri":"artifact://live_bidi_streaming_multi_agent/user/1e867c7f-dbe1-4268-a7bc-7a9fa5fbd16c/_adk_live/adk_live_audio_storage_input_audio_1762916981593.pcm#0","mime_type":"audio/pcm"}}],"role":"user"},"invocation_id":"e-7a28a060-29bf-4483-bc5c-17248698a897","author":"user","actions":{"state_delta":{},"artifact_delta":{},"requested_auth_configs":{},"requested_tool_confirmations":{}},"id":"14a5859f-6b6c-46ed-9f28-e5008793b1c6","timestamp":1762916981.5932002}

output sample event:
506c9df4-e143-4ebc-90a5-2f5b2eb26754|live_bidi_streaming_multi_agent|user|1e867c7f-dbe1-4268-a7bc-7a9fa5fbd16c|e-7a28a060-29bf-4483-bc5c-17248698a897|1762916986.10579|{"content":{"parts":[{"file_data":{"file_uri":"artifact://live_bidi_streaming_multi_agent/user/1e867c7f-dbe1-4268-a7bc-7a9fa5fbd16c/_adk_live/adk_live_audio_storage_output_audio_1762916986105.pcm;rate=24000#0","mime_type":"audio/pcm;rate=24000"}}],"role":"model"},"invocation_id":"e-7a28a060-29bf-4483-bc5c-17248698a897","author":"model","actions":{"state_delta":{},"artifact_delta":{},"requested_auth_configs":{},"requested_tool_confirmations":{}},"id":"506c9df4-e143-4ebc-90a5-2f5b2eb26754","timestamp":1762916986.105794}

Co-authored-by: Hangfei Lin <hangfei@google.com>
PiperOrigin-RevId: 831512074
2025-11-12 13:16:13 -08:00
Shangjie Chen a501c59ac4 feat: Support registering custom services from local files
This change introduces a mechanism for users to register their own custom backend services for sessions, memory, and artifacts without modifying the ADK framework. This enhances the extensibility of ADK.

Two methods of registration are supported, both by placing a file in the parent directory of the agents.

**YAML Configuration (services.yaml or .yml)**

This is the recommended approach for simple services that can be instantiated with a constructor like MyService(uri="...", **kwargs).

Example services.yaml:

```
services:
  - scheme: mysession
    type: session
    class: my_package.my_module.MyCustomSessionService
```

**Python Registration (services.py)**

For services requiring more complex initialization logic, users can define factory functions in a services.py file.

Example services.py

```
from google.adk.cli.service_registry import get_service_registry
from my_package.my_module import MyCustomSessionService

def my_session_factory(uri: str, **kwargs):
    # custom initialization logic
    return MyCustomSessionService(...)

get_service_registry().register_session_service("mysession", my_session_factory)
```

ADK will load services from services.yaml/.yml first, and then from services.py. If the same service scheme is defined in both, the registration in services.py will take precedence.

To use a registered service, specify its URI via the corresponding command-line flag, e.g., `--session_service_uri=mysession://....`

Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 831211371
2025-11-11 21:59:19 -08:00
Hangfei Lin 01bac62f0c chore: Update agent instructions and retry limit in plugin_reflect_tool_retry sample
The agent's instructions are updated to guide it to guess a positive integer, starting from 50 and decreasing, using the `guess_number_tool` until the target is found. The maximum number of retries for the `CustomRetryPlugin` is increased from 6 to 10.

Co-authored-by: Hangfei Lin <hangfei@google.com>
PiperOrigin-RevId: 830984481
2025-11-11 10:52:44 -08:00
Hangfei Lin 2b0f953255 feat: Add artifact metadata support and a new sample for context offloading
Enhanced `save_artifact` in `callback_context.py` to accept `custom_metadata` and added `get_artifact_version` to retrieve artifact details.

Introduced a new sample, `context_offloading_with_artifact`, demonstrating how to use ADK artifacts to offload large data from the LLM context. The sample includes:
-   `QueryLargeDataTool`: Generates mock sales reports, saves them as artifacts with custom metadata, and injects the artifact content into the LLM request immediately after creation.
-   `CustomLoadArtifactsTool`: Provides summaries of available artifacts to the LLM based on metadata and loads artifact content on demand when `load_artifacts` is called.

Co-authored-by: Hangfei Lin <hangfei@google.com>
PiperOrigin-RevId: 830592786
2025-11-10 14:10:10 -08:00
Max Ind 116b26c33e feat: add plugin for returning GenAI Parts from tools into the model request
Added to mitigate https://github.com/google/adk-python/issues/3064

Co-authored-by: Max Ind <maxind@google.com>
PiperOrigin-RevId: 830135940
2025-11-09 11:47:08 -08:00
Lê Nam Khánh 51dee43f08 docs: fix typos in some files
Merge https://github.com/google/adk-python/pull/3444

This PR fixes typos in the file file using codespell.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3444 from khanhkhanhlele:Fix/typos/20251107165249 e9629a8c0255ddc0764cb0df7f36ebe61bd6515e
PiperOrigin-RevId: 829516217
2025-11-07 11:32:32 -08:00
George Weale e0e762598e docs: Use a trimmed ADK AgentConfig schema in Agent Builder Assistant
This trimmed schema includes only the fields relevant to agent shells, tool wiring, and common generation parameters, improving efficiency and focus.

The default model for the assistant has also been updated to "gemini-2.5-pro"

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 829513627
2025-11-07 11:26:18 -08:00
Google Team Member a0cf97eba2 feat: Some small infra fixes to the gepa demo colab
PiperOrigin-RevId: 829240716
2025-11-06 20:44:25 -08:00
Google Team Member d118479ccf feat: Improve gepa voter agent demo colab
PiperOrigin-RevId: 829208761
2025-11-06 19:13:11 -08:00
Google Team Member f1f44675e4 ADK changes
PiperOrigin-RevId: 829136628
2025-11-06 15:55:26 -08:00
George Weale 7ea4aed35b fix: Add support for structured output schemas in LiteLLM models
Add `_to_litellm_response_format` to convert ADK's `response_schema` types (Pydantic models, JSON schema dicts) into the format needed by LiteLLM for JSON object/schema constraints

Close #1967

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 829037987
2025-11-06 11:29:10 -08:00
Google Team Member f167890d00 feat: Add documentation and instructions to help configure gepa experiments
PiperOrigin-RevId: 828911323
2025-11-06 05:31:24 -08:00
Josh Soref 59d422ca21 chore: Fix spelling in contributing
Merge https://github.com/google/adk-python/pull/3394

This PR corrects misspellings identified by the [check-spelling action](https://github.com/marketplace/actions/check-spelling)

Note: while I use tooling to identify errors, the tooling doesn't _actually_ provide the corrections, I'm picking them on my own. I'm a human, and I may make mistakes.

### Testing Plan

The misspellings have been reported at https://github.com/jsoref/adk-python/actions/runs/19056081305/attempts/1#summary-54426435973

The action reports that the changes in this PR would make it happy: https://github.com/jsoref/adk-python/actions/runs/19056081446/attempts/1#summary-54426436321

**Unit Tests:**

- [ ] I have added or updated unit tests for my change.
- [ ] All unit tests pass locally.

_Please include a summary of passed `pytest` results._

**Manual End-to-End (E2E) Tests:**

_Please provide instructions on how to manually test your changes, including any
necessary setup or configuration. Please provide logs or screenshots to help
reviewers better understand the fix._

### Checklist

- [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document.
- [x] 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.
- [ ] I have manually tested my changes end-to-end.
- [ ] Any dependent changes have been merged and published in downstream modules.

### Additional context

- https://github.com/google/adk-python/pull/3382#issuecomment-3488654110

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3394 from jsoref:spelling-contributing c3d5e342c4350f7cae9f8f0c6638b176f2e30e80
PiperOrigin-RevId: 828659867
2025-11-05 15:43:25 -08:00
Wei Sun (Jack) fa5c546a55 fix(tools): Add proper cleanup for AgentTool to prevent MCP session errors
Merge https://github.com/google/adk-python/pull/3411

## Summary

Fixes AgentTool cleanup to prevent MCP session errors by calling `runner.close()` after sub-agent execution.

## Problem

When using AgentTool with MCP tools, the runner cleanup happened during garbage collection in a different async task context, causing:
```
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
```

## Solution

- Call `await runner.close()` immediately after sub-agent execution in AgentTool
- This ensures MCP sessions and other resources are cleaned up in the correct async task context
- Updated test mock to include the close() method

## Demo Agents

Added two comprehensive demo agents showing how to use AgentTool with MCP tools:

### mcp_in_agent_tool_remote (SSE mode)
- Uses HTTP/SSE connection to remote MCP server
- Zero-installation setup with `uvx`
- Demonstrates server-side MCP deployment pattern

### mcp_in_agent_tool_stdio (stdio mode)
- Uses subprocess connection with automatic server launch
- Fully automatic setup with `uvx` subdirectory syntax
- Demonstrates embedded MCP deployment pattern

Both demos:
- Use Gemini 2.5 Flash
- Include example prompts for JSON Schema exploration
- Have comprehensive READMEs with architecture diagrams
- Follow ADK agent structure conventions

## Testing

-  All existing unit tests pass
-  Manual testing with both SSE and stdio modes
-  Verified cleanup happens in correct async context
-  No more cancel scope errors with MCP tools

## Related

- Fixes #1112
- Related to #929

Co-authored-by: Wei Sun (Jack) <weisun@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3411 from google:fix/agent-tool-mcp-cleanup 9ae753b5a4
PiperOrigin-RevId: 828651896
2025-11-05 15:23:39 -08:00
George Weale d9ec07d39b docs: Improve Agent Builder Assistant schema reference for prompts
Replace the full JSON schema dump with a compact text summary of key AgentConfig components like LlmAgent, ToolConfig, and GenerateContentConfig

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 828627911
2025-11-05 14:23:18 -08:00
Google Team Member e02f177790 feat: Improve gepa tau-bench colab for external use
PiperOrigin-RevId: 828579343
2025-11-05 12:23:47 -08:00
Hangfei Lin aa77834e2e chore: Update Gemini Live model names in live bidi streaming sample
The sample agent now uses updated model names for Gemini Live, including a new Vertex model as the default and a new AI Studio model option.

Co-authored-by: Hangfei Lin <hangfei@google.com>
PiperOrigin-RevId: 828515811
2025-11-05 09:57:59 -08:00
Google Team Member 63353b2b74 feat: Refactor gepa sample code and clean-up user demo colab
PiperOrigin-RevId: 828293079
2025-11-04 22:16:55 -08:00
Kathy Wu 88032cf5c5 feat: Support MCP prompts
Add support for MCP prompts via the McpInstructionProvider class, which can be specified as an agent's instruction.

Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 828166051
2025-11-04 15:48:21 -08:00
Shan Cao d4c63fc562 chore: Add model tracking to LiteLlm and introduce a LiteLLM with fallbacks demo
Related: #2292

Co-authored-by: Shan Cao <caoshan@google.com>
PiperOrigin-RevId: 828024955
2025-11-04 10:10:09 -08:00
George Weale e25beb4bce docs: Refine ADK triaging agent labeling guidelines and response format
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 828022792
2025-11-04 10:05:20 -08:00
George Weale c33a680b54 docs: Add instructions to prevent tool hallucination
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 827632446
2025-11-03 13:41:03 -08:00
Josh Soref aa1233608a chore: Fix spelling
Merge https://github.com/google/adk-python/pull/2447

This PR corrects misspellings identified by the [check-spelling action](https://github.com/marketplace/actions/check-spelling)

The misspellings have been reported at https://github.com/jsoref/adk-python/actions/runs/16840838898/attempts/1#summary-47711379253

The action reports that the changes in this PR would make it happy: https://github.com/jsoref/adk-python/actions/runs/16840839269/attempts/1#summary-47711380479

Note: while I use tooling to identify errors, the tooling doesn't _actually_ provide the corrections, I'm picking them on my own. I'm a human, and I may make mistakes.

I've included a couple of changes to make CI happy. Personally, I object to CI being in a state of "random drive by person who adds a blank line in the middle of a file must fix all the preexisting bugs in the file", but that appears to be the state for this repository.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2447 from jsoref:spelling d85398e7fd154d124d477c6af6181481a01f34e0
PiperOrigin-RevId: 827629615
2025-11-03 13:33:53 -08:00
Kathy Wu e8526f7e06 fix: Fix credential manager so that it supports the ServiceAccountCredentialExchanger
This fixes MCP authentication for gcloud service accounts. Previously it was failing to authenticate tool calls.

Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 826639044
2025-10-31 14:55:06 -07:00
Lavi Nigam 0487eea2ab feat: add run_debug() helper method for quick agent experimentation
Merge https://github.com/google/adk-python/pull/3345

Add run_debug() helper method to InMemoryRunner that reduces agent execution boilerplate from 7-8 lines to just 2 lines, making it ideal for quick experimentation, notebooks, and getting started with ADK.

**Key changes:**
• Introduce run_debug() to reduce boilerplate from 7-8 lines to 2 lines
• Enable quick testing in notebooks, REPL, and during development
• Support single or multiple messages with automatic session management
• Add verbose flag to show/hide tool calls and intermediate processing
• Add quiet flag to suppress console output while capturing events
• Extract event printing logic to reusable utility (utils/_debug_output.py)
• Include comprehensive test suite with 21 test cases covering all part types
• Provide complete working example with 8 usage patterns
• **This is a convenience method for experimentation, not a replacement for run_async()**

### Link to Issue or Description of Change

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

* N/A - New feature to improve developer experience

**2. Or, if no issue exists, describe the change:**

**Problem:**

Developers need to write 7-8 lines of boilerplate code just to test a simple agent interaction during development. This creates friction for:

* New developers getting started with ADK
* Quick experimentation in Jupyter notebooks or Python REPL
* Debugging agent behavior during development
* Writing examples and tutorials
* Rapid prototyping of agent capabilities

**Solution:**

Introduce `run_debug()` as a convenience helper method specifically designed for quick experimentation and getting started scenarios. This method:

* **Is NOT a replacement for `run_async()`** - it's a developer convenience tool
* **Reduces boilerplate** from 7-8 lines to just 2 lines for simple testing
* **Handles session management automatically** with sensible defaults
* **Provides debugging visibility** with optional verbose flag for tool calls
* **Supports common patterns** like multiple messages and event capture
* **Type-safe implementation** using direct attribute access instead of getattr()

### Before vs After Comparison

**BEFORE - Current approach requires 7-8 lines of boilerplate:**

```python
from google.adk import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

# Define a simple agent
agent = Agent(
    model="gemini-2.5-flash",
    instruction="You are a helpful assistant"
)

# Need all this boilerplate just to test the agent
APP_NAME = "default"
USER_ID = "default"
session_service = InMemorySessionService()
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
session = await session_service.create_session(
    app_name=APP_NAME, user_id=USER_ID, session_id="default"
)
content = types.Content(role="user", parts=[types.Part.from_text("Hello")])
async for event in runner.run_async(
    user_id=USER_ID, session_id=session.id, new_message=content
):
    if event.content and event.content.parts:
        print(event.content.parts[0].text)
```

**AFTER - With run_debug() helper, just 2 lines:**

```python
from google.adk import Agent
from google.adk.runners import InMemoryRunner

# Define the same agent
agent = Agent(
    model="gemini-2.5-flash",
    instruction="You are a helpful assistant"
)

# Test it with just 2 lines!
runner = InMemoryRunner(agent=agent)
await runner.run_debug("Hello")
```

### API Design

```python
async def run_debug(
    self,
    user_messages: str | list[str],
    *,
    user_id: str = 'debug_user_id',
    session_id: str = 'debug_session_id',
    run_config: RunConfig | None = None,
    quiet: bool = False,
    verbose: bool = False,
) -> list[Event]:
```

**Parameters:**

* `user_messages`: Single message string or list of messages (required)
* `user_id`: User identifier (default: 'debug_user_id')
* `session_id`: Session identifier for conversation continuity (default: 'debug_session_id')
* `run_config`: Optional advanced configuration
* `quiet`: Suppress console output (default: False)
* `verbose`: Show detailed tool calls and responses (default: False)

**Key Features:**

* **Always returns events** - Simplifies API, no conditional return type
* **Type-safe implementation** - Uses direct attribute access on Pydantic models
* **Text buffering** - Consecutive text parts printed without repeated author prefix
* **Smart truncation** - Long tool args/responses truncated for readability
* **Clean session management** - Get-then-create pattern, no try/except
* **Reusable printing logic** - Extracted to utils/_debug_output.py for other tools

### Implementation Highlights

**1. Event Printing Utility (utils/_debug_output.py):**
* Modular print_event() function for displaying events
* Text buffering to combine consecutive text parts
* Configurable truncation for different content types:
  - Function args: 50 chars max
  - Function responses: 100 chars max
  - Code output: 100 chars max
* Supports all ADK part types (text, function_call, executable_code, inline_data, file_data)

**2. Session Management:**
```python
# Clean get-then-create pattern (no try/except)
session = await self.session_service.get_session(
    app_name=self.app_name, user_id=user_id, session_id=session_id
)
if not session:
    session = await self.session_service.create_session(
        app_name=self.app_name, user_id=user_id, session_id=session_id
    )
```

**3. Type-Safe Event Processing:**
* Direct attribute access on Pydantic models (no getattr() or hasattr())
* Proper handling of all part types
* Leverages `from __future__ import annotations` for duck typing

### Important Note on Scope

`run_debug()` is a **convenience method for experimentation only**. For production applications requiring:

* Custom session services (Spanner, Cloud SQL)
* Fine-grained event processing control
* Error recovery and resumability
* Performance optimization
* Complex authentication flows

Continue using the standard `run_async()` method. The `run_debug()` helper is specifically designed to lower the barrier to entry and speed up the development/testing cycle.

### Testing Plan

**Unit Tests (21 test cases in tests/unittests/runners/test_runner_debug.py):**

**Core functionality (7 tests):**
*  Single message execution and event return
*  Multiple messages in sequence
*  Quiet mode (suppresses output)
*  Custom session_id configuration
*  Custom user_id configuration
*  RunConfig passthrough
*  Session persistence across calls

**Part type handling (8 tests):**
*  Tool calls and responses (verbose mode)
*  Executable code parts
*  Code execution result parts
*  Inline data (images)
*  File data references
*  Mixed part types in single event
*  Long output truncation
*  Verbose flag behavior (show/hide tools)

**Edge cases (6 tests):**
*  None text filtering
*  Existing session handling
*  Empty parts list
*  None event content
*  Verbose=False hides tool calls
*  Verbose=True shows tool calls

**All 21 tests passing in 3.8s** ✓

**Manual End-to-End (E2E) Tests:**

Tested all 8 example patterns in contributing/samples/runner_debug_example/main.py:

1.  Minimal 2-line usage
2.  Multiple sequential messages
3.  Session persistence across calls
4.  Multiple user sessions (Alice & Bob)
5.  Verbose mode for tool visibility
6.  Event capture with quiet mode
7.  Custom RunConfig integration
8.  Before/after comparison

### Files Changed

**Core implementation:**
* src/google/adk/runners.py - Added run_debug() method (~60 lines)
* src/google/adk/utils/_debug_output.py - Event printing utility (~106 lines)

**Tests:**
* tests/unittests/runners/test_runner_debug.py - Comprehensive test suite (21 tests)

**Examples:**
* contributing/samples/runner_debug_example/agent.py - Sample agent with tools
* contributing/samples/runner_debug_example/main.py - 8 usage examples
* contributing/samples/runner_debug_example/README.md - Complete documentation

### Checklist

- [x] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes (21/21 passing)
- [x] I have manually tested my changes end-to-end (8 examples tested)
- [x] Code follows ADK style guide (relative imports, type hints, 2-space indentation)
- [x] Ran ./autoformat.sh before committing
- [x] Any dependent changes have been merged and published in downstream modules

### Additional Context

**Example with Tools (verbose mode):**

```python
# Create agent with tools
agent = Agent(
    model="gemini-2.5-flash",
    instruction="You can check weather and do calculations",
    tools=[get_weather, calculate]
)

# Test with verbose to see tool calls
runner = InMemoryRunner(agent=agent)
await runner.run_debug("What's the weather in SF?", verbose=True)

# Output:
# User > What's the weather in SF?
# agent > [Calling tool: get_weather({'city': 'San Francisco'})]
# agent > [Tool result: {'result': 'Foggy, 15°C (59°F)'}]
# agent > The weather in San Francisco is foggy, 15°C (59°F).
```

**Complete Example Included:**

The PR includes a full working example in `contributing/samples/runner_debug_example/` with:
* Agent with weather and calculator tools
* 8 different usage patterns
* Comprehensive README with troubleshooting
* Safe AST-based expression evaluation

**Breaking Changes:** None - this is purely additive.

**Security:** Example uses AST-based expression evaluation instead of eval().

**Code Quality:**
* Type-safe implementation (no getattr() or hasattr())
* Modular design (printing logic separated into utility)
* Follows ADK conventions (relative imports, from __future__ import annotations)
* Comprehensive error handling (gracefully handles None content, empty parts)
* Well-documented with docstrings and inline comments
END_PUBLIC
```

---

## Key Changes from Original:

1.  Updated parameter name: `user_queries` → `user_messages`
2.  Updated parameter name: `session_name` → `session_id`
3.  Updated parameter name: `print_output` → `quiet`
4.  Removed `return_events` parameter
5.  Updated test count: 23 → 21
6.  Changed "queries" → "messages" throughout
7.  Added implementation highlights section
8.  Added details about utils/_debug_output.py
9.  Updated default values to debug_user_id/debug_session_id
10.  Noted type-safe implementation
11.  Added Code Quality section
12.  Updated API signature to match final refactored version
13.  Removed optional return type (always returns list[Event])

Co-authored-by: Wei Sun (Jack) <weisun@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3345 from lavinigam-gcp:adk-runner-helper e0050b9f152d0f0e49e6501610d2c59a754fc571
PiperOrigin-RevId: 826607817
2025-10-31 13:28:02 -07:00