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
The auth preprocessor built the token exchange out of whatever the client echoed
back, including the `auth_scheme`. That scheme names the token endpoint the
exchange posts to, so a client could point it at a server it controls. It also
honoured responses for function call IDs the session never issued, where there
is no request to check them against at all.
Both now come from the `adk_request_credential` call this server issued. A
response with no matching call is dropped.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 967358755
Merge https://github.com/google/adk-python/pull/6646
Reject yaml and ruamel unsafe/full loaders in config agent to prevent RCE.
Note: ruamel is a transitive dev/test dependency, but is blocked to prevent RCE if present in the environment.
PiperOrigin-RevId: 967340296
Merge https://github.com/google/adk-python/pull/3960
This PR improves the performance of `LlmAsJudge.evaluate_invocations()` by
running LLM evaluation calls concurrently instead of executing them one-by-one.
PiperOrigin-RevId: 967276232
Extract cache creation (write) tokens from LiteLLM and Anthropic model
usage metadata and map them to the GenerateContentResponseUsageMetadata
object. This ensures they are recorded in telemetry, allowing correct cost
calculations for prompt caching with providers like Bedrock.
Close#5835
PiperOrigin-RevId: 967259812
`McpToolset._get_auth_headers` held the only complete mapping from a resolved
`AuthCredential` to request headers: OAuth2 access tokens, HTTP bearer, HTTP
basic, other HTTP schemes, `additional_headers`, and header-located API keys.
`McpTool._get_headers` already duplicates it, and the next component that needs
to authenticate an outgoing request would be writing the third copy.
Move it to `google.adk.auth._auth_headers.build_auth_headers` and have
`McpToolset` delegate. The extracted function is the existing body with the
auth scheme passed in as an argument instead of read off `self._auth_config`.
The only differences are the text of the API-key warning and the logger it is
emitted on; no test asserts either. The existing `test_mcp_toolset_auth.py`
cases still exercise the mapping through the toolset, and the new tests cover
the function directly, including branches those cases cannot reach: a
non-bearer HTTP scheme, `additional_headers` with no usable scheme or token,
and an API key with no scheme at all.
`McpTool._get_headers` is deliberately left alone: it raises where the toolset
logs and warns on unexchanged service-account credentials, so folding it in
would not be a pure refactor.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 966867565
This prevents Click from expanding wildcard arguments on Windows (e.g. `*` in `--allow_origins "*"`), avoiding errors when Click attempts to parse expanded filenames as unexpected positional arguments.
Fixes#6248
PiperOrigin-RevId: 966756317
This CL introduces `_json_utils.safe_json_loads` to provide a uniform ValueError
when JSON parsing fails, wrapping the underlying json.JSONDecodeError.
Initial callsites in evaluations, sessions, and some models have been migrated.
Merge https://github.com/google/adk-python/pull/5858
PiperOrigin-RevId: 966722835
When one agent hands off to another, `_present_other_agent_message` replays the
first agent's turn to the second as a `role="user"` message -- the same channel
the real user speaks on -- interpolating the text straight into
`[agent] said: ...`. Nothing marks where the quoted transcript ends, so a
payload the first agent was talked into emitting reads to the second agent as a
fresh directive. Anyone who can chat to a low-privilege front-end agent can
therefore aim instructions at whatever tools the agent it transfers to holds.
Every relayed payload -- text, thoughts, tool arguments, tool results -- is now
quoted between explicit markers, and the leading part of the message states
that what sits between them is data to read and not instructions to follow.
Markers occurring inside a payload are elided first, so quoted content cannot
close its own block and carry on speaking as the framework.
The markers, the preamble and the quoting helpers live in
`flows/llm_flows/_fencing.py`. The unit tests and the conformance harness both
have to spell the expected framing, so it sits in a module of its own rather
than inside `contents.py`, where they would have to reach for private names.
This raises the bar rather than closing the class: a model can still be talked
round by text it was told to distrust. What it removes is the structural
ambiguity that made a relayed payload indistinguishable from a user turn.
Relayed turns now cost the preamble plus two marker lines per part, and
anything matching on the old `For context: [x] said: y` shape needs updating.
The conformance replay harness is one such matcher, and now reduces a relayed
turn to the payload it carries before comparing, so recordings cut before the
fencing still replay.
Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 966694665
ADK instruments inference itself, unless it detects
opentelemetry-instrumentation-google-genai, in which case it stands down and
that library's telemetry is what users get. Only the first was ever tested.
Every case now runs twice -- once over a MockModel with ADK's own
instrumentation, once over a real Gemini with the instrumentor and a
mocked-out google.genai SDK. The goldens stay what they were, ADK's own
recording pinned in full. How the instrumentor's recording of the same run
differs is in functional_divergences.json: one group per span, log or metric,
one entry per slot within it, with an example of what each side recorded, the
tests it turns up in, and the kind (adk_bug, otel_bug or desired_behavior)
and reason a developer owes for it. A gap that is new, or recorded without an
explanation, fails the case that produces it, so a new one cannot appear
silently.
The gaps therefore live in one reviewable file rather than smeared through
every golden. The cost is that the instrumentor's recording cannot be rebuilt
from disk: only the native path is pinned value for value, and the other is
held to having no unexplained gap against it.
functional/_divergences.py is the whole of it. It overlays the two recordings
as JSON, so it descends into JSON-valued attributes too: two payloads that
differ in one field diverge in that field rather than wholesale. Divergences
are named owner-relative -- the span, log or metric they sit on, plus the
route within it -- so one gap is one entry however deep it sits, however
often it recurs, and in however many goldens.
25 divergences, all explained. The first adk_bug among them: ADK leaves
error.type off the inference span on a failed call, reporting the failure
only on the enclosing spans and the duration metric.
The MCP scenario's server now resolves the same tool the canned conversation
calls, so one conversation drives every scenario and the MCP recording covers
a tool call arriving through MCP rather than only the tool definitions.
test_smoke.py pins the private internals the harness reaches into -- the
semconv stability cache it resets per case, the SDK call the instrumentor has
to wrap, and the MCP session manager it patches -- so an upgrade that renames
one fails there rather than as a puzzling golden diff.
Co-authored-by: Max Ind <maxind@google.com>
PiperOrigin-RevId: 966591641
The functional goldens sorted a span's children by name and its log records
by their own contents. That is deterministic, but it reads nothing like the
run it records: the tool call sorts before the inference that asked for it,
and a golden diff moves records around for reasons that have nothing to do
with the change under review.
The scenarios are single-threaded, so emission order is deterministic too.
Child spans are now ordered by start time and log records by their observed
timestamp, and the goldens read down the run.
Co-authored-by: Max Ind <maxind@google.com>
PiperOrigin-RevId: 966554122
This change introduces several security enhancements to BigQuery query tools:
- Strict identifier validation for table and column names to prevent injection.
- Secure escaping of string literals.
- Subquery validation using dry-run execution to ensure only SELECT statements are executed in dynamic contexts.
- Improved exception handling to distinguish between query validation errors and infrastructure failures.
- Input type validation for parameters like horizon and anomaly thresholds.
- Comprehensive parameterized test suites for security boundary cases.
PiperOrigin-RevId: 966430775
Merge https://github.com/google/adk-python/pull/4344
**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):**
- Closes: #4343
**Problem:**
`AgentEvaluator.evaluate` did not register custom metrics from `EvalConfig`, so custom metrics worked in `adk eval` but not in pytest-based evals.
**Solution:**
Align `AgentEvaluator` with the CLI eval flow by registering custom metrics via a per-run metric registry and a shared default `MetricInfo` helper. The per-run registry is a fork of `DEFAULT_METRIC_EVALUATOR_REGISTRY` (new `MetricEvaluatorRegistry.fork()`), so the custom metrics declared by one eval config never leak into another run.
### Testing Plan
Add unit coverage for the registration behavior and a lightweight integration example that uses a custom metric.
**Unit Tests:**
- [x] I have added or updated unit tests for my change.
- [x] All unit tests pass locally.
```
% pytest tests/unittests/evaluation tests/unittests/cli
614 passed, 308 warnings in 12.74s
```
**Manual End-to-End (E2E) Tests:**
```
% pytest tests/integration/test_with_test_file.py::test_with_custom_metric
tests/integration/test_with_test_file.py . [100%]
1 passed, 12 warnings in 3.57s
```
### 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 change keeps `AgentEvaluator` behavior consistent with `adk eval` while avoiding CLI-layer dependencies.
`fork()` returns an isolated copy seeded with the source registry's contents, rather than a bare `MetricEvaluatorRegistry()`. The seeding is what keeps this backwards compatible: registering an `Evaluator` subclass on `DEFAULT_METRIC_EVALUATOR_REGISTRY` is the only way to plug one in, since an eval config can only name a scoring function. Callers who do that today (including ones replacing the evaluator behind a standard metric name) would otherwise silently fall back to the stock evaluator, with no error and a different score. Covered by `test_evaluate_eval_set_keeps_evaluators_from_the_default_registry`.
Co-authored-by: Yi Liu <yiliuly@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/4344 from ftnext:agent-evaluator-support-custom-metric 3c844a9817d16954c683b164a88a86a82e8f9cf1
PiperOrigin-RevId: 966416454
Verify that the final non-partial event of a stream replaces the artifact (append=False) because it contains the accumulated content.
PiperOrigin-RevId: 966350716
Merge https://github.com/google/adk-python/pull/6660
Change service-account OpenAPI helpers to return an OAuth2 client-credentials scheme so CredentialManager can perform token exchange.
Also, bypass the credential service caching for all SERVICE_ACCOUNT credentials. This ensures we don't cache exchanged tokens that cannot be refreshed, but means token exchange will run on each tool execution if the manager/exchanger is not reused.
Fixes#6656
PiperOrigin-RevId: 966305100
Two places in the live path wrote into configuration the caller still owns, so
a `RunConfig` came back out of a run holding fields the caller never set, and
a `RunConfig` reused for a later run carried them into it.
The basic request processor aliased two `RunConfig` sub-models straight into
`LiveConnectConfig` rather than copying them, and live request assembly then
mutates both while the session runs: `BaseLlmFlow.run_live` stamps every
server-issued handle onto `session_resumption` when it reconnects and sets
`transparent` there on a Vertex reconnect, and sets
`initial_history_in_client_content` on `history_config` when it seeds a fresh
connection with history. Deep copy both sub-models in `_build_basic_request`.
This matches the treatment `_copy_request_scoped_fields` already gives
`llm_request.config` in the same function, and for the same reason: request
assembly must not write through into configuration the caller still owns. An
absent sub-config stays `None` rather than becoming an empty object.
`Runner.run_live` filled in its AUDIO default by assigning to the caller's
`response_modalities`, so a config that expressed no preference came back out
of the run pinned to AUDIO, and a config reused for a later text run would ask
for audio. Write that default to a copy instead. The copy is shallow: deep
copying a `RunConfig` raises `TypeError: cannot pickle` when `http_options`
holds a live httpx client, and nothing there writes through into a sub-model.
Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 966257920
ADK already treats a display-only thought part as invisible for LLM context, but
it only checks that for a whole event, so a summary that shares an event with a
function call or an answer is handed back to the model on every later request of
the session. The NL planning request processor now drops those parts from the
outbound request, keeping any thought that carries a thought signature, a
function call or response, or
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 966239711
A streaming tool could only use one Live response scheduling mode for its
entire stream, taken from the tool-wide `response_scheduling`. A tool that
mixes quiet progress updates with an urgent alert had no way to say so.
A yielded `types.FunctionResponse` now carries the payload in `response` and
the mode for that one chunk in `scheduling`. Anything yielded plainly still
falls back to the tool-wide default, so existing tools are unaffected.
```python
async def monitor_threshold(query: str):
# Adds to context without interrupting the model.
yield types.FunctionResponse(
response={'status': 'fetching data...'},
scheduling=types.FunctionResponseScheduling.SILENT,
)
# Interrupts ongoing generation.
yield types.FunctionResponse(
response={'alert': 'threshold exceeded'},
scheduling=types.FunctionResponseScheduling.INTERRUPT,
)
# Falls back to the tool-wide default.
yield {'final_summary': 'done'}
```
Only `response` and `scheduling` are read from the yielded object. `id` and
`name` have to address the function call being answered, which a tool cannot
know, so ADK keeps owning them. The unwrapping runs before media extraction,
so a chunk that names a scheduling can still return inline media parts.
Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 966209467
Merge https://github.com/google/adk-python/pull/6462
A remote peer could self-approve a pending dangerous tool call via A2A because
inbound messages are treated as user role. This change ignores confirmations
arriving over A2A channels.
Closes#6461
PiperOrigin-RevId: 966208121
ADK already treats a display-only thought part as invisible for LLM context, but
it only checks that for a whole event, so a summary that shares an event with a
function call or an answer is handed back to the model on every later request of
the session. The NL planning request processor now drops those parts from the
outbound request, keeping any thought that carries a thought signature, a
function call or response, or a server-side tool call. Stored sessions are
unchanged, so a caller that displays the model's reasoning still gets it.
Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 966074428
Implements the update_data_agent tool mimicking the MCP toolbox implementation,
allowing the updating of Gemini Data Agents.
PiperOrigin-RevId: 966070636
A caller that resumes an earlier live session by passing a handle in
`RunConfig.session_resumption.handle` only got that handle onto the wire. The
basic request processor forwards it to `LiveConnectConfig`, but every other
part of the run keys off `InvocationContext.live_session_resumption_handle`,
which was populated exclusively from a server-issued
`session_resumption_update`. The run therefore behaved as if the session were
new: it replayed the whole conversation through `send_history()` even though
the server already held that state, declared that history as
`initial_history_in_client_content`, left `transparent` unset on the Vertex AI
backend, and raised instead of reconnecting when the socket dropped before the
server issued its first handle.
Seed `InvocationContext.live_session_resumption_handle` in
`BaseLlmFlow.run_live` from the handle request assembly has already put on
`llm_request.live_connect_config.session_resumption`, before the connect loop,
so the first connection is treated as a resumption in the same way as any
mid-session reconnect. Reading the assembled request rather than the
`RunConfig` keeps the seed on the same object the reconnect path goes on to
write, and honors a handle set by any request processor rather than only one
set through `RunConfig`. Agent transfer is unaffected because it already
clears both the invocation handle and the deep-copied run config handle, so a
child agent still starts a fresh live session.
Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 966066295