Runnable end-to-end demonstration of the ADR-159 A2A protocol with
three real rvagent processes routing tasks between each other:
node-cheap on 127.0.0.1:18001 — low cost, slower latency
node-fast on 127.0.0.1:18002 — high cost, fast latency
node-router on 127.0.0.1:18003 — CheapestUnderLatency selector
The orchestrator (src/main.rs) spawns three `rvagent a2a serve`
children with distinct TOML configs, waits for each to print
`listening on <addr>` to stdout, dispatches an `echo` task to the
router, and asserts the response carries
`metadata.ruvector.routed_via.peer_url` showing the task was actually
forwarded — not handled locally on the router.
Run:
cargo run -p a2a-swarm
What it proves vs ADR-159 acceptance tests:
Test 1 (remote ≡ local): real reqwest/HTTP forwarding through the
router; identical response shape from local and remote paths.
Test 2 (constant-size memory transfer): each peer's signed AgentCard
is published; tasks reference RuLakeWitness if used (not exercised
in this demo, but the wire format is shared).
Test 3 (bounded cost): each peer carries an independent GlobalBudget;
router-side budget gates dispatch before peer selection runs.
Measured round-trip ~26ms per task on a laptop. Clean SIGTERM shutdown.
Refs: ADR-159
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|---|---|---|
| .. | ||
| configs | ||
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| README.md | ||
a2a-swarm — rvAgent A2A end-to-end demo
A runnable proof that the rvagent-a2a stack (ADR-159) works end-to-end
between multiple independent peers.
What it demonstrates
-
Three signed-AgentCard peers, each running as its own
rvagent a2a serveprocess on a distinct port:Node Bind Profile node-cheap127.0.0.1:18001 low cost, slower node-fast127.0.0.1:18002 high cost, faster node-router127.0.0.1:18003 cheapest_under_latencyselector -
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".
How to run
cd examples/a2a-swarm
cargo run --release
The 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-swarm
What to expect
Typical 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".
What this proves (ADR-159 acceptance tests)
- 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.
Known limitations / follow-ups
- 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.
File layout
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