From 740efa2ac26794bbd5f35ac76f6a685888292b66 Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:59:29 -0500 Subject: [PATCH] Merge User and Agent Card Descriptions #20849 (#20850) --- packages/core/src/agents/agentLoader.ts | 2 +- packages/core/src/agents/registry.test.ts | 175 ++++++++++++++++++++++ packages/core/src/agents/registry.ts | 37 ++++- packages/core/src/agents/types.ts | 2 + 4 files changed, 209 insertions(+), 7 deletions(-) diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index bdc59de746..226c133461 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -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 diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index c5f2faa06f..b7977f37bd 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -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, diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 41483c9c21..cf1d95a834 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -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}`, diff --git a/packages/core/src/agents/types.ts b/packages/core/src/agents/types.ts index b9994d8b4a..3704746810 100644 --- a/packages/core/src/agents/types.ts +++ b/packages/core/src/agents/types.ts @@ -119,6 +119,8 @@ export interface RemoteAgentDefinition< > extends BaseAgentDefinition { 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