← back to A2a Lab
README.md
163 lines
# a2a-lab
Local install + reference workspace for the **A2A (Agent2Agent) protocol** — the
open standard (originated at Google, now under the Linux Foundation) for AI agents
to discover each other (via an *agent card*) and exchange tasks/messages over
HTTP + JSON-RPC/SSE. Complementary to MCP: **MCP** connects an agent to tools/data;
**A2A** connects agents to *other agents*.
Installed 2026-08-01 (DW YOLO session, "install any A2A software").
## What's installed
| SDK | Package | Version | Where |
|-----|---------|---------|-------|
| Python (official) | `a2a-sdk[http-server]` | 1.1.2 | `.venv/` (Python 3.14.6) |
| JavaScript (official) | `@a2a-js/sdk` | latest | `node_modules/` (Node 26.4) |
- Python deps pinned in `requirements.txt` (base is lean: httpx + json-rpc + pydantic;
`[http-server]` adds starlette + sse-starlette; gRPC / SQL / telemetry are extra extras).
- Both SDKs smoke-tested — see `smoke_test.py` (Python) and `smoke.js` (Node).
## Current-API import paths (a2a-sdk 1.1.2)
The 1.x layout differs from older tutorials — **do not** use `a2a.client.A2AClient`
or `a2a.server.apps.A2AStarletteApplication` (those are pre-1.0 and no longer exist).
```python
from a2a.types import AgentCard, AgentCapabilities # types
from a2a.client import ClientFactory, create_client, A2ACardResolver # client
from a2a.server.request_handlers import DefaultRequestHandler # server
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.routes import add_a2a_routes_to_fastapi, create_jsonrpc_routes # mount on FastAPI/Starlette
```
## Quickstart
```bash
cd ~/Projects/a2a-lab
. .venv/bin/activate
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`.
## TK Bridge — A2A over the REAL cross-agent DM system (verified)
`tk_agent.py` exposes Steve's live `tk` agent-to-agent DM log
(`~/.claude/tickets/events.jsonl`) as an A2A-discoverable agent, so any A2A client
can query it over the standard protocol. **READ-ONLY by design** — it folds/reads
the event log (faithful port of `ticket-system/lib.js`'s `inbox`/`knownAgents`) and
never appends a `dm`/`read` event, so it can't pollute the shared store.
```bash
python tk_agent.py # terminal 1 — 127.0.0.1:41242
A2A_BASE=http://127.0.0.1:41242 python client.py "agents"
A2A_BASE=http://127.0.0.1:41242 python client.py "inbox showroom-builder all"
```
Verified live: "agents" → 214 known agent identities; "inbox <agent> all" → that
agent's real DMs (mid / from→to / ticket / text) over A2A.
**Skills:** `agents`, `inbox` (read) + `dm <agent> <text>` (write, **gated**). The write
is OFF by default — a discovery bridge must not mutate the shared cross-agent log on a
POST. Enable per-process with `TK_BRIDGE_ALLOW_WRITE=1`; `TK_EVENTS=<path>` redirects the
store (used to verify writes against a temp file with zero pollution of the real log):
```bash
TK_EVENTS=/tmp/tk-test.jsonl TK_BRIDGE_ALLOW_WRITE=1 python tk_agent.py
A2A_BASE=http://127.0.0.1:41242 python client.py "dm vp-operations check the pg lock canary"
# → sent M-00001 a2a-bridge→vp-operations: check the pg lock canary
```
## Cabinet agent cards (generated, verified)
`cabinet_cards.py` turns `~/Projects/agent-cabinet/cabinet.yaml` (the org chart:
President → VP → Director → Skills) into one A2A `AgentCard` per VP — each director's
`owns` line becomes an `AgentSkill`. Output: `cards/<vp>.agent-card.json`.
```bash
python cabinet_cards.py # → 12 cabinet cards / 166 skills in cards/
```
Verified: all 12 cards round-trip back into valid proto `AgentCard`s. This is the
registry a live A2A directory would serve so officers/directors are **discoverable and
callable over the protocol** (the `supportedInterfaces` URLs are a convention until a
directory server assigns real ports — that's the next step).
Note: `cabinet.yaml` is **not strictly-valid YAML** (its `owns: [...]` flow lists carry
unquoted prose with `:` and `/`, which PyYAML rejects), so the generator uses a tolerant
line parser. That invalidity is a latent bug for anything that YAML-loads the file.
## Cabinet Directory — serve + route the cards over A2A (verified)
`cabinet_directory.py` loads the generated `cards/` and exposes the whole cabinet as one
discoverable A2A agent, so the org chart is **queryable and routable over the protocol**:
```bash
python cabinet_directory.py # 127.0.0.1:41243, 12 officers loaded
A2A_BASE=http://127.0.0.1:41243 python client.py "list"
A2A_BASE=http://127.0.0.1:41243 python client.py "find scrape a vendor catalog to shopify"
A2A_BASE=http://127.0.0.1:41243 python client.py "card vp-security"
```
Skills: `list` (all officers + skill counts), `find <task>` (rank officers by trigger/skill
match — e.g. "rotate a leaked api key" → **vp-security**), `card <vp>` (full card). Verified
end-to-end over JSON-RPC. This is the discovery/routing layer the cards were built for.
## SDK-native client + semantic routing (verified)
**`client_sdk.py`** — the official a2a-sdk client stack (vs `client.py`'s raw JSON-RPC):
`A2ACardResolver.get_agent_card()` → `ClientFactory(ClientConfig(httpx_client=…)).create(card)`
→ async `client.send_message(SendMessageRequest(...))`, reading text from the `StreamResponse`
oneof. The SDK owns the proto request + `SendMessage` method; we supply `A2A-Version:1.0`
via the httpx client. Verified against the echo agent and the directory.
**Semantic `find`** (`semantic.py`) — replaces raw keyword-hit-count with a lexical-semantic
score: TF-IDF term weighting + a domain synonym map + char-trigram cosine. So
"harden the firewall against intruders" → **vp-security 0.474** (matched backdoor/breach/cve/
firewall/harden/intrusion, not just one literal word). Zero deps, keyword fallback if it errors.
> Why not neural embeddings? This Ollama runs **without `--embeddings`** (enabling it is a
> gated fleet-wide change), and a paid embedding API is off the $0-local default. The
> lexical-semantic scorer is the honest $0/no-dep upgrade; swap in Ollama/OpenAI embeddings
> in `semantic.Scorer` if that changes.
## Next steps (not done — future work)
- Add an A2A *client-side* helper using the SDK's own `create_client` (proto
`SendMessageRequest` + async `StreamResponse`) alongside the raw-JSON-RPC client.
- Serve the generated cabinet agent cards (see `cabinet_cards.py`) from a live A2A
directory so officers/directors are discoverable and callable over the protocol.