Feat: port the Eventarc publish tool from adk-python (Part 1/2) - #533
Open
AmaadMartin wants to merge 4 commits into
Open
Feat: port the Eventarc publish tool from adk-python (Part 1/2)#533AmaadMartin wants to merge 4 commits into
AmaadMartin wants to merge 4 commits into
Conversation
added 3 commits
August 2, 2026 09:54
Adds core/src/integrations/eventarc with the EventarcToolset and its generic publish_message tool, the CloudEvent assembly and validation ported from adk-python's _message_tool.py, and the LRU/TTL-cached publisher client from _client.py. @google-cloud/eventarc-publishing is an optional peer dependency, mirroring the optional extra in adk-python; it is loaded with a dynamic import so users who do not publish events never pull in google-gax. google-gax pins google-auth-library to an exact patch version, which npm would otherwise install as a second copy whose AuthClient type is distinct from the one @google/adk depends on. A narrowly scoped override keeps a single copy so the credentials config can be typed without a cast.
The entries added for @google-cloud/eventarc-publishing were written by a client configured against a mirror, so their resolved URLs pointed somewhere CI cannot authenticate to. The integrity hashes are unchanged and were each verified against registry.npmjs.org.
Both references are type-only, so TypeScript already elides them, but marking them explicitly means a later value use becomes a compile error instead of a silent hard dependency on an optional peer.
Moves the isRecord predicate, which has no Eventarc content, to core/src/utils/object_utils.ts so the Eventarc modules stop each carrying a copy, and exports the extension-attribute key pattern once from the Eventarc config. Also drops the Python spelling from the base64 error: the value the model sends is `true`, not `True`.
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
Closes: #issue_number
Related: #issue_number
Problem: An ADK agent has no way to emit a CloudEvent into an existing Eventarc Advanced event-driven pipeline instead of calling downstream services directly.
adk-pythonshipsgoogle.adk.integrations.eventarc; a case-insensitive search foreventarcacrosscore/src,dev/srcandintegrations/srcofadk-jsreturns zero hits.Solution: Part 1 of 2 ports the generic publish path from
adk-python'ssrc/google/adk/integrations/eventarc/:core/src/integrations/eventarc/config.ts—EventarcToolConfig,EventarcCredentialsConfigand their default resolution (ported from_config.py).core/src/integrations/eventarc/client.ts— the LRU + TTL cachedPublisherClient(ported from_client.py,CACHE_MAX_SIZE = 10,CACHE_TTL_MS = 30 min).core/src/integrations/eventarc/message_tool.ts—publishMessageplus the genericpublish_messagetool: validation, base64 decoding, content-type inference, CloudEvent assembly and W3C trace-context injection (ported from_message_tool.py).core/src/integrations/eventarc/eventarc_toolset.ts—EventarcToolset(ported from_eventarc_toolset.py), which is also the module's public barrel.core/src/index.ts.core/src/utils/object_utils.ts— theisRecordpredicate, which has no Eventarc content and is used by both parts of the stack, so it lives in shared utils rather than being copied per feature file. It is deliberately not added to the package barrel: relocating an internal helper does not make it public API.Part 2 (stacked on this branch) adds
_domain_specific_publish.py: theMISSING/OMITsentinels,AgentProvided, the CloudEvent attribute bindings andEventarcToolset.createPublishTool. The work is split because the full port is ~3,800 lines; each part is independently complete and reviewable.Collision check:
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000returned 428 open PRs; none mentions Eventarc, CloudEvents or publishing, andgh pr diff --name-onlyon the three plausibly adjacent PRs (#445 engines, #323 eslintno-extraneous-dependencies, #466 Firestore session service) shows no overlap withcore/src/integrations/eventarc/. #445 and #323 also touchcore/package.json, but different fields (enginesanddevDependenciesrespectively), so this branches frommainrather than stacking on them.Why
@google-cloud/eventarc-publishingis an optional peer dependency: the package pulls ingoogle-gaxand@grpc/grpc-js. Making it a harddependencyof@google/adkwould impose that tree on every ADK JS user for an integration most will not use, and it would diverge fromadk-python, wheregoogle-cloud-eventarc-publishing>=0.10,<1appears only in theall,gcpandtestextras. It is therefore declared asdevDependencies(so CI can typecheck and test it) plus an optionalpeerDependenciesentry, matching the@mikro-orm/*driver precedent incore/src/sessions/db/operations.ts, and loaded withawait import().peerDependenciesMetadid not exist incore/package.jsonbefore; adding it is intentional.Why the root
overridesentry:google-gax@5pinsgoogle-auth-libraryto the exact version10.5.0, so npm installs a second copy next to the^10.3.0copy@google/adkalready depends on. BecauseClientOptions.authClientis typed against that copy's client classes, anAuthClientfrom the copy ADK depends on is not assignable to it even though it is the same class at runtime. The fix is to stop duplicating the package rather than to cast:This is scoped to
google-gax, which is itself new in this PR (it is absent frommain's lockfile), so no pre-existing dependency resolution changes: the lockfile diff is 237 pure insertions with zero deletions.Divergences from
adk-python(local convention wins for what never leaves the process; parity wins for anything observable across the boundary — model-facing parameter names, result keys, status strings, error text and defaults):publish_timeout: float = 15.0(seconds)publishTimeoutMs?: number, default15_000CallOptions.timeoutis milliseconds and every other timeout inadk-jsis*Ms. Same effective 15 s.ValidationErroronEventarcToolConfig(project_id=123)adk-jsoptions type is; the compiler enforces it.config_test.tscovers default resolution instead._get_credential_idinspectsgoogle.authinternals (_source_credentials,_credential_access_boundary,_credential_source,_subject_token_supplier)google-auth-libraryexposes (email,targetPrincipal,audience, a SHA-256 of the refresh token), falling back to aWeakMapper-object idos.getpid()projectId | scopes | credentialIdscopeswas added because two configs with the same auth client but different scopes must not share a channel.get_publisher_client(user_agent=...)adk-eventarc-tool google-adk/<version>, sent as gaxlibName/libVersion.close()dclient_test.tspins both.custom_attributes must be a dict; key check isk.isalnum() and k.islower()custom_attributes must be an object; key check is/^[a-z0-9]+$/'123'.islower()isFalsein Python, so an all-digit key is rejected there despite the error message and the CloudEvents spec both allowing digits; the TS check follows the stated contract. Invisible to every ported case.json.dumpsemits'{"foo": "bar"}';str(True)is'True'JSON.stringifyemits'{"foo":"bar"}';String(true)is'true'datais typedAnythrough signature introspectionType.STRINGwith a description telling the model to JSON-encode structured payloadsSchemahas no "any" type.publishMessage()still acceptsunknownand performs the full type introspection, so every content-type branch is reachable and tested.opentelemetryguarded bytry/except ImportError@opentelemetry/api@1.9.0is a hard dependency ofcore. With no propagator registered the carrier is simply empty, which is the same observable outcome, andmessage_tool_test.tspins both cases.@experimental(FeatureName.EVENTARC_TOOLSET)@experimentaladk-js'sexperimentaldecorator takes no arguments, and itsFeatureRegistryis an unrelated runtime kill-switch.error_detailsisrepr(e)error_detailsis the error messagereprhas no JS equivalent; the message is what the model can act on.Not silently reduced: nothing in the specification for this part was skipped. The only intentional omissions are rows 3, 4 and 5 above, each of which is a Python-internal detail with no TypeScript counterpart.
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.
npm run ts:checkis red onmainas well; the error set is byte-identicalwith and without this branch, and no file in this diff appears in it.
CI on this PR is green:
run-testspasses on ubuntu-latest, macos-latest andwindows-latest. The Windows job is flaky on two pre-existing tests that this
diff does not touch, and needed re-runs:
webui_test.tscould not bind anephemeral port (
listen EACCES: permission denied ::1:49723), andunsafe_local_code_executor_test.ts > should execute shell code and return stdouthits its 5 s budget spawning PowerShell. ubuntu and macOS have passedon every run. For what it is worth, the Windows job on this branch takes 8m44s
against 8m59s for a concurrent PR that does not add these dependencies, so the
larger install is not what is pushing that test over its budget.
Coverage of the new code, measured with
npx vitest run --project unit:core core/test/integrations/eventarc --coverage.enabled --coverage.include='core/src/integrations/eventarc/**':client.tsconfig.tsmessage_tool.tseventarc_toolset.tsThe single uncovered branch is the
isRecord(input) ? input : {}fallback in the tool callback.FunctionTooltypes the callback argument asunknownbut always passes itsRecord<string, unknown>argument bag, so theelsearm is unreachable; a narrower callback parameter is rejected bystrictFunctionTypes. The guard is kept rather than removed because the value isunknownat the type level.Proving the tests can fail. Each new test was run against mutated source and observed to fail:
Buffer.from(s, 'base64')(which silently discards invalid characters) →rejects a payload outside the base64 alphabetandrejects a base64 payload with a truncated final groupfail withexpected ERROR but the publish succeeded.timewithDate.parsealone, dropping the RFC 3339 regex →rejects date without a time componentfails withexpected ERROR but the publish succeeded(Date.parse('2026-06-03')is happy).removePublisherClienton the publish-error path →evicts the cached client when the publish call failsfails withexpected "spy" to be called with arguments: [ { …(2) } ].getPublisherClient→evicts and closes the least recently used client when fullfails withexpected [] to have a length of 1 but got +0.rebuilds the client once the TTL has elapsedfails withexpected [] to have a length of 1 but got +0.subjecton the CloudEvent instead of in its attributes →puts the subject in the attributes rather than on the eventfails withexpected undefined to be 'orders/42'.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
A real end-to-end run needs a GCP project, an Eventarc Advanced message bus and
roles/eventarc.publisher, so it cannot run in CI. To verify manually:npm i @google-cloud/eventarc-publishingin the consuming project and authenticate with Application Default Credentials (gcloud auth application-default login).gcloud eventarc message-buses create orders --location=us-central1, then a pipeline/enrollment that forwards to a Cloud Run service you can watch.new EventarcToolset({toolConfig: {projectId: process.env.GOOGLE_CLOUD_PROJECT}})and ask it to publish a message toprojects/$GOOGLE_CLOUD_PROJECT/locations/us-central1/messageBuses/orders.{status: 'SUCCESS', message_id: ...}and that the subscriber receives a CloudEvent whoseidmatchesmessage_id.{status: 'ERROR', error_details: ...}rather than throwing an unhandled rejection, and that the next call rebuilds the channel.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.