Perf: compile all discovered agent entrypoints in one esbuild build - #814
Open
AmaadMartin wants to merge 6 commits into
Open
Perf: compile all discovered agent entrypoints in one esbuild build#814AmaadMartin wants to merge 6 commits into
AmaadMartin wants to merge 6 commits into
Conversation
added 6 commits
August 8, 2026 08:49
AgentLoader discovery ran one independent esbuild.build() per entrypoint, so every build re-resolved and re-parsed the same dependency graph. The loader now discovers first, then compiles the whole batch with one build per output module format. Each agent keeps its own temp directory and output file, so the emitted bundles are unchanged. The replace-dirname plugin takes a map of entry directories instead of one closed-over path, which is what made a batched build impossible before.
The esbuild cluster in agent_loader.ts grew to about 200 lines and no longer belongs in a module whose job is loading agents. It now lives in dev/src/utils/agent_compiler.ts, together with the module-type and node_modules helpers only it uses, and the plugin tests move with it. compileEntrypoints returns an array in input order instead of a Map, so callers index it and no longer need a non-null assertion. The plugin's setup() takes Pick<PluginBuild, 'onLoad'>, the only hook it uses, so a test stub needs no cast. The batch scratch directory prefix is now adk_agent_build, which no longer prefix-matches adk_agent_loader.
esbuild-shim-plugin ships index.d.ts, so the suppression silenced nothing. tsc reports the equivalent @ts-expect-error as unused.
This was referenced Aug 8, 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
N/A
Problem:
AgentLoaderdiscovery runs one independentesbuild.build()per discovered entrypoint. Every build re-resolves and re-parses the same dependency graph. The plugin that rewrites__dirnamecloses over one file path, which is what makes a shared build impossible.Solution: The loader now discovers every entrypoint first, then compiles the whole batch with one
esbuild.build()per output module format (one build in the common case, two for a directory that mixes CommonJS and ESM).replaceDirnamePlugintakes a map of entry directories instead of one path. Each agent keeps its own temp directory and output file, so the emitted bundles do not change. The esbuild concern moves to a newdev/src/utils/agent_compiler.ts, because it grew to about 200 lines inside a module whose job is loading agents.Measured effect (esbuild 0.25.12, node v22.22.2, Linux, warm, 9 repetitions in a fresh process each, fixture
tests/integration/app_loader/discovery):listAgents()end to end, 4 entrypointslistAgents()end to end, 1 entrypointThe compile phase gets about 24% faster, but it is only about a quarter of
listAgents(). End to end that is about 140 ms of 2.86 s, or about 5%. The dominant cost is importing four separate ~5.7 MB bundles, and this change does not touch it. The single-entrypoint case is about 13 ms slower (about 1%), which is the cost of the extra scratch directory and the rename.The four bundles are byte-identical before and after (same SHA-256 for all four).
Known behaviour change: a file deleted between the directory scan and the build used to be skipped, because the
statinload()raisedAgentFileLoadingError. It now fails the batch, because compilation happens before thatstat. This is a development-time race and the file watcher re-runs discovery.Disclosed test edits: about a dozen existing test bodies changed.
dev/test/utils/agent_loader_test.tsmocksesbuild.buildand asserts the option object, so it has to change with the implementation. Every case is preserved. The list:outfileassertions becameoutdir+outExtension+{in, out}entry points, and the twelve copy-pasted mock bodies became onemockEsbuildBuildhelper.createTempDirnow returns a fresh directory per call, because the loader calls it twice per load.'uses js loader for non-ts files','returns js loader for mts files'and'returns js loader for cts files'collapsed into oneit.eachover the three extensions. All three cases still run, under the same three names.'throws if rootAgent is not found'usedrejects.toThrow(<full message>), which needs the compiled path up front; the path is now a fresh temp directory, so the test captures the rejection with.catchand assertstoBeInstanceOf(Error)plus the same full message.'does not preload agents again if already preloaded'spied on the deleted privateloadAgentFromFile; it now asserts the observable thing, that a secondpreloadAgents()issues no further build.replaceDirnamePlugincases moved todev/test/utils/agent_compiler_test.tswith the code they cover.No test was deleted, skipped or weakened.
No new suppressions:
replaceDirnamePlugin'ssetupnow takesPick<esbuild.PluginBuild, 'onLoad'>, the only hook it uses, so the test stub passes{onLoad: vi.fn()}directly. That deletes the sixas unknown as esbuild.PluginBuildcasts the old plugin tests carried.compileEntrypointsreturns an array in input order rather than aMap, which removes the non-null assertion at its call sites. The diff also drops the@ts-ignoreon theesbuild-shim-pluginimport: the package shipsindex.d.ts, so the directive was dead.Collision check:
gh pr list --repo AmaadMartin/adk-js --state open --limit 400plusgh pr diff --name-onlyon every open PR touchingdev/src/utils/agent_loader.ts(#264, #285, #457, #480, #511, #633, #653, #664, #674, #703, #725, #726, #731, #755, #793). None batches the builds. This PR branches frommainand leaves the{filter: /.*/}andgetDirFileslines alone.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.
tests/integration/app_loader/app_loader_test.tsis unchanged. It passed on the first revision of this branch and on CI.npm run ts:checkreports 287 pre-existing errors on this branch and 287 onmain, none of them in the files this PR touches.npm run docs:checkpasses.Ten mutations prove the new tests fail against broken code:
expected undefined to be 'js'groupByModuleTypereturns one groupexpected "spy" to be called 2 times, but got 1 timesgroupByModuleTypereturns one group per entryexpected "spy" to be called 1 times, but got 3 timescatchcleanuppromise resolved "undefined" instead of rejectingexpected 1 to be 3removeFolder(buildDir)promise resolved "undefined" instead of rejectingshouldCompileguardexpected "spy" to not be called at all, but actually been called 1 timesexpected [ { …(2) } ] to deeply equal []promise resolved "[]" instead of rejectingcompiled[0])ENOENT: no such file or directory, unlink '.../agent-HA7it6/agent1.cjs'Every line and branch this PR adds to
dev/src/utils/agent_loader.tsanddev/src/utils/agent_compiler.tsis covered, except four helpers moved verbatim out ofagent_loader.ts(getFileModuleType,getTypeFromPackageJson,linkProjectNodeModules,getProjectNodeModulesDir). Their coverage is unchanged; the move is byte-identical.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Returns
["service_alpha","service_beta","standalone_agent","standalone_app"], the same four names as before.adk runon a single agent file exercises the batch-of-one path:Prints
Running agent standalone_agent_name, type exit to exit.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.