mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
feat(cli): improve /agents refresh logging (#26442)
This commit is contained in:
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user