A2A (Agent2Agent) の helloworld エージェントを動かし curl で JSON-RPC を呼んでインタフェースを確認する

llmpython

A2A (Agent2Agent) は AI エージェント同士がやり取りするためのオープンなプロトコル。 2025年4月に Google から発表されたのち Linux Foundation に寄贈され、2026年3月に v1 がリリースされた

サンプルを pull してきて動かしてみる。

$ 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)

コードを見ると次のようにエージェントやスキルのメタデータが定義されている。

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)

これらの情報は create_agent_card_routes() によって作られた /.well-known/agent-card.json エンドポイントで返される。 このエンドポイントには extendedAgentCard の情報は含まれず、認証後 GetExtendedAgentCard method を呼ぶことで取得できる。

$ 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"
}

処理は AgentExecutor.execute() で行う。引数で EventQueue が渡されるので state や artifact を入れる。

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')

これに対して JSON-RPC で SendMessage を呼ぶと次のような結果が返る。v1.0 で method 名が message/send から変わった

$ 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"
}

Accept: text/event-stream でリクエストを送ると Server Side 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"}