Skip to content

Feat(tool): support streaming intermediate results with non live- #4170 - #6883

Open
Lin-Nikaido wants to merge 1 commit into
google:mainfrom
Lin-Nikaido:feat/streaming-tool-intermediate-results
Open

Feat(tool): support streaming intermediate results with non live- #4170#6883
Lin-Nikaido wants to merge 1 commit into
google:mainfrom
Lin-Nikaido:feat/streaming-tool-intermediate-results

Conversation

@Lin-Nikaido

Copy link
Copy Markdown
Contributor

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

Link to Issue or Description of Change

Problem:
Enable to will_continue-like function response in BaseLlmFlow.run_async method with streaming mode.
It expected the tool returns generator, and the runner.async_run method when streaming_mode: StreamingMode.SSE yields the generator result as each Event. also, the streaming_mode is not SSE there is no change.

Solution:
A clear and concise description of what you want to happen and why you choose
this solution.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required
for all PRs that are not small documentation or typo fixes.

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.

run pytest ./tests/unittests/flows/ -> all passed
image

run full suite unittests
Failed 4 tests.

image

Note on the five test failures in a full tests/unittests run

A full-suite run reports five failures. None of them touch code this PR changes, and both causes are environment-dependent — one is a latent timing race that surfaces or not depending on filesystem timestamp granularity and per-test timing, the other depends on how the environment was installed. Details below in case they are useful upstream.

FAILED tests/unittests/cli/utils/test_cli_deploy.py::TestValidateAgentImport::test_success_with_app_export
FAILED tests/unittests/cli/utils/test_cli_deploy.py::TestValidateAgentImport::test_raises_on_basellm_import_error
FAILED tests/unittests/cli/utils/test_cli_deploy.py::TestValidateAgentImport::test_cleans_up_sys_modules
FAILED tests/unittests/test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[agent]
FAILED tests/unittests/test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[runner]
TestValidateAgentImport (3 failures) — stale importlib directory cache; invalidate_caches() is never called

All three fail the same way, with an import error for the temp package itself:

click.exceptions.ClickException: Failed to import agent module:
No module named 'test_raises_on_basellm_import_0'

_validate_agent_import (src/google/adk/cli/cli_deploy.py:604-615) puts the parent of the agent directory on sys.path and imports the package by name:

parent_dir=os.path.dirname(agent_src_path)
module_name=os.path.basename(agent_src_path)
...
ifparent_dirnotinsys.path:
sys.path.insert(0, parent_dir)
try:
module=importlib.import_module(f'{module_name}.agent')

There is no importlib.invalidate_caches(). CPython caches a FileFinder per sys.path entry in sys.path_importer_cache, and that finder keeps a listing of the directory, refreshed only when the directory's st_mtimediffers from the value it recorded. sys.path is restored in the finally block, but sys.path_importer_cache is global and is not — so the finder for parent_dir outlives each call.

Every test in the class gets its tmp_path under the same pytest basetemp, so parent_dir is the same directory for all of them and its finder is created once, on the first import. Inode timestamps are updated at kernel tick granularity, which measured 1 ms on this machine:

400 mkdir in one parent → 21 distinct parent st_mtime values
median mtime step: 1,000,000 ns (mean 19 new subdirectories per distinct mtime)

So whenever two of these tests create their tmp_path within the same millisecond, the second one's directory is invisible to the cached finder and the import fails with ModuleNotFoundError for a package that is sitting on disk. That is order- and timing-dependent, which is why which tests fail varies between machines and why the whole class passes on slower ones (25/25 clean runs of the class here).

Reduced repro, no pytest involved — it reproduces on any machine where consecutive iterations land inside one timestamp tick:

importimportlib, pathlib, sys, tempfilebase=tempfile.mkdtemp()
sys.path.insert(0, base)
fails=0foriinrange(300):
d=pathlib.Path(base) /f"pkg{i}"d.mkdir()
(d/"__init__.py").touch()
(d/"agent.py").write_text("root_agent = 'x'\n")
# importlib.invalidate_caches() # <-- uncomment for the fixtry:
importlib.import_module(f"pkg{i}.agent")
exceptImportError:
fails+=1forkin (f"pkg{i}", f"pkg{i}.agent"):
sys.modules.pop(k, None)
print(fails, "/300 failures")

Result here: 119–129 failures out of 300 as written, 0 out of 300 with importlib.invalidate_caches() uncommented.

The fix is the one line CPython's docs prescribe for exactly this situation ("If you are dynamically importing a module that was created since the interpreter began execution … call invalidate_caches()") — before the import_module call at cli_deploy.py:615:

# The agent directory was created after this process started, so importlib's# cached directory listing for its parent may not contain it yet.importlib.invalidate_caches()
module=importlib.import_module(f'{module_name}.agent')

Worth noting for prioritisation: _validate_agent_import currently has no caller in src/ — the validation was removed from the deploy path and --skip-agent-import-validation is deprecated (cli_deploy.py:1334). So today the impact is confined to the flaky test class, and the race would only become user-visible if the function is wired back in. Not included in this PR — unrelated to the change here.

test_entry_point_loads_only_allowlisted_packages[agent|runner]httpx2, absent under constraints-3.11.txt

The import chain is one hop:

google/adk/agents/llm_agent.py:34 from google.genai import types
└─ google/genai/types.py:132 import httpx2

google/genai/types.py imports it inside try: ... except ImportError: pass, purely to widen a type annotation:

try:
importhttpx2if_is_httpx_imported:
HttpxClient=Union[httpx.Client, httpx2.Client]

No ADK module imports httpx2 — this is the same optional-annotation pattern _ENTRY_POINT_PACKAGE_ALLOWLIST already excuses for aiohttp and PIL:

google.genai.types annotates optional fields with aiohttp and Pillow types and imports whichever of the two the environment happens to have. No ADK module imports either one, so these are absent in some installs.

It shows up because anthropic 1.0.0 requires httpx2 unconditionally (Requires-Dist: httpx2<3,>=2.0.0; openai and starlette require it only under extras), and pyproject.toml declares anthropic>=0.78 with no upper bound. CI never sees it because constraints-3.11.txt pins anthropic==0.117.0 (line 81) and has no httpx2 entry, so the except ImportError branch is taken there. Installing without the constraints file resolves anthropic to 1.0.0 and pulls httpx2 in.

Cost, for whatever the allowlist decision is worth — most of httpx2's dependency tree is shared with httpx, which is already loaded:

import httpx124 ms
import httpx, httpx2147 ms
marginal cost of httpx2~23 ms
from google.adk.agents import Agent (total)1216 ms

(-X importtime reports 67.8 ms of self time for the module, but the in-process marginal cost with httpx already imported is ~23 ms, about 2% of entry-point import time.)

The fix the failure message itself prescribes is one line — 'httpx2' in _ENTRY_POINT_PACKAGE_ALLOWLIST's google.genai.types optional-annotation block, alongside aiohttp and PIL. Not included here for the same reason as above.

Everything else passes; the per-PR counts are in the description.

Manual End-to-End (E2E) Tests:

  1. Create tool with yields like this.
asyncdefsearch(q: str) ->AsyncGenerator[dict[str, Any], None]:
yield {'status': 'inProgress', 'message': f'searching {q}'}
yield {'status': 'inProgress', 'message': 'reading pages'}
yield {'status': 'ok', 'result': ['page1', 'page2']}
  1. Create Agent with the tool
  2. Call some agent with the tool

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.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

This PR is follow newest version from #

Sign up for freeto 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.

Master issue: [Streaming Tools] support streaming intermediate results for tools for non-streaming case

2 participants

@Lin-Nikaido@xuanyang15