[object Object]

← back to A2a Lab

add working A2A echo demo (SDK server + raw JSON-RPC client), verified end-to-end

4dc181fbf642b40dbabaa4192fc85bd51f3b1335 · 2026-08-01 19:32:47 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4dc181fbf642b40dbabaa4192fc85bd51f3b1335
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Aug 1 19:32:47 2026 -0700

    add working A2A echo demo (SDK server + raw JSON-RPC client), verified end-to-end
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 README.md        | 32 ++++++++++++++++++
 client.py        | 62 +++++++++++++++++++++++++++++++++++
 requirements.txt |  4 +++
 server.py        | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 197 insertions(+)

diff --git a/README.md b/README.md
index dd0f62a..6e7b355 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,38 @@ python smoke_test.py     # Python SDK import + version check
 node smoke.js            # JS SDK import check
 ```
 
+## Working demo (verified end-to-end)
+
+An "Echo Agent" that upper-cases your text, exercising the full A2A round-trip:
+
+```bash
+cd ~/Projects/a2a-lab
+. .venv/bin/activate
+python server.py            # terminal 1 — listens on 127.0.0.1:41241
+python client.py "hello"    # terminal 2 — discovers the card + sends a message
+```
+
+- `server.py` — SDK stack: `EchoAgentExecutor(AgentExecutor)` → `DefaultRequestHandler`
+  + `InMemoryTaskStore` + a proto `AgentCard`, served via Starlette
+  (`create_agent_card_routes` + `create_jsonrpc_routes`) under uvicorn.
+- `client.py` — deliberately a **raw JSON-RPC httpx** call (no SDK client), so it
+  self-documents the wire protocol: GET the card → POST `SendMessage`.
+
+Verified output: client gets `{"result": {"message": {"parts": [{"text": "echo: HELLO"}]}}}`.
+
+### A2A wire contract in a2a-sdk 1.1.2 (proto-native — differs from old docs!)
+
+1. **Discovery:** `GET /.well-known/agent-card.json` (proto→JSON is **camelCase**:
+   `protocolBinding`, `supportedInterfaces`).
+2. **Method names are gRPC-style PascalCase** — `SendMessage` (NOT `message/send`),
+   `GetTask`, `CancelTask`, `SendStreamingMessage` (SSE), etc.
+3. **Params are a protobuf `SendMessageRequest`** parsed via `ParseDict`:
+   `{"message": {"messageId": "...", "role": "ROLE_USER", "parts": [{"text": "..."}]}}`
+   — enum values are proto **names** (`ROLE_USER`/`ROLE_AGENT`), fields camelCase.
+4. **Protocol version travels in the `A2A-Version` HTTP header** — a MISSING header
+   silently defaults to legacy `0.3` and the 1.0 handler rejects it with `-32009`.
+   Always send `A2A-Version: 1.0`.
+
 ## Next steps (not done — future work)
 
 - Build a minimal A2A *server* that exposes one of Steve's cabinet agents (or the
diff --git a/client.py b/client.py
new file mode 100644
index 0000000..61e6a4b
--- /dev/null
+++ b/client.py
@@ -0,0 +1,62 @@
+"""Minimal A2A (Agent2Agent) demo client — spec-compliant raw JSON-RPC over httpx.
+
+Deliberately does NOT use the SDK client, so it self-documents the A2A wire protocol:
+  1. DISCOVER  — GET the agent card from /.well-known/agent-card.json
+  2. INVOKE    — POST a JSON-RPC 2.0 `message/send` with a text message
+  3. READ      — print the agent's reply
+
+Run (server must be up): . .venv/bin/activate && python client.py
+"""
+from __future__ import annotations
+
+import json
+import sys
+import uuid
+
+import httpx
+
+BASE = "http://127.0.0.1:41241"
+
+
+def discover(base: str) -> dict:
+    card = httpx.get(f"{base}/.well-known/agent-card.json", timeout=10).json()
+    print(f"① DISCOVERED agent card → name={card.get('name')!r} version={card.get('version')!r}")
+    ifaces = card.get("supportedInterfaces") or card.get("supported_interfaces") or []
+    if ifaces:
+        print(f"   interface → {ifaces[0]}")
+    return card
+
+
+def send_message(base: str, text: str) -> dict:
+    # a2a-sdk 1.1.2 uses gRPC-style method names (SendMessage) and parses params
+    # as a protobuf SendMessageRequest (camelCase fields, enum-name roles).
+    payload = {
+        "jsonrpc": "2.0",
+        "id": str(uuid.uuid4()),
+        "method": "SendMessage",
+        "params": {
+            "message": {
+                "messageId": str(uuid.uuid4()),
+                "role": "ROLE_USER",
+                "parts": [{"text": text}],
+            }
+        },
+    }
+    print(f"② SEND SendMessage → {text!r}")
+    # The server defaults a missing A2A-Version header to legacy '0.3'; signal 1.0.
+    headers = {"A2A-Version": "1.0"}
+    resp = httpx.post(f"{base}/", json=payload, headers=headers, timeout=30)
+    return resp.json()
+
+
+def main() -> int:
+    text = sys.argv[1] if len(sys.argv) > 1 else "hello a2a"
+    discover(BASE)
+    result = send_message(BASE, text)
+    print("③ RESPONSE:")
+    print(json.dumps(result, indent=2))
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/requirements.txt b/requirements.txt
index b6e0c7f..e9a6bbd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ anyio==4.14.2
 certifi==2026.7.22
 cffi==2.1.0
 charset-normalizer==3.4.9
+click==8.4.2
 cryptography==50.0.0
 google-api-core==2.33.0
 google-auth==2.56.2
@@ -22,6 +23,9 @@ pycparser==3.0
 pydantic==2.13.4
 pydantic_core==2.46.4
 requests==2.34.2
+sse-starlette==3.4.6
+starlette==1.3.1
 typing-inspection==0.4.2
 typing_extensions==4.16.0
 urllib3==2.7.0
+uvicorn==0.52.1
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..fc145ab
--- /dev/null
+++ b/server.py
@@ -0,0 +1,99 @@
+"""Minimal A2A (Agent2Agent) demo server — a2a-sdk 1.1.2, current proto API.
+
+An "Echo Agent": receives a text message and returns it upper-cased. It publishes
+a standard A2A agent card at /.well-known/agent-card.json and serves the JSON-RPC
+A2A endpoint at / . Proves the SDK server stack (AgentExecutor + DefaultRequestHandler
++ InMemoryTaskStore + Starlette route builders).
+
+Run: . .venv/bin/activate && python server.py   (listens on 127.0.0.1:41241)
+"""
+from __future__ import annotations
+
+import uuid
+
+import uvicorn
+from starlette.applications import Starlette
+
+from a2a.types import (
+    AgentCard,
+    AgentInterface,
+    AgentCapabilities,
+    AgentSkill,
+    Message,
+    Part,
+    Role,
+)
+from a2a.server.agent_execution import AgentExecutor, RequestContext
+from a2a.server.events import EventQueue
+from a2a.server.request_handlers import DefaultRequestHandler
+from a2a.server.tasks import InMemoryTaskStore
+from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
+
+HOST = "127.0.0.1"
+PORT = 41241
+
+
+class EchoAgentExecutor(AgentExecutor):
+    """The agent's brain: read the user's text, reply with it upper-cased."""
+
+    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
+        # get_user_input() concatenates the text parts of the incoming message.
+        try:
+            user_text = context.get_user_input() or ""
+        except Exception:
+            user_text = ""
+        reply = Message(
+            message_id=str(uuid.uuid4()),
+            role=Role.ROLE_AGENT,
+            parts=[Part(text=f"echo: {user_text.upper()}")],
+        )
+        await event_queue.enqueue_event(reply)
+
+    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
+        raise NotImplementedError("Echo agent does not support cancellation.")
+
+
+def build_agent_card() -> AgentCard:
+    return AgentCard(
+        name="Echo Agent",
+        description="Minimal A2A demo agent — returns your text upper-cased.",
+        version="0.1.0",
+        supported_interfaces=[
+            AgentInterface(
+                url=f"http://{HOST}:{PORT}/",
+                protocol_binding="JSONRPC",
+                protocol_version="1.0",
+            )
+        ],
+        capabilities=AgentCapabilities(streaming=False, push_notifications=False),
+        default_input_modes=["text/plain"],
+        default_output_modes=["text/plain"],
+        skills=[
+            AgentSkill(
+                id="echo",
+                name="Echo",
+                description="Uppercase-echo the input text.",
+                tags=["demo", "echo"],
+                examples=["hello world"],
+            )
+        ],
+    )
+
+
+def build_app() -> Starlette:
+    card = build_agent_card()
+    handler = DefaultRequestHandler(
+        agent_executor=EchoAgentExecutor(),
+        task_store=InMemoryTaskStore(),
+        agent_card=card,
+    )
+    routes = [
+        *create_agent_card_routes(card),          # GET /.well-known/agent-card.json
+        *create_jsonrpc_routes(handler, "/"),      # POST /  (A2A JSON-RPC)
+    ]
+    return Starlette(routes=routes)
+
+
+if __name__ == "__main__":
+    print(f"A2A Echo Agent on http://{HOST}:{PORT}  (card: /.well-known/agent-card.json)")
+    uvicorn.run(build_app(), host=HOST, port=PORT, log_level="warning")

← f3cea00 install A2A SDKs (python a2a-sdk 1.1.2 + @a2a-js/sdk), smoke  ·  back to A2a Lab  ·  add read-only A2A bridge over the live tk cross-agent DM log 89535d8 →