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

View File

@@ -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.',
});
});
});

View File

@@ -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),
};

View File

@@ -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', () => {

View File

@@ -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.

View File

@@ -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' });

View File

@@ -46,8 +46,6 @@ export class AgentRegistry {
* Discovers and loads agents.
*/
async initialize(): Promise<void> {
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<void> {
A2AClientManager.getInstance().clearCache();
this.agents.clear();
await this.loadAgents();
coreEvents.emitAgentsRefreshed();
}
private async loadAgents(): Promise<void> {
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.`,
);
}
}

View File

@@ -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<CoreEvents> {
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.