Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions dev/src/utils/agent_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ const FILE_MODULE_TYPE_EXTENSION_MAP = {
[FileModuleType.ESM]: '.mjs',
};

/**
* Packages that must never be inlined into the bundled agent file.
*
* See http://mikro-orm.io/docs/deployment#deploy-a-bundle-of-entities-and-dependencies-with-esbuild for more details
*/
const EXTERNAL_PACKAGES = [
'sqlite3',
'better-sqlite3',
'mysql',
'mysql2',
// Native addons must remain external so Node can resolve their
// platform-specific assets at runtime.
'onnxruntime-node',
'oracledb',
'pg-native',
'pg-query-stream',
'tedious',
'libsql',
// Optional peer dependencies of vite and eslint that are not
// installed and MUST NOT be bundled.
'lightningcss',
'jiti',
'jiti/package.json',
];

/**
* Metadata for a file.
*/
Expand Down Expand Up @@ -187,26 +212,9 @@ export class AgentFile {
bundle: this.options.bundle,
minify: this.options.bundle,
plugins: [replaceDirnamePlugin(filePath, originalDir), shimPlugin()],
// See http://mikro-orm.io/docs/deployment#deploy-a-bundle-of-entities-and-dependencies-with-esbuild for more details
external: [
'sqlite3',
'better-sqlite3',
'mysql',
'mysql2',
// Native addons must remain external so Node can resolve their
// platform-specific assets at runtime.
'onnxruntime-node',
'oracledb',
'pg-native',
'pg-query-stream',
'tedious',
'libsql',
// Optional peer dependencies of vite and eslint that are not
// installed and MUST NOT be bundled.
'lightningcss',
'jiti',
'jiti/package.json',
],
// esbuild rejects `external` unless `bundle` is enabled, so the
// allowlist is only passed when the agent file is actually bundled.
...(this.options.bundle ? {external: EXTERNAL_PACKAGES} : {}),
});

this.cleanupDirPath = outputDir;
Expand Down
64 changes: 64 additions & 0 deletions dev/test/utils/agent_loader_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,70 @@ describe('AgentLoader', () => {
await agentFile.dispose();
});

it('omits esbuild "external" when bundling is disabled', async () => {
const agentPath = path.join(tempAgentsDir, 'agent2.ts');
await fs.writeFile(agentPath, agent2TsContent);

(esbuild.build as Mock).mockImplementation(async () => {
await fs.writeFile(compiledPath('agent2.cjs'), agent2CjsContentMocked);
return Promise.resolve();
});

const agentFile = new AgentFile(agentPath, {
compile: true,
bundle: false,
});
const agent = await agentFile.load();

expect(agent.name).toEqual('agent2');
const buildOptions = (esbuild.build as Mock).mock.calls[0][0];
expect(buildOptions).not.toHaveProperty('external');
expect(buildOptions).toMatchObject({bundle: false, minify: false});

await agentFile.dispose();
});

it('omits esbuild "external" when bundle option is not provided', async () => {
const agentPath = path.join(tempAgentsDir, 'agent2.ts');
await fs.writeFile(agentPath, agent2TsContent);

(esbuild.build as Mock).mockImplementation(async () => {
await fs.writeFile(compiledPath('agent2.cjs'), agent2CjsContentMocked);
return Promise.resolve();
});

const agentFile = new AgentFile(agentPath, {compile: true});
const agent = await agentFile.load();

expect(agent.name).toEqual('agent2');
expect((esbuild.build as Mock).mock.calls[0][0]).not.toHaveProperty(
'external',
);

await agentFile.dispose();
});

it('compiles and loads a .ts agent with real esbuild when bundle is disabled', async () => {
const {build: realEsbuildBuild} =
await vi.importActual<typeof import('esbuild')>('esbuild');
(esbuild.build as Mock).mockImplementationOnce(realEsbuildBuild);

const agentPath = path.join(tempAgentsDir, 'agent2.ts');
await fs.writeFile(agentPath, agent2TsContent);

const agentFile = new AgentFile(agentPath, {
compile: true,
bundle: false,
});
const agent = await agentFile.load();

expect(agent.name).toEqual('agent2');
const compiled = await fs.readFile(compiledPath('agent2.cjs'), 'utf8');
expect(compiled).toContain('require("@google/adk")');

await agentFile.dispose();
});

it('throws specific error if file does not exist', async () => {
const agentPath = path.join(tempAgentsDir, 'non_existent.js');
const agentFile = new AgentFile(agentPath);
Expand Down
Loading