diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index b314e8da2..f1223e0f2 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -1311,7 +1311,7 @@ from google.adk.agents import Agent from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService -from a2a.types import AgentCapabilities, AgentCard, AgentSkill +from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill from bedrock_agentcore.runtime import serve_a2a from model.load import load_model @@ -1405,7 +1405,6 @@ runner = Runner( card = AgentCard( name=agent.name, description=agent.description, - url="http://localhost:9000/", version="0.1.0", capabilities=AgentCapabilities(streaming=True), skills=[ @@ -1418,6 +1417,13 @@ card = AgentCard( ], default_input_modes=["text"], default_output_modes=["text"], + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url="http://localhost:9000/", + ) + ], ) if __name__ == "__main__": @@ -1487,11 +1493,11 @@ description = "AgentCore A2A Agent using Google ADK" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "a2a-sdk >= 0.2.0, < 1.0.0", + "a2a-sdk[http-server] >= 1.0.1, < 2.0.0", "aws-opentelemetry-distro", - "bedrock-agentcore[a2a] >= 1.0.3", - "google-adk >= 1.0.0, < 2.0.0", - "google-genai >= 1.0.0, < 2.0.0", + "bedrock-agentcore[a2a-v1] >= 1.19.0", + "google-adk[a2a] >= 2.5.0, < 3.0.0", + "google-genai >= 2.9.0, < 3.0.0", # 1.13.0 was yanked for broken imports. "opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0", ] @@ -1579,11 +1585,11 @@ import os from langchain_core.tools import tool from langgraph.prebuilt import create_react_agent from opentelemetry.instrumentation.langchain import LangchainInstrumentor +from a2a.helpers import new_task_from_user_message from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import TaskUpdater -from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part, TextPart -from a2a.utils import new_task +from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill, Part from bedrock_agentcore.runtime import serve_a2a from model.load import load_model @@ -1675,7 +1681,7 @@ class LangGraphA2AExecutor(AgentExecutor): self.graph = graph async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: - task = context.current_task or new_task(context.message) + task = context.current_task or new_task_from_user_message(context.message) if not context.current_task: await event_queue.enqueue_event(task) updater = TaskUpdater(event_queue, task.id, task.context_id) @@ -1684,7 +1690,7 @@ class LangGraphA2AExecutor(AgentExecutor): result = await self.graph.ainvoke({"messages": [("user", user_text)]}) response = result["messages"][-1].content - await updater.add_artifact([Part(root=TextPart(text=response))]) + await updater.add_artifact([Part(text=response)]) await updater.complete() async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: @@ -1694,7 +1700,6 @@ class LangGraphA2AExecutor(AgentExecutor): card = AgentCard( name="{{ name }}", description="A LangGraph agent on Bedrock AgentCore", - url="http://localhost:9000/", version="0.1.0", capabilities=AgentCapabilities(streaming=True), skills=[ @@ -1707,6 +1712,13 @@ card = AgentCard( ], default_input_modes=["text"], default_output_modes=["text"], + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url="http://localhost:9000/", + ) + ], ) if __name__ == "__main__": @@ -1858,7 +1870,7 @@ description = "AgentCore A2A Agent using LangChain + LangGraph" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "a2a-sdk >= 0.2.0, < 1.0.0", + "a2a-sdk[http-server] >= 1.0.1, < 2.0.0", {{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0", {{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0", {{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0", @@ -1866,7 +1878,7 @@ dependencies = [ {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0", {{/if}}"aws-opentelemetry-distro", "opentelemetry-instrumentation-langchain >= 0.59.0", - "bedrock-agentcore[a2a] >= 1.8.0", + "bedrock-agentcore[a2a-v1] >= 1.19.0", "botocore[crt] >= 1.35.0", "langgraph >= 0.2.0", ] @@ -2209,7 +2221,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0", - {{/if}}"a2a-sdk[all] >= 0.2.0, < 1.0.0", + {{/if}}"a2a-sdk[all] >= 0.3.0, < 0.4.0", "aws-opentelemetry-distro", "bedrock-agentcore[a2a] >= 1.9.1", "botocore[crt] >= 1.35.0", diff --git a/src/assets/python/a2a/googleadk/base/main.py b/src/assets/python/a2a/googleadk/base/main.py index 2714c5c90..8b439920b 100644 --- a/src/assets/python/a2a/googleadk/base/main.py +++ b/src/assets/python/a2a/googleadk/base/main.py @@ -5,7 +5,7 @@ from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService -from a2a.types import AgentCapabilities, AgentCard, AgentSkill +from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill from bedrock_agentcore.runtime import serve_a2a from model.load import load_model @@ -99,7 +99,6 @@ def list_files(path: str) -> str: card = AgentCard( name=agent.name, description=agent.description, - url="http://localhost:9000/", version="0.1.0", capabilities=AgentCapabilities(streaming=True), skills=[ @@ -112,6 +111,13 @@ def list_files(path: str) -> str: ], default_input_modes=["text"], default_output_modes=["text"], + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url="http://localhost:9000/", + ) + ], ) if __name__ == "__main__": diff --git a/src/assets/python/a2a/googleadk/base/pyproject.toml b/src/assets/python/a2a/googleadk/base/pyproject.toml index 9bf385764..69dc659d5 100644 --- a/src/assets/python/a2a/googleadk/base/pyproject.toml +++ b/src/assets/python/a2a/googleadk/base/pyproject.toml @@ -9,11 +9,11 @@ description = "AgentCore A2A Agent using Google ADK" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "a2a-sdk >= 0.2.0, < 1.0.0", + "a2a-sdk[http-server] >= 1.0.1, < 2.0.0", "aws-opentelemetry-distro", - "bedrock-agentcore[a2a] >= 1.0.3", - "google-adk >= 1.0.0, < 2.0.0", - "google-genai >= 1.0.0, < 2.0.0", + "bedrock-agentcore[a2a-v1] >= 1.19.0", + "google-adk[a2a] >= 2.5.0, < 3.0.0", + "google-genai >= 2.9.0, < 3.0.0", # 1.13.0 was yanked for broken imports. "opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0", ] diff --git a/src/assets/python/a2a/langchain_langgraph/base/main.py b/src/assets/python/a2a/langchain_langgraph/base/main.py index 2ab8f6ff8..5960203a4 100644 --- a/src/assets/python/a2a/langchain_langgraph/base/main.py +++ b/src/assets/python/a2a/langchain_langgraph/base/main.py @@ -4,11 +4,11 @@ from langchain_core.tools import tool from langgraph.prebuilt import create_react_agent from opentelemetry.instrumentation.langchain import LangchainInstrumentor +from a2a.helpers import new_task_from_user_message from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import TaskUpdater -from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part, TextPart -from a2a.utils import new_task +from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill, Part from bedrock_agentcore.runtime import serve_a2a from model.load import load_model @@ -100,7 +100,7 @@ def __init__(self, graph): self.graph = graph async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: - task = context.current_task or new_task(context.message) + task = context.current_task or new_task_from_user_message(context.message) if not context.current_task: await event_queue.enqueue_event(task) updater = TaskUpdater(event_queue, task.id, task.context_id) @@ -109,7 +109,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non result = await self.graph.ainvoke({"messages": [("user", user_text)]}) response = result["messages"][-1].content - await updater.add_artifact([Part(root=TextPart(text=response))]) + await updater.add_artifact([Part(text=response)]) await updater.complete() async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: @@ -119,7 +119,6 @@ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None card = AgentCard( name="{{ name }}", description="A LangGraph agent on Bedrock AgentCore", - url="http://localhost:9000/", version="0.1.0", capabilities=AgentCapabilities(streaming=True), skills=[ @@ -132,6 +131,13 @@ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None ], default_input_modes=["text"], default_output_modes=["text"], + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url="http://localhost:9000/", + ) + ], ) if __name__ == "__main__": diff --git a/src/assets/python/a2a/langchain_langgraph/base/pyproject.toml b/src/assets/python/a2a/langchain_langgraph/base/pyproject.toml index e67af7505..4b2627483 100644 --- a/src/assets/python/a2a/langchain_langgraph/base/pyproject.toml +++ b/src/assets/python/a2a/langchain_langgraph/base/pyproject.toml @@ -9,7 +9,7 @@ description = "AgentCore A2A Agent using LangChain + LangGraph" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "a2a-sdk >= 0.2.0, < 1.0.0", + "a2a-sdk[http-server] >= 1.0.1, < 2.0.0", {{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0", {{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0", {{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0", @@ -17,7 +17,7 @@ dependencies = [ {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0", {{/if}}"aws-opentelemetry-distro", "opentelemetry-instrumentation-langchain >= 0.59.0", - "bedrock-agentcore[a2a] >= 1.8.0", + "bedrock-agentcore[a2a-v1] >= 1.19.0", "botocore[crt] >= 1.35.0", "langgraph >= 0.2.0", ] diff --git a/src/assets/python/a2a/strands/base/pyproject.toml b/src/assets/python/a2a/strands/base/pyproject.toml index fc69f0030..a67fd7dc3 100644 --- a/src/assets/python/a2a/strands/base/pyproject.toml +++ b/src/assets/python/a2a/strands/base/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0", - {{/if}}"a2a-sdk[all] >= 0.2.0, < 1.0.0", + {{/if}}"a2a-sdk[all] >= 0.3.0, < 0.4.0", "aws-opentelemetry-distro", "bedrock-agentcore[a2a] >= 1.9.1", "botocore[crt] >= 1.35.0", diff --git a/src/cli/operations/dev/__tests__/invoke-a2a.test.ts b/src/cli/operations/dev/__tests__/invoke-a2a.test.ts index e390af6c3..a01210fa5 100644 --- a/src/cli/operations/dev/__tests__/invoke-a2a.test.ts +++ b/src/cli/operations/dev/__tests__/invoke-a2a.test.ts @@ -1,5 +1,5 @@ import { DevServerError } from '../../../../lib/errors/types'; -import { invokeA2AStreaming } from '../invoke-a2a'; +import { fetchA2AAgentCard, invokeA2AStreaming } from '../invoke-a2a'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const mockFetch = vi.fn(); @@ -210,3 +210,35 @@ describe('invokeA2AStreaming', () => { expect(chunks.length).toBeGreaterThan(0); }); }); + +describe('fetchA2AAgentCard', () => { + beforeEach(() => { + mockFetch.mockReset(); + }); + + it('uses the v1 agent card endpoint', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve({ name: 'v1-agent' }), + }); + + const card = await fetchA2AAgentCard(9000); + + expect(card?.name).toBe('v1-agent'); + expect(mockFetch.mock.calls[0]![0]).toBe('http://localhost:9000/.well-known/agent-card.json'); + }); + + it('falls back to the v0.3 agent card endpoint', async () => { + mockFetch.mockResolvedValueOnce({ ok: false, status: 404 }).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve({ name: 'legacy-agent' }), + }); + + const card = await fetchA2AAgentCard(9000); + + expect(card?.name).toBe('legacy-agent'); + expect(mockFetch.mock.calls[1]![0]).toBe('http://localhost:9000/.well-known/agent.json'); + }); +}); diff --git a/src/cli/operations/dev/invoke-a2a.ts b/src/cli/operations/dev/invoke-a2a.ts index 2491297c1..44189c059 100644 --- a/src/cli/operations/dev/invoke-a2a.ts +++ b/src/cli/operations/dev/invoke-a2a.ts @@ -10,6 +10,7 @@ export interface A2AAgentCard { description?: string; version?: string; url?: string; + supportedInterfaces?: { protocolBinding?: string; protocolVersion?: string; url?: string }[]; skills?: { id?: string; name?: string; description?: string; tags?: string[] }[]; capabilities?: { streaming?: boolean }; defaultInputModes?: string[]; @@ -17,7 +18,7 @@ export interface A2AAgentCard { } /** - * Fetch the A2A agent card from /.well-known/agent.json. + * Fetch the A2A agent card, falling back to the v0.3 endpoint. * Returns null if not available (retries on connection errors). */ export async function fetchA2AAgentCard(port: number, logger?: SSELogger): Promise { @@ -26,19 +27,22 @@ export async function fetchA2AAgentCard(port: number, logger?: SSELogger): Promi for (let attempt = 0; attempt < maxRetries; attempt++) { try { - const res = await fetch(`http://localhost:${port}/.well-known/agent.json`, { - method: 'GET', - headers: { Accept: 'application/json' }, - }); + for (const path of ['/.well-known/agent-card.json', '/.well-known/agent.json']) { + const res = await fetch(`http://localhost:${port}${path}`, { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + + if (!res.ok) { + if (res.status === 404 && path === '/.well-known/agent-card.json') continue; + logger?.log?.('warn', `Agent card not available (${res.status})`); + return null; + } - if (!res.ok) { - logger?.log?.('warn', `Agent card not available (${res.status})`); - return null; + const card = (await res.json()) as A2AAgentCard; + logger?.log?.('system', `A2A agent card: ${card.name ?? 'unnamed'}`); + return card; } - - const card = (await res.json()) as A2AAgentCard; - logger?.log?.('system', `A2A agent card: ${card.name ?? 'unnamed'}`); - return card; } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); if (isConnectionError(error) && attempt < maxRetries - 1) { diff --git a/src/cli/operations/dev/web-ui/__tests__/a2a-proxy.test.ts b/src/cli/operations/dev/web-ui/__tests__/a2a-proxy.test.ts new file mode 100644 index 000000000..9cac9c402 --- /dev/null +++ b/src/cli/operations/dev/web-ui/__tests__/a2a-proxy.test.ts @@ -0,0 +1,81 @@ +import { handleA2AAgentCard } from '../handlers/a2a-proxy.js'; +import type { RouteContext } from '../handlers/route-context.js'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +function mockReq(): IncomingMessage { + return { + url: '/api/a2a/agent-card?agentName=test-agent', + headers: { host: 'localhost:8081' }, + } as unknown as IncomingMessage; +} + +function mockRes(): ServerResponse & { _status: number; _body: string } { + const res = { + _status: 0, + _body: '', + writeHead(status: number) { + res._status = status; + return res; + }, + setHeader: vi.fn(), + end(body?: string) { + if (body) res._body = body; + }, + }; + return res as unknown as ServerResponse & { _status: number; _body: string }; +} + +function mockCtx(): RouteContext { + return { + options: { mode: 'dev' }, + runningAgents: new Map([['test-agent', { server: {} as never, port: 8082, protocol: 'A2A' }]]), + startingAgents: new Map(), + agentErrors: new Map(), + setCorsHeaders: vi.fn(), + readBody: vi.fn(), + } as unknown as RouteContext; +} + +describe('handleA2AAgentCard', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('uses the v1 agent card endpoint', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ name: 'v1-agent' }), + }); + vi.stubGlobal('fetch', fetchMock); + const res = mockRes(); + + await handleA2AAgentCard(mockCtx(), mockReq(), res); + + expect(fetchMock).toHaveBeenCalledOnce(); + expect(fetchMock.mock.calls[0]![0]).toBe('http://localhost:8082/.well-known/agent-card.json'); + expect(res._status).toBe(200); + expect(JSON.parse(res._body)).toEqual({ success: true, card: { name: 'v1-agent' } }); + }); + + it('falls back to the v0.3 agent card endpoint on 404', async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce({ ok: false, status: 404 }) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve({ name: 'legacy-agent' }), + }); + vi.stubGlobal('fetch', fetchMock); + const res = mockRes(); + + await handleA2AAgentCard(mockCtx(), mockReq(), res); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock.mock.calls[1]![0]).toBe('http://localhost:8082/.well-known/agent.json'); + expect(res._status).toBe(200); + expect(JSON.parse(res._body)).toEqual({ success: true, card: { name: 'legacy-agent' } }); + }); +}); diff --git a/src/cli/operations/dev/web-ui/api-types.ts b/src/cli/operations/dev/web-ui/api-types.ts index b5ea2be12..672bb1583 100644 --- a/src/cli/operations/dev/web-ui/api-types.ts +++ b/src/cli/operations/dev/web-ui/api-types.ts @@ -420,12 +420,13 @@ export interface A2AAgentSkill { tags?: string[]; } -/** A2A agent card returned by /.well-known/agent.json */ +/** A2A agent card returned by /.well-known/agent-card.json */ export interface A2AAgentCard { name?: string; description?: string; version?: string; url?: string; + supportedInterfaces?: { protocolBinding?: string; protocolVersion?: string; url?: string }[]; skills?: A2AAgentSkill[]; capabilities?: { streaming?: boolean }; defaultInputModes?: string[]; diff --git a/src/cli/operations/dev/web-ui/handlers/a2a-proxy.ts b/src/cli/operations/dev/web-ui/handlers/a2a-proxy.ts index 11d41852c..fc63eafeb 100644 --- a/src/cli/operations/dev/web-ui/handlers/a2a-proxy.ts +++ b/src/cli/operations/dev/web-ui/handlers/a2a-proxy.ts @@ -27,10 +27,16 @@ export async function handleA2AAgentCard( } try { - const cardRes = await fetch(`http://localhost:${running.port}/.well-known/agent.json`, { + let cardRes = await fetch(`http://localhost:${running.port}/.well-known/agent-card.json`, { method: 'GET', headers: { Accept: 'application/json' }, }); + if (cardRes.status === 404) { + cardRes = await fetch(`http://localhost:${running.port}/.well-known/agent.json`, { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + } if (!cardRes.ok) { res.writeHead(502, { 'Content-Type': 'application/json' });