ComputerUseToolset passed the url the model supplied straight to the
browser driver, without checking it. Now navigate runs the same checks
load_web_page does. A url that fails returns an error to the model
instead of reaching the driver.
If the agent is meant to drive the browser against an internal host,
pass allow_private_network_access=True
Co-authored-by: Jason Zhang <jasoncz@google.com>
PiperOrigin-RevId: 963643277
VertexAiRagMemoryService ran top-k retrieval across the whole configured
corpus and dropped the other tenants' contexts afterwards. The response filter
made the result correct, but ranking still competed against every app and user
in the corpus, so a busy corpus could crowd a caller's own memories out of the
top-k entirely, and foreign context was transferred only to be discarded. This
is a recall and data transfer problem, not a disclosure one: no memory
belonging to another app or user was ever returned to the caller.
search_memory now lists the corpus, keeps the files whose display name names
the requesting app and user, and passes those file ids to VertexRagStore so
ranking happens inside that set. When the caller owns no files, retrieval is
skipped and an empty response is returned. Callers can now see memories that
the previous ranking crowded out, so result counts can go up.
Scoping is best effort and adds no permission requirement, but it does add up
to 10 list calls to each search. Those calls run on the SDK async surface, so
they are awaited rather than blocking the event loop. Scoping is abandoned,
rather than applied to the files listed so far, whenever the listing cannot be
completed: either a listing failure, such as a deployment whose credentials
can retrieve but not list, or a corpus larger than the roughly 1000 files that
page budget covers. Retrieval then runs unscoped exactly as it did before,
which the response filter still makes correct, and the reason is logged.
Applying a partial listing would instead hide the caller's own memories.
Corpora past that size therefore keep the old ranking behavior permanently.
Server-side metadata filtering was considered and rejected: it matches on the
RagFile user_metadata field, which is output only on uploaded files and which
this service has never populated, so it would exclude every memory already
stored.
No public interface changes.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 963641027
The PR and issue triaging agents each kept their own copy of the component
-> owner map and they had drifted: the PR agent was missing several
components (skills, auth, bq, cli, integrations, workflow), and its
ALLOWED_LABELS gate -- which controls what the agent may apply -- was also
stale, so those labels could be neither applied nor assigned (a skills PR
was labeled "core" and assigned to the wrong owner).
Move the map into component_owners.py as the single source of truth and
import it verbatim as LABEL_TO_OWNER in both agents, so the two are always
identical and cannot drift. Derive the PR agent's ALLOWED_LABELS from
LABEL_TO_OWNER so a newly-owned component is allowed automatically.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 963611217
Concurrent session writers can append against a stale revision, and the different session backends surfaced that conflict inconsistently. A stale post-response compaction write could then fail a turn that had already completed. This raises a consistent StaleSessionError (still a ValueError subclass) across the database, SQLite, and Firestore services, and discards only the stale compaction summary while keeping the raw turns.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 963599677
Some MCP servers declare a string-typed field whose enum lists integer
values. Gemini requires enum members to match the declared string type, so
the tool declaration was rejected and the integration failed. Normalize enum
values to their string form when the effective (non-null) type is string,
leaving numeric enums on numeric types untouched.
Close#3401
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 963579568
Live tool execution (`FunctionTool._call_live` and `__call_tool_live` in
flows/llm_flows/functions.py) called the wrapped function directly instead of
going through `BaseTool.run_async`. Every guardrail and preprocessing step the
async path applies was silently skipped in live sessions.
Remove `_call_live` and route live tool execution through `__call_tool_async` /
`tool.run_async`. Behavior changes that follow from the unification:
- A tool gated behind `require_confirmation` is no longer executed unattended in
a live session. The check was previously skipped and the tool body ran; the
call is now refused and a confirmation request is recorded. See the limitation
below -- this is the request half only.
- Parameter preprocessing (Pydantic model coercion) now applies in live mode.
- `BaseTool` subclasses overriding `run_async` now execute polymorphically in
live mode instead of being invoked as plain functions.
- A streaming tool that raises now returns an error FunctionResponse instead of
leaving the live session waiting for a response that never arrives.
- `_get_mandatory_args` no longer counts `_ignore_params` (`tool_context`,
`input_stream`) as mandatory, so schema generation and validation report only
the parameters actually required from the caller.
Known limitation: human-in-the-loop confirmation is still not end-to-end in live
mode. The request is raised but cannot be answered, because the live flow never
emits an `adk_request_confirmation` function call, the confirmation request
processor only runs once before the live connection opens, and the live
execution path does not accept a `ToolConfirmation`. A confirmation-gated tool
therefore cannot be approved and resumed inside a live session. TODOs in the
code mark the sites that need to change; closing the loop is follow-up work.
Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 963548842
`McpTool` registered under the verbatim name the remote server advertised, with
no check against the names the framework itself puts on the wire. A server that
advertised `adk_request_credential`, `adk_request_confirmation`,
`adk_request_input` or `transfer_to_agent` therefore had its own tool dispatched
in place of the framework's, so it could harvest the credentials meant for an
auth callback or route the conversation to an agent of its choosing.
`McpToolset.get_tools` now drops any tool carrying one of those four names and
logs that it did, and `McpTool.__init__` refuses the name outright. The listing
skips rather than raises because a single reserved name would otherwise fail the
whole `list_tools` call and take the server's honest tools down with it; the
constructor check is the backstop for anything that builds an `McpTool`
directly. Only exact matches are refused, so `transfer_to_agent_v2` still
registers.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 963544587
Merge https://github.com/google/adk-python/pull/5402
## Summary
- normalize Pub/Sub subscription and Eventarc source metadata before reusing them as session user ids
- replace slash-separated resource paths with path-safe -- delimiters while preserving the full resource identity
- add trigger endpoint regression tests that verify the created sessions are stored under the normalized user ids
## Testing
- python3 -m py_compile src/google/adk/cli/trigger_routes.py tests/unittests/cli/test_trigger_routes.py
- python3 -m pytest tests/unittests/cli/test_trigger_routes.py -k "path_safe or with_subscription_metadata or source_from_ce_header" (fails during collection in this environment: ModuleNotFoundError: No module named 'fastapi')
Co-authored-by: George Weale <gweale@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/5402 from MukundaKatta:codex/trigger-user-id-path-safe a97467903bcadc92fc7a916ccef95638c58b3a65
PiperOrigin-RevId: 963505905
Pure code motion, ahead of two changes that would otherwise be hard to read.
The one module holding all of the scaffolding becomes one module per concern:
functional_test_helpers.py -> functional/_scenarios.py (what gets driven)
functional/_digests.py (what it recorded)
functional/_recording.py (driving one case)
functional/_aclosing.py (the aclosing check)
Everything else keeps the path it had, so a change in flight against the
cases, the tests or the goldens still applies.
Recording a case moves out of the tests and out of regenerate.py, which had
a copy each, into the one record_case() they now share.
No behaviour change: every golden is byte-identical to what it was, and
regenerating them all reproduces them exactly.
Co-authored-by: Max Ind <maxind@google.com>
PiperOrigin-RevId: 963418069
Reject `app_name`, `eval_set_id`, and `eval_set_result_id` values that
contain path separators, traversal segments, or null bytes before using
them to build GCS blob names in `GcsEvalSetsManager` and
`GcsEvalSetResultsManager`. Without this, a caller who controls these
identifiers on a shared evaluation bucket could compose a blob key that
addresses another app's eval sets or results.
This applies the same `_path_validation.validate_path_segment` guard that
already protects the local eval set/result managers, closing the gap for
the GCS-backed implementations.
Co-authored-by: Yi Liu <yiliuly@google.com>
PiperOrigin-RevId: 963199199
Adds the mode field and relaxes the root-only restriction to depend on it.
mode='single_turn' makes LlmAgent.model_post_init's duck-typed lookup wrap this
agent in _SingleTurnAgentTool, so the parent calls it as an inline tool with a
request the parent composes, rather than transferring the conversation to it.
The field is a narrow Literal because AntigravityAgent is not an LlmAgent, so
LlmAgent's other modes have no meaning here; _managed_agent.py narrows it the
same way.
The root-only restriction exists because the SDK harness runs its own agent
loop and owns its own conversation, so it cannot take part in ADK's
multi-agent delegation. That is a statement about how the agent is invoked,
not about how it is configured: under mode='single_turn' the parent composes a
self-contained request, no session history has to reach the harness, and the
harness's conversation does not outlive the call. So the parent guard now
defers to mode, and mode is frozen so an adopted agent cannot be mutated back
into a state the guard would have rejected.
Giving the agent sub_agents stays blocked in every mode -- the harness would
never dispatch to an ADK child regardless of how the agent was invoked. The
two restrictions now raise separate messages, because a caller who passed
sub_agents cannot act on advice about mode.
Trajectory bookkeeping is skipped under single_turn, and config.save_dir is no
longer required, since it exists only to persist and resume trajectories.
Skipping it also fixes a latent bug: conversation_id is derived from the ADK
session, not the call, and _run_impl leaves the session unchanged, so without
this a second single-turn call in one session would find the first call's
trajectory and resume it -- silently, defeating the isolation the mode exists
to provide.
The two pre-existing run-path tests keep their assertions: the mode=None path
is unchanged. test_resumed_replayed_steps_are_skipped gains one assertion that
pins the persistence branch, which had no coverage before.
README gains a Single-Turn Sub-Agents section, and three pre-existing claims
are corrected: trajectory files are named by a sha256 digest rather than
'<session_id>_<agent_name>', save_dir is no longer unconditionally required,
and omitting it makes the SDK allocate a temporary directory per call that
nothing removes.
Co-authored-by: Haran Rajkumar <haranrk@google.com>
PiperOrigin-RevId: 963194529
AntigravityAgent inherits BaseAgent._run_impl, the default adapter that lets
an agent run as a workflow node. That adapter leaves two gaps for any node
whose caller passes an input and reads an output:
- input: it accepts node_input and discards it, so a caller's composed request
is silently replaced by the original end-user message. This override threads
it into user_content, as ManagedAgent does.
- output: it never sets event.output, and the node runner reads results only
from event.output or node_info.message_as_output with no fallback to event
content, so the caller receives None.
Output is the last complete response, not the first: the SDK emits one per
model turn between tool calls, so RemoteA2aAgent's first-event promotion would
return the model's opening remark. Matches AgentTool's last_content behavior.
It is emitted on a trailing event rather than promoted in place, because which
response is final is unknowable until the stream ends and Context.output
raises on a second assignment.
A completed run that produced no model text emits output='' rather than
skipping the event. A text-less run is a real outcome -- a cancelled turn drops
its SYSTEM_MESSAGE step and yields nothing -- and returning None there would be
indistinguishable from the framework gap this override exists to close.
AgentTool makes the same choice with 'last_error_message or ""'. A run that
fails still raises and emits no output event.
No behavior change for an AntigravityAgent run as a root agent, which is the
only way it can be used until the next CL in this stack.
Co-authored-by: Haran Rajkumar <haranrk@google.com>
PiperOrigin-RevId: 963172936
- Support mode="task" in the root LlmAgent to register task completion tools.
- Document the A2A runner/executor completion contract where task-mode agents trigger termination via the finish_task tool.
- Add tests verifying that A2aAgentExecutor publishes COMPLETED task status upon receiving finish_task event from root task-mode agent.
PiperOrigin-RevId: 963135460
RemoteA2aAgent now rewrites human-input pause responses (adk_request_input,
adk_request_confirmation, adk_request_credential, and the mock input/auth calls)
to text before forwarding them to a remote agent on resume, matched by the
function call name. This stops Runner._validate_new_message from rejecting a
resumed message that mixes function responses with text. Credential (AuthConfig)
payloads are dropped instead of forwarded, and real long-running tool responses
are preserved so the peer can resume them by id.
PiperOrigin-RevId: 963124368
Merge https://github.com/google/adk-python/pull/6114
## Link to Issue or Description of Change
Closes : #6093
**Problem:**
On Agent Engine deployments served by the ADK API server, every call to the
`/api/stream_reasoning_engine` route with a synchronous streaming `class_method`
(e.g. `stream_query`) ends with `RuntimeError: coroutine raised StopIteration`
after the last chunk is streamed.
The cause is the sync-to-async adapter `_aiter_from_iter` in
`src/google/adk/cli/fast_api.py` (lines 916–922 in v2.2.0):
async def _aiter_from_iter(iterator):
while True:
try:
chunk = await run_in_threadpool(next, iterator)
yield chunk
except StopIteration:
break
The `except StopIteration` is unreachable. When the iterator is exhausted,
`next()` raises `StopIteration` inside the worker thread, anyio sets it on a
future, and it propagates out of the `run_in_threadpool` coroutine frame.
Python (PEP 479) forbids `StopIteration` escaping a coroutine and converts it
to `RuntimeError("coroutine raised StopIteration")` before the `except` clause
ever sees it.
**Affected versions:** Regression introduced in v2.2.0 — the route and the
buggy adapter were added in the same commit. Not present in the v1.x line
(verified absent at v1.35.0).
**Solution:**
Stop relying on `StopIteration` crossing the await boundary; use a sentinel
default so iterator exhaustion never raises across it:
_SENTINEL = object()
async def _aiter_from_iter(iterator):
while True:
chunk = await run_in_threadpool(next, iterator, _SENTINEL)
if chunk is _SENTINEL:
break
yield chunk
This is the minimal, idiomatic fix; the stream now terminates cleanly when the
sync generator is exhausted.
## Testing Plan
**Unit Tests:**
- [x] I have added or updated unit tests for my change.
- [x] All unit tests pass locally.
Added `test_gemini_stream_reasoning_engine_sync_generator` plus a
`test_app_with_gemini_enterprise_sync_stream` fixture in
`tests/unittests/cli/test_fast_api.py`. The pre-existing stream test used an
*async* generator (the `isasyncgenfunction` branch) and never exercised the
buggy sync-generator path. The new test fails on the unpatched code with
`RuntimeError` and passes with the fix.
pytest summary:
$ pytest tests/unittests/cli/test_fast_api.py -k stream_reasoning_engine -q
3 passed, 79 deselected
$ pytest tests/unittests/cli/test_fast_api.py -q
82 passed
**Manual End-to-End (E2E) Tests:**
The failure and the fix reproduce standalone in ~15 lines, independent of any
model or deployment:
import asyncio
from starlette.concurrency import run_in_threadpool
async def _aiter_from_iter(iterator): # old, buggy version
while True:
try:
chunk = await run_in_threadpool(next, iterator)
yield chunk
except StopIteration:
break
async def main():
def gen():
yield 1
yield 2
async for c in _aiter_from_iter(gen()):
print("chunk:", c)
asyncio.run(main())
# chunk: 1
# chunk: 2
# RuntimeError: coroutine raised StopIteration <-- before the fix
With the sentinel version above, the same script prints the two chunks and
exits cleanly with no exception. Originally observed on a live Vertex AI Agent
Engine deployment (google-adk==2.2.0, Python 3.11) where every `stream_query`
call logged the RuntimeError after the final chunk.
## Checklist
- [x] I have read the 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.
- [x] I have manually tested my changes end-to-end.
- [ ] Any dependent changes have been merged and published in downstream modules.
## Additional context
Original server traceback:
ERROR: Exception in ASGI application
Traceback (most recent call last):
File ".../starlette/responses.py", line 250, in stream_response
async for chunk in self.body_iterator:
File ".../google/adk/cli/fast_api.py", line 797, in json_generator
async for chunk in output:
File ".../google/adk/cli/fast_api.py", line 919, in _aiter_from_iter
chunk = await run_in_threadpool(next, iterator)
File ".../starlette/concurrency.py", line 32, in run_in_threadpool
return await anyio.to_thread.run_sync(func)
File ".../anyio/to_thread.py", line 63, in run_sync
return await get_async_backend().run_sync_in_worker_thread(
File ".../anyio/_backends/_asyncio.py", line 2518, in run_sync_in_worker_thread
return await future
RuntimeError: coroutine raised StopIteration
Occurs 100% of the time on every sync streaming request once the generator is
exhausted. The bug is model-agnostic (purely in the FastAPI streaming adapter).
Co-authored-by: George Weale <gweale@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/6114 from surajit-1306:fix/stream-reasoning-engine-stopiteration e5ee866074fefc56418ec03441e3706617f9d755
PiperOrigin-RevId: 962875380
Merge https://github.com/google/adk-python/pull/4414
**Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.**
### Link to Issue or Description of Change
**1. Link to an existing issue (if applicable):**
- Related: #4410
- Fixes: #2602
**Problem:**
`AgentEvaluator.evaluate()` did not support built-in eval set result persistence, making it harder to reuse the same workflow as CLI/Web paths that already use `EvalSetResultsManager`.
Also, introducing new parameters in the middle of method signatures would break positional-argument compatibility for existing users.
**Solution:**
This PR adds optional eval result persistence to `AgentEvaluator` while preserving backward compatibility:
- Add optional parameters to `AgentEvaluator.evaluate()` and `AgentEvaluator.evaluate_eval_set()`:
- `app_name: Optional[str] = None`
- `eval_set_results_manager: Optional[EvalSetResultsManager] = None`
- Persist results per eval set (a single save aggregating all `EvalCaseResult`s), aligning `AgentEvaluator` with existing CLI/Web/API (`LocalEvalService`) persistence behavior.
- Resolve `app_name` from explicit input first, then derive from `agent_module` (including `.agent` suffix handling).
- Save results before failure assertion so failed eval runs still leave artifacts for inspection.
- Keep existing positional argument behavior by appending new parameters at the end of public method signatures.
- Add/extend tests to verify:
- explicit and derived `app_name`
- save-on-failure behavior
- argument propagation from `evaluate()` to `evaluate_eval_set()`
- positional-argument backward compatibility
- Add an integration usage example for `app_name` omission with `LocalEvalSetResultsManager`.
- For multi-run evals, all runs and eval cases are aggregated into a single result file per eval set (each run contributes one `EvalCaseResult`).
### Testing Plan
**Unit Tests:**
- [x] I have added or updated unit tests for my change.
- [x] All unit tests pass locally.
```
% pytest tests/unittests/evaluation
======================== 357 passed, 169 warnings in 9.68s =========================
```
**Manual End-to-End (E2E) Tests:**
```
% pytest tests/integration/test_with_test_file.py::test_with_single_test_file_saves_eval_set_result
======================== 1 passed, 14 warnings in 5.24s ========================
```
Verify a result file is created under: `<tmp_path>/<derived_app_name>/.adk/eval_history/*.evalset_result.json` (e.g., 1 file containing 2 `EvalCaseResult`s when num_runs=2 on a single-case eval fixture).
This is helpful for debugging failed integration tests.
### 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.
- [x] I have manually tested my changes end-to-end.
- [x] Any dependent changes have been merged and published in downstream modules.
### Additional context
- This PR intentionally preserves public API positional compatibility by appending new optional parameters at the tail of method signatures.
- A generated local eval result JSON file may exist in the working tree from manual verification and is intentionally not part of the code change.
Co-authored-by: Yi Liu <yiliuly@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/4414 from ftnext:agent-evaluator-save-evalset-result 873973e549c0a4b25b83e1ef81e1b4148ab4e379
PiperOrigin-RevId: 962597058
Add an unconditional per-row event_id assigned before enqueue so Storage Write
API retry duplicates are identifiable, and add an opt-in exactly_once_delivery
mode that uses one loop-local committed stream with explicit offsets, sticky
ambiguous-send state, an offset_conflict drop bucket, and non-blocking stream
rotation. Expose finish_reason and sanitized error_message on final LLM
responses only, so progressive SSE does not double count. Emit NODE_OUTPUT and
NODE_ERROR for final workflow-node results while keeping model finish and block
diagnostics classified as LLM_RESPONSE. Remove the dead module-level
OpenTelemetry tracer allocation.
Co-authored-by: Haiyuan Cao <haiyuan@google.com>
PiperOrigin-RevId: 962420983