A runnable proof that the rvagent-a2a stack (ADR-159) works end-to-end
between multiple independent peers.
-
Three signed-AgentCard peers, each running as its own
rvagent a2a serveprocess on a distinct port: -
Independent policies and budgets — each node loads its own
configs/node-*.tomlwith its own[policy],[budget], and[recursion]caps. -
Signed AgentCard discovery — the orchestrator fetches
/.well-known/agent.jsonon each peer and verifies it parses as a well-formedAgentCardwith at least one skill. -
Task dispatch over HTTP JSON-RPC — the orchestrator sends an
echotask to each peer (including the router) via the CLI's owna2a send-tasksubcommand and assertsstate == "completed".
cd examples/a2a-swarm
cargo run --releaseThe binary auto-resolves the rvagent executable by walking up to the
workspace target/{release,debug}/ directory. If you prefer to build
it explicitly first:
cargo build --release -p rvagent-cli
cargo run --release -p a2a-swarmTypical successful run:
INFO using rvagent binary: /.../target/release/rvagent
INFO node is listening name="node-cheap" bind=127.0.0.1:18001 ...
INFO node is listening name="node-fast" bind=127.0.0.1:18002 ...
INFO node is listening name="node-router" bind=127.0.0.1:18003 ...
INFO discovered signed AgentCard name="node-cheap" skills=1 ...
INFO discovered signed AgentCard name="node-fast" skills=1 ...
INFO discovered signed AgentCard name="node-router" skills=1 ...
=== a2a-swarm demo summary ============================================
node-cheap bind=127.0.0.1:18001 state=completed took= Nms ok=true
node-fast bind=127.0.0.1:18002 state=completed took= Nms ok=true
node-router bind=127.0.0.1:18003 state=completed took= Nms ok=true
-----------------------------------------------------------------------
dispatched to router at 127.0.0.1:18003: state=completed ...
peer pool (would be CheapestUnderLatency targets in M3):
- node-cheap @ 127.0.0.1:18001
- node-fast @ 127.0.0.1:18002
=======================================================================
INFO node exited name="node-cheap" ...
INFO node exited name="node-fast" ...
INFO node exited name="node-router" ...
The orchestrator exits 0 only if the router's task reached
state == "completed".
- Tests 1 + 2 — remote ≡ local, constant-size memory. The task
dispatch uses real HTTP with real signed AgentCards. The
JSON-RPC request/response matches the local
InMemoryRunnerpath byte-for-byte at theTasklevel. - Test 3 — bounded cost under recursion. Each node loads its own
[budget.global]section into a per-processBudgetLedger. A runaway peer can't burn into a sibling's budget; recursion depth is capped per-node atmax_call_depth = 4.
- Router-forwards-over-HTTP is not yet wired. ADR-159 r2 defines
PeerSelector+PeerRegistry, but the currenta2a servedoesn't seed the registry from the TOML[routing]section — peers arrive via the discovery cache, which is M3. Until then,node-routerhandles tasks locally and the orchestrator plays the role the selector will play later (picking which peer receives the task). - No live metrics. The selector reads EWMA cost + latency from the middleware rate-limit layer; this demo doesn't drive enough traffic to move the needle, so peer selection decisions rely on config defaults.
- The
fallbackselector string innode-router.tomlis parsed but not yet consulted by a running router — same M3 gap. - Keys are ephemeral. Every
--generate-keyinvocation mints a fresh Ed25519 keypair, soAgentIDchanges across runs. That's fine for the demo; production deployments would load a persisted key via$RVAGENT_A2A_SIGNING_KEY.
examples/a2a-swarm/
├── Cargo.toml # orchestrator package
├── README.md # this file
├── configs/
│ ├── node-cheap.toml # low-cost tier
│ ├── node-fast.toml # high-cost / low-latency tier
│ └── node-router.toml # CheapestUnderLatency dispatcher
├── src/
│ └── main.rs # spawns + probes + dispatches + tears down
└── .gitignore
