test(cross-language): compare adk-js against adk-python on the upstream samples - #809
Open
kalenkevich wants to merge 2 commits into
Open
test(cross-language): compare adk-js against adk-python on the upstream samples#809kalenkevich wants to merge 2 commits into
kalenkevich wants to merge 2 commits into
Conversation
…am samples
We had no way to answer "does adk-js actually behave like adk-python?" beyond
reading both sources. This runs the two `adk run` CLIs over the same scenario
and diffs what happened.
Both CLIs already speak the same two dialects: `--replay` takes
`{state, queries}`, and `--save_session` dumps the session as JSON in the same
shape. So the harness drives the real CLIs and compares structured events
rather than scraped stdout, which is what makes the comparison trustworthy
enough to act on.
The model is nondeterministic, so wording is never a failure. What must agree
is what the framework decides — tool calls and their argument names, which
agents produced events, transfers, state, artifacts, errors. Event packaging
differences are reported but do not fail.
Cases come from adk-python's contributing/samples: 71 catalogued, 56 with a TS
port and a Python shim that imports the real upstream sample and pins its
model, so the vendored checkout is never edited. The 15 that cannot run each
carry a specific reason; "adk-js has no equivalent for this" is a finding, not
an omission.
The model is pinned identically on both sides. The two runtimes ship different
defaults, so a sample that omits `model=` would otherwise compare two different
models and blame the framework for the difference.
Vitest always validates the catalogue and loads every agent, which is free; the
live comparison needs ADK_PARITY_LIVE=1.
A first pass at this suite could not be regressed against. Comparing two full runs, with neither framework changing, 18% of cases changed verdict — enough that a case-level result was a coin toss rather than a finding. Two causes, both of which the harness was scoring as runtime divergence. Model nondeterminism: the model calls a tool on one run and not the next. Each case is now compared `--repeats` times (default 3) and a difference is reported only when a majority of repeats saw it. Anything seen but not carried is listed under "Reproducibility" so the noise stays visible without counting — one case alone shed five such dimensions, including a spurious "blocked". Failed model calls: a 429, a 503 or an empty completion says nothing about either framework, but MODEL_RETURNED_NO_CONTENT on one side was being reported as a structural difference. Those are now classified `infrastructure`, retried once, and excluded from the verdict. Deliberately excepted is a deterministic 4xx that only one runtime provokes — adk-python inlining an image/bmp artifact Vertex rejects, or combining output_schema with function calling, are exactly the findings worth keeping. A single-run report now says in bold that it produces leads, not findings. Also makes the vitest load check async; blocking the worker's event loop for a minute starved the reporter RPC and failed the run with an unhandled "Timeout calling onTaskUpdate".
AmaadMartin
reviewed
Aug 25, 2026
AmaadMartin
left a comment
Collaborator
There was a problem hiding this comment.
The harness is test-only and well-built. I verified it adds no any, no instanceof, and no public API, and the one no-control-regex suppression (ANSI stripping) is legitimate. One blocker: package.json downgrades the version from 2.0.0 to 1.6.0, so a merge reverts the version on main. CI is green at this SHA, but run-tests does not check the version, so the fix belongs in the diff.
| { | ||
| "name": "adk", | ||
| "version": "2.0.0", | ||
| "version": "1.6.0", |
Collaborator
There was a problem hiding this comment.
Not a nit. Restore the version to 2.0.0.
"version": "1.6.0",The merge base has 2.0.0; this branch sets 1.6.0. A merge downgrades the version on main. The PR description says package.json only gains two scripts, so this reads as a stale rebase artifact. Set it back to "version": "2.0.0".
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.
Link to Issue or Description of Change
Problem:
We had no way to answer "does adk-js actually behave like adk-python?" beyond
reading both sources. Behavioural drift between the two SDKs was invisible
until someone hit it.
Solution:
Run the same scenario through both runtimes'
adk runCLIs and diff whathappened. Test cases are the adk-python
contributing/samples.Why the CLIs and not an in-process harness. Both already speak the same two
dialects:
adk run --replay <file>takes{"state": …, "queries": […]}ineach runtime, and
--save_sessiondumps the resultingSessionas JSON in thesame camelCase shape. So the harness drives the real CLIs and compares
structured events — never scraped stdout.
Wording is never a failure. The model phrases things differently between
two calls, let alone two SDKs. What must agree is what the framework decides:
structural)cosmetic/infrastructure)modelVersion,nodeInfoFinal answers go side by side in the report with a token-overlap score, so a
human can spot answers differing in substance without that ever failing a run.
The model is pinned identically on both sides. The two runtimes ship
different defaults, so a sample that omits
model=would compare two differentmodels and blame the framework for the difference.
One run of an LLM-backed case proves nothing. Measured across two full runs
with neither framework changing, 18% of cases changed verdict. So each case
is compared
--repeatstimes (default 3) and a difference is reported only ifa majority of repeats saw it; anything seen but not carried is listed under
Reproducibility as noise. One case alone shed five such dimensions, including
a spurious "blocked". A
--repeats 1report says in bold that it producesleads, not findings.
Likewise a 429, 503, timeout or empty completion is classified
infrastructure: retried once and excluded from the verdict. The oneexception is deliberate — a deterministic 4xx that only one runtime provokes
stays a finding, because that is the interesting case.
Coverage: 71 cases catalogued, 56 runnable. Each has a TS port under
agents/ts/and a Python shim underagents/py/that imports the realupstream sample and pins its model — the vendored checkout is never edited, so
git -C adk-python statusstays clean. The 15 that cannot run carry a specificreason; "adk-js has no equivalent for this" is recorded as a finding, not
quietly skipped. Families needing MCP, A2A, OAuth, BigQuery, live audio or
non-Gemini models are out of scope (A2A already has its own suite next door).
What it found. Three reproducible divergences, all stable at 3 repeats:
to the workflow (
_workflow.py:226), adk-js to each node(
function_node.ts:214). Breaks session portability.transfer_to_agentsignature —agent_namevsagentName, plus amissing enum constraint. Fixed in fix(core): make transfer and runtime tool confirmation work across runtimes #808.
approval (fixed in fix(core): make transfer and runtime tool confirmation work across runtimes #808); adk-python re-raises the gate forever and the
model then narrates success it never achieved.
Plus 12 documented capability gaps (no
staticInstruction, no planners, nologprobs on
LlmResponse, 5 plugin modules vs adk-python's 15, …).Setup and use:
The rendered report is not committed — it is a snapshot of one run against
one pair of versions, so a checked-in copy is stale the moment either side
moves. It is gitignored alongside
runs/, the vendored checkout and the venv.Testing Plan
Unit Tests:
Under vitest this is two tiers, so CI gets value for free:
upstream sample, every runnable case has both halves, every non-runnable one
has a reason) and load all 112 agents through both runtimes' real loaders.
No model calls.
ADK_PARITY_LIVE=1— the actual side-by-side comparison.The live suite is ~19 minutes and ~340 model calls at 3 repeats, so it is
deliberately opt-in and not wired into any workflow by this PR.
npm run ts:check:parityandeslint/prettierare clean. The tree isexcluded from the root
tsconfigbecause it imports with explicit.tsextensions — the harness runs under Node's
--experimental-strip-types, whichresolves the real file and will not rewrite a
.jsspecifier onto a.tsfile.Manual End-to-End (E2E) Tests:
Needs a Gemini backend:
GOOGLE_CLOUD_PROJECT(Vertex, via ADC) orGOOGLE_API_KEY. Results quoted above are adk-python 2.7.1 (pinned at1d89e0ff) against adk-js 1.6.0 ongemini-2.5-flashvia Vertex.Notes for reviewers
sample agents (57 TS agents + 69 three-line Python shims). The reviewable
surface is
harness/(7 files),cases/(9), and the README.tests/cross_language/at apinned ref, provisioned by
setup.shand gitignored. Worth an explicitopinion if you would rather it lived elsewhere.
package.jsongains two scripts andtsconfig.jsonone exclude; nothing else outside the new directory.Checklist