Skip to content

Fix: mark AgentFile disposed even when nothing was compiled - #725

Open
AmaadMartin wants to merge 1 commit into
mainfrom
fix/agent-file-dispose-uncompiled
Open

Fix: mark AgentFile disposed even when nothing was compiled#725
AmaadMartin wants to merge 1 commit into
mainfrom
fix/agent-file-dispose-uncompiled

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
    N/A

  2. Or, if no issue exists, describe the change:

Problem: AgentFile.dispose() sets the disposed flag inside the if (this.cleanupFilePath) branch. load() only assigns cleanupFilePath when it compiles, so an AgentFile built with {compile: false, bundle: false} is never marked disposed. getFilePath() then keeps returning a path after dispose(), and an await using scope leaves a live object behind. The CLI reaches this through --compile false --bundle false, and AdkApiServerOptions.agentFileLoadOptions reaches it as well.

Solution: I hoisted this.disposed = true out of the branch, so dispose() marks the instance disposed on its first call. The unlink and removeFolder cleanup stays guarded by cleanupFilePath, so an uncompiled instance still does no filesystem work and the user's original agent file is untouched. The flag is set before the await, so a failed unlink cannot leave a usable instance.

The production change is one moved line:

     if (this.disposed) {
       return;
     }
+    this.disposed = true;

     if (this.cleanupFilePath) {
-      this.disposed = true;
       await fsPromises.unlink(this.cleanupFilePath);

Caller audit: getFilePath() has one production caller, copyAgentFiles in dev/src/cli/deploy/deploy_utils.ts. Both deploy flows call it inside the try and call agentLoader.disposeAll() in the finally, so every getFilePath() call precedes any dispose(). cli_run.ts and adk_api_server.ts use await using but never call getFilePath(). No production caller changes behaviour.

Overlap check: I scanned the open PRs for work on AgentFile. #653 is the nearest neighbour; it adds a new disposeSync() method and does not touch dispose(), so the two do not collide. Note that disposeSync() in #653 keeps an uncompiled instance usable, which will differ from dispose() after this change; whichever lands second should align the two.

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.

Two tests in dev/test/utils/agent_loader_test.ts, next to the existing 'returns original file path if not compiled':

  1. 'throws when getting file path if an uncompiled agent is disposed' — the regression test.
  2. 'disposes an uncompiled agent file without touching the filesystem' — pins that cleanup stays conditional, and that a second dispose() does not reject.

Proof each test can fail. I ran both against mutated source:

Mutation Result
Move this.disposed = true back inside the if (this.cleanupFilePath) branch Test 1 fails: expected [Function] to throw an error. Test 2 still passes.
Drop the guard: await fsPromises.unlink(this.cleanupFilePath ?? this.filePath) Test 2 fails: promise rejected "Error: ENOENT: no such file or directory" instead of resolving. Test 1 still passes.

Each test catches a defect the other misses.

Coverage. dispose() is fully covered by the file's suite: 12 of 12 statements and 4 of 4 branch arms, measured with @vitest/coverage-v8. Both arms of the cleanupFilePath conditional run, and the if (this.disposed) return fast path runs twice.

Commands run, on the pushed commit:

npx vitest run --project unit:dev dev/test/utils/agent_loader_test.ts   # 33 passed
npx vitest run --project unit:dev dev/test/cli/cli_deploy_cloud_run_test.ts \
    dev/test/cli/cli_deploy_agent_engine_test.ts \
    dev/test/server/adk_api_server_test.ts                             # 96 passed
npx vitest run --project unit:dev                                      # 243 passed, 1 pre-existing failure
npm run build                                                          # clean
npx eslint dev/src/utils/agent_loader.ts dev/test/utils/agent_loader_test.ts   # clean
npx prettier <both files> --check                                      # clean
npm run ts:check                                                       # 280 pre-existing errors, 0 in dev/

Two failures are pre-existing and unrelated to this change. I confirmed both by stashing the change and re-running:

  • dev/test/cli/cli_create_test.ts > 'should handle Vertex AI selection with gcloud defaults' fails identically on a clean tree. It reads local gcloud defaults.
  • npm run ts:check reports the same 280 errors with and without this change, all in core/test/** and none in dev/.

CI. run-tests passes on ubuntu, macos and windows. The first attempt needed re-runs for three flakes, all in files this PR does not touch:

  • macos: tests/integration/app_loader/app_loader_test.ts timed out at 40000ms. It passed on ubuntu in the same attempt and passes on macos after a re-run.
  • windows: core/test/code_executors/unsafe_local_code_executor_test.ts > 'should execute shell code and return stdout' timed out at 5000ms, and the spawned test server exited early at tests/integration/test_case_utils.ts:341.
  • The first windows leg reported fail, but the matrix cancelled it when macos failed, so that result was inconclusive.

Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.

import {AgentFile} from '@google/adk-devtools';

const agentFile = new AgentFile('/path/to/agent.js', {
  compile: false,
  bundle: false,
});
await agentFile.load();
await agentFile.dispose();
agentFile.getFilePath();

Before this change the last line returns the path. After it, the line throws Agent is disposed and can not be used, which matches what a compiled agent file already does.

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.

AgentFile.dispose() set the disposed flag inside the cleanupFilePath
branch. cleanupFilePath is only assigned when load() compiles, so an
AgentFile built with {compile: false, bundle: false} was never marked
disposed. getFilePath() kept returning a path after dispose(), and an
"await using" scope left a live object behind.

Hoist the flag out of the branch. The unlink and removeFolder cleanup
stays guarded by cleanupFilePath, so an uncompiled instance still does
no filesystem work and the user's original agent file is untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant