Fix: only pass esbuild "external" when bundling is enabled in AgentFile.load() - #275
Open
AmaadMartin wants to merge 2 commits into
Open
Fix: only pass esbuild "external" when bundling is enabled in AgentFile.load()#275AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
This was referenced Jul 30, 2026
Open
Open
Owner
Author
7 tasks
added 2 commits
August 9, 2026 01:29
esbuild rejects the `external` option unless `bundle` is enabled, so
`AgentFile.load()` hard-failed with `Cannot use "external" without
"bundle"` for every non-truthy `bundle` value -- breaking `adk web|run|
api_server|deploy --bundle false` and `new AgentFile(p, {compile: true})`.
Hoist the allowlist to a module-scope `EXTERNAL_PACKAGES` constant
(contents unchanged) and spread it into the build options only when
bundling, so the key is genuinely absent otherwise. `bundle: true`, the
default on every code path, produces an identical esbuild invocation.
Drop a tautological toHaveProperty('external') assertion -- the
toMatchObject two lines above already asserts
external: expect.arrayContaining(['onnxruntime-node']), which fails when
the key is absent. Scope the real-esbuild implementation to the single
build the test triggers so it cannot leak into later tests
(vi.clearAllMocks clears calls but keeps implementations).
AmaadMartin
force-pushed
the
fix/agent-loader-esbuild-external-without-bundle
branch
from
August 9, 2026 08:36
2475bfd to
155ee79
Compare
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:
AgentFile.load()builds one esbuild options object and passesexternalunconditionally, whilebundleis taken straight fromAgentFileOptions.bundle. esbuild rejectsexternalunless bundling is on, so any non-truthybundlehard-fails:shouldCompile = compile || bundle, so the compile branch is still entered for{compile: true, bundle: false}and the build runs and throws. The thrown value is an esbuildBuildFailure, not anAgentFileLoadingError, soAgentLoaderre-throws it and the whole invocation dies instead of skipping the file.This breaks a documented first-class CLI knob.
--bundle [boolean]is registered asBUNDLE_AGENT_FILEindev/src/cli/cli.tsand attached to five commands (web,api_server,run,deploy cloud_run,deploy agent_engine), andgetBooleanmaps--bundle falseto booleanfalse. It also breaks the library surface:new AgentFile(p, {compile: true, bundle: false}), and equallynew AgentFile(p, {compile: true})— withbundleomitted it isundefined, which esbuild treats the same asfalsefor this validation. The fix therefore keys off truthiness, not=== false.Verified against the esbuild the repo actually resolves (
dev/package.jsonpins^0.25.9;0.25.12installed here). All five combinations probed directly:bundle: false+externalCannot use "external" without "bundle"bundle: undefined+externalCannot use "external" without "bundle"bundle: false, noexternalbundle: undefined, noexternalbundle: false+external: undefinedSolution: pass
externalonly when bundling is actually enabled.EXTERNAL_PACKAGESconstant (contents, order, and explanatory comments unchanged). The repo's TypeScript guidelines put static string lists at file level rather than inline inside a method, and it keeps the build-options object readable.external: [...]with...(this.options.bundle ? {external: EXTERNAL_PACKAGES} : {}).A conditional spread rather than
external: undefined: although esbuild also acceptsexternal: undefined, the postcondition worth pinning is'external' in buildOptions === Boolean(options.bundle), and only the spread genuinely omits the key.Deliberately unchanged, and checked rather than assumed:
packages: 'bundle'stays ungated — esbuild imposes no equivalent restriction on it, and{bundle: false, packages: 'bundle'}builds cleanly.linkProjectNodeModuleskeeps running on this path. The unbundled artifact resolves its bare imports at import time through that symlink, so it is load-bearing here, not incidental.try/catchand no error wrapping. Genuine esbuild failures (a syntax error in the user's agent file) must keep propagating exactly as today; swallowing them would silently hide broken agents fromadk web.bundle: true— the default inDEFAULT_AGENT_FILE_OPTIONSand on every CLI command — produces a byte-identical esbuild invocation, so the common path is untouched. No public API, type, export, or CLI surface change. The misleading--bundlehelp text (a copy of--compile's) is a separate concern and is not touched here.Collision check (required before implementing): scanned all 181 open PRs on the fork for
bundle/external/esbuild/agent.?loader/agent.?file. None implements this fix. Only #264 ("Perf: scope the agent-loader esbuild plugin filter and skip node_modules during discovery") touches the same two files, in disjoint regions (replaceDirnamePluginandgetDirFiles, vs. theesbuild.buildoptions object here).git merge-treeagainst that branch reports zero conflicts, so this is branched frommainrather than stacked.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.
Three tests added to
dev/test/utils/agent_loader_test.ts, all inside the existingdescribe('AgentFile')block:omits esbuild "external" when bundling is disabled—{compile: true, bundle: false}; asserts the options object has noexternalkey and matches{bundle: false, minify: false}.omits esbuild "external" when bundle option is not provided—{compile: true}, i.e. thebundle: undefinedrow; asserts noexternalkey.compiles and loads a .ts agent with real esbuild when bundle is disabled— swaps the module mock for the realesbuild.buildviavi.importActualand drives a fullload(). Asserts the agent loads (agent.name === 'agent2') and that the emitted.cjscontains a literalrequire("@google/adk"), i.e. the dependency really was left external rather than inlined. This is the only test in the file that exercises real esbuild, and the only one that can catch the reported error, since a mockedbuildcan only ever echo back what it was handed.The existing positive case (
loads .ts agent file and compiles it) already pinsbundle: true, minify: true, external: expect.arrayContaining(['onnxruntime-node'])and is untouched — no existing test was weakened, skipped, or deleted.Proof the tests can fail (mutation testing). Coverage alone is not evidence, so each test was run against mutated source:
Mutation A — revert the source hunk entirely (restore the unconditional
external). All three new tests fail:Mutation B — invert the new guard (
...(!this.options.bundle ? …)). Catches the other arm: the pre-existing positive test now fails too, proving thebundle: truepath is still pinned.Coverage. The fix adds exactly one branch. Measured on the changed file with the targeted suite: both arms of the new ternary at
agent_loader.ts:219are executed (35 hits truthy / 3 hits falsy), and no added line is uncovered — 100% new-line and new-branch coverage. Global thresholds invitest.config.tsare untouched.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Setup: a scratch project with
package.json, anode_modules/@google/adklink to this checkout'score, and anagent.tsexportingrootAgent(aBaseAgentsubclass whoserunAsyncImplyields onecreateEvent(...), so no model credentials are needed).Reproduce the bug. With the guard removed from the built loader,
adk run --bundle false ./agent.tsaborts before the agent ever loads:adk run --bundle false ./agent.ts(fixed) — loads and responds:adk web --bundle false --port <port> ./agent.ts(fixed) — server starts,GET /list-appsreturns["agent"], and after creating a sessionPOST /runreturns the agent's event:The emitted artifact is genuinely unbundled: 1,746 bytes containing
require("@google/adk").Regression check, no flag (
bundledefaults totrue):adk run ./agent.tsloads and responds identically.Full local validation on the pushed commit (
npm installfrom a clean tree, so no lockfile churn):npm run build,npm run lint,npm run format:check,npm run docs:checkall exit 0, plus the targeted suite above.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.