Feat: port the Application Integration / Integration Connectors clients from adk-python (Part 1/2) - #578
Open
AmaadMartin wants to merge 5 commits into
Open
Feat: port the Application Integration / Integration Connectors clients from adk-python (Part 1/2)#578AmaadMartin wants to merge 5 commits into
AmaadMartin wants to merge 5 commits into
Conversation
added 3 commits
August 3, 2026 08:57
…ents The Application Integration and Integration Connectors clients both need an access token (from ADC or an explicit service-account key file) and the same mapping from transport/status failures onto user-facing messages. Extract that once so neither client reimplements it. Token caching is left to google-auth-library rather than hand-rolled, and every request carries an AbortSignal timeout.
Generates the OpenAPI spec for an Application Integration trigger, or for an Integration Connector connection's entity operations and actions. Wire strings (paths, $refs, schema defaults, descriptions) are byte-identical to the Python reference so the generated spec stays interchangeable. The long-running-operation poll is bounded instead of looping forever, and it only sleeps between attempts.
getQuotaProjectId() resolves an auth client of its own, so an unavailable ADC surfaced the raw google-auth error instead of the message that asks the caller for a service account.
added 2 commits
August 3, 2026 10:17
…it into the query The Integration Connector spec appends `#<operation>_<entity>` to the shared `:execute` endpoint so several operations can occupy distinct spec paths. prepareRequestParams split the path on '?' and handed the remainder — fragment included — to URLSearchParams, which percent-encoded it, so the request went out with triggerId=api_trigger%2FExecuteConnection%23list_Issues and named a trigger that does not exist. adk-python drops the fragment before rebuilding the URL (rest_api_tool.py: urlunparse(parsed._replace(query='', fragment=''))); do the same here.
…them strictly The five entity path-item builders and the five request-schema builders shared one body each and differed only in a verb, a summary, a description and one $ref slug, so they become a single ENTITY_OPERATIONS table plus one buildEntityOperation(); the five-arm switch in IntegrationClient collapses into a lookup that keeps the parity error message. A refs() helper replaces the $ref literal repeated once per field. The fragments are now typed as OpenAPIV3 path items and schemas, which removes the 'as OpenAPIV3.Document' cast that laundered a deliberately untyped value into a strict one. The x-* extension interface moves here, where it is produced, instead of being redeclared by the consumer. Also drops ConnectionsClient.connectorPayload, a pass-through to the exported converter that never touched 'this' (the converter moves next to the other spec-shaping functions), inlines three one-use indirections, makes the tool name and instruction parameters required since every caller passes them, and moves toMessage to core/src/utils/error_utils.ts, where the same expression is hand-rolled in nine other places. Byte parity with the Python reference is unchanged: every builder's output was re-diffed against connections_client.py after the refactor. The test expectations are the same Python-derived literals, reached through the new entry point.
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
Closes: #issue_number
Related: #issue_number
Problem: adk-python ships a first-class Google Cloud Application Integration / Integration Connectors toolset (
src/google/adk/tools/application_integration_tool/). adk-js has nothing equivalent, so a TypeScript user who wants an agent to LIST/GET/CREATE/UPDATE/DELETE entities on a connector, or invoke a connector action, has to hand-write the OpenAPI spec and the Application Integration:executeplumbing themselves. A case-insensitive search forApplicationIntegration/IntegrationConnectoracrosscore/src,dev/src,integrations/src,tests/anddocs/returns zero hits today.Solution: Part 1 of 2 — port the two API clients that generate the OpenAPI spec. Part 2 (stacked on this branch) adds the toolset and the tool that consume them.
This part adds:
core/src/tools/application_integration_tool/clients/integration_client.ts—IntegrationClient. Calls:generateOpenApiSpecfor an integration's API triggers, and assembles the connector spec for a connection's entity operations and actions.core/src/tools/application_integration_tool/clients/connections_client.ts—ConnectionsClient. Reads connection details, entity schemas and action schemas from the Integration Connectors API, and converts a connector JSON schema into an OpenAPI schema.core/src/tools/application_integration_tool/clients/connector_spec_builders.ts— the spec fragments Python keeps as@staticmethods onConnectionsClient. They never touchthis, so per the JS guidelines they are module-level functions. Python's five near-identical entity path-item builders and five request-schema builders collapse into oneENTITY_OPERATIONStable plus a singlebuildEntityOperation(), and arefs()helper replaces the{$ref: '#/components/schemas/<name>'}literal that was otherwise spelled out once per field. The fragments are typed asOpenAPIV3path items and schemas rather thanRecord<string, unknown>, so no cast is needed to hand the finished document to the parser.core/src/tools/application_integration_tool/clients/api_request.ts—AccessTokenProvider+executeApiCall, the token/HTTP/error-mapping layer both clients share.core/src/utils/service_account_utils.ts—parseServiceAccountCredential, shared by the token provider and (in part 2) the toolset.core/src/tools/openapi_tool/rest_api_tool.ts— a two-line fix toprepareRequestParams(see below). This is the first change in the repo to emit spec paths carrying a URL fragment, and without it every generated connector path would go out with a corrupted query string.Only
EntityOperationsis added tocore/src/common.ts— it appears in the toolset's public options type in part 2. The clients themselves stay internal: nothing outside this module constructs them, and adk-python keeps them out of its package__init__too. They can be published later if a caller needs them.No new dependency.
google-auth-libraryis already acoredependency andopenapi-typesis already a devDependency (types only);globalThis.fetchis already the HTTP client used byrest_api_tool.tsandintegrations/agent_registry.Collision check.
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000(477 open PRs) plusgh pr diff --name-onlyon every plausibly adjacent PR (#462, #463, #464, #465, #528, #529) found no open PR touchingcore/src/tools/application_integration_tool/. The only textual overlap is the append-only export block incore/src/common.ts, which several unrelated PRs also append to.Why this PR touches
rest_api_tool.tsThe connector spec puts several operations on one endpoint and keeps their spec paths distinct with a fragment:
adk-python drops that fragment before issuing the request —
rest_api_tool.pydoesurlunparse(parsed_url._replace(query="", fragment=""))and re-adds onlyparse_qs(parsed_url.query)— so the wire value istriggerId=api_trigger/ExecuteConnection.adk-js did not.
prepareRequestParamssplit the path on?and handed the whole remainder, fragment included, toURLSearchParams, which percent-encoded it:so the service would receive
triggerId = "api_trigger/ExecuteConnection#list_Issues"— a trigger that does not exist — andRestApiTool.runAsyncwould swallow the failure into{error: 'Failed to execute API call: ...'}. That would have broken every entity operation and every action in connection mode.The fix mirrors Python: drop the fragment before the query string is read. Integration mode is unaffected (that spec comes from the API and carries no fragments), and no other spec in the repo produces a fragment, so nothing else changes behaviour. Two new cases in
rest_api_tool_test.tspin it, and both fail if the fix is reverted (see the table below).Parity notes (adk-python is the reference)
The wire format is byte-identical, and was re-verified after the table refactor. Every builder's output was diffed against the Python literals by exec'ing
connections_client.pyand comparingjson.dumpsoutput field by field — including the multi-linelist_operationdescription with its embedded newlines and 16-space indentation. The test expectations inconnector_spec_builders_test.tswere generated from that Python output, so drift fails loudly.Deliberate divergences, all of them local-convention or correctness wins that leave the observable payload unchanged:
utils/_mtls_utils.get_api_endpoint(...), which swaps in*.mtls.googleapis.com. adk-js has no mTLS utility (zero hits formtlsacrosscore/src,dev/src,integrations/src) andgoogle-auth-libraryfor Node does not expose theshould_use_client_cert()probe the Python helper relies on. This port uses the plain endpoints only; a partial hand-rolled helper would be worse than none. The two Python mTLS tests are correspondingly not ported."404" in str(e) or "400" in str(e), which false-positives on any error text containing those digits. This port checksresponse.status. Same message, no false positives.credentialsand checks.expired.google-auth-library's clients already cache and refresh internally, so one client is held per client instance andgetAccessToken()is called per request. The two Python cache tests are replaced by a test asserting the auth client is constructed once and consulted per call._poll_operationiswhile not done: ...; time.sleep(1)— unbounded, and it sleeps even after the operation reports done. This port sleeps only between attempts, caps atMAX_POLL_ATTEMPTS, and throwsTimed out waiting for operation {id} to complete.fetchhas no timeout option, so every request carriesAbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS), matching Python'stimeout=30.x-goog-user-project. Python prefers the ADC-discovered project over the configured one; this port usesquotaProjectId ?? project, so a caller that names a project gets billed for that project rather than whatever ADC happens to resolve. The header is still omitted entirely when an explicit service account is supplied, as in Python.ConnectionsClient.connector_payloadis a pass-through that never touchesthis, so it is not reproduced as a method: callers use the module-levelconvertJsonSchemaToOpenApiSchemadirectly, which lives beside the other spec-shaping functions.''; every call site here passes them, so the defaults would be dead.No
any, no@ts-expect-error, noeslint-disable, no coverage suppression was added. The one cast in this part isJSON.parse(...) as OpenAPIV3.Documentat the decode boundary, matching the existingopenapi_toolset.ts:45idiom.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.
npx vitest run --project unit:core core/test/tools/application_integration_tool core/test/utils/service_account_utils_test.ts→ 99 passed, and 100% statements / branches / functions / lines on every file this PR adds (measured with--coverage.includescoped to the new paths).Also run on the pushed commit:
npm run build,npx tsc --noEmit -p core/tsconfig.json,npx eslint "core/**/*.ts",npx prettier --check "core/**/*.ts",bash scripts/check_license.sh,npm run docs:check— all clean.CI status: green. All five checks pass on the reviewed commit
b183bf83(run 30837733973):run-testson ubuntu-latest, macos-latest and windows-latest, the cross-languagerun-testsjob, andcheck-license. Earlier runs on this branch failed on windows-latest, but only ever on pre-existing flaky child-process suites (UnsafeLocalCodeExecutorshell/python 5s timeouts,run_skill_inline_script_tool,run_skill_script_tool,app_loaderdiscovery,build_setupts_commonjs,load_mcp_resource_e2e) — a different subset each time, none of them touching this diff, and the same job fails on the fork's ownmain(run 30669370416).Proof the tests can fail. Each mutation was applied to the source, the targeted test file re-run, then the mutation reverted:
getConnectorBaseSpec()emitsdefault: 'LIST'instead of'LIST_ENTITIES'expected { openapi: '3.0.1', …(5) } to deeply equal { openapi: '3.0.1', …(5) }getConnectionDetailsalways readsserviceDirectory(drops thehost ? tlsServiceDirectory : …branch)expected { name: 'test-connection', …(3) } to deeply equal { … }executeApiCalltreats only 404 (not 400) as an invalid requestexpected [Function] to throw error including 'Invalid request. Please check the val…' but got 'Request error: 400 Bad Request'url = url.split('#')[0]fromprepareRequestParamsdrops a path fragment instead of folding it into the query,drops a path fragment that carries no query stringlistresponse description from theENTITY_OPERATIONStableexpected { post: { …(7) } } to deeply equal { post: { …(7) } }refs()at#/components/schema/instead of#/components/schemas/expected { openapi: '3.0.1', …(5) } to deeply equal { openapi: '3.0.1', …(5) }buildEntityOperationaccept any verb instead of returningundefinedpromise resolved "{ openapi: '3.0.1', …(5) }" instead of rejectingManual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Not runnable in CI — it needs a real GCP project with Application Integration provisioned and a live connector connection. With ADC pointed at such a project:
ExecuteConnectionwith triggerapi_trigger/ExecuteConnectionin the same region as the connection (this is the standard Application Integration setup for connector execution).const spec = await new IntegrationClient({project, location, connection, entityOperations: {Issues: ['LIST']}}).getOpenApiSpecForConnection('jira', '');spec.pathscontains/v2/projects/<p>/locations/<l>/integrations/ExecuteConnection:execute?triggerId=api_trigger/ExecuteConnection#list_Issuesandspec.components.schemas.connectorInputPayload_Issuesreflects the connector's real entity schema.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.