When running with a mocked `SessionService` whose `append_event` returns a `Mock`
object, `user_event.branch` dynamically evaluates to an `AsyncMock`/`MagicMock`
instead of `None` or a `str`. Guarding with `isinstance(..., str)` ensures
`invocation_context.branch` is only updated when a real string branch is
present, preventing downstream Pydantic validation and Proto conversion errors.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 970212560
modules that own those concepts
No behavior change. `Runner` had grown four private helpers that parse node
paths, classify branches and scan events, none of which depend on runner state:
- `_strip_run_ids_from_path` -> `_NodePathBuilder.static_path`. Node paths are
`/`-separated and owned by `_NodePathBuilder`, which already strips the run id
off the leaf segment; this generalises that to every segment instead of
reimplementing the parsing in `Runner`.
- `_is_tool_branch` -> `_BranchPath.is_tool_branch`. Branches are
`.`-separated and owned by `_BranchPath`, which the helper already depended on.
- `_collect_function_call_ids` -> `_collect_function_call_ids` in
`flows/llm_flows/functions`, next to `find_event_by_function_call_id`.
- `_find_static_node_path` -> `find_static_node_path` in `workflow/_base_node`,
the module that defines the node tree it walks.
Keeping the two path types with their own classes also removes a hazard: node
paths and branches both look like `name@id` segments but use different
separators, so parsing them by hand in a third module invites applying the wrong
one.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 970192989
The dev-server test-run endpoint spawned pytest inside a fire-and-forget
asyncio.create_task() and piped its output through an unbounded asyncio.Queue.
Nothing owned that task, so a client that disconnected mid-run left pytest, its
descendants, and the output pump running until the server itself exited, and
the queue could grow without bound while no consumer was draining it.
The response iterator now owns the subprocess for its whole lifetime: it spawns
pytest, reads bounded chunks straight off the pipe so the client applies
natural backpressure, and terminates the process tree in a finally block.
Termination reaches descendants rather than just the direct child - on POSIX
pytest is started as its own process-group leader and signalled with os.killpg,
and on Windows it runs in a new process group torn down with taskkill /T.
Cleanup escalates from a graceful signal to a forced kill after a bounded wait,
and falls back to signalling the direct child if the process group turns out
not to exist.
Cleanup runs under an anyio shield, so the cancel scope the server cancels on
client disconnect cannot interrupt it partway. The shield covers the common
case, where the disconnect arrives while the iterator is parked reading pytest
output or awaiting process exit. It is not a guarantee on every path: if the
disconnect lands while the iterator is suspended at a yield, the async
generator is dropped rather than cancelled, and its finally block runs at
async-generator finalization instead. That finalization does happen under
CPython, but its timing is not deterministic.
Behavior change: disconnecting from the test-output stream now aborts the
in-flight pytest run. Previously the run continued to completion in the
background after the client went away. Nothing persists the result of a run -
the output is only streamed - so a background completion was unobservable, but
a caller that relied on starting a run and hanging up must now keep the
response stream open until it ends. The endpoint path, its parameters, and the
streamed byte content are unchanged.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 970107514
Any event the node authored is eligible, including a plain text turn, which is
all a non-resumable sub-agent may leave behind. Tool messages are excluded: a
tool's user-facing message is authored under the agent's name and carries the
agent's node path, but is published on `<tool>@<function_call_id>`, so a branch
whose trailing id matches a function call in the session is skipped. Nodes are
matched by static node path (run ids stripped), falling back to author/name for
path-less legacy events, disambiguating sub-agents that share a name. At the
resume sites the scan is scoped to the invocation being resumed; the
new-invocation and live paths use the most recent matching event.
This supersedes the earlier (unsubmitted) cl/944151553, whose path-matching and
invocation-scoping approach is folded in here.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 970044047
This adds a default "telemetry" key initialized to null in runtime-config.json
Co-authored-by: Yifan Wang <wanyif@google.com>
PiperOrigin-RevId: 970012184
Content assembly decided whether to preserve adk- function call ids by
resolving AnthropicLlm, LiteLlm and OpenAIResponsesLlm through inline
try/except imports on every LLM request. Python does not cache a failed
import, so an install without those optional packages re-ran the module
finder and re-executed the shim module bodies each time. The lookup now
lives in a memoized module-private helper.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 969983972
`CheckableMcpHttpClientFactory` exists to add `@runtime_checkable` to the SDK's
`McpHttpClientFactory`. Pydantic compiles a Protocol-annotated field into an
`is-instance` validator, and that fails at class construction time on a
protocol without it, so `SseConnectionParams` and
`StreamableHTTPConnectionParams` cannot declare `httpx_client_factory` any
other way.
The base class it inherits is not public. It lives in
`mcp.shared._httpx_utils`, is absent from that module's `__all__`, and reaches
ADK only because `mcp.client.streamable_http` happens to re-export it. A
release that stops re-exporting it makes this module fail to import, and with
it every MCP tool.
Declare the protocol here instead. Structural typing means a factory written
against either declaration satisfies both, so nothing else changes. The
signature still has to match the SDK's: `_DebugHttpxClientFactory` wraps the
given factory and calls it by keyword, and `sse_client` receives that wrapper,
typed there with the SDK's own protocol.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 969961072
Adds six histograms for one agent invocation's token spend, alongside the
existing `gen_ai.invoke_agent.{inference,tool}_calls`:
adk.experimental.invoke_agent.input_tokens
adk.experimental.invoke_agent.output_tokens
adk.experimental.invoke_agent.total_tokens
adk.experimental.invoke_agent.cache_read.input_tokens
adk.experimental.invoke_agent.reasoning.output_tokens
adk.experimental.invoke_agent.tool.input_tokens
Gated, as `adk.experimental.*` metrics are, on the experimental-telemetry
opt-in: `RunConfig.telemetry.adk_experimental_telemetry_opt_in`, falling back
to `ADK_EXPERIMENTAL_TELEMETRY`.
PiperOrigin-RevId: 969833352
When resuming a single-turn agent node from an HITL tool confirmation pause,
prepare_llm_agent_input was unconditionally appending a synthetic user event,
shadowing the user's FunctionResponse and causing an infinite confirmation loop.
This change skips synthetic user input injection when resume_inputs are present.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 969178203
`_is_session_disconnected` reads `session._read_stream._closed` and
`session._write_stream._closed`. Four attribute reads, all four private to the
MCP SDK, none of them promised.
The probe is not the only liveness signal. `create_session` pairs it with
`SessionContext._is_task_alive`, which ADK owns and which catches strictly
more: a crashed transport can leave both streams open while the task behind
them is already dead. So a missing attribute has a sensible answer -- treat
the session as connected and let the task check decide -- and no reason to
take down the call with an `AttributeError`.
Read the four defensively and say in the docstring where liveness actually
comes from. No behaviour change while the SDK keeps the streams: a closed
stream still reports disconnected, and either stream counts.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 968726270
Merge https://github.com/google/adk-python/pull/6701
## Summary
Implements **Cloud Build private worker pool** support for `adk deploy agent_engine` ([#2141](https://github.com/google/adk-python/issues/2141)).
### Why this is needed
Enterprise / VPC-SC Agent Engine deploys often **cannot use the default public Cloud Build pool**. Without a way to point the build at a private worker pool, `adk deploy agent_engine` fails for teams that require private networking, org build policies, or connectivity to private resources.
The Vertex Agent Engine SDK already accepts this via `config.build_config.worker_pool` → `spec.build_spec.worker_pool`, but ADK never exposed it on the CLI or documented a first-class config key.
### What we did
- Added `--worker_pool` to `adk deploy agent_engine`
- Accepted the same value from `.agent_engine_config.json` as either:
- top-level `"worker_pool": "projects/.../workerPools/..."` (convenience), or
- `"build_config": {"worker_pool": "..."}` (native SDK shape)
- Nested the value into `agent_config["build_config"]["worker_pool"]` before `client.agent_engines.update(...)`
- Validated the Cloud Build resource name format early with a clear error
- Preserved other `build_config` fields (e.g. build `service_account`)
- CLI flag overrides config-file values (same pattern as `display_name`)
### How it fits
```
adk deploy agent_engine --worker_pool=...
│
▼
to_agent_engine(... worker_pool=...)
│
▼
agent_config["build_config"]["worker_pool"] = <resource name>
│
▼
vertexai.Client().agent_engines.update(config=agent_config)
│
▼
Cloud Build runs on the private worker pool
```
### Usage
```bash
adk deploy agent_engine \
--project=my-project \
--region=us-central1 \
--worker_pool=projects/my-project/locations/us-central1/workerPools/my-private-pool \
my_agent
```
Or in `.agent_engine_config.json`:
```json
{
"worker_pool": "projects/my-project/locations/us-central1/workerPools/my-private-pool"
}
```
### Verification
- Confirmed `worker_pool` was **not** previously implemented in ADK (`rg` / deploy path audit)
- Confirmed Vertex SDK mapping in `vertexai/_genai/agent_engines.py` (`build_config.worker_pool` → `spec.build_spec.worker_pool`)
- Added unit tests for validation, config nesting, CLI passthrough, and deploy config forwarding
- `uv run pytest tests/unittests/cli/utils/test_cli_deploy.py` → **60 passed**
Fixes#2141
## Test plan
- [x] Unit tests for `_validate_worker_pool` (valid + malformed)
- [x] Unit tests for `_apply_worker_pool_to_agent_config` (CLI, config top-level, override, preserve other build_config fields)
- [x] `to_agent_engine` forwards `build_config.worker_pool` on `agent_engines.update`
- [x] CLI `--worker_pool` reaches `to_agent_engine`
- [ ] Maintainer review of CLI naming / config-file shape
- [ ] Optional: end-to-end deploy against a real private worker pool in a VPC-SC project
---
cc @klateefa @yeesian @wuliang229 @Jacksunwei @hangfei @llalitkumarrr @GWeale
I claimed this on [#2141](https://github.com/google/adk-python/issues/2141#issuecomment-5274468133) (self-assign needs triage permissions — please assign me `@a2105z` if that helps routing). Ready for review whenever you have a moment.
Co-authored-by: Yifan Wang <wanyif@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/6701 from a2105z:feat/agent-engine-worker-pool c59661ae1c2e244037e85f5b0a577b9cff70197b
PiperOrigin-RevId: 968673077
`_detect_error_in_response` reads `isError` off the dumped `CallToolResult`.
MCP SDK 1.x names that field `isError`, so the lookup works. 2.x renames it to
`is_error`, so `model_dump` emits the snake_case key and the lookup returns
`None` for every result. Tool errors would stop being reported to telemetry,
with nothing in the logs to say so.
`_get_declaration` has the same problem one step earlier: it reads
`inputSchema` and `outputSchema`, which 2.x renames and removes. That one at
least fails loudly with an `AttributeError`.
Read both spellings. `_read_field` returns the first attribute a model
defines, so it picks up the camelCase name on 1.x and the snake_case name on
2.x, and raises a named error if a future release renames the field again.
`model_dump(by_alias=True)` would have been a smaller change, but it is not
equivalent: every MCP result model aliases `meta` to `_meta`, so dumping by
alias would rename that key in the payload ADK returns to the caller. The
casing of the returned dict is left alone here and belongs with the SDK
upgrade itself.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 968586089
`SessionContext._start` builds `ClientSession`'s `read_timeout_seconds` twice,
once for stdio and once for SSE and streamable HTTP, each with its own inline
`timedelta(...) if ... is not None else None`.
Move that to `_read_timeout`. ADK carries every timeout as float seconds and
now converts once, at the boundary where the SDK is called.
This is a plain de-duplication today. It also isolates a difference between
MCP SDK versions: 1.x types `read_timeout_seconds` as a `timedelta` and 2.x
types it as a float, so the conversion is the only line that has to change.
The explicit `is None` check is deliberate. A zero timeout is a real value, not
a missing one, and a truthiness check would silently turn it into "no timeout".
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 968580764
A streaming tool used to only talk to the model: every value it yielded came
back as a FunctionResponse. Therefore it costs model context and could derail the model's reasoning.
An Event with message yielded by a streaming tool is now addressed to the user instead. It is enqueued on the invocation's event queue, so the runner appends it to the session and streams it to the client, and it is never sent over the live model
connection. Plain values are still sent to the model as FunctionResponse. A tool can mix and match any number of each, in any order.
Runner's run_live also now initializes the invocation's event queue and merges it with the live agent's own event stream. Without a queue, anything running under
the live agent that enqueues an event -- a streaming tool, or a node -- fails
with "_event_queue is not set".
Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 968533249
Merge https://github.com/google/adk-python/pull/6698
Adds a `retention` parameter to `MultimodalToolResultsPlugin` to allow
tool-returned parts to persist across turns in a session.
Fixes#6695
PiperOrigin-RevId: 968187977
`GCPSkillRegistry.search_skills` built a `Frontmatter` for every hit with no
per-item error handling. `Frontmatter.name` must be kebab-case (or snake_case
behind the feature flag), but the catalog holds names outside that set -- the
first-party entry `cloud.google.com-agent-platform-eval-flywheel` has dots. The
first such hit raised a pydantic `ValidationError` and took down the whole
call, so search returned nothing at all against any catalog that holds one
non-conforming entry. An entry with an empty description did the same.
Skip the entry and log a warning instead. The caller does not control what the
catalog holds, so one entry it never asked about must not break discovery for
everything else.
A name that is not a string gets the same treatment. `.split` on it raises
before validation is reached, which would sink the call the same way.
Fixes#6838
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 968161389
Merge https://github.com/google/adk-python/pull/6032
In the Workflow run path, capture the before_run_callback result and early exit if it is Content, matching the non-workflow path behavior.
Fixes#6013
PiperOrigin-RevId: 968159218
Merge https://github.com/google/adk-python/pull/5035
Enable injecting pre-configured LLM clients into Gemini and AnthropicLlm models to support multi-agent systems with distinct configurations.
Fixes#5027
PiperOrigin-RevId: 968151995
Reverts #6462. An agent served over A2A could no longer satisfy a
human-in-the-loop tool confirmation at all: the request converter stamped
`a2a_metadata` on every inbound invocation unconditionally, and the
confirmation processor returned early whenever that key was present, dropping
the operator's legitimate approval along with a forged one. Where A2A is the
only channel reaching the human operator, every tool requiring confirmation was
permanently blocked.
The guard also did not close the vector it targeted: #6461 notes `/run` and
`/run_sse` are unauthenticated by default, so the same forged approval still
lands over plain HTTP. Gating on the transport rather than on the identity of
the approving principal is at once too broad and too narrow.
Reopens#6461.
Co-authored-by: Xuan Yang <xygoogle@google.com>
PiperOrigin-RevId: 968106938
`AgentRegistry.get_mcp_toolset` already derives an `auth_scheme`/`auth_credential`
pair from a registered resource's auth provider binding, but
`get_remote_a2a_agent` ignored bindings entirely. A caller who registered an A2A
agent behind an auth provider had no way to reach it: the returned
`RemoteA2aAgent` sent unauthenticated requests, and the caller could not supply
a credential either.
Accept `auth_scheme`, `auth_credential` and `continue_uri` on
`get_remote_a2a_agent` and forward the pair to `RemoteA2aAgent`. When no scheme
is passed, resolve the agent's `authProviderBinding` into a
`GcpAuthProviderScheme`. The signature and the resolution path mirror
`get_mcp_toolset`, so both registered resource types authenticate the same way.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 968076388
Allow callers to explicitly specify the Google Cloud location when listing data agents, following standard three-level precedence (explicit argument, toolset config location, falling back to global).
PiperOrigin-RevId: 968067492
`RemoteA2aAgent` could not authenticate its calls; a caller had to bake a static
token into a custom `httpx_client`. Every other ADK component with a remote
endpoint takes an `auth_scheme`/`auth_credential` pair.
Accept that pair and an optional `credential_key`. `CredentialManager` resolves
the credential once per invocation, and the headers go on the card fetch and the
message send; with nothing to send, the agent emits `adk_request_credential`.
The interceptors and the derived key are per agent, so one agent's token cannot
reach another agent's host.
`build_auth_headers` also stops sending `Bearer None` for a tokenless OAuth2
credential. The interactive round trip needs an `LlmAgent` parent; the
`AgentRegistry` path resolves server-side.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 968025847
**Problem:**
contents.py was over 1,400 lines and mixed high-level LLM request content assembly with low-level event compaction filtering and missing function-call recovery logic.
**Solution:**
Extract _process_compaction_events and _recover_compacted_function_calls into a private helper module _content_compaction.py, preserving re-exports in contents.py for backward compatibility, and move corresponding unit tests into test__content_compaction.py.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 967989388