diff --git a/package-lock.json b/package-lock.json index ccd58eefdc..84230f4ad7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2500,6 +2500,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2680,6 +2681,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2713,6 +2715,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -3081,6 +3084,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -3114,6 +3118,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -3166,6 +3171,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -4403,6 +4409,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4680,6 +4687,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5691,6 +5699,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6135,8 +6144,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", @@ -7426,7 +7434,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -8750,6 +8757,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -9352,7 +9360,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -9362,7 +9369,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9372,7 +9378,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -9626,7 +9631,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -9645,7 +9649,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9654,15 +9657,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -10948,6 +10949,7 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.6.tgz", "integrity": "sha512-QHl6l1cl3zPCaRMzt9TUbTX6Q5SzvkGEZDDad0DmSf5SPmT1/90k6pGPejEvDCJprkitwObXpPaTWGHItqsy4g==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -14141,8 +14143,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -14723,6 +14724,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14733,6 +14735,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16992,6 +16995,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17218,7 +17222,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -17226,6 +17231,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -17410,6 +17416,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17572,7 +17579,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -17628,6 +17634,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -17744,6 +17751,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17757,6 +17765,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -18463,6 +18472,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18894,6 +18904,7 @@ "version": "0.24.0-nightly.20251227.37be16243", "license": "Apache-2.0", "dependencies": { + "@a2a-js/sdk": "^0.3.7", "@google-cloud/logging": "^11.2.1", "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", @@ -19025,6 +19036,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/core/package.json b/packages/core/package.json index e69c6545fa..f01dfc82bc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,6 +21,7 @@ "dist" ], "dependencies": { + "@a2a-js/sdk": "^0.3.7", "@google-cloud/logging": "^11.2.1", "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 097253db65..f369e59b21 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -11,6 +11,7 @@ import type { AgentDefinition, LocalAgentDefinition } from './types.js'; import type { Config } from '../config/config.js'; import { debugLogger } from '../utils/debugLogger.js'; import { coreEvents, CoreEvent } from '../utils/events.js'; +import { A2AClientManager } from './a2a-client-manager.js'; import { DEFAULT_GEMINI_FLASH_LITE_MODEL, GEMINI_MODEL_ALIAS_AUTO, @@ -26,10 +27,16 @@ vi.mock('./toml-loader.js', () => ({ .mockResolvedValue({ agents: [], errors: [] }), })); +vi.mock('./a2a-client-manager.js', () => ({ + A2AClientManager: { + getInstance: vi.fn(), + }, +})); + // A test-only subclass to expose the protected `registerAgent` method. class TestableAgentRegistry extends AgentRegistry { - testRegisterAgent(definition: AgentDefinition): void { - this.registerAgent(definition); + async testRegisterAgent(definition: AgentDefinition): Promise { + await this.registerAgent(definition); } } @@ -237,8 +244,8 @@ describe('AgentRegistry', () => { }); describe('registration logic', () => { - it('should register a valid agent definition', () => { - registry.testRegisterAgent(MOCK_AGENT_V1); + it('should register a valid agent definition', async () => { + await registry.testRegisterAgent(MOCK_AGENT_V1); expect(registry.getDefinition('MockAgent')).toEqual(MOCK_AGENT_V1); expect( mockConfig.modelConfigService.getResolvedConfig({ @@ -257,7 +264,7 @@ describe('AgentRegistry', () => { }); }); - it('should register a remote agent definition', () => { + it('should register a remote agent definition', async () => { const remoteAgent: AgentDefinition = { kind: 'remote', name: 'RemoteAgent', @@ -265,11 +272,16 @@ describe('AgentRegistry', () => { agentCardUrl: 'https://example.com/card', inputConfig: { inputs: {} }, }; - registry.testRegisterAgent(remoteAgent); + + vi.mocked(A2AClientManager.getInstance).mockReturnValue({ + loadAgent: vi.fn().mockResolvedValue({ name: 'RemoteAgent' }), + } as unknown as A2AClientManager); + + await registry.testRegisterAgent(remoteAgent); expect(registry.getDefinition('RemoteAgent')).toEqual(remoteAgent); }); - it('should log remote agent registration in debug mode', () => { + it('should log remote agent registration in debug mode', async () => { const debugConfig = makeFakeConfig({ debugMode: true }); const debugRegistry = new TestableAgentRegistry(debugConfig); const debugLogSpy = vi @@ -284,31 +296,35 @@ describe('AgentRegistry', () => { inputConfig: { inputs: {} }, }; - debugRegistry.testRegisterAgent(remoteAgent); + vi.mocked(A2AClientManager.getInstance).mockReturnValue({ + loadAgent: vi.fn().mockResolvedValue({ name: 'RemoteAgent' }), + } as unknown as A2AClientManager); + + await debugRegistry.testRegisterAgent(remoteAgent); expect(debugLogSpy).toHaveBeenCalledWith( `[AgentRegistry] Registered remote agent 'RemoteAgent' with card: https://example.com/card`, ); }); - it('should handle special characters in agent names', () => { + it('should handle special characters in agent names', async () => { const specialAgent = { ...MOCK_AGENT_V1, name: 'Agent-123_$pecial.v2', }; - registry.testRegisterAgent(specialAgent); + await registry.testRegisterAgent(specialAgent); expect(registry.getDefinition('Agent-123_$pecial.v2')).toEqual( specialAgent, ); }); - it('should reject an agent definition missing a name', () => { + it('should reject an agent definition missing a name', async () => { const invalidAgent = { ...MOCK_AGENT_V1, name: '' }; const debugWarnSpy = vi .spyOn(debugLogger, 'warn') .mockImplementation(() => {}); - registry.testRegisterAgent(invalidAgent); + await registry.testRegisterAgent(invalidAgent); expect(registry.getDefinition('MockAgent')).toBeUndefined(); expect(debugWarnSpy).toHaveBeenCalledWith( @@ -316,13 +332,13 @@ describe('AgentRegistry', () => { ); }); - it('should reject an agent definition missing a description', () => { + it('should reject an agent definition missing a description', async () => { const invalidAgent = { ...MOCK_AGENT_V1, description: '' }; const debugWarnSpy = vi .spyOn(debugLogger, 'warn') .mockImplementation(() => {}); - registry.testRegisterAgent(invalidAgent as AgentDefinition); + await registry.testRegisterAgent(invalidAgent as AgentDefinition); expect(registry.getDefinition('MockAgent')).toBeUndefined(); expect(debugWarnSpy).toHaveBeenCalledWith( @@ -330,41 +346,41 @@ describe('AgentRegistry', () => { ); }); - it('should overwrite an existing agent definition', () => { - registry.testRegisterAgent(MOCK_AGENT_V1); + it('should overwrite an existing agent definition', async () => { + await registry.testRegisterAgent(MOCK_AGENT_V1); expect(registry.getDefinition('MockAgent')?.description).toBe( 'Mock Description V1', ); - registry.testRegisterAgent(MOCK_AGENT_V2); + await registry.testRegisterAgent(MOCK_AGENT_V2); expect(registry.getDefinition('MockAgent')?.description).toBe( 'Mock Description V2 (Updated)', ); expect(registry.getAllDefinitions()).toHaveLength(1); }); - it('should log overwrites when in debug mode', () => { + it('should log overwrites when in debug mode', async () => { const debugConfig = makeFakeConfig({ debugMode: true }); const debugRegistry = new TestableAgentRegistry(debugConfig); const debugLogSpy = vi .spyOn(debugLogger, 'log') .mockImplementation(() => {}); - debugRegistry.testRegisterAgent(MOCK_AGENT_V1); - debugRegistry.testRegisterAgent(MOCK_AGENT_V2); + await debugRegistry.testRegisterAgent(MOCK_AGENT_V1); + await debugRegistry.testRegisterAgent(MOCK_AGENT_V2); expect(debugLogSpy).toHaveBeenCalledWith( `[AgentRegistry] Overriding agent 'MockAgent'`, ); }); - it('should not log overwrites when not in debug mode', () => { + it('should not log overwrites when not in debug mode', async () => { const debugLogSpy = vi .spyOn(debugLogger, 'log') .mockImplementation(() => {}); - registry.testRegisterAgent(MOCK_AGENT_V1); - registry.testRegisterAgent(MOCK_AGENT_V2); + await registry.testRegisterAgent(MOCK_AGENT_V1); + await registry.testRegisterAgent(MOCK_AGENT_V2); expect(debugLogSpy).not.toHaveBeenCalledWith( `[AgentRegistry] Overriding agent 'MockAgent'`, @@ -373,12 +389,10 @@ describe('AgentRegistry', () => { it('should handle bulk registrations correctly', async () => { const promises = Array.from({ length: 100 }, (_, i) => - Promise.resolve( - registry.testRegisterAgent({ - ...MOCK_AGENT_V1, - name: `Agent${i}`, - }), - ), + registry.testRegisterAgent({ + ...MOCK_AGENT_V1, + name: `Agent${i}`, + }), ); await Promise.all(promises); @@ -387,7 +401,7 @@ describe('AgentRegistry', () => { }); describe('inheritance and refresh', () => { - it('should resolve "inherit" to the current model from configuration', () => { + it('should resolve "inherit" to the current model from configuration', async () => { const config = makeFakeConfig({ model: 'current-model' }); const registry = new TestableAgentRegistry(config); @@ -396,7 +410,7 @@ describe('AgentRegistry', () => { modelConfig: { ...MOCK_AGENT_V1.modelConfig, model: 'inherit' }, }; - registry.testRegisterAgent(agent); + await registry.testRegisterAgent(agent); const resolved = config.modelConfigService.getResolvedConfig({ model: getModelConfigAlias(agent), @@ -415,7 +429,7 @@ describe('AgentRegistry', () => { modelConfig: { ...MOCK_AGENT_V1.modelConfig, model: 'inherit' }, }; - registry.testRegisterAgent(agent); + await registry.testRegisterAgent(agent); // Verify initial state let resolved = config.modelConfigService.getResolvedConfig({ @@ -429,6 +443,17 @@ describe('AgentRegistry', () => { model: 'new-model', }); + // Since the listener is async but not awaited by emit, we should manually + // trigger refresh or wait. + await vi.waitFor(() => { + const resolved = config.modelConfigService.getResolvedConfig({ + model: getModelConfigAlias(agent), + }); + if (resolved.model !== 'new-model') { + throw new Error('Model not updated yet'); + } + }); + // Verify refreshed state resolved = config.modelConfigService.getResolvedConfig({ model: getModelConfigAlias(agent), @@ -443,9 +468,9 @@ describe('AgentRegistry', () => { name: 'AnotherAgent', }; - beforeEach(() => { - registry.testRegisterAgent(MOCK_AGENT_V1); - registry.testRegisterAgent(ANOTHER_AGENT); + beforeEach(async () => { + await registry.testRegisterAgent(MOCK_AGENT_V1); + await registry.testRegisterAgent(ANOTHER_AGENT); }); it('getDefinition should return the correct definition', () => { @@ -472,9 +497,9 @@ describe('AgentRegistry', () => { ); }); - it('should return formatted list of agents when agents are available', () => { - registry.testRegisterAgent(MOCK_AGENT_V1); - registry.testRegisterAgent({ + it('should return formatted list of agents when agents are available', async () => { + await registry.testRegisterAgent(MOCK_AGENT_V1); + await registry.testRegisterAgent({ ...MOCK_AGENT_V2, name: 'AnotherAgent', description: 'Another agent description', diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 72aac95077..38b28ffcc7 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -11,6 +11,7 @@ import type { AgentDefinition } from './types.js'; import { loadAgentsFromDirectory } from './toml-loader.js'; import { CodebaseInvestigatorAgent } from './codebase-investigator.js'; import { IntrospectionAgent } from './introspection-agent.js'; +import { A2AClientManager } from './a2a-client-manager.js'; import { type z } from 'zod'; import { debugLogger } from '../utils/debugLogger.js'; import { @@ -47,7 +48,12 @@ export class AgentRegistry { this.loadBuiltInAgents(); coreEvents.on(CoreEvent.ModelChanged, () => { - this.refreshAgents(); + this.refreshAgents().catch((e) => { + debugLogger.error( + '[AgentRegistry] Failed to refresh agents on model change:', + e, + ); + }); }); if (!this.config.isAgentsEnabled()) { @@ -63,9 +69,9 @@ export class AgentRegistry { ); coreEvents.emitFeedback('error', `Agent loading error: ${error.message}`); } - for (const agent of userAgents.agents) { - this.registerAgent(agent); - } + await Promise.allSettled( + userAgents.agents.map((agent) => this.registerAgent(agent)), + ); // Load project-level agents: .gemini/agents/ (relative to Project Root) const folderTrustEnabled = this.config.getFolderTrust(); @@ -80,9 +86,9 @@ export class AgentRegistry { `Agent loading error: ${error.message}`, ); } - for (const agent of projectAgents.agents) { - this.registerAgent(agent); - } + await Promise.allSettled( + projectAgents.agents.map((agent) => this.registerAgent(agent)), + ); } else { coreEvents.emitFeedback( 'info', @@ -135,20 +141,22 @@ export class AgentRegistry { CodebaseInvestigatorAgent.runConfig.max_turns, }, }; - this.registerAgent(agentDef); + this.registerLocalAgent(agentDef); } // Register the introspection agent if it's explicitly enabled. if (introspectionSettings.enabled) { - this.registerAgent(IntrospectionAgent); + this.registerLocalAgent(IntrospectionAgent); } } - private refreshAgents(): void { + private async refreshAgents(): Promise { this.loadBuiltInAgents(); - for (const agent of this.agents.values()) { - this.registerAgent(agent); - } + await Promise.allSettled( + Array.from(this.agents.values()).map((agent) => + this.registerAgent(agent), + ), + ); } /** @@ -156,9 +164,26 @@ export class AgentRegistry { * it will be overwritten, respecting the precedence established by the * initialization order. */ - protected registerAgent( + protected async registerAgent( + definition: AgentDefinition, + ): Promise { + if (definition.kind === 'local') { + this.registerLocalAgent(definition); + } else if (definition.kind === 'remote') { + await this.registerRemoteAgent(definition); + } + } + + /** + * Registers a local agent definition synchronously. + */ + protected registerLocalAgent( definition: AgentDefinition, ): void { + if (definition.kind !== 'local') { + return; + } + // Basic validation if (!definition.name || !definition.description) { debugLogger.warn( @@ -175,37 +200,79 @@ export class AgentRegistry { // Register model config. // TODO(12916): Migrate sub-agents where possible to static configs. - if (definition.kind === 'local') { - const modelConfig = definition.modelConfig; - let model = modelConfig.model; - if (model === 'inherit') { - model = this.config.getModel(); - } + const modelConfig = definition.modelConfig; + let model = modelConfig.model; + if (model === 'inherit') { + model = this.config.getModel(); + } - const runtimeAlias: ModelConfigAlias = { - modelConfig: { - model, - generateContentConfig: { - temperature: modelConfig.temp, - topP: modelConfig.top_p, - thinkingConfig: { - includeThoughts: true, - thinkingBudget: modelConfig.thinkingBudget ?? -1, - }, + const runtimeAlias: ModelConfigAlias = { + modelConfig: { + model, + generateContentConfig: { + temperature: modelConfig.temp, + topP: modelConfig.top_p, + thinkingConfig: { + includeThoughts: true, + thinkingBudget: modelConfig.thinkingBudget ?? -1, }, }, - }; + }, + }; - this.config.modelConfigService.registerRuntimeModelConfig( - getModelConfigAlias(definition), - runtimeAlias, + this.config.modelConfigService.registerRuntimeModelConfig( + getModelConfigAlias(definition), + runtimeAlias, + ); + } + + /** + * Registers a remote agent definition asynchronously. + */ + protected async registerRemoteAgent( + definition: AgentDefinition, + ): Promise { + if (definition.kind !== 'remote') { + return; + } + + // Basic validation + if (!definition.name || !definition.description) { + debugLogger.warn( + `[AgentRegistry] Skipping invalid agent definition. Missing name or description.`, ); + return; + } + + if (this.agents.has(definition.name) && this.config.getDebugMode()) { + debugLogger.log(`[AgentRegistry] Overriding agent '${definition.name}'`); } // Log remote A2A agent registration for visibility. - if (definition.kind === 'remote' && this.config.getDebugMode()) { - debugLogger.log( - `[AgentRegistry] Registered remote agent '${definition.name}' with card: ${definition.agentCardUrl}`, + try { + const clientManager = A2AClientManager.getInstance(); + const agentCard = await clientManager.loadAgent( + definition.name, + definition.agentCardUrl, + ); + if (agentCard.skills && agentCard.skills.length > 0) { + definition.description = agentCard.skills + .map( + (skill: { name: string; description: string }) => + `${skill.name}: ${skill.description}`, + ) + .join('\n'); + } + if (this.config.getDebugMode()) { + debugLogger.log( + `[AgentRegistry] Registered remote agent '${definition.name}' with card: ${definition.agentCardUrl}`, + ); + } + this.agents.set(definition.name, definition); + } catch (e) { + debugLogger.warn( + `[AgentRegistry] Error loading A2A agent "${definition.name}":`, + e, ); } }