Running the A2A (Agent2Agent) helloworld Agent and Calling JSON-RPC with curl to Inspect the Interface
llmpythonA2A (Agent2Agent) is an open protocol for AI agents to communicate with each other. It was announced by Google in April 2025, then donated to the Linux Foundation, and v1 was released in March 2026.
Pull the sample and try running it.
$ git clone https://github.com/a2aproject/a2a-samples.git
$ cd a2a-samples/samples/python/agents/helloworld
$ uv run .
INFO: Started server process [310015]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:9999 (Press CTRL+C to quit)
Looking at the code, the agent and skill metadata are defined as follows.
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import (
create_agent_card_routes,
create_jsonrpc_routes,
)
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentInterface,
AgentSkill,
)
skill = AgentSkill(
id='hello_world',
name='Returns hello world',
description='just returns hello world',
tags=['hello world'],
examples=['hi', 'hello world'],
)
public_agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
version='0.0.1',
default_input_modes=['text/plain'],
default_output_modes=['text/plain'],
capabilities=AgentCapabilities(
streaming=True, extended_agent_card=True
),
supported_interfaces=[
AgentInterface(
protocol_binding='JSONRPC',
url='http://127.0.0.1:9999',
)
],
skills=[skill],
)
request_handler = DefaultRequestHandler(
agent_executor=HelloWorldAgentExecutor(),
task_store=InMemoryTaskStore(),
agent_card=public_agent_card,
extended_agent_card=extended_agent_card,
)
routes = []
routes.extend(create_agent_card_routes(public_agent_card))
routes.extend(create_jsonrpc_routes(request_handler, '/'))
app = Starlette(routes=routes)
uvicorn.run(app, host='127.0.0.1', port=9999)
This information is returned from the /.well-known/agent-card.json endpoint created by create_agent_card_routes(). The endpoint does not include extendedAgentCard information; that can be retrieved by calling the GetExtendedAgentCard method after authentication.
$ curl http://127.0.0.1:9999/.well-known/agent-card.json | jq
{
"name": "Hello World Agent",
"description": "Just a hello world agent",
"supportedInterfaces": [
{
"url": "http://127.0.0.1:9999",
"protocolBinding": "JSONRPC"
}
],
"version": "0.0.1",
"capabilities": {
"streaming": true,
"extendedAgentCard": true
},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"text/plain"
],
"skills": [
{
"id": "hello_world",
"name": "Returns hello world",
"description": "just returns hello world",
"tags": [
"hello world"
],
"examples": [
"hi",
"hello world"
]
}
],
"preferredTransport": "JSONRPC",
"protocolVersion": "0.3",
"supportsAuthenticatedExtendedCard": true,
"url": "http://127.0.0.1:9999"
}
The processing happens in AgentExecutor.execute(). An EventQueue is passed as an argument, and state and artifacts are enqueued into it.
from a2a.helpers import (
new_task_from_user_message,
new_text_artifact,
new_text_message,
)
from a2a.server.agent_execution import AgentExecutor, RequestContext
class HelloWorldAgentExecutor(AgentExecutor):
"""Test AgentProxy Implementation."""
def __init__(self) -> None:
self.agent = HelloWorldAgent()
async def execute(
self,
context: RequestContext,
event_queue: EventQueue,
) -> None:
"""Execute the agent process and enqueue the final response."""
task = context.current_task or new_task_from_user_message(
context.message
)
await event_queue.enqueue_event(task)
await event_queue.enqueue_event(
TaskStatusUpdateEvent(
task_id=context.task_id,
context_id=context.context_id,
status=TaskStatus(
state=TaskState.TASK_STATE_WORKING,
message=new_text_message('Processing request...'),
),
)
)
result = await self.agent.invoke()
await event_queue.enqueue_event(
TaskArtifactUpdateEvent(
task_id=context.task_id,
context_id=context.context_id,
artifact=new_text_artifact(name='result', text=result),
)
)
await event_queue.enqueue_event(
TaskStatusUpdateEvent(
task_id=context.task_id,
context_id=context.context_id,
status=TaskStatus(state=TaskState.TASK_STATE_COMPLETED),
)
)
async def cancel(
self, context: RequestContext, event_queue: EventQueue
) -> None:
"""Raise exception as cancel is not supported."""
raise Exception('cancel not supported')
Calling SendMessage against this with JSON-RPC returns the following result. The method name changed from message/send in v1.0.
$ curl -X POST http://127.0.0.1:9999/ \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
--data '{
"jsonrpc": "2.0",
"id": "1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "m1",
"role": "ROLE_USER",
"parts": [
{"text": "Hi"}
]
}
}
}' | jq
{
"result": {
"task": {
"id": "4b8a26fc-6d19-47cd-969f-299b133a3db0",
"contextId": "eeaf4afc-c905-42d6-a094-a1640eb291ca",
"status": {
"state": "TASK_STATE_COMPLETED"
},
"artifacts": [
{
"artifactId": "276f0b78-df06-44dd-982b-e495f94b22df",
"name": "result",
"parts": [
{
"text": "Hello, World!"
}
]
}
],
"history": [
{
"messageId": "m1",
"contextId": "eeaf4afc-c905-42d6-a094-a1640eb291ca",
"taskId": "4b8a26fc-6d19-47cd-969f-299b133a3db0",
"role": "ROLE_USER",
"parts": [
{
"text": "Hi"
}
]
},
{
"messageId": "992ffcdc-6460-4151-b95e-8fd02b9f9ca6",
"role": "ROLE_AGENT",
"parts": [
{
"text": "Processing request..."
}
]
}
]
}
},
"id": "1",
"jsonrpc": "2.0"
}
Sending a request with Accept: text/event-stream enables streaming via Server Sent Events.
$ curl -N -X POST http://127.0.0.1:9999/ \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "A2A-Version: 1.0" \
--data '{
"jsonrpc": "2.0",
"id": "1",
"method": "SendStreamingMessage",
"params": {
"message": {
"messageId": "m1",
"role": "ROLE_USER",
"parts": [
{"text": "Hi"}
]
}
}
}'
data: {"result": {"task": {"id": "d9abadef-d57e-4bf5-b66a-eeebeaf0c21e", "contextId": "e9f88d43-9465-4875-abc7-c4dac56f8b89", "status": {"state": "TASK_STATE_SUBMITTED"}, "history": [{"messageId": "m1", "contextId": "e9f88d43-9465-4875-abc7-c4dac56f8b89", "taskId": "d9abadef-d57e-4bf5-b66a-eeebeaf0c21e", "role": "ROLE_USER", "parts": [{"text": "Hi"}]}]}}, "id": "1", "jsonrpc": "2.0"}
data: {"result": {"statusUpdate": {"taskId": "d9abadef-d57e-4bf5-b66a-eeebeaf0c21e", "contextId": "e9f88d43-9465-4875-abc7-c4dac56f8b89", "status": {"state": "TASK_STATE_WORKING", "message": {"messageId": "3270cd18-7bd9-483a-bd62-cf47aa93f791", "role": "ROLE_AGENT", "parts": [{"text": "Processing request..."}]}}}}, "id": "1", "jsonrpc": "2.0"}
data: {"result": {"artifactUpdate": {"taskId": "d9abadef-d57e-4bf5-b66a-eeebeaf0c21e", "contextId": "e9f88d43-9465-4875-abc7-c4dac56f8b89", "artifact": {"artifactId": "a3f1f687-a6dd-4bfe-b5a4-012083753627", "name": "result", "parts": [{"text": "Hello, World!"}]}}}, "id": "1", "jsonrpc": "2.0"}
data: {"result": {"statusUpdate": {"taskId": "d9abadef-d57e-4bf5-b66a-eeebeaf0c21e", "contextId": "e9f88d43-9465-4875-abc7-c4dac56f8b89", "status": {"state": "TASK_STATE_COMPLETED"}}}, "id": "1", "jsonrpc": "2.0"}