Merge User and Agent Card Descriptions #20849 (#20850)

This commit is contained in:
Adam Weidman
2026-03-02 12:59:29 -05:00
committed by GitHub
parent 703759cfae
commit 740efa2ac2
4 changed files with 209 additions and 7 deletions

View File

@@ -417,7 +417,7 @@ export function markdownToAgentDefinition(
return {
kind: 'remote',
name: markdown.name,
description: markdown.description || '(Loading description...)',
description: markdown.description || '',
displayName: markdown.display_name,
agentCardUrl: markdown.agent_card_url,
auth: markdown.auth

View File

@@ -572,6 +572,181 @@ describe('AgentRegistry', () => {
);
});
it('should merge user and agent description and skills when registering a remote agent', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'RemoteAgentWithDescription',
description: 'User-provided description',
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
const mockAgentCard = {
name: 'RemoteAgentWithDescription',
description: 'Card-provided description',
skills: [
{ name: 'Skill1', description: 'Desc1' },
{ name: 'Skill2', description: 'Desc2' },
],
};
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
loadAgent: vi.fn().mockResolvedValue(mockAgentCard),
clearCache: vi.fn(),
} as unknown as A2AClientManager);
await registry.testRegisterAgent(remoteAgent);
const registered = registry.getDefinition('RemoteAgentWithDescription');
expect(registered?.description).toBe(
'User Description: User-provided description\nAgent Description: Card-provided description\nSkills:\nSkill1: Desc1\nSkill2: Desc2',
);
});
it('should include skills when agent description is empty', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'RemoteAgentWithSkillsOnly',
description: 'User-provided description',
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
const mockAgentCard = {
name: 'RemoteAgentWithSkillsOnly',
description: '',
skills: [{ name: 'Skill1', description: 'Desc1' }],
};
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
loadAgent: vi.fn().mockResolvedValue(mockAgentCard),
clearCache: vi.fn(),
} as unknown as A2AClientManager);
await registry.testRegisterAgent(remoteAgent);
const registered = registry.getDefinition('RemoteAgentWithSkillsOnly');
expect(registered?.description).toBe(
'User Description: User-provided description\nSkills:\nSkill1: Desc1',
);
});
it('should handle empty user or agent descriptions and no skills during merging', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'RemoteAgentWithEmptyAgentDescription',
description: 'User-provided description',
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
const mockAgentCard = {
name: 'RemoteAgentWithEmptyAgentDescription',
description: '', // Empty agent description
skills: [],
};
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
loadAgent: vi.fn().mockResolvedValue(mockAgentCard),
clearCache: vi.fn(),
} as unknown as A2AClientManager);
await registry.testRegisterAgent(remoteAgent);
const registered = registry.getDefinition(
'RemoteAgentWithEmptyAgentDescription',
);
// Should only contain user description
expect(registered?.description).toBe(
'User Description: User-provided description',
);
});
it('should not accumulate descriptions on repeated registration', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'RemoteAgentAccumulationTest',
description: 'User-provided description',
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
const mockAgentCard = {
name: 'RemoteAgentAccumulationTest',
description: 'Card-provided description',
skills: [{ name: 'Skill1', description: 'Desc1' }],
};
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
loadAgent: vi.fn().mockResolvedValue(mockAgentCard),
clearCache: vi.fn(),
} as unknown as A2AClientManager);
// Register first time
await registry.testRegisterAgent(remoteAgent);
let registered = registry.getDefinition('RemoteAgentAccumulationTest');
const firstDescription = registered?.description;
expect(firstDescription).toBe(
'User Description: User-provided description\nAgent Description: Card-provided description\nSkills:\nSkill1: Desc1',
);
// Register second time with the SAME object
await registry.testRegisterAgent(remoteAgent);
registered = registry.getDefinition('RemoteAgentAccumulationTest');
expect(registered?.description).toBe(firstDescription);
});
it('should allow registering a remote agent with an empty initial description', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'EmptyDescAgent',
description: '', // Empty initial description
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
loadAgent: vi.fn().mockResolvedValue({
name: 'EmptyDescAgent',
description: 'Loaded from card',
}),
clearCache: vi.fn(),
} as unknown as A2AClientManager);
await registry.testRegisterAgent(remoteAgent);
const registered = registry.getDefinition('EmptyDescAgent');
expect(registered?.description).toBe(
'Agent Description: Loaded from card',
);
});
it('should provide fallback for skill descriptions if missing in the card', async () => {
const remoteAgent: AgentDefinition = {
kind: 'remote',
name: 'SkillFallbackAgent',
description: 'User description',
agentCardUrl: 'https://example.com/card',
inputConfig: { inputSchema: { type: 'object' } },
};
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
loadAgent: vi.fn().mockResolvedValue({
name: 'SkillFallbackAgent',
description: 'Card description',
skills: [{ name: 'SkillNoDesc' }], // Missing description
}),
clearCache: vi.fn(),
} as unknown as A2AClientManager);
await registry.testRegisterAgent(remoteAgent);
const registered = registry.getDefinition('SkillFallbackAgent');
expect(registered?.description).toContain(
'SkillNoDesc: No description provided',
);
});
it('should handle special characters in agent names', async () => {
const specialAgent = {
...MOCK_AGENT_V1,

View File

@@ -335,9 +335,10 @@ export class AgentRegistry {
}
// Basic validation
if (!definition.name || !definition.description) {
// Remote agents can have an empty description initially as it will be populated from the AgentCard
if (!definition.name) {
debugLogger.warn(
`[AgentRegistry] Skipping invalid agent definition. Missing name or description.`,
`[AgentRegistry] Skipping invalid agent definition. Missing name.`,
);
return;
}
@@ -360,24 +361,48 @@ export class AgentRegistry {
debugLogger.log(`[AgentRegistry] Overriding agent '${definition.name}'`);
}
const remoteDef = definition;
// Capture the original description from the first registration
if (remoteDef.originalDescription === undefined) {
remoteDef.originalDescription = remoteDef.description;
}
// Log remote A2A agent registration for visibility.
try {
const clientManager = A2AClientManager.getInstance();
// Use ADCHandler to ensure we can load agents hosted on secure platforms (e.g. Vertex AI)
const authHandler = new ADCHandler();
const agentCard = await clientManager.loadAgent(
definition.name,
definition.agentCardUrl,
remoteDef.name,
remoteDef.agentCardUrl,
authHandler,
);
const userDescription = remoteDef.originalDescription;
const agentDescription = agentCard.description;
const descriptions: string[] = [];
if (userDescription?.trim()) {
descriptions.push(`User Description: ${userDescription.trim()}`);
}
if (agentDescription?.trim()) {
descriptions.push(`Agent Description: ${agentDescription.trim()}`);
}
if (agentCard.skills && agentCard.skills.length > 0) {
definition.description = agentCard.skills
const skillsList = agentCard.skills
.map(
(skill: { name: string; description: string }) =>
`${skill.name}: ${skill.description}`,
`${skill.name}: ${skill.description || 'No description provided'}`,
)
.join('\n');
descriptions.push(`Skills:\n${skillsList}`);
}
if (descriptions.length > 0) {
definition.description = descriptions.join('\n');
}
if (this.config.getDebugMode()) {
debugLogger.log(
`[AgentRegistry] Registered remote agent '${definition.name}' with card: ${definition.agentCardUrl}`,

View File

@@ -119,6 +119,8 @@ export interface RemoteAgentDefinition<
> extends BaseAgentDefinition<TOutput> {
kind: 'remote';
agentCardUrl: string;
/** The user-provided description, before any remote card merging. */
originalDescription?: string;
/**
* Optional authentication configuration for the remote agent.
* If not specified, the agent will try to use defaults based on the AgentCard's