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
Add granular developer guide for BaseRunner and InMemoryRunner covering session
lifecycle resolution, RunConfig, and event streaming under
docs/guides/runners/runner/index.md, and real-time bidirectional streaming with
run_live and LiveRequestQueue under docs/guides/runners/runner/live.md.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 968085075
`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
Merge https://github.com/google/adk-python/pull/6550
### Link to Issue or Description of Change
**1. Link to an existing issue (if applicable):**
N/A
**2. Or, if no issue exists, describe the change:**
**Problem:**
`adk test --rebuild`, `dev_server.py` (ADK Web test creation), and `cli.py` (JSONL stream output) write/output JSON using `json.dump` / `json.dumps` with the default `ensure_ascii=True` and without explicit UTF-8 encoding.
As a result, Japanese and other non-ASCII event text is converted to `\uXXXX` escape sequences, making test fixtures and CLI outputs difficult to read and review.
**Solution:**
- Write rebuilt and saved test fixtures as UTF-8 with `ensure_ascii=False`.
- Output CLI JSONL events and schemas with `ensure_ascii=False`.
- Add regression unit tests verifying non-ASCII text preservation in rebuilt tests, web server test creation, and CLI event printing.
### Testing Plan
**Unit Tests:**
- [x] I have added or updated unit tests for my change.
- [x] All relevant unit tests pass locally.
### 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 relevant unit tests pass locally with my changes.
- [ ] I have manually tested my changes end-to-end.
- [x] Any dependent changes have been merged and published in downstream modules. (N/A: no dependent changes.)
### Additional context
No public APIs or fixture schemas are changed. Rebuilt files remain JSON-compatible; only the textual representation of non-ASCII characters changes.
Co-authored-by: Yi Liu <yiliuly@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/6550 from ftnext:preserve-unicode-in-rebuilt-tests c9a1197c7c5ded4afed86279acfcc2025135fd9c
PiperOrigin-RevId: 967917608
A client-side tool's outcome never reaches the trajectory, so an ADK caller
saw the function_call and never the function_response. This adds the buffer
that captures the outcome from the SDK's post-tool-call and on-tool-error
hooks, and the converter that drains it into a matching function_response
event. Nothing calls it yet.
Co-authored-by: Haran Rajkumar <haranrk@google.com>
PiperOrigin-RevId: 967595598
When support_cfc=True is used in run_async, delegate to run_live directly to prevent duplicate tool execution.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 967506529
UnsafeLocalCodeExecutor ran each program as a multiprocessing spawn child,
which had to import this package before it could run a single line, costing
about 2.3 seconds per execution. It now runs the program in a plain child
interpreter, the shape ContainerCodeExecutor already uses, which brings a
trivial program down to about 35 milliseconds. The result now comes from the
child's exit status and pipes rather than a queue the child has to write to,
so a program that dies without reporting anything no longer leaves the agent
waiting forever, and the traceback the model is shown no longer opens with a
frame from inside this package.
One behavior change follows from taking the result from the exit status: a
program calling sys.exit(0) is now reported as having succeeded. It was
previously reported as a failure, because the spawn child raised SystemExit
before it could write to the result queue.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 967496975
`_construct_message_parts_from_session` rebuilds the outgoing message from
the session history. It drops `function_response` parts that carry
credential material, but not `function_call` parts. An
`adk_request_credential` call carries a serialized `AuthConfig` in its
arguments, including `raw_auth_credential` (an OAuth2 client secret or a
service account key), and `BaseLlmFlow` appends such an event to the session
whenever a toolset asks the client for a credential. A `RemoteA2aAgent` in
that session then replays the secret to the remote peer.
Drop credential-bearing `function_call` parts too. The scrub runs before
`_present_other_agent_message`, which renders a `function_call` as text with
its arguments inlined, so a later scrub would be too late.
A call counts as credential-bearing by name, or by shape when the name is not
one we know. Only `adk_request_credential` counts by name; the mock auth call
is left alone, because its args hold the peer's own prompt and it stands in for
the peer's last text part. The shape check reads the AuthConfig out of the
`authConfig` field of the AuthToolArguments envelope. A response carries the
AuthConfig flat, so the response-side check reads the top level. Reading the
top level of a request would match nothing, and would drop any ordinary call
that takes an `auth_scheme` argument.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 967478046
`to_mcp_server` keeps one ADK session per MCP connection, so successive tool
calls on that connection form a single conversation. It keyed that map on
`ctx.session`.
That key is correct on MCP SDK 1.x, where the server builds one session object
per connection. It is wrong on 2.x: the server builds a fresh `ServerSession`
for every inbound request and holds the connection on the session's private
`_connection`. The key would change on every call, so every tool call would
start a new conversation. Nothing raises. The agent just forgets.
Route the key through `_connection_key`, which reads `_connection` when the SDK
provides it and falls back to the session when it does not. That gives one key
per connection on both versions, and leaves 1.x behaviour unchanged.
The fallback degrades to one session per request on purpose. It must not fall
back to an object shared by all connections, because separate clients would
then share one conversation.
The private attribute is a stopgap. SDK 2.x already defines a public
`mcp.server.context.Context.connection`, but the server does not hand that
class to tool functions yet, so a tool's `Context` has no public route to its
connection.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 967462559
Gemini models reject requests that combine built-in search tools (such as
GoogleSearchTool or VertexAiSearchTool) with function declarations (such as
transfer_to_agent) with "400 INVALID_ARGUMENT: Tool use with function calling
is unsupported".
This change addresses this in two ways:
1. When built-in search tools (GoogleSearchTool / VertexAiSearchTool) have
`bypass_multi_tools_limit=True` in an agent hierarchy, `multiple_tools`
now accounts for transfer targets so the tool is converted to its
function-tool equivalent (GoogleSearchAgentTool / DiscoveryEngineSearchTool),
allowing it to cleanly coexist with `transfer_to_agent`.
2. When built-in search tools are used without bypass in an agent hierarchy,
`_AgentTransferLlmRequestProcessor` skips injecting `transfer_to_agent`
and transfer instructions, ensuring only the built-in search tool is sent
to the model without conflict.
Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 967423502
FileArtifactService chose the next version by listing the versions directory
and then created that directory as a separate step. Two saves racing on the
same artifact read the same version list, and the loser's mkdir raised
FileExistsError. An interrupted save was worse: the version directory existed
but held no payload, and because version discovery counts any integer-named
directory, that empty directory became the latest version and load_artifact
returned None instead of the previous good content.
A save now reserves a version by creating a hidden `.{version}.pending`
directory, which mkdir makes atomic and mutually exclusive, writes the payload
and metadata inside it, and publishes the result with a single os.replace onto
`versions/{version}`. Version discovery ignores the staging directories, so a
version becomes visible only once it is complete, and a save that raises
removes its own staging directory. A save whose version list went stale
re-checks before publishing and takes the next free version instead, so
os.replace always lands on a name that does not exist. That last part matters
on Windows, where os.replace maps to MoveFileEx and reports an error when the
destination names an existing directory.
The guarantee is scoped to process-level faults. An exception or a signal never
publishes a partial version, but nothing is fsynced, so a power loss can still
make the rename durable ahead of the file contents and expose an empty
`versions/{version}`. Full durability would need explicit fsyncs and is not
addressed here, and version directories that earlier releases already left
empty are not repaired.
Behavior change: published version numbers are no longer guaranteed to be
contiguous. A reservation abandoned by a signal or a host failure leaves its
`.{version}.pending` directory behind; that directory is never read and never
published, but it holds its version number permanently. There is deliberately
no in-process sweep, because nothing distinguishes an abandoned reservation
from a slow concurrent write, so reclaiming those directories is left to the
operator. The class-level layout comment records the staging directory and the
non-contiguity.
The public artifact-service API and the completed on-disk version layout are
unchanged.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 967413806
Gemini 3.x Live does not begin generating from `send_client_content` alone; it
starts only once it receives realtime input. As a result, seeding a new live
connection with prior conversation history left the model silent until the user
spoke again. This is most visible right after an agent transfer, where the
sub-agent is expected to reply immediately.
`GeminiLlmConnection.send_history` now follows the replayed history with a
minimal placeholder realtime input for Gemini 3.x Live models, gated on the
history ending with a user turn (the same condition that sets `turn_complete`).
Histories that end on a model turn still leave the model waiting for new user
input, preserving the documented `send_history` behavior. Other model families
are unaffected.
Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 967411170