Fix: merge AgentFileOptions per field so partial options keep the compile/bundle defaults - #309
Open
AmaadMartin wants to merge 3 commits into
Open
Fix: merge AgentFileOptions per field so partial options keep the compile/bundle defaults#309AmaadMartin wants to merge 3 commits into
AmaadMartin wants to merge 3 commits into
Conversation
added 3 commits
July 30, 2026 06:12
AgentFile and AgentLoader declared DEFAULT_AGENT_FILE_OPTIONS as a
whole-object default parameter, which TypeScript applies only when the
argument is undefined. Any caller-supplied object replaced the defaults
wholesale, so a partial bag such as {moduleType: FileModuleType.ESM}
resolved compile and bundle to undefined and skipped compilation
entirely -- handing a raw .ts agent file to Node's ESM loader.
Resolve the options in each constructor by spreading the caller's
object over the defaults, so omitted fields fall back and each instance
owns its own object instead of aliasing the shared module-level const.
Cover the three partial-options shapes that previously lost a default
({moduleType}, {compile: true}, {bundle: false}), the explicit opt-out
that must keep skipping compilation, and AgentLoader forwarding the
merged options into every AgentFile it constructs.
Resolve the defaults only in AgentFile, which is where they are read.
AgentLoader forwards its bag verbatim, so a second identical merge there
was a no-op with a keep-in-sync hazard; its default becomes an empty bag
instead of the shared module-level constant.
Also drop the {@link} to the unexported DEFAULT_AGENT_FILE_OPTIONS,
which typedoc cannot resolve from an exported interface.
This was referenced Jul 30, 2026
7 tasks
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:
AgentFileandAgentLoaderdeclared their defaults as a whole-objectdefault parameter value:
A TypeScript default parameter is applied only when the argument is
undefined, so passingany object replaces the defaults wholesale. Every field of
AgentFileOptionsis optional,so a partial bag compiles cleanly and silently loses
compile/bundle:load()then computesshouldCompile = this.options.compile || this.options.bundleasfalseand skips the whole esbuild block: no compilation, noreplaceDirnamePlugin, nonode_modulessymlink, andgetFilePath()returns the raw source path — the path the CloudRun / Agent Engine deployers copy into the deployment bundle. For a
.tsagent it simplythrows at import:
The first-party CLI escapes this because
getAgentFileOptions(dev/src/cli/cli.ts:74)always materialises
compileandbundleas concrete booleans viagetBoolean, and--compile/--bundleare both.default(true). The bug is therefore reachable only fromprogrammatic callers —
AdkApiServer,runAgent,deployToCloudRun,deployToAgentEngine— which is precisely the audience of the exported
@google/adk-devtoolsAPI.A second, smaller defect from the same line: the default handed out the module-level
DEFAULT_AGENT_FILE_OPTIONSobject by reference, so every option-less instance shared onemutable object (
readonlypins the binding, not the contents).Solution: resolve the options in the
AgentFileconstructor by spreading the caller'sobject over the defaults, so omitted fields fall back per field and each instance owns its
own object:
AgentLoaderreadsthis.optionsnowhere exceptnew AgentFile(path, this.options), so itdoes not repeat the merge — a second merge would be idempotent and would only create a
keep-in-sync hazard. Its default becomes
{}instead of the shared constant, which removesthe by-reference hand-out and keeps the "defaults live in exactly one place" property true
rather than coincidental. Constructor arity and positional order are unchanged in both
classes, so
new AgentLoader(dir, opts, watch)and the three-argument call inadk_api_server.tskeep working.This introduces no new pattern: the repo already defaults options bags per field elsewhere
(e.g.
adk_api_server.ts:100-114uses??per field). The agent-loader constructors werethe outlier.
Deliberately not done, to keep the diff minimal: no undefined-stripping helper, no
??-per-field ladder, no genericmergeDefaultsutility, no new exported "resolved options"type, and
AgentFileOptionskeeps all three fields optional (moduleTypemust stay absentby default so
load()can fall back togetFileModuleType(filePath)).Behaviour change to review deliberately. The merge flips exactly one class of caller —
one that passes an options object omitting
compileand/orbundle:{compile: false, bundle: false}{compile: true}{bundle: false}{moduleType: 'esm'}Row 3 is the one that is arguably a behaviour change rather than a pure fix: someone who
wrote
{compile: true}meaning "compile but don't bundle" now also gets bundling. It isstill the correct reading of the contract —
AgentFileOptionshas no way to express"omitted" distinctly from "default", the documented default for
bundleistrue, and thealternative (defaulting only the fields the caller did not mention unless they mentioned a
sibling field) is unimplementable and unreadable. Callers who want transpile-without-bundle
should say
{compile: true, bundle: false}, which is now honoured exactly.Plain spread semantics are accepted deliberately and documented on the interface:
{compile: undefined}yieldsundefined, nottrue.exactOptionalPropertyTypesis off sothis is expressible, but no in-repo caller does it.
Collision check (open PRs on this fork,
--state open --limit 100). Three open PRs touchdev/src/utils/agent_loader.ts: #285 (externalize third-party packages), #275 (only passesbuild
externalwhen bundling), #264 (scope the esbuild plugin filter / skipnode_modules). I diffed all three: none touches either constructor, so nothing alreadylands this change and there is no textual conflict — this PR branches from
mainrather thanstacking. One semantic interaction is worth flagging for whoever merges second: #275 adds a
test
omits esbuild "external" when bundle option is not providedthat constructsnew AgentFile(agentPath, {compile: true})and asserts noexternalkey. Under this fix{compile: true}resolvesbundletotrue, so that assertion becomes wrong and the testmust be re-pointed at
{compile: true, bundle: false}. #285 carries the same logic. Nochange is needed here today because neither is merged.
Public-repo hygiene: the diff and commit messages were grepped for internal-only
references (tracker ids, internal hostnames, short links); none are present.
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.
Five cases added to the existing
describeblocks indev/test/utils/agent_loader_test.ts, reusing the file's existing harness (the hoistedvi.mock('esbuild')factory, the(esbuild.build as Mock).mockImplementationpattern, thecompiledPathhelper and thefileUtils.getTempDirmock). No new mock layer, no new testfile, and no existing test was modified, weakened or deleted — all 30 pre-existing cases
still pass unchanged.
inherits the compile and bundle defaults when only moduleType is given{moduleType: ESM}now compiles: esbuild called withformat: 'esm',bundle: true,minify: true, outfileagent2.mjsinherits the bundle default when only compile is giveninherits the compile default when only bundle is given{bundle: false}still transpiles (bundle: false, minify: false) instead of skipping esbuild entirelydoes not compile when compile and bundle are explicitly falseforwards options merged with the defaults to every agent fileAgentLoaderforwarding, plus the third positionalwatchForChangesargument surviving the signature changeOne new fixture,
agent2MjsContentMocked, is required because the ESM case writes a.mjsartifact and the existing CJS fixture's
exports.rootAgentcannot beimported from one.A sixth case from the plan ("no options behaves as before") was not added: the
pre-existing
loads .ts agent file and compiles italready constructsnew AgentFile(path)with no options and asserts
bundle: true, minify: trueon the esbuild call, so it isalready pinned. An "instance isolation" case was also dropped: with nothing mutating
this.options, the property is unobservable without reaching into aprivatefield, and atest that fails only for the same reason as case 1 is not a real signal.
Proof the new tests can fail (mutation). Mutated the single merge site back to the
unfixed semantics:
Result — the four merge-dependent tests FAIL, the explicit-opt-out guard correctly still
passes:
Representative message:
(The same mutation also breaks four pre-existing tests, since removing the merge removes the
no-options default too — further confirmation the merge is load-bearing.)
Commands run locally on the pushed commit:
tests/integration/app_loader/app_loader_test.tswas run unmodified and passes; itexercises the real, unmocked esbuild path via
new AgentLoader(projectPath), confirming theno-options path is unchanged. Note it flakes independently of this change: its
beforeAllruns
npm installin a fixture and intermittently exceeds the hook timeout (reported as6 skipped,tests 160.03s, nothing executed). Two of three local runs hit that; the thirdpassed all 6. This is a known pre-existing issue with open PRs against it, and a fixture
npm installcannot be affected by a constructor change.npm run ts:checkis not run by CI and fails onmaintoday with pre-existing errors incore/**andtests/**; this change adds zero errors underdev/.No suppressions of any kind were added — no
any,as any,as never,as unknown as,@ts-ignore,@ts-expect-error,eslint-disable, or coverage-ignore.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Run against the built
devpackage with real esbuild and no mocks:@google/adkresolvable and a TypeScript agent:Observed before the fix (
devrebuilt frommain):Observed after the fix:
i.e. the partial bag now yields a compiled
.mjsartifact, and the explicit opt-out stillcorrectly skips compilation.
CLI paths were verified by inspection to be bit-for-bit unaffected:
getAgentFileOptions(
dev/src/cli/cli.ts:74-84) always emitscompileandbundleas concrete booleans, so themerge is a no-op for
adk run/adk web/adk deploy.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.