Skip to content

Commit 1469329

Browse files
authored
chore: a2a integration: add agent card. (#187)
1 parent c52e77f commit 1469329

3 files changed

Lines changed: 605 additions & 1 deletion

File tree

core/src/a2a/agent_card.ts

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {AgentCard, AgentInterface, AgentSkill} from '@a2a-js/sdk';
8+
import {BaseAgent} from '../agents/base_agent.js';
9+
import {
10+
InvocationContext,
11+
InvocationContextParams,
12+
} from '../agents/invocation_context.js';
13+
import {isLlmAgent, LlmAgent} from '../agents/llm_agent.js';
14+
import {isLoopAgent, LoopAgent} from '../agents/loop_agent.js';
15+
import {isParallelAgent} from '../agents/parallel_agent.js';
16+
import {ReadonlyContext} from '../agents/readonly_context.js';
17+
import {isSequentialAgent} from '../agents/sequential_agent.js';
18+
import {BaseTool, isBaseTool} from '../tools/base_tool.js';
19+
import {isBaseToolset} from '../tools/base_toolset.js';
20+
21+
export async function getA2AAgentCard(
22+
agent: BaseAgent,
23+
transports: AgentInterface[],
24+
): Promise<AgentCard> {
25+
return {
26+
name: agent.name,
27+
description: agent.description || '',
28+
protocolVersion: '0.3.0',
29+
version: '1.0.0',
30+
skills: await buildAgentSkills(agent),
31+
url: transports[0].url,
32+
capabilities: {
33+
extensions: [],
34+
stateTransitionHistory: false,
35+
pushNotifications: false,
36+
streaming: true,
37+
},
38+
defaultInputModes: ['text'],
39+
defaultOutputModes: ['text'],
40+
additionalInterfaces: transports,
41+
};
42+
}
43+
44+
/**
45+
* Builds a list of AgentSkills based on agent descriptions and types.
46+
* This information can be used in AgentCard to help clients understand agent capabilities.
47+
*
48+
* @param agent The agent to build skills for.
49+
* @returns A promise resolving to a list of AgentSkills.
50+
*/
51+
export async function buildAgentSkills(
52+
agent: BaseAgent,
53+
): Promise<AgentSkill[]> {
54+
const [primarySkills, subAgentSkills] = await Promise.all([
55+
buildPrimarySkills(agent),
56+
buildSubAgentSkills(agent),
57+
]);
58+
59+
return [...primarySkills, ...subAgentSkills];
60+
}
61+
62+
async function buildPrimarySkills(agent: BaseAgent): Promise<AgentSkill[]> {
63+
if (isLlmAgent(agent)) {
64+
return buildLLMAgentSkills(agent);
65+
}
66+
67+
return buildNonLLMAgentSkills(agent);
68+
}
69+
70+
async function buildSubAgentSkills(agent: BaseAgent): Promise<AgentSkill[]> {
71+
const subAgents = agent.subAgents;
72+
const result: AgentSkill[] = [];
73+
74+
for (const sub of subAgents) {
75+
const skills = await buildPrimarySkills(sub);
76+
for (const subSkill of skills) {
77+
const skill: AgentSkill = {
78+
id: `${sub.name}_${subSkill.id}`,
79+
name: `${sub.name}: ${subSkill.name}`,
80+
description: subSkill.description,
81+
tags: [`sub_agent:${sub.name}`, ...subSkill.tags],
82+
};
83+
result.push(skill);
84+
}
85+
}
86+
87+
return result;
88+
}
89+
90+
async function buildLLMAgentSkills(agent: LlmAgent): Promise<AgentSkill[]> {
91+
const skills: AgentSkill[] = [
92+
{
93+
id: agent.name,
94+
name: 'model',
95+
description: await buildDescriptionFromInstructions(agent),
96+
tags: ['llm'],
97+
},
98+
];
99+
100+
if (agent.tools && agent.tools.length > 0) {
101+
for (const toolUnion of agent.tools) {
102+
if (isBaseTool(toolUnion)) {
103+
skills.push(toolToSkill(agent.name, toolUnion));
104+
} else if (isBaseToolset(toolUnion)) {
105+
const tools = await toolUnion.getTools();
106+
107+
for (const tool of tools) {
108+
skills.push(toolToSkill(agent.name, tool));
109+
}
110+
}
111+
}
112+
}
113+
114+
return skills;
115+
}
116+
117+
function toolToSkill(prefix: string, tool: BaseTool): AgentSkill {
118+
let description = tool.description;
119+
if (!description) {
120+
description = `Tool: ${tool.name}`;
121+
}
122+
123+
return {
124+
id: `${prefix}-${tool.name}`,
125+
name: tool.name,
126+
description: description,
127+
tags: ['llm', 'tools'],
128+
};
129+
}
130+
131+
function buildNonLLMAgentSkills(agent: BaseAgent): AgentSkill[] {
132+
const skills: AgentSkill[] = [
133+
{
134+
id: agent.name,
135+
name: getAgentSkillName(agent),
136+
description: buildAgentDescription(agent),
137+
tags: [getAgentTypeTag(agent)],
138+
},
139+
];
140+
141+
const subAgents = agent.subAgents;
142+
if (subAgents.length > 0) {
143+
const descriptions = subAgents.map(
144+
(sub) => sub.description || 'No description',
145+
);
146+
skills.push({
147+
id: `${agent.name}-sub-agents`,
148+
name: 'sub-agents',
149+
description: `Orchestrates: ${descriptions.join('; ')}`,
150+
tags: [getAgentTypeTag(agent), 'orchestration'],
151+
});
152+
}
153+
154+
return skills;
155+
}
156+
157+
function buildAgentDescription(agent: BaseAgent): string {
158+
const descriptionParts: string[] = [];
159+
160+
if (agent.description) {
161+
descriptionParts.push(agent.description);
162+
}
163+
164+
if (agent.subAgents.length > 0) {
165+
if (isLoopAgent(agent)) {
166+
descriptionParts.push(buildLoopAgentDescription(agent));
167+
} else if (isParallelAgent(agent)) {
168+
descriptionParts.push(buildParallelAgentDescription(agent));
169+
} else if (isSequentialAgent(agent)) {
170+
descriptionParts.push(buildSequentialAgentDescription(agent));
171+
}
172+
}
173+
174+
if (descriptionParts.length > 0) {
175+
return descriptionParts.join(' ');
176+
} else {
177+
return getDefaultAgentDescription(agent);
178+
}
179+
}
180+
181+
function buildSequentialAgentDescription(agent: BaseAgent): string {
182+
const subAgents = agent.subAgents;
183+
const descriptions: string[] = [];
184+
185+
subAgents.forEach((sub, i) => {
186+
let subDescription = sub.description;
187+
if (!subDescription) {
188+
subDescription = `execute the ${sub.name} agent`;
189+
}
190+
191+
if (i === 0) {
192+
descriptions.push(`First, this agent will ${subDescription}.`);
193+
} else if (i === subAgents.length - 1) {
194+
descriptions.push(`Finally, this agent will ${subDescription}.`);
195+
} else {
196+
descriptions.push(`Then, this agent will ${subDescription}.`);
197+
}
198+
});
199+
200+
return descriptions.join(' ');
201+
}
202+
203+
function buildParallelAgentDescription(agent: BaseAgent): string {
204+
const subAgents = agent.subAgents;
205+
const descriptions: string[] = [];
206+
207+
subAgents.forEach((sub, i) => {
208+
let subDescription = sub.description;
209+
if (!subDescription) {
210+
subDescription = `execute the ${sub.name} agent`;
211+
}
212+
213+
if (i === 0) {
214+
descriptions.push(`This agent will ${subDescription}`);
215+
} else if (i === subAgents.length - 1) {
216+
descriptions.push(`and ${subDescription}`);
217+
} else {
218+
descriptions.push(`, ${subDescription}`);
219+
}
220+
});
221+
222+
return `${descriptions.join(' ')} simultaneously.`;
223+
}
224+
225+
function buildLoopAgentDescription(agent: LoopAgent): string {
226+
const maxIterationsVal = agent.maxIterations;
227+
let maxIterations = 'unlimited';
228+
if (
229+
typeof maxIterationsVal === 'number' &&
230+
maxIterationsVal < Number.MAX_SAFE_INTEGER
231+
) {
232+
maxIterations = maxIterationsVal.toString();
233+
}
234+
235+
const subAgents = agent.subAgents;
236+
const descriptions: string[] = [];
237+
238+
subAgents.forEach((sub, i) => {
239+
let subDescription = sub.description;
240+
if (!subDescription) {
241+
subDescription = `execute the ${sub.name} agent`;
242+
}
243+
244+
if (i === 0) {
245+
descriptions.push(`This agent will ${subDescription}`);
246+
} else if (i === subAgents.length - 1) {
247+
descriptions.push(`and ${subDescription}`);
248+
} else {
249+
descriptions.push(`, ${subDescription}`);
250+
}
251+
});
252+
253+
return `${descriptions.join(' ')} in a loop (max ${maxIterations} iterations).`;
254+
}
255+
256+
async function buildDescriptionFromInstructions(
257+
agent: LlmAgent,
258+
): Promise<string> {
259+
const descriptionParts: string[] = [];
260+
if (agent.description) {
261+
descriptionParts.push(agent.description);
262+
}
263+
264+
if (agent.instruction) {
265+
let instructionStr: string;
266+
if (typeof agent.instruction === 'function') {
267+
const dummyContext = new ReadonlyContext(
268+
new InvocationContext({
269+
agent: agent,
270+
} as unknown as InvocationContextParams),
271+
);
272+
try {
273+
instructionStr = await agent.instruction(dummyContext);
274+
} catch (e) {
275+
console.warn('Failed to resolve dynamic instruction for AgentCard', e);
276+
instructionStr = '';
277+
}
278+
} else {
279+
instructionStr = agent.instruction;
280+
}
281+
282+
if (instructionStr) {
283+
descriptionParts.push(replacePronouns(instructionStr));
284+
}
285+
}
286+
287+
const root = agent.rootAgent;
288+
if (isLlmAgent(root) && root.globalInstruction) {
289+
let globalInstructionStr: string;
290+
if (typeof root.globalInstruction === 'function') {
291+
const dummyContext = new ReadonlyContext(
292+
new InvocationContext({
293+
agent: agent,
294+
} as unknown as InvocationContextParams),
295+
);
296+
try {
297+
globalInstructionStr = await root.globalInstruction(dummyContext);
298+
} catch (e) {
299+
console.warn(
300+
'Failed to resolve dynamic global instruction for AgentCard',
301+
e,
302+
);
303+
globalInstructionStr = '';
304+
}
305+
} else {
306+
globalInstructionStr = root.globalInstruction;
307+
}
308+
309+
if (globalInstructionStr) {
310+
descriptionParts.push(replacePronouns(globalInstructionStr));
311+
}
312+
}
313+
314+
if (descriptionParts.length > 0) {
315+
return descriptionParts.join(' ');
316+
} else {
317+
return getDefaultAgentDescription(agent);
318+
}
319+
}
320+
321+
// Replaces pronouns and conjugate common verbs for agent description.
322+
// Examples: "You are" -> "I am", "your" -> "my"
323+
function replacePronouns(instruction: string): string {
324+
const substitutions = [
325+
{original: 'you were', target: 'I was'},
326+
{original: 'you are', target: 'I am'},
327+
{original: "you're", target: 'I am'},
328+
{original: "you've", target: 'I have'},
329+
{original: 'yours', target: 'mine'},
330+
{original: 'your', target: 'my'},
331+
{original: 'you', target: 'I'},
332+
];
333+
334+
let result = instruction;
335+
for (const sub of substitutions) {
336+
// Only replace whole words, case insensitive
337+
const pattern = new RegExp(`\\b${sub.original}\\b`, 'gi');
338+
result = result.replace(pattern, sub.target);
339+
}
340+
return result;
341+
}
342+
343+
function getDefaultAgentDescription(agent: BaseAgent): string {
344+
if (isLoopAgent(agent)) {
345+
return 'A loop workflow agent';
346+
} else if (isSequentialAgent(agent)) {
347+
return 'A sequential workflow agent';
348+
} else if (isParallelAgent(agent)) {
349+
return 'A parallel workflow agent';
350+
} else if (isLlmAgent(agent)) {
351+
return 'An LLM-based agent';
352+
} else {
353+
return 'A custom agent';
354+
}
355+
}
356+
357+
function getAgentTypeTag(agent: BaseAgent): string {
358+
if (isLoopAgent(agent)) {
359+
return 'loop_workflow';
360+
} else if (isSequentialAgent(agent)) {
361+
return 'sequential_workflow';
362+
} else if (isParallelAgent(agent)) {
363+
return 'parallel_workflow';
364+
} else if (isLlmAgent(agent)) {
365+
return 'llm_agent';
366+
} else {
367+
return 'custom_agent';
368+
}
369+
}
370+
371+
function getAgentSkillName(agent: BaseAgent): string {
372+
if (isLlmAgent(agent)) {
373+
return 'model';
374+
}
375+
if (isWorkflowAgent(agent)) {
376+
return 'workflow';
377+
}
378+
return 'custom';
379+
}
380+
381+
function isWorkflowAgent(agent: BaseAgent): boolean {
382+
return (
383+
isLoopAgent(agent) || isSequentialAgent(agent) || isParallelAgent(agent)
384+
);
385+
}

core/src/agents/loop_agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class LoopAgent extends BaseAgent {
5353
*/
5454
readonly [LOOP_AGENT_SIGNATURE_SYMBOL] = true;
5555

56-
private readonly maxIterations: number;
56+
readonly maxIterations: number;
5757

5858
constructor(config: LoopAgentConfig) {
5959
super(config);

0 commit comments

Comments
 (0)