Real-time agent-coordination substrate for PulseEngine — named agents subscribe to channels and coordinate/agree, with every message a typed, signed, traceable fact. Augments (does not replace) the GitHub-issue coordination loop.
Status: spike. This proves the architecture + the cross-talk controls run on the lighter (NATS-core + wasmtime + WAC) path, with NATS and sigil stubbed at their seams. It is the input to the lighter-vs-wasmCloud decision.
| Layer | What | Built with |
|---|---|---|
| Named agents | each agent = a capability-isolated wasm component | wasm component, A2A-style identity |
| Coordination logic | type / sign / shape / decide | wasm components, WAC-composed |
| Protocol contract | channel · message · speech-acts | a WIT world (agora:agent) |
| Durable spine (native TCB) | named channels, durable fan-out, replay | NATS/JetStream (self-hosted) |
| Record | every message → signed fact | rivet + sigil (Rekor-style) |
| Human window | watch live + inject + out-of-band kill | thrum |
The durable transport is deliberately native (NATS), not wasm: wasi:messaging
disclaims persistence/ack/delivery, so reimplementing it in wasm is gold-plating.
"WCM to the extreme" is spent where it pays off — the logic, capability
isolation, and verification (witness/scry/sigil) — not the transport.
agent/ is a real wasm component (pure coordination logic, no ambient
authority). host/ is the transport + capability layer (where NATS and sigil swap
in) that runs it on wasmtime and enforces the cross-talk controls the research
validated against the Hermes infinite-ack-loop postmortem
(NousResearch/hermes-agent#32791):
- capability channel-scoping (structural) — agents hold no handle to
secret-ops, so it is never delivered to them. 8 deliveries blocked. - unconditional self-echo filter (Hermes rule #1) —
sender == medropped on every channel, never per-channel overridable. 12 echoes dropped. - hop-count / TTL — the deliberately chatty agent would loop forever (the Hermes failure); the hop budget bounds it (3→2→1→0) and it converges.
- idempotency — each (agent, message-id) processed once.
- signed identity + speech acts + rivet record — every message carries a
(stubbed) sigil signature, a FIPA-style
act, and is mirrored tofacts/coordination.yamlas a typed rivet fact.
# Canonical: hermetic component build via the PulseEngine ruleset (Bazel) —
# native wasm32-wasip2 through wasi-sdk 29 (= WASI 0.2.6).
bazel build //agent:agent
# Quick path (same native-p2 component, no preview1 adapter):
cd agent && cargo component build --release --target wasm32-wasip2
# Run the host (loads the p2 component, enforces the cross-talk controls):
cd host && cargo run --release # `cargo test` asserts the controls (8/12/converge/6)- NATS/JetStream — the host's in-memory
busVec stands in for the durable log. Real JetStream gives the global sequence (ordering), durable consumers (= the watermark/pending_gates replay), andNats-Msg-Iddedup. - sigil —
sigis an FNV stub; realwsc sign --keylessswaps in (blocked onpulseengine/sigil#164, the wasip2 parser). - rivet — facts are written as YAML; real
rivet(0.17 present) ingests them. - out-of-band human kill — Hermes rule #2: thrum must hold a privileged kill at the gateway, not an in-channel "stop". Not in this spike.
The lighter path works and is fully functional. Friction encountered (the real decision input):
cargo componentstill defaults its core target to the legacywasm32-wasip1(preview1 + adapter), and honors neither.cargo/config.tomlbuild.targetnor a metadata key — so the build pins--target wasm32-wasip2explicitly. That yields a native component-model component (importswasi:io/wasi:cli@0.2.x, no preview1 adapter); the host'swasmtime_wasi::p2linker satisfies it.fromis a reserved WIT keyword (→sender).stdpulls WASI imports, so the host needs awasmtime-wasilinker + the version-specificWasiView/WasiCtxViewboilerplate (had to read the crate source to get the 41.x API right). This host-embedding plumbing is exactly what wasmCloud would absorb — at the cost of running wasmCloud as a system and its transport providers being native anyway.
Read: for a small team building one substrate, the lighter path is viable; the friction is one-time host plumbing. wasmCloud is the graduation path if the lattice features (wadm, multi-host, provider ecosystem) start paying for themselves.
WASI 0.3.0 (Preview 3) was ratified 2026-06-11 — it rebases WASI onto the
Component Model's native async primitives (async func, stream<T>, future<T>).
This spike deliberately builds on stable wasm32-wasip2 today, not preview1 and not
p3, because:
- The agent is pure coordination logic — its only WASI surface is what
stdpulls in; it gains nothing concrete from p3's async streams. - The Rust
wasm32-wasip3target is still tier-3 ("does not yet build" without alibc[patch]; needs nightly +-Z build-std+wasi-sdk ≥22), and itsstdstill emits p2 imports during the transition — so a p3 build today would add major toolchain friction (contradicting the lighter-path thesis above) for p2 imports anyway. - p3 host support lands in wasmtime 43+; this host is on 41.
Where p3 actually pays off for agora — and the adoption path when we take it:
- The transport seam (the real win). p3's
stream<T>/future<T>map cleanly onto JetStream consumers — backpressure, ordering, and async delivery become first-class in the WIT contract instead of host plumbing. This is the layer that's stubbed today (the in-membus), so it's the right place to adopt p3. - Host: bump
wasmtime/wasmtime-wasi41 → 43+ and switchwasmtime_wasi::p2::add_to_linker_sync→p3::add_to_linker_async(async linker, asynccall_coordinate). - Agent: move to
wasm32-wasip3once it reaches tier-2 andstdmigrates off p2 imports — then the gap closes with nobuild-std/wasi-sdk friction.
See: wasi.dev/roadmap, rustc — wasm32-wasip3, Async Components on wasmCloud with WASI P3.