Fix: let AgentLoader own the AgentFile lifetime the dev API server borrows - #830
Open
AmaadMartin wants to merge 3 commits into
Open
Fix: let AgentLoader own the AgentFile lifetime the dev API server borrows#830AmaadMartin wants to merge 3 commits into
AmaadMartin wants to merge 3 commits into
Conversation
added 3 commits
August 8, 2026 15:53
…orrows The dev API server bound the loader-owned AgentFile with `await using` in the agent-graph handler and in executeAgentRun(). Finishing the first request therefore unlinked the compiled bundle and removed its temp directory, and left the cached handle disposed for the rest of the process. Both call sites now borrow the handle with `const`. disposeAll() also empties the cache and clears the preloaded flag, so a loader used after disposeAll() re-scans instead of lending a disposed handle.
The API server fake returned a fresh AgentFile per getAgentFile() call, so the suite could not see a borrower disposing the shared handle. The new lifecycle block lends one CountingAgentFile and asserts the run and graph endpoints never dispose it. Two loader tests assert getAgentFile() lends a live handle and re-scans after disposeAll().
invalidateAll() and disposeAll() both empty preloadedAgents and clear agentsAlreadyPreloaded before ending the handles' lives. takeAgentFiles() does that once, so the two lifetime-ending paths cannot drift.
AmaadMartin
force-pushed
the
fix/agent-loader-owns-agent-file-lifetime
branch
from
August 8, 2026 23:26
cd76334 to
83ee5d3
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
Problem:
AgentLoader.getAgentFile()lends theAgentFilethe loader keeps in its cache, but the dev API server bound that shared handle withawait usingin the agent-graph handler and inexecuteAgentRun(). Finishing the first/runtherefore unlinked the compiled bundle and removed its temp directory, which also removes thenode_modulessymlink that everyexternalpackage resolves through at runtime.AgentLoader.disposeAll()had the same ownership confusion: it disposed every cached entry but kept it, so a latergetAgentFile()returned a disposed handle. Both defaults make this the normal path, because--compileand--bundleare.default(true)indev/src/cli/cli.ts.Solution: Both call sites now borrow the handle with
const. Only the loader ends anAgentFile's life, anddisposeAll()now drops the cache entries the wayinvalidateAll()already did, so a reused loader re-scans instead of lending a corpse. Both dispose paths drain the cache through one new private helper,takeAgentFiles(), so they cannot drift.dev/src/cli/cli_run.tsconstructs its ownAgentFile, so itsawait usingis correct and stays, andAgentFile.dispose()is unchanged.This supersedes #726, which fixes the two call sites but leaves
disposeAll()alone and adds a loader test that pins the disposed-handle behaviour. Please close #726 in favour of this PR.Collision check: I listed all open PRs on the fork and read the diff of every PR that touches
dev/src/utils/agent_loader.tsordev/src/server/adk_api_server.ts(#726, #727, #731, #755, #778, #793, #814, #823). Only #726 overlaps. #823 relocates the run block into a shared helper and keepsawait using, so it conflicts textually but does not fix the defect. #793 replaces the preload flag with a generation counter and is a rename-level conflict insidedisposeAll().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:
npx vitest run --project unit:dev dev/test/server/adk_api_server_test.ts dev/test/utils/agent_loader_test.ts— 89 passed.Three tests in a new
agent file lifecycleblock assert the run and graph endpoints never dispose the borrowed handle. Two loader tests assertgetAgentFile()lends one live handle, and lends a fresh usable one afterdisposeAll(). The shared fake in the server suite returns a new object per call, which is why the suite could not see this defect; I left it alone and swappedgetAgentFilein place inside the new block, as three existing tests already do.No existing test is edited, deleted or skipped.
Every line I added to
dev/src/runs under those two test files. I read the v8 JSON report to confirm it:adk_api_server.ts:349and:1049, every statement intakeAgentFiles(), and both of its callers have a non-zero hit count.Proof each new test can fail. I mutated one line at a time and re-ran the new tests:
await usingback at the graph siteexpected 2 to be +0andexpected 1 to be +0await usingback at the run siteexpected 2 to be +0andexpected 1 to be +0disposeAll()body reverted tomainre-scans and lends a fresh AgentFile after disposeAllfails:expected AgentFile{ …(6) } not to be AgentFile{ …(6) }agentsAlreadyPreloaded = trueremoved frompreloadAgents()lends the same live AgentFile to repeated getAgentFile callsfails:expected AgentFile{ …(6) } to be AgentFile{ …(6) }Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
I ran a real
adk api_serverwith no mocks, on this branch and on the unfixed build.agents/myagent.tsexporting aBaseAgentsubclass that yields one event, so no model credentials are needed.TMPDIR=/tmp/probe node dev/dist/esm/cli_entrypoint.js api_server ./agents --port 8791.POST /apps/myagent/users/u1/sessions/s1, thenPOST /runtwice, thenGET /apps/myagent/users/u1/sessions/s1/events/<id>/graphtwice.find "$TMPDIR" -name '*.cjs'after each step.Unfixed build: the bundle and its
adk_agent_loader-*directory are gone after the first/run. This branch: both survive all four requests, all four return 200, andCtrl-Cremoves the bundle.CI
All five checks pass on head
83ee5d3d, includingrun-testson ubuntu-latest, macos-latest and windows-latest.The first windows-latest attempt failed. Two tests in
tests/integration/app_loader/app_loader_test.tshit the 40000ms per-test budget, in a run whose whole suite took 551s. Those tests calllistApps(),listAgents()andgetAppFile(), none of which my change touches;disposeAll()runs only inafterAlland did not report an error. The same job passed on the previous head of this branch, which had identicaldisposeAll()behaviour. I re-ran the job with no code change and it passed in 10m31s, so this is a slow-runner timeout and not a regression.Checklist