feat(cli): improve /agents refresh logging (#26442)

This commit is contained in:
Coco Sheng
2026-05-04 15:40:48 -04:00
committed by GitHub
parent 78877942ec
commit 0d6bd29752
5 changed files with 190 additions and 69 deletions
+1 -2
View File
@@ -459,7 +459,7 @@ describe('AgentRegistry', () => {
await registry.initialize();
// Verify ackService was called with the URL, not the file hash
// Verify ackService was called with the raw URL to avoid breaking changes
expect(ackService.isAcknowledged).toHaveBeenCalledWith(
expect.anything(),
'RemoteAgent',
@@ -467,7 +467,6 @@ describe('AgentRegistry', () => {
);
// Also verify that the agent's metadata was updated to use the URL as hash
// Use getDefinition because registerAgent might have been called
expect(registry.getDefinition('RemoteAgent')?.metadata?.hash).toBe(
'https://example.com/card',
);
+105 -62
View File
@@ -8,7 +8,11 @@ import * as crypto from 'node:crypto';
import { Storage } from '../config/storage.js';
import { CoreEvent, coreEvents } from '../utils/events.js';
import type { AgentOverride, Config } from '../config/config.js';
import type { AgentDefinition, LocalAgentDefinition } from './types.js';
import {
type AgentDefinition,
type LocalAgentDefinition,
type AgentReloadSummary,
} from './types.js';
import { getAgentCardLoadOptions, getRemoteAgentTargetUrl } from './types.js';
import { loadAgentsFromDirectory } from './agentLoader.js';
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
@@ -80,13 +84,53 @@ export class AgentRegistry {
/**
* Clears the current registry and re-scans for agents.
*/
async reload(): Promise<void> {
async reload(): Promise<AgentReloadSummary> {
const previousAgents = new Map(this.agents);
const reloadErrors: string[] = [];
this.config.getA2AClientManager()?.clearCache();
await this.config.reloadAgents();
this.agents.clear();
this.allDefinitions.clear();
await this.loadAgents();
await this.loadAgents(reloadErrors);
const currentAgents = Array.from(this.agents.values());
const newAgents: string[] = [];
const updatedAgents: string[] = [];
const deletedAgents: string[] = [];
let localCount = 0;
let remoteCount = 0;
for (const agent of currentAgents) {
if (agent.kind === 'local') {
localCount++;
} else if (agent.kind === 'remote') {
remoteCount++;
}
const prev = previousAgents.get(agent.name);
if (!prev) {
newAgents.push(agent.name);
} else if (agent.metadata?.hash !== prev.metadata?.hash) {
updatedAgents.push(agent.name);
}
}
for (const prevName of previousAgents.keys()) {
if (!this.agents.has(prevName)) {
deletedAgents.push(prevName);
}
}
coreEvents.emitAgentsRefreshed();
return {
totalLoaded: currentAgents.length,
localCount,
remoteCount,
newAgents,
updatedAgents,
deletedAgents,
errors: reloadErrors,
};
}
/**
@@ -113,7 +157,7 @@ export class AgentRegistry {
coreEvents.off(CoreEvent.ModelChanged, this.onModelChanged);
}
private async loadAgents(): Promise<void> {
private async loadAgents(errors?: string[]): Promise<void> {
this.agents.clear();
this.allDefinitions.clear();
this.loadBuiltInAgents();
@@ -132,21 +176,20 @@ export class AgentRegistry {
debugLogger.warn(
`[AgentRegistry] Error loading user agent: ${error.message}`,
);
coreEvents.emitFeedback('error', `Agent loading error: ${error.message}`);
const msg = `Agent loading error: ${error.message}`;
errors?.push(msg);
coreEvents.emitFeedback('error', msg);
}
await Promise.allSettled(
userAgents.agents.map(async (agent) => {
try {
await this.registerAgent(agent);
this.ensureRemoteAgentHash(agent);
await this.registerAgent(agent, errors);
} catch (e) {
debugLogger.warn(
`[AgentRegistry] Error registering user agent "${agent.name}":`,
e,
);
coreEvents.emitFeedback(
'error',
`Error registering user agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`,
);
const msg = `Error registering user agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`;
debugLogger.warn(`[AgentRegistry] ${msg}`, e);
errors?.push(msg);
coreEvents.emitFeedback('error', msg);
}
}),
);
@@ -159,10 +202,9 @@ export class AgentRegistry {
const projectAgentsDir = this.config.storage.getProjectAgentsDir();
const projectAgents = await loadAgentsFromDirectory(projectAgentsDir);
for (const error of projectAgents.errors) {
coreEvents.emitFeedback(
'error',
`Agent loading error: ${error.message}`,
);
const msg = `Agent loading error: ${error.message}`;
errors?.push(msg);
coreEvents.emitFeedback('error', msg);
}
const ackService = this.config.getAcknowledgedAgentsService();
@@ -171,21 +213,7 @@ export class AgentRegistry {
const agentsToRegister: AgentDefinition[] = [];
for (const agent of projectAgents.agents) {
// If it's a remote agent, use the agentCardUrl as the hash.
// This allows multiple remote agents in a single file to be tracked independently.
if (agent.kind === 'remote') {
if (!agent.metadata) {
agent.metadata = {};
}
agent.metadata.hash =
agent.agentCardUrl ??
(agent.agentCardJson
? crypto
.createHash('sha256')
.update(agent.agentCardJson)
.digest('hex')
: undefined);
}
this.ensureRemoteAgentHash(agent);
if (!agent.metadata?.hash) {
agentsToRegister.push(agent);
@@ -212,16 +240,12 @@ export class AgentRegistry {
await Promise.allSettled(
agentsToRegister.map(async (agent) => {
try {
await this.registerAgent(agent);
await this.registerAgent(agent, errors);
} catch (e) {
debugLogger.warn(
`[AgentRegistry] Error registering project agent "${agent.name}":`,
e,
);
coreEvents.emitFeedback(
'error',
`Error registering project agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`,
);
const msg = `Error registering project agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`;
debugLogger.warn(`[AgentRegistry] ${msg}`, e);
errors?.push(msg);
coreEvents.emitFeedback('error', msg);
}
}),
);
@@ -238,16 +262,12 @@ export class AgentRegistry {
await Promise.allSettled(
extension.agents.map(async (agent) => {
try {
await this.registerAgent(agent);
await this.registerAgent(agent, errors);
} catch (e) {
debugLogger.warn(
`[AgentRegistry] Error registering extension agent "${agent.name}":`,
e,
);
coreEvents.emitFeedback(
'error',
`Error registering extension agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`,
);
const msg = `Error registering extension agent "${agent.name}": ${e instanceof Error ? e.message : String(e)}`;
debugLogger.warn(`[AgentRegistry] ${msg}`, e);
errors?.push(msg);
coreEvents.emitFeedback('error', msg);
}
}),
);
@@ -314,11 +334,12 @@ export class AgentRegistry {
*/
protected async registerAgent<TOutput extends z.ZodTypeAny>(
definition: AgentDefinition<TOutput>,
errors?: string[],
): Promise<void> {
if (definition.kind === 'local') {
this.registerLocalAgent(definition);
} else if (definition.kind === 'remote') {
await this.registerRemoteAgent(definition);
await this.registerRemoteAgent(definition, errors);
}
}
@@ -416,6 +437,7 @@ export class AgentRegistry {
*/
protected async registerRemoteAgent<TOutput extends z.ZodTypeAny>(
definition: AgentDefinition<TOutput>,
errors?: string[],
): Promise<void> {
if (definition.kind !== 'remote') {
return;
@@ -544,17 +566,14 @@ export class AgentRegistry {
this.addAgentPolicy(definition);
} catch (e) {
// Surface structured, user-friendly error messages for known failure modes.
let msg: string;
if (e instanceof A2AAgentError) {
coreEvents.emitFeedback(
'error',
`[${definition.name}] ${e.userMessage}`,
);
msg = `[${definition.name}] ${e.userMessage}`;
} else {
coreEvents.emitFeedback(
'error',
`[${definition.name}] Failed to load remote agent: ${e instanceof Error ? e.message : String(e)}`,
);
msg = `[${definition.name}] Failed to load remote agent: ${e instanceof Error ? e.message : String(e)}`;
}
errors?.push(msg);
coreEvents.emitFeedback('error', msg);
debugLogger.warn(
`[AgentRegistry] Error loading A2A agent "${definition.name}":`,
e,
@@ -704,4 +723,28 @@ export class AgentRegistry {
getDiscoveredDefinition(name: string): AgentDefinition | undefined {
return this.allDefinitions.get(name);
}
/**
* Ensures that remote agents have a content-based hash for trust verification and change detection.
*/
private ensureRemoteAgentHash(agent: AgentDefinition): void {
if (agent.kind !== 'remote') {
return;
}
if (!agent.metadata) {
agent.metadata = {};
}
// To avoid a breaking change for existing users, we continue to use
// the raw URL as the hash for URL-based remote agents.
if (agent.agentCardUrl) {
agent.metadata.hash = agent.agentCardUrl;
} else if (agent.agentCardJson) {
agent.metadata.hash = crypto
.createHash('sha256')
.update(agent.agentCardJson)
.digest('hex');
}
}
}
+13
View File
@@ -369,3 +369,16 @@ export interface RunConfig {
*/
maxTurns?: number;
}
/**
* Summary of an agent reload operation.
*/
export interface AgentReloadSummary {
totalLoaded: number;
localCount: number;
remoteCount: number;
newAgents: string[];
updatedAgents: string[];
deletedAgents: string[];
errors: string[];
}