Fix: round-trip EventActions losslessly through VertexAiSessionService - #503
Open
AmaadMartin wants to merge 4 commits into
Open
Fix: round-trip EventActions losslessly through VertexAiSessionService#503AmaadMartin wants to merge 4 commits into
AmaadMartin wants to merge 4 commits into
Conversation
added 4 commits
August 1, 2026 23:06
The Agent Engine Sessions API spells ADK's transferToAgent as transferAgent. appendEvent copied event.actions onto the request verbatim, so the field went out under a name the read path never looks for and the transfer target was lost on the legacy (no rawEvent) channel. Both directions now copy the whole actions object through one rename map instead of a hand-maintained field list, so a field added to EventActions persists without another change here.
Review feedback: two single-entry inverse rename tables and a keyMap lookup generalised over one field. Pass the two names as arguments instead, drop the no-op ExtendedEventActions cast and the defensive copy of an argument the helper never mutates, and let the helper take the optional API actions directly.
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Link to an existing issue (if applicable):
No public issue is associated with this report; it was found while reading the
Vertex AI session persistence code against its adk-python counterpart.
Or, if no issue exists, describe the change:
Problem:
VertexAiSessionServicetranslatedEventActionsbetween ADK andthe Agent Engine Sessions API with a different hand-written field list on each
side, which made the translation asymmetric and lossy.
Asymmetric field name (a real bug).
appendEventcopiedevent.actionsonto the request verbatim, so the transfer target went out on the wire as
transferToAgent. The read path looks for the API's spelling instead(
actions['transferAgent'],vertex_ai_session_service.ts:557before thischange). adk-python writes
'transfer_agent'and renames it back on read(
src/google/adk/sessions/vertex_ai_session_service.py,append_event/_from_api_event); adk-js implemented only the read half,so a
transferToAgentvalue could not survive a save + load.Hand-maintained read allowlist (a latent trap).
_fromApiEventrebuiltEventActionsfield by field. The list happened to cover every field oftoday's
EventActions, but nothing forces anyone to extend it, so the nextfield added to
EventActionswould be silently dropped on reload.Scope of impact (stated precisely).
appendEventalso stores the wholeevent under
config.rawEvent, and_fromApiEventreturns early fromrawEventwhen it is present, so neither defect is observable while that channel works.
They bite on the fallback the service itself implements and expects to hit:
appendEvent'scatchretries the append withoutrawEvent, and sessionswritten before
rawEventsupport existed still take the legacy path. So: a realbug on a real code path, not a bug on every write.
Solution: stop enumerating fields. Both directions now copy the whole
actions object through one helper that renames a single key
(
transferToAgent<->transferAgent),skipping
null/undefinedvalues and passing every other key throughuntouched. The read path builds its result with the existing
createEventActions()so the four container defaults keep living in one place.Adding a field to
EventActionsnow requires no change tovertex_ai_session_service.tsfor it to persist.Notes on the design:
across the boundary, so the wire name is
transferAgent(matchingadk-python and the API's documented field), while the local ADK model keeps
its
transferToAgentname. Compaction precedence also mirrors adk-python:customMetadata._compactionoverrides anycompactionarriving insideactions.untouched, an event written by today's adk-js (with
transferToAgenton thewire) still deserializes to
actions.transferToAgent. Nothing that readscorrectly today reads incorrectly after this change. A test pins this.
recursive converter would rewrite user-controlled keys inside
stateDelta,artifactDeltaandcustomMetadata. The only translation needed is the onesemantic field rename.
converters are module-private and the tests drive them through the public
appendEvent/getSessionwith an injectedSessionsclient.6-key allowlist. It is queued separately and is not touched here.
rawEventretrycatchinappendEvent(lines 467-480) is the onlyuncovered region left in the file. It is pre-existing, untouched by this
change, and is the subject of a separate change; the legacy read path it
produces is covered here by serving events with
rawEventstripped.tests/integration/sessions/vertex_ai_session_service_test.tsbuilds a thirdSessionsclient the same way the two existing suites in that file do. Thatconstruction trips a known duplicate-
@google/genaitype mismatch (privatecustomBaseUrl), which already occurs twice in that file onmain. CI doesnot typecheck the test tree (
validation.yamlruns build / tests / lint /format / docs), and the root cause is package deduplication, not this change,
so no suppression or cast was added to hide it.
Collision check.
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000(402 open PRs) plusgh pr diff --name-onlyon every plausibly adjacentPR. Seven open PRs touch
core/src/sessions/vertex_ai_session_service.ts(#270, #474, #287, #201, #331,#303, #227), but none touches the actions conversion: grepping their diffs for
transferAgent|transferToAgent|_fromApiEvent|partialCopy|renameActionreturnsone unrelated hit (#303 retypes the
eventMetadatapartialCopy). No PR landsthis change, so this branches from
mainrather than stacking.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Eight new cases in
core/test/sessions/vertex_ai_session_service_test.ts(
EventActions round trip), all additive — no existing case was edited,weakened, skipped or deleted, and all 57 pre-existing cases still pass
unmodified (including the exact-match assertion on the append payload).
Coverage of
core/src/sessions/vertex_ai_session_service.tsfrom the targetedunit run: 96.84% lines / 90.29% branches for the whole file; every line and
branch added by this change is covered. The lcov report shows the only
uncovered lines are 207 and 467-480, both pre-existing and untouched.
Proof the new tests can fail. Each was run against mutated source and
observed to FAIL (source restored with
git checkoutafter each):toApiActionsrenamestransferToAgent->transferToAgent(no write rename)expected { stateDelta: {}, …(4) } to match object { transferAgent: 'agent-b' }fromApiActionsrenamestransferAgent->transferAgent(no read rename)expected undefined to be 'agent-b'fromApiActionsrenames in the write directionexpected undefined to be 'agent-b'if (value == null) continue;expected null to be undefinedif (compactionData)overrideexpected { startTime: 1, endTime: 2, …(1) } to deeply equal { startTime: 1600000000000, …(2) }rawEventtoundefined(drop the early return)expected 'stale-agent' to be 'agent-b'config.actions = toApiActions(event.actions ?? createEventActions())expected { stateDelta: {}, …(3) } to be undefinedexpected { stateDelta: { counter: 1 }, …(6) } to deeply equal { stateDelta: { counter: 1 }, …(7) }Note the mutation table also shows why both a write-name test and a round-trip
test are needed: with the write rename removed, the round-trip test still passes
(the read path's pass-through carries the wrong key back), so only the
assertions on the request payload catch a wrong wire name.
Manual End-to-End (E2E) Tests:
No manual E2E against a live Agent Engine: the defect only manifests on the
rawEvent-less fallback channel, which cannot be triggered on demand fromoutside the service. The integration suite is the substitute — it stands up a
loopback
http.Serverbehind the real@google-cloud/vertexaiSessionsclient and the real
@google/genaiApiClient, records the actual appendrequest body, serves that body back from the session
get+ eventslistendpoints with
rawEventstripped, and asserts the actions surviveappendEvent->getSession. That is the only test that can catch a wrong wirename, because a mock-object test uses the same key on both sides.
To run it yourself:
Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.
CI note. The first
run-tests (windows-latest)run failed incore/test/code_executors/unsafe_local_code_executor_test.ts > should execute shell code and return stdout(Test timed out in 5000ms) — a Windows-onlyshell-exec flake in a file this PR does not touch (unrelated PR #502 fails on
windows-latest too, with different tests). It passed on rerun; all six checks
are green.