3478 Commits

Author SHA1 Message Date
adk-bot a5791dab0b chore: update last-release-sha for next main release v2.6.0 2026-07-30 17:54:13 +00:00
adk-bot 0ebac26d1e chore(release/candidate): release 2.6.0 (#6515) 2026-07-30 10:53:47 -07:00
George Weale 7fd8760270 perf: avoid quadratic text/audio accumulation in streaming
StreamingResponseAggregator accumulated streamed text with `+=` on instance
attributes; the live audio cache combined chunks with `bytes +=`. Use
list/join and b''.join instead.

`+=` on an attribute compiles to STORE_ATTR, so it does not get CPython's
in-place concat optimization that makes the same statement on a local variable
cheap. Every chunk therefore re-copies the whole buffer, making aggregation
O(n^2) in the length of the response. It is invisible on short replies and
only bites long streamed answers and long live-audio captures, which is why it
has not surfaced as a bug report.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 956076793
2026-07-29 14:04:47 -07:00
George Weale b3c9783427 feat: refresh expired OAuth2 tokens in OpenAPI credential exchanger
When an OpenAPI OAuth2 credential has an expired access token and a
refresh token, refresh it (reusing the shared OAuth2 helpers) before
wrapping it as a bearer token, instead of returning the stale token.
Refresh failures fall back to the existing token.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 956058160
2026-07-29 13:31:00 -07:00
Lucas Kang a58220cd05 feat: add capability to log commands run in CLI
- Wrap main Click group execution with a custom TelemetryGroup class to track CLI execution metrics.
- Record command name, subcommand, flags, duration, exit code, and exception type when consent is enabled.
- Exclude 'telemetry' command from metrics logging.

Co-authored-by: Lucas Kang <lucaskang@google.com>
PiperOrigin-RevId: 956018260
2026-07-29 12:10:46 -07:00
George Weale 8207880101 fix: do not mount a cluster credential into the GKE code sandbox
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 956005057
2026-07-29 11:48:21 -07:00
Google Team Member eee700a016 fix: harden A2A metadata serialization and parser type validation
Fixes a crash in the A2A converter pipeline caused by type validation failures when handling `custom_metadata`.

1. **Serialization Fix**: Updates legacy event serialization to use standard `json.dumps` for plain dictionaries and lists. Previously, these relied on standard stringification (`str()`), which produces single-quoted (invalid JSON) representations in Python, causing downstream JSON parsing failures.
2. **Parser Hardening**: Enforces strict type checking during inbound metadata extraction. Explicitly validates that `custom_metadata` decomposes to a `dict` before passing it to downstream models. Raw string fallbacks (from failed JSON decodes) are now properly discarded instead of causing Pydantic validation errors.

Includes regression tests for both collection serialization and type-safe metadata fallback in the A2A conversion pipeline.

PiperOrigin-RevId: 956003782
2026-07-29 11:46:15 -07:00
George Weale 802a0793f0 fix: populate finish_reason on Anthropic LLM responses
Wire the existing to_google_genai_finish_reason mapping into both the
non-streaming and streaming responses; previously finish_reason was
always unset on Claude responses.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955985680
2026-07-29 11:14:40 -07:00
George Weale 8882ed6a68 chore: fix mypy strict type errors in adk a2a
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955961604
2026-07-29 10:33:47 -07:00
George Weale ba1736783b fix: send the bare input_schema payload from AgentTool again
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955951433
2026-07-29 10:15:32 -07:00
George Weale 0598c9ba26 fix: narrow broad except in agent identity credential providers
The GAPIC retrieve calls only raise GoogleAPIError / GoogleAuthError, so
catch those (plus TimeoutError on the polling paths) and wrap them with
context as before. Other exceptions now propagate instead of being masked
as credential failures, surfacing real bugs.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955931557
2026-07-29 09:39:07 -07:00
George Weale 761f1ac75d fix: stop tracing credentials passed via config.http_options
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955927070
2026-07-29 09:31:40 -07:00
Max Ind 8930d9b193 feat(telemetry): add request-driven metric export for Agent Engine
On the Vertex AI Agent Runtime CPU is throttled the instant a request
finishes, so a background periodic metric exporter is starved between
requests and drops data. This adds a request-driven metric
reader (and the span processor + middleware that drive it) that
collects and exports metrics on the request path, where CPU is
guaranteed, without adding latency to any request.

The default GCP metric exporter is also switched to a raw OTLP push exporter
over telemetry.googleapis.com.

Co-authored-by: Max Ind <maxind@google.com>
PiperOrigin-RevId: 955921308
2026-07-29 09:17:28 -07:00
Lucas Kang 6bab08fc80 feat: add telemetry consent check, status commands, and interrupt safety to CLI
- Prompts the user during their first interactive CLI subcommand execution to opt in to anonymized telemetry tracking.
- Implements telemetry subcommand group with enable, disable, and status actions to change settings persistently via ~/.adk/config.json.
- Gracefully handles KeyboardInterrupt and EOFError: defaults preference to off for the current session without saving to disk.
- Differentiates unconfigured default-off state from explicitly disabled state in status outputs.
- Adds comprehensive unit tests validating prompts, interrupt triggers, status commands, and preference storage.

Co-authored-by: Lucas Kang <lucaskang@google.com>
PiperOrigin-RevId: 955514787
2026-07-28 15:48:21 -07:00
Anas Khan ecf6d13f64 fix: close AsyncDaytona client in DaytonaEnvironment.close
Merge https://github.com/google/adk-python/pull/6307

PiperOrigin-RevId: 955513593
2026-07-28 15:46:02 -07:00
George Weale 2cf543322b fix: present client certificate and use mTLS endpoint for API Hub calls
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955505332
2026-07-28 15:32:24 -07:00
George Weale 57f3af24a0 perf: cache the FunctionTool declaration across LLM calls
FunctionTool._get_declaration ran pydantic create_model + JSON-schema
generation for every tool on every LLM step, even though the result is fixed
once the tool is constructed. Memoize the build keyed by (func, ignored
params, API variant, feature flag) and return a copy so callers (e.g. toolset
prefixing) can still mutate the result.

The cache is module-level rather than per-instance because a bare callable in
LlmAgent.tools is re-wrapped into a fresh FunctionTool on every step, which
defeats an instance cache for exactly the callers this costs the most.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955481497
2026-07-28 14:44:27 -07:00
Lucas Kang 2280f1cc5b feat: add telemetry metrics collection for ADK CLI execution
Introduce user-opt-in telemetry tracking to collect command run metrics, durations, and environment details. Telemetry requests are processed in an asynchronous background daemon process, protecting against execution latency. Includes backoff rate-limiting compliance to prevent server-side DoS conditions.

Co-authored-by: Lucas Kang <lucaskang@google.com>
PiperOrigin-RevId: 955460451
2026-07-28 14:07:53 -07:00
George Weale 623da4930a fix: serialize eval criteria as their concrete subclass
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955456124
2026-07-28 14:01:18 -07:00
Fede Kamelhar 625ef1aa69 feat(integrations): add OCI Generative AI provider
Adds OCIGenAILlm under integrations/oci/, for Google Gemini and other models
hosted on Oracle Cloud Infrastructure Generative AI. Optional install:
pip install google-adk[oci]. LLMRegistry auto-routing and the
google.adk.models import surface are preserved.

The OpenAI-compatible transport from the source PR (OCIGenAIOpenAILlm) is
not taken. It reimplemented the message, tool and response conversion plus
the streaming loop that OpenAILlm already provides; the right form is a
small subclass overriding the OpenAI client, which cannot live in
integrations/ while OpenAILlm is still experimental. It can land separately
once that settles.

The OCI client is now built once per instance rather than per request, so a
call no longer re-reads the OCI config from disk.

Merge https://github.com/google/adk-python/pull/5285

Closes #5069

Co-authored-by: George Weale <gweale@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/5285 from fede-kamel:feat/oci-generative-ai 0230acc0a93b7e43014f2ef3a8b89de463a50bd8
PiperOrigin-RevId: 955453382
2026-07-28 13:56:27 -07:00
Jason Zhang 94832a5151 docs: Add developer unit guide for ReflectAndRetryToolPlugin
The new guide explains what the plugin does, a get-started
example, how the retry loop and per-tool failure tracking
work, the configuration options, advanced applications,
and its limitations.

Co-authored-by: Jason Zhang <jasoncz@google.com>
PiperOrigin-RevId: 955444373
2026-07-28 13:40:52 -07:00
George Weale a1792a712a fix: scope the tool thread pool to its event loop
Tool thread pools lived in a process-global registry keyed by max_workers and
were never shut down, so every distinct worker count ever requested left a pool
of idle threads alive for the life of the process. The registry is now keyed
weakly by the event loop a pool serves, and each pool is shut down once that
loop is collected. Executors are not bound to a loop, so the defect here is
leaked idle threads rather than threads doing work on a loop they do not belong
to.

Tool calls keep their own pool rather than moving to the loop's default
executor. The loop uses that executor for its own work, including name
resolution, so sharing it would let a blocking tool starve the loop and would
also drop the adk_tool_executor thread name.

max_workers keeps its meaning as the size of that pool, so invocations sharing
a loop share its threads. Because a pool now belongs to one loop rather than to
the process, a program driving several loops at once can hold max_workers tool
threads per loop where it previously held that many in total. Tool execution is
otherwise untouched: cancelling a call still abandons a thread that has already
started, and still drops a call that has not started.

This path only runs when tool_thread_pool_config is set, so the default
configuration is unaffected. There are no public API changes; the pool accessor
is module private.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955439650
2026-07-28 13:32:54 -07:00
George Weale 80a05b7f63 fix(memory): make Vertex RAG uploads async-safe
VertexAiRagMemoryService.add_session_to_memory and search_memory are both
declared async but called the synchronous RAG SDK, so every upload and
retrieval blocked the event loop for the duration of the HTTP round trip.
Both now use the SDK async surface (Client(...).aio) and await the calls.

Each operation owns one async client and closes it in a finally, so the
underlying HTTP session is released on success, failure and cancellation
alike. The close is shielded: a single cancellation is survivable without a
shield, but a second one arriving while the close is suspended - an enclosing
deadline expiring while an inner one is already unwinding, for example -
would otherwise interrupt the close itself and leak the HTTP session.

The session transcript is written to a plaintext temporary file before
upload. Previously that file was removed only on the success path, so a
failed or cancelled upload left the transcript on disk indefinitely. Removal
now happens in a finally, and the path is recorded before the write so a
failed write is cleaned up too. Corpus names are validated before the file is
written, and the file is opened with an explicit utf-8 encoding instead of
the locale default.

Multi-corpus uploads stay sequential and fail fast, because the RAG API
offers no cross-corpus transaction or rollback. The per-corpus RAG handle is
fetched once instead of rebuilt on every iteration.

Behavior change: add_session_to_memory now raises ValueError when a
configured RAG resource has no rag_corpus, which is reachable by constructing
VertexAiRagMemoryService() with no arguments. That configuration previously
wrote the transcript to disk and then failed inside the SDK with
corpus_name=None, so this replaces a late, opaque failure with an early one.
The message changed from "Rag resources must be set." to "rag_corpus must be
set on every RAG resource.", which describes the condition actually checked.
The CLI factory rejects an empty corpus and always passes a fully-qualified
name, so it is unaffected.

The async and sync RAG upload surfaces shipped in the same SDK release, so
this does not raise the minimum google-cloud-aiplatform version.

Scope note: this removes the blocking SDK calls, but the client is still
constructed synchronously, and when neither an explicit project nor a
fully-qualified corpus name resolves a project id, that constructor loads
application default credentials on the event loop. Token refresh on the
request path is already offloaded to a thread by the SDK. These methods are
therefore not yet fully non-blocking.

Public method signatures are unchanged and no public symbol is added.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955437238
2026-07-28 13:28:59 -07:00
Eva 550189ce4f fix: wrap input_schema payload in ReAct prompt and propagate tool_choice to LiteLLM
Merge https://github.com/google/adk-python/pull/5924
END_PUBLIC
## Summary

Fixes two issues that prevent Claude-family models from entering the ReAct tool-calling loop when used via LiteLLM inside a nested `AgentTool`:

1. **`AgentTool` with `input_schema`** — the serialized JSON payload sent as the first message causes Claude to interpret the request as already complete and respond directly without calling any tools.
2. **`tool_choice` not propagated** — `llm_request.config.tool_config.function_calling_config.mode` was not forwarded to LiteLLM's `completion_args`, so callers could not enforce tool use at the request level.

## Changes

### `src/google/adk/tools/agent_tool.py`
Wrap the serialized `input_schema` JSON in a natural-language instruction that explicitly asks the inner agent to use its available tools before producing a response. This keeps Claude in ReAct mode regardless of the message content format.

### `src/google/adk/models/lite_llm.py`
Read `llm_request.config.tool_config.function_calling_config.mode` and map it to LiteLLM's `tool_choice` parameter:
- `ANY` → `"required"`
- `NONE` → `"none"`
- `AUTO` → provider default (unchanged, key omitted from `completion_args`)

`_get_completion_inputs` now returns a 5-tuple `(messages, tools, response_format, generation_params, tool_choice)`.

## Unit Tests Added

### `tests/unittests/tools/test_agent_tool.py`
- `test_run_async_no_input_schema_passes_request_unchanged`: without `input_schema`, the content passed to the inner runner is `args['request']` verbatim.
- `test_run_async_with_input_schema_wraps_in_natural_language`: with `input_schema`, the text begins with `"Process the following structured request"`, contains `"Request:\n"` followed by the JSON payload, and is not a bare JSON blob.
- `test_run_async_with_input_schema_text_not_raw_json`: asserts the text does not start with `{`.

### `tests/unittests/models/test_litellm.py`
- `test_get_completion_inputs_tool_choice_none_without_tool_config`: `tool_choice` is `None` with no `tool_config`.
- `test_get_completion_inputs_tool_choice_required_for_any_mode`: returns `"required"` for `ANY` mode.
- `test_get_completion_inputs_tool_choice_none_for_none_mode`: returns `"none"` for `NONE` mode.
- `test_get_completion_inputs_tool_choice_none_for_auto_mode`: returns `None` for `AUTO` mode.
- `test_generate_content_async_propagates_tool_choice_required`: `acompletion` receives `tool_choice="required"` for `ANY`.
- `test_generate_content_async_propagates_tool_choice_none_mode`: `acompletion` receives `tool_choice="none"` for `NONE`.
- `test_generate_content_async_omits_tool_choice_for_auto_mode`: `tool_choice` key absent from `completion_args` for `AUTO`.
- `test_generate_content_async_omits_tool_choice_without_tool_config`: `tool_choice` key absent when no `tool_config`.

Also updated all existing `_get_completion_inputs` call sites (10 occurrences) to unpack the new 5-tuple.

## Pytest Results

```
tests/unittests/tools/test_agent_tool.py + tests/unittests/models/test_litellm.py
1 failed (pre-existing: test_custom_schema[GOOGLE_AI] — unrelated to this PR),
302 passed, 1 skipped in 2.98s

New tests: 11 passed (3 agent_tool + 8 litellm)
```

The 1 pre-existing failure (`test_custom_schema[GOOGLE_AI]`) is a `pydantic.ValidationError` that reproduces on the base branch before any of these changes and is unrelated to this fix.

## Related

- Fixes #5926
- Addresses #773 (expose tool_choice to callers via FunctionCallingConfig)
- Related #1063 (fixed FunctionDeclaration description in v1.20, different issue)

Co-authored-by: George Weale <gweale@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/5924 from ecanlar:fix/agent-tool-input-schema-tool-choice-litellm 9cd8f31167e0a2a55c76e6cc8ea4a7d0850fd243
PiperOrigin-RevId: 955433983
2026-07-28 13:23:15 -07:00
George Weale 93db97db33 feat: add --extra_packages option to adk deploy agent_engine
The agent_engine deploy path only uploaded a fixed set of source
packages, so users could not ship extra local libraries alongside their
agent. Add a repeatable `--extra_packages` option (also settable via an
`extra_packages` key in the agent platform config file) that stages each
given file or directory into the build context, appends it to
source_packages, and copies it into the image with `/app` prepended to
PYTHONPATH so it is importable at runtime.

Close #3936

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955429600
2026-07-28 13:15:44 -07:00
Harineko0 5a12ee0998 fix(a2a): Promote RemoteA2aAgent response to workflow node output
Merge https://github.com/google/adk-python/pull/5852

## Link to Issue or Description of Change

### Problem

When a `RemoteA2aAgent` is used as a static node in a `Workflow` graph that feeds into a `JoinNode`, the joined output contains `None` for every `RemoteA2aAgent` predecessor.

Reproducer (simplified from a real coordinator graph):

```python
parallel_investigation_join = JoinNode(name="parallel_investigation_join")
Workflow(
    edges=[
        ("START", account_context_agent, parallel_investigation_join),  # RemoteA2aAgent
        ("START", ticket_history_agent, parallel_investigation_join),   # RemoteA2aAgent
        ("START", diagnostics_agent, parallel_investigation_join),      # RemoteA2aAgent
        ...
    ]
)
```

Observed `JoinNode` input:

```yaml
parallel_investigation_join:
  account_context_agent: null
  ticket_history_agent: null
  diagnostics_agent: null
```

Root cause: `RemoteA2aAgent` inherits the default `BaseAgent._run_impl`, which iterates `run_async` and yields events without ever setting `event.output` or `event.node_info.message_as_output`. As a result, `NodeRunner._track_event_in_context` leaves `ctx.output` as `None`, and `Workflow._handle_completion` never records an entry in `loop_state.node_outputs` for that predecessor. `JoinNode` then sees `None` for it.

`LlmAgent` already solves the equivalent problem by overriding `_run_impl` and promoting the model's text reply to `event.output` (via `process_llm_agent_output` in `_llm_agent_wrapper.py`). `RemoteA2aAgent` had no equivalent hook.

### Solution

Add a workflow-only override of `_run_impl` on `RemoteA2aAgent` that mirrors `LlmAgent`'s behavior. For each event yielded by `BaseAgent._run_impl`, a new `_promote_response_to_output` helper joins the text of all parts that are **not** thoughts, function calls, or function responses, assigns it to `event.output`, and sets `event.node_info.message_as_output = True` (consistent with `LlmAgent`, prevents `NodeRunner._flush_output_and_deltas` from emitting a duplicate trailing output event).

The helper skips:

- partial events (streaming chunks)
- events not authored by this agent
- events whose `event.output` is already set
- events whose content carries only thoughts (streaming `working` / `submitted` task statuses that the legacy `_handle_a2a_response` marks `thought=True`)
- events whose content carries only function calls (the `input_required` / `auth_required` mock function call inserted by `_create_mock_function_call_for_required_user_input` — those should remain interrupts, not outputs)
- events whose A2A task state is non-final (`submitted`, `working`, `input-required`, `auth-required`, `unknown`). The v2 integration path (`_handle_a2a_response_v2`) delegates to converters that do **not** mark streaming `working` text as `thought=True`, so the thought filter alone is not enough. Without this guard, a `working` text event and the subsequent `completed` text event would each try to set `event.output`, causing `NodeRunner` to raise `ValueError: Output already set` on the second event and aborting the run before the real final answer ever surfaced. The state is read from `event.custom_metadata['a2a:response']['status']['state']`, which `_run_async_impl` already stamps before yield. Plain `A2AMessage` responses (no status field) and terminal task states (`completed`, `failed`, `canceled`, `rejected`) still promote.

In addition, `_run_impl` short-circuits after the first successful promotion. This protects against the case where a server emits multiple terminal-state events for one run (e.g. a `completed` status update followed by trailing artifact updates on the same already-completed task) — only the first terminal event becomes the node's output, subsequent ones pass through untouched.

Scope is intentionally narrow: only the agent boundary is touched. `to_adk_event.py` and the workflow scheduler are unchanged, since the same workaround (promoting content → output at the agent layer) is what `LlmAgent` does and what keeps the fix local.

## Testing Plan

### Unit Tests

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

Added `TestRemoteA2aAgentWorkflowOutput` in `tests/unittests/agents/test_remote_a2a_agent.py` (20 cases).

### Manual End-to-End (E2E) Tests

I ran the failing workflow described in the **Problem** section against [a real ADK app](https://github.com/gdsc-osaka/customer-support-agent-example/blob/2b2882ed0bd0e918aeaaa38b2e420f129db47dc1/agents/coordinator/agent.py#L27): a `Workflow` graph whose `START` fans out into multiple `RemoteA2aAgent` nodes that all feed into a single `JoinNode`. Each remote specialist runs as its own A2A server; the coordinator runs the workflow and forwards the joined dict to a downstream synthesis step.

Before the fix:

<img width="1624" height="1060" alt="Screenshot 2026-05-26 at 15 32 06" src="https://github.com/user-attachments/assets/22ddb04b-4c1d-4c75-a2ec-d4f1e87bc825" />

After the fix:

<img width="1580" height="1016" alt="Screenshot 2026-05-26 at 15 30 25" src="https://github.com/user-attachments/assets/0848a663-504f-4e60-99ff-81aa70daf164" />

### 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

The fix mirrors the pattern `LlmAgent` already uses (`process_llm_agent_output` in `src/google/adk/workflow/_llm_agent_wrapper.py`), keeping output-promotion at the agent boundary rather than touching the workflow scheduler or the A2A converters. This minimizes blast radius and avoids regressions in non-workflow usages of `RemoteA2aAgent`, where the `_run_impl` path is not exercised.

Co-authored-by: George Weale <gweale@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/5852 from Harineko0:fix/remote-a2a-agent-workflow-output 0f262921e5ccbe24fbb33427dd378fad857cd8da
PiperOrigin-RevId: 955426327
2026-07-28 13:10:02 -07:00
George Weale 2eca8b11ce fix(ci): mark imported PRs as merged even if already closed
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955418614
2026-07-28 12:57:07 -07:00
Yifan Wang 8d2ded3bec feat: add agent identity auth manager finalize endpoint for 3 legged OAuth flow with auth manager
Co-authored-by: Yifan Wang <wanyif@google.com>
PiperOrigin-RevId: 955397715
2026-07-28 12:14:40 -07:00
George Weale b6c257572b docs: explain how an accepted pull request lands
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955382556
2026-07-28 11:47:52 -07:00
George Weale 02e32a4d53 feat: make agent evaluation compatible with pre-loaded artifacts
Artifacts are keyed by (app_name, user_id, session_id), but AgentEvaluator
gave callers no way to supply an artifact service and always generated a
random session id per eval case, so pre-loaded artifacts were unreachable
during eval. Thread an optional artifact_service through
AgentEvaluator.evaluate and evaluate_eval_set into LocalEvalService, and
add an optional SessionInput.session_id that is honored per eval case so
each case can target the session its artifacts live under. Both additions
default to preserving today's behavior.

A pinned session id is reused rather than replaced: when a session already
exists under that id the eval runs against it, so a session the caller
prepared keeps its events and state. Three consequences of reusing:

- SessionInput.state is applied only when the session has to be created.
  Pinning an existing session and also setting state does not merge that
  state into the existing session.
- With num_runs > 1 every run of a pinned case shares one session, so
  events accumulate and later runs see earlier runs' history. Cases that
  do not pin an id are unaffected and still get a fresh session per run.
- Two eval cases pinned to the same id share that session and its
  artifacts, and append to it concurrently when cases run in parallel. A
  pinned id should be unique per eval case.

Close #2075

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955381704
2026-07-28 11:46:01 -07:00
Sarath Francis a60d5b9522 fix(tools): bind JSONDecodeError in ToolConnectionAnalyzer.analyze
Merge https://github.com/google/adk-python/pull/5942

**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

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

**Problem:**

`ToolConnectionAnalyzer.analyze()` in
`src/google/adk/tools/environment_simulation/tool_connection_analyzer.py`
crashes whenever the analyzed LLM returns a response that is not valid JSON.

The JSON parse is wrapped in a `try/except`, but the `except` clause does not
bind the exception:

```python
except json.JSONDecodeError:
    logging.warning(
        "Failed to parse tool connection analysis from LLM. Proceeding"
        " without connection map. Error: %s\nLLM Output:\n%s",
        e,                # <-- 'e' was never defined
        response_text,
    )
    return ToolConnectionMap(stateful_parameters=[])
```

The warning log references `e`, but the `except` clause binds nothing. So the
moment a non-JSON response is parsed, the handler itself raises
`NameError: name 'e' is not defined`. This masks the real parse error and
crashes `analyze()` instead of degrading gracefully to an empty
`ToolConnectionMap` as the surrounding code clearly intends.

Reproduced traceback (LLM mocked to return `"this is not json at all"`):

```text
  File ".../tool_connection_analyzer.py", line 136, in analyze
    response_json = json.loads(clean_json_text.strip())
  ...
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

  File ".../tool_connection_analyzer.py", line 141, in analyze
    e,
    ^
NameError: name 'e' is not defined
```

**Solution:**

Bind the caught exception with `as e` so the handler can log the real parse
error and return an empty `ToolConnectionMap` as designed:

```python
except json.JSONDecodeError as e:
    logging.warning(...)
    return ToolConnectionMap(stateful_parameters=[])
```

This is a one-character fix; the surrounding logging and fallback behaviour are
unchanged. After the fix, the same input logs the underlying parse error and
returns `ToolConnectionMap(stateful_parameters=[])` without raising.

### Testing Plan

**Unit Tests:**

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

This branch of `analyze()` previously had zero coverage, so I added
`tests/unittests/tools/environment_simulation/test_tool_connection_analyzer.py`
covering:

- the malformed-JSON path (regression guard — fails with `NameError` on the
  pre-fix code, passes after the fix),
- the valid-JSON path, and
- the Markdown code-fence stripping path.

`pytest` summary (low parallelism, as run locally):

```text
$ pytest tests/unittests/tools/environment_simulation/ -n2
======================= 12 passed, 11 warnings in 1.64s ========================
```

```text
$ pytest tests/unittests/tools/environment_simulation/test_tool_connection_analyzer.py -n0 -v
...test_malformed_json_returns_empty_map_without_crashing PASSED
...test_valid_json_is_parsed_into_connection_map PASSED
...test_fenced_json_is_stripped_before_parsing PASSED
======================== 3 passed, 5 warnings in 0.57s =========================
```

Verified the regression test fails on the unfixed code with the exact
`NameError: name 'e' is not defined` and passes after the fix. `isort` and
`pyink` report no changes on the modified files.

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

Reproduced directly against the analyzer with a mocked LLM:

```python
analyzer = ToolConnectionAnalyzer(llm_name=..., llm_config=...)
# LLM mocked to return a non-JSON string.
result = await analyzer.analyze([some_tool])
```

- Before the fix: raises `NameError: name 'e' is not defined`.
- After the fix: logs
  `Failed to parse tool connection analysis from LLM... Error: Expecting value: line 1 column 1 (char 0)`
  and returns `ToolConnectionMap(stateful_parameters=[])`.

### 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.
- [ ] Any dependent changes have been merged and published in downstream modules.

### Additional context

The affected module is marked `@experimental(FeatureName.ENVIRONMENT_SIMULATION)`.

Co-authored-by: George Weale <gweale@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/5942 from sarathfrancis90:fix-tool-connection-analyzer-jsondecodeerror 536dd569f44eca5eb54d46268cbbcbdb34077374
PiperOrigin-RevId: 955373698
2026-07-28 11:32:37 -07:00
George Weale b3abcb2b28 fix: preserve explicit false and zero OpenAPI query parameters
RestApiTool dropped query parameters whose value was falsy, so an explicit
False or 0 was sent identically to an omitted parameter. APIs that
distinguish an absent parameter from an explicit false/zero (boolean
filters, pagination offsets) received incorrect requests. Filter query
parameters on "is not None" instead of truthiness.

Close #6287

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955373236
2026-07-28 11:31:45 -07:00
George Weale bc550991b9 fix(agents): await cancelled tasks in pre-3.11 ParallelAgent merge
`_merge_agent_run_pre_3_11()` cancelled its background tasks but did
not await them, so a sibling task could still be mid-`async for` when
the caller ran `aclose()` on the same generator and hit `RuntimeError:
aclose(): asynchronous generator is already running`. That cleanup
race could mask the original sub-agent failure.

Close #5297

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955369097
2026-07-28 11:23:35 -07:00
George Weale 425dda1904 fix: canonicalize context cache fingerprint for stable hashing
Serialize the fingerprint with sorted JSON keys, exclude_none, and a
stable tool order so reordered tools or SDK field drift no longer change
the hash. Existing sessions take a one-time cache miss while the stored
fingerprint is recomputed.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955368831
2026-07-28 11:22:58 -07:00
George Weale 6264576784 perf(tools): avoid double Schema serialization in Optional/Union dedup
The any_of dedup loop in `_parse_schema_from_parameter` called
`Schema.model_dump_json` twice per Union member — once for the membership
check and once for the set add. Compute the JSON key once and reuse it,
halving Schema serialization for every tool param with `Optional`/`Union`
type on every parse.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955361274
2026-07-28 11:09:49 -07:00
George Weale 40ec9a85d1 test(plugins): mock the BigQuery plugin collaborators from their real contracts
Five tests passed vacuously. The plugin logs and swallows exceptions from
shutdown() and from on_event_callback, so mocks that violated the real
collaborator contract aborted the code under test instead of failing the test.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955359560
2026-07-28 11:06:59 -07:00
Jason Zhang 66a72337b3 fix(plugins): restore failure counter properties on retry tool plugin
Restore private `_scoped_failure_counters` and `_lock` properties
on `ReflectAndRetryToolPlugin` for backward compatibility.

Keep scoped failure counters after creation.

Co-authored-by: Jason Zhang <jasoncz@google.com>
PiperOrigin-RevId: 955323115
2026-07-28 10:07:59 -07:00
George Weale fb55d4a669 fix(a2a): honor task cancellation instead of raising NotImplementedError
Both executors now publish the terminal canceled status update through one
shared helper, so tasks/cancel reaches the canceled state instead of failing
with an internal error.

final=True in that helper is load-bearing but is not covered by CI. a2a-sdk
0.3.x treats a status update as terminal only when final=True; 1.x removed the
field and infers finality from the canceled state. uv.lock resolves 1.1.0, so
no test exercises the 0.3.x branch -- setting final=False here would pass the
entire suite while hanging every 0.3.x server until it times out. 0.3.x was
verified by hand against 0.3.12, 0.3.20, 0.3.22 and 0.3.26; a 0.3.x CI job is
the only thing that would keep it verified.

execute() still deliberately does not catch asyncio.CancelledError: it is a
BaseException, so it already passes through "except Exception" untouched, and
catching it would publish a failed status update racing the canceled one.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 955278982
2026-07-28 08:39:30 -07:00
Google Team Member 3dd1156c33 feat(a2a): support per-invocation auth headers when fetching agent cards
The agent card fetch could only be authenticated via a pre-built httpx
client with credentials fixed at construction time; request_interceptors
only wrap send_message, so the card GET stayed unauthenticated.

Add card_request_interceptors on A2aRemoteAgentConfig (symmetric with
request_interceptors). Each CardRequestInterceptor.before_request is an
async hook returning a typed A2aCardRequestConfig whose headers are injected
into the card request.

PiperOrigin-RevId: 955226237
2026-07-28 06:40:04 -07:00
Jialong 455853b5bc fix: scope replay sequence to the current invocation
Merge https://github.com/google/adk-python/pull/6498

Closes #6497

PiperOrigin-RevId: 955036653
2026-07-27 22:48:38 -07:00
Yufeng He 3a9a88c975 fix: single-flight Discovery Engine mode detection
Merge https://github.com/google/adk-python/pull/6106

Make the auto-detect probe single-flight per tool instance. The first cold caller detects the datastore mode and caches it; concurrent callers re-check the cached mode after the lock and go straight to the detected mode. The successful CHUNKS case is cached too, since the result mode is a datastore property rather than a query property.

Closes: #6101
PiperOrigin-RevId: 954907232
2026-07-27 17:00:36 -07:00
Kathy Wu b315b0024a fix: Call mtls.should_use_mtls_endpoint with client_cert_available argument
Fixes failing unit test - there was a false negative in "auto" mode when a certificate was present

Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 954884006
2026-07-27 16:10:17 -07:00
zhangzherui 95feafa3b2 fix: isolate delegated task branches
Merge https://github.com/google/adk-python/pull/6495

Fixes #6457

PiperOrigin-RevId: 954877821
2026-07-27 15:58:00 -07:00
Amy Wu 75c773ed9d feat: Publish companion constraints-3.11.txt and constraints-3.12.txt file for transitive dependency protection (4 day buffer to protect from supply chain attack)
For example, use pip install google-adk -c constraints-3.12.txt

PiperOrigin-RevId: 954863904
2026-07-27 15:33:06 -07:00
George Weale 0a70337f29 fix: raise SessionNotFoundError when appending to a missing session
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 954860721
2026-07-27 15:27:37 -07:00
Haiyuan Cao 9adf0113ea fix(plugins): complete BigQuery Agent Analytics privacy and shutdown hardening
Harden BigQueryAgentAnalyticsPlugin so it fails closed around formatter and
parser output, redacts sensitive mappings, diagnostic text, signed URIs, and
raw prompt text before inline or GCS storage, validates existing table schema
type and mode recursively at startup, and reports shutdown success only after
all owned work, clients, and executors are drained or closed. Queue accounting
is O(1), JSON nesting is bounded consistently across Python versions, and remote
drains are created and finalized on their owning event loop.

Co-authored-by: Haiyuan Cao <haiyuan@google.com>
PiperOrigin-RevId: 954858186
2026-07-27 15:22:12 -07:00
Anas Khan bb6d547738 test: exercise real connect path in test_connect
Merge https://github.com/google/adk-python/pull/6394

PiperOrigin-RevId: 954855829
2026-07-27 15:17:10 -07:00
George Weale 46aaa313f5 test: make unit contracts platform neutral
Pre-emptive: every CI job is ubuntu-latest, so none of these tests fail today.
No assertion is weakened - each replacement is equivalent or stricter on Linux.

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 954835278
2026-07-27 14:37:57 -07:00
Google Team Member 66cf08d187 feat: Update agent_registry to handle mTLS endpoints internally
Transforms connection URIs conditionally based on mtls.should_use_mtls_endpoint() and client_cert checks, addressing the mTLS implementation guidelines safely.

PiperOrigin-RevId: 954825139
2026-07-27 14:18:26 -07:00
h-tsuboi918 1478e1aa21 ci: ignore OAuth scopes in endpoint check
Merge https://github.com/google/adk-python/pull/6245

Fixes #6238

PiperOrigin-RevId: 954813339
2026-07-27 13:56:21 -07:00