diff --git a/packages/cli/src/ui/commands/agentsCommand.test.ts b/packages/cli/src/ui/commands/agentsCommand.test.ts index 1e871bae6a..5c1fe5892d 100644 --- a/packages/cli/src/ui/commands/agentsCommand.test.ts +++ b/packages/cli/src/ui/commands/agentsCommand.test.ts @@ -82,4 +82,40 @@ describe('agentsCommand', () => { expect.any(Number), ); }); + + it('should reload the agent registry when refresh subcommand is called', async () => { + const reloadSpy = vi.fn().mockResolvedValue(undefined); + mockConfig.getAgentRegistry = vi.fn().mockReturnValue({ + reload: reloadSpy, + }); + + const refreshCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'refresh', + ); + expect(refreshCommand).toBeDefined(); + + const result = await refreshCommand!.action!(mockContext, ''); + + expect(reloadSpy).toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: 'Agents refreshed successfully.', + }); + }); + + it('should show an error if agent registry is not available during refresh', async () => { + mockConfig.getAgentRegistry = vi.fn().mockReturnValue(undefined); + + const refreshCommand = agentsCommand.subCommands?.find( + (cmd) => cmd.name === 'refresh', + ); + const result = await refreshCommand!.action!(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Agent registry not found.', + }); + }); }); diff --git a/packages/cli/src/ui/commands/agentsCommand.ts b/packages/cli/src/ui/commands/agentsCommand.ts index 516c326662..d904e8ca78 100644 --- a/packages/cli/src/ui/commands/agentsCommand.ts +++ b/packages/cli/src/ui/commands/agentsCommand.ts @@ -8,8 +8,8 @@ import type { SlashCommand, CommandContext } from './types.js'; import { CommandKind } from './types.js'; import { MessageType, type HistoryItemAgentsList } from '../types.js'; -export const agentsCommand: SlashCommand = { - name: 'agents', +const agentsListCommand: SlashCommand = { + name: 'list', description: 'List available local and remote agents', kind: CommandKind.BUILT_IN, autoExecute: true, @@ -49,3 +49,38 @@ export const agentsCommand: SlashCommand = { return; }, }; + +const agentsRefreshCommand: SlashCommand = { + name: 'refresh', + description: 'Reload the agent registry', + kind: CommandKind.BUILT_IN, + action: async (context: CommandContext) => { + const { config } = context.services; + const agentRegistry = config?.getAgentRegistry(); + if (!agentRegistry) { + return { + type: 'message', + messageType: 'error', + content: 'Agent registry not found.', + }; + } + + await agentRegistry.reload(); + + return { + type: 'message', + messageType: 'info', + content: 'Agents refreshed successfully.', + }; + }, +}; + +export const agentsCommand: SlashCommand = { + name: 'agents', + description: 'Manage agents', + kind: CommandKind.BUILT_IN, + subCommands: [agentsListCommand, agentsRefreshCommand], + action: async (context: CommandContext, args) => + // Default to list if no subcommand is provided + agentsListCommand.action!(context, args), +}; diff --git a/packages/core/src/agents/a2a-client-manager.test.ts b/packages/core/src/agents/a2a-client-manager.test.ts index 4406cac966..6d6561c963 100644 --- a/packages/core/src/agents/a2a-client-manager.test.ts +++ b/packages/core/src/agents/a2a-client-manager.test.ts @@ -162,6 +162,20 @@ describe('A2AClientManager', () => { "[A2AClientManager] Loaded agent 'TestAgent' from http://test.agent/card", ); }); + + it('should clear the cache', async () => { + await manager.loadAgent('TestAgent', 'http://test.agent/card'); + expect(manager.getAgentCard('TestAgent')).toBeDefined(); + expect(manager.getClient('TestAgent')).toBeDefined(); + + manager.clearCache(); + + expect(manager.getAgentCard('TestAgent')).toBeUndefined(); + expect(manager.getClient('TestAgent')).toBeUndefined(); + expect(debugLogger.debug).toHaveBeenCalledWith( + '[A2AClientManager] Cache cleared.', + ); + }); }); describe('sendMessage', () => { diff --git a/packages/core/src/agents/a2a-client-manager.ts b/packages/core/src/agents/a2a-client-manager.ts index c00fdfee43..ff379f1719 100644 --- a/packages/core/src/agents/a2a-client-manager.ts +++ b/packages/core/src/agents/a2a-client-manager.ts @@ -104,6 +104,15 @@ export class A2AClientManager { return agentCard; } + /** + * Invalidates all cached clients and agent cards. + */ + clearCache(): void { + this.clients.clear(); + this.agentCards.clear(); + debugLogger.debug('[A2AClientManager] Cache cleared.'); + } + /** * Sends a message to a loaded agent. * @param agentName The name of the agent to send the message to. diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 84a9001a03..073dd5ac10 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -99,7 +99,7 @@ describe('AgentRegistry', () => { const agentCount = debugRegistry.getAllDefinitions().length; expect(debugLogSpy).toHaveBeenCalledWith( - `[AgentRegistry] Initialized with ${agentCount} agents.`, + `[AgentRegistry] Loaded with ${agentCount} agents.`, ); }); @@ -444,6 +444,37 @@ describe('AgentRegistry', () => { }); }); + describe('reload', () => { + it('should clear existing agents and reload from directories', async () => { + const config = makeFakeConfig({ enableAgents: true }); + const registry = new TestableAgentRegistry(config); + + const initialAgent = { ...MOCK_AGENT_V1, name: 'InitialAgent' }; + await registry.testRegisterAgent(initialAgent); + expect(registry.getDefinition('InitialAgent')).toBeDefined(); + + const newAgent = { ...MOCK_AGENT_V1, name: 'NewAgent' }; + vi.mocked(tomlLoader.loadAgentsFromDirectory).mockResolvedValue({ + agents: [newAgent], + errors: [], + }); + + const clearCacheSpy = vi.fn(); + vi.mocked(A2AClientManager.getInstance).mockReturnValue({ + clearCache: clearCacheSpy, + } as unknown as A2AClientManager); + + const emitSpy = vi.spyOn(coreEvents, 'emitAgentsRefreshed'); + + await registry.reload(); + + expect(clearCacheSpy).toHaveBeenCalled(); + expect(registry.getDefinition('InitialAgent')).toBeUndefined(); + expect(registry.getDefinition('NewAgent')).toBeDefined(); + expect(emitSpy).toHaveBeenCalled(); + }); + }); + describe('inheritance and refresh', () => { it('should resolve "inherit" to the current model from configuration', async () => { const config = makeFakeConfig({ model: 'current-model' }); diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index ee42795a66..8a35a70241 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -46,8 +46,6 @@ export class AgentRegistry { * Discovers and loads agents. */ async initialize(): Promise { - this.loadBuiltInAgents(); - coreEvents.on(CoreEvent.ModelChanged, () => { this.refreshAgents().catch((e) => { debugLogger.error( @@ -57,6 +55,22 @@ export class AgentRegistry { }); }); + await this.loadAgents(); + } + + /** + * Clears the current registry and re-scans for agents. + */ + async reload(): Promise { + A2AClientManager.getInstance().clearCache(); + this.agents.clear(); + await this.loadAgents(); + coreEvents.emitAgentsRefreshed(); + } + + private async loadAgents(): Promise { + this.loadBuiltInAgents(); + if (!this.config.isAgentsEnabled()) { return; } @@ -99,7 +113,7 @@ export class AgentRegistry { if (this.config.getDebugMode()) { debugLogger.log( - `[AgentRegistry] Initialized with ${this.agents.size} agents.`, + `[AgentRegistry] Loaded with ${this.agents.size} agents.`, ); } } diff --git a/packages/core/src/utils/events.ts b/packages/core/src/utils/events.ts index 84058f9d99..89dd02395f 100644 --- a/packages/core/src/utils/events.ts +++ b/packages/core/src/utils/events.ts @@ -107,6 +107,7 @@ export enum CoreEvent { SettingsChanged = 'settings-changed', HookStart = 'hook-start', HookEnd = 'hook-end', + AgentsRefreshed = 'agents-refreshed', } export interface CoreEvents { @@ -119,6 +120,7 @@ export interface CoreEvents { [CoreEvent.SettingsChanged]: never[]; [CoreEvent.HookStart]: [HookStartPayload]; [CoreEvent.HookEnd]: [HookEndPayload]; + [CoreEvent.AgentsRefreshed]: never[]; } type EventBacklogItem = { @@ -220,6 +222,13 @@ export class CoreEventEmitter extends EventEmitter { this.emit(CoreEvent.HookEnd, payload); } + /** + * Notifies subscribers that agents have been refreshed. + */ + emitAgentsRefreshed(): void { + this.emit(CoreEvent.AgentsRefreshed); + } + /** * Flushes buffered messages. Call this immediately after primary UI listener * subscribes.