feat(core, ui): Add /agents refresh command. (#16204)

This commit is contained in:
joshualitt
2026-01-09 09:33:59 -08:00
committed by GitHub
parent c1401682ed
commit 041463d112
7 changed files with 154 additions and 6 deletions
@@ -82,4 +82,40 @@ describe('agentsCommand', () => {
expect.any(Number), 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.',
});
});
}); });
+37 -2
View File
@@ -8,8 +8,8 @@ import type { SlashCommand, CommandContext } from './types.js';
import { CommandKind } from './types.js'; import { CommandKind } from './types.js';
import { MessageType, type HistoryItemAgentsList } from '../types.js'; import { MessageType, type HistoryItemAgentsList } from '../types.js';
export const agentsCommand: SlashCommand = { const agentsListCommand: SlashCommand = {
name: 'agents', name: 'list',
description: 'List available local and remote agents', description: 'List available local and remote agents',
kind: CommandKind.BUILT_IN, kind: CommandKind.BUILT_IN,
autoExecute: true, autoExecute: true,
@@ -49,3 +49,38 @@ export const agentsCommand: SlashCommand = {
return; 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),
};
@@ -162,6 +162,20 @@ describe('A2AClientManager', () => {
"[A2AClientManager] Loaded agent 'TestAgent' from http://test.agent/card", "[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', () => { describe('sendMessage', () => {
@@ -104,6 +104,15 @@ export class A2AClientManager {
return agentCard; 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. * Sends a message to a loaded agent.
* @param agentName The name of the agent to send the message to. * @param agentName The name of the agent to send the message to.
+32 -1
View File
@@ -99,7 +99,7 @@ describe('AgentRegistry', () => {
const agentCount = debugRegistry.getAllDefinitions().length; const agentCount = debugRegistry.getAllDefinitions().length;
expect(debugLogSpy).toHaveBeenCalledWith( 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', () => { describe('inheritance and refresh', () => {
it('should resolve "inherit" to the current model from configuration', async () => { it('should resolve "inherit" to the current model from configuration', async () => {
const config = makeFakeConfig({ model: 'current-model' }); const config = makeFakeConfig({ model: 'current-model' });
+17 -3
View File
@@ -46,8 +46,6 @@ export class AgentRegistry {
* Discovers and loads agents. * Discovers and loads agents.
*/ */
async initialize(): Promise<void> { async initialize(): Promise<void> {
this.loadBuiltInAgents();
coreEvents.on(CoreEvent.ModelChanged, () => { coreEvents.on(CoreEvent.ModelChanged, () => {
this.refreshAgents().catch((e) => { this.refreshAgents().catch((e) => {
debugLogger.error( debugLogger.error(
@@ -57,6 +55,22 @@ export class AgentRegistry {
}); });
}); });
await this.loadAgents();
}
/**
* Clears the current registry and re-scans for agents.
*/
async reload(): Promise<void> {
A2AClientManager.getInstance().clearCache();
this.agents.clear();
await this.loadAgents();
coreEvents.emitAgentsRefreshed();
}
private async loadAgents(): Promise<void> {
this.loadBuiltInAgents();
if (!this.config.isAgentsEnabled()) { if (!this.config.isAgentsEnabled()) {
return; return;
} }
@@ -99,7 +113,7 @@ export class AgentRegistry {
if (this.config.getDebugMode()) { if (this.config.getDebugMode()) {
debugLogger.log( debugLogger.log(
`[AgentRegistry] Initialized with ${this.agents.size} agents.`, `[AgentRegistry] Loaded with ${this.agents.size} agents.`,
); );
} }
} }
+9
View File
@@ -107,6 +107,7 @@ export enum CoreEvent {
SettingsChanged = 'settings-changed', SettingsChanged = 'settings-changed',
HookStart = 'hook-start', HookStart = 'hook-start',
HookEnd = 'hook-end', HookEnd = 'hook-end',
AgentsRefreshed = 'agents-refreshed',
} }
export interface CoreEvents { export interface CoreEvents {
@@ -119,6 +120,7 @@ export interface CoreEvents {
[CoreEvent.SettingsChanged]: never[]; [CoreEvent.SettingsChanged]: never[];
[CoreEvent.HookStart]: [HookStartPayload]; [CoreEvent.HookStart]: [HookStartPayload];
[CoreEvent.HookEnd]: [HookEndPayload]; [CoreEvent.HookEnd]: [HookEndPayload];
[CoreEvent.AgentsRefreshed]: never[];
} }
type EventBacklogItem = { type EventBacklogItem = {
@@ -220,6 +222,13 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
this.emit(CoreEvent.HookEnd, payload); 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 * Flushes buffered messages. Call this immediately after primary UI listener
* subscribes. * subscribes.