@vchatemail/pi-a2a-network
vchat.email A2A coordination extension for pi.dev — agent discovery, messaging, and fabric membership. Installed via curl -fsSL https://vchat.email/join.sh | sh
Package details
Install @vchatemail/pi-a2a-network from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@vchatemail/pi-a2a-network- Package
@vchatemail/pi-a2a-network- Version
0.1.2- Published
- Jul 15, 2026
- Downloads
- 201/mo · 201/wk
- Author
- chatek.co
- License
- Apache-2.0
- Types
- extension, skill
- Size
- 209.7 KB
- Dependencies
- 6 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
],
"skills": [
"./skill/SKILL.md"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-a2a-network
Agent-to-agent coordination for pi.dev via local NATS. Single-user, single-machine. Auto-managed server, token auth, persistent roster, offline queue, group messaging, opt-in buddy protocol, and multi-fabric peer discovery.
pi-a2a turns every pi.dev session into a discoverable, addressable agent on a
shared local NATS fabric. Multiple pi instances (even from different project
directories) discover each other, sync mailboxes, broadcast to groups, queue
offline messages, and build a persistent buddy roster — all on the same NATS
server at 127.0.0.1:14222.
Uses the Synadia Agent Protocol v0.3 wire format — same chunk protocol as H2A prompts.
Features
- Multi-fabric discovery — Each project directory is its own "fabric".
All agents share the same local NATS server. The TUI panel groups discovered
agents per fabric:
● Discovered (project-a): ● project-a-a1b2c3 ● project-a-3689c4 ● Discovered (project-b): ● project-b-d0530c - Presence state machine — Explicit status transitions (online → busy → idle → offline) carried in heartbeat payload. Peer agents detect status in real time.
- Per-fabric roster — One JSON file per fabric at
~/.pi/agent/roster.json,~/.pi/agent/roster.{fabric}.json. Buddies are stored with their source fabric and preserved across restarts. Cross-fabric buddies persist (not pruned by stale cleanup). - Unique per-process identity — Each pi process generates a random 6-char nonce
in memory. Concurrent instances get different agent IDs (e.g.
project-a-a1b2c3vsproject-a-d4e5f6). SetSYNADIA_PI_NONCEenv var to pin for debugging. - Bidirectional mailbox sync — Push outgoing + pull incoming messages in one round trip. No AI wait during sync — AI responses queued in outbox for next handshake.
- Offline message queue — File-based store-and-forward at
~/.pi/agent/pending/{fabric}/. Queued when target is offline, drained on session_start. - Group messaging (N:N) — Subscribe to
agents.a2a.group.<id>to join. Subscription IS membership. Fire-and-forget broadcast. - Friend request protocol — Opt-in buddy list via A2A intents:
friend_request→friend_accept/friend_reject. Buddies from any fabric. - Token authentication — Shared token scoped to
~/.config/pi-a2a/server.seed. Unauthorized local processes cannot connect. - Peer discovery — Find other agents via
$SRV.INFO.agents(NATS Micro), grouped by fabric metadata. Your own agent appears in the discovered list too. - JSONL-persisted inbox — A2A messages stored in pi.dev's session format.
Survives restarts, browseable via
/resume,/tree. - Rate limited — Per-sender token bucket (5 msg/s, burst 10). Sync exempt.
- Sensitive message support —
sensitivity: "silent"suppresses TUI notification. - TUI agent panel — Right-side overlay showing My / Buddies / per-fabric Discovered sections with color-coded status dots.
Architecture
All Projects Share One NATS Server
Unlike the original design (separate server per project), all pi instances
connect to the same local NATS at 127.0.0.1:14222. Fabric isolation is
done through the fabric metadata field in each agent's NATS Micro
service registration. The project directory name determines the fabric ID.
Shared NATS server (127.0.0.1:14222)
agents.hb.pi.{owner}.project-a-a1b2c3 (fabric: "project-a")
agents.hb.pi.{owner}.project-b-d0530c (fabric: "project-b")
agents.hb.pi.{owner}.project-c-e7f3a1 (fabric: "project-c")
~/.pi/agent/
├── roster.json # current project fabric roster
├── roster.project-b.json # project-b fabric roster
├── roster.project-c.json # project-c fabric roster
├── pending/
│ ├── local/ # current fabric pending
│ ├── project-b/ # project-b pending
│ └── project-c/ # project-c pending
└── sessions/ # pi.dev session files (JSONL)
FabricRegistry
FabricRegistry in src/a2a/fabric.ts manages the single NATS connection
and scopes rosters/queues by fabric. It does NOT maintain separate connections
or agent registrations per fabric — all routing goes through the same local
NATS server.
| Function | Implementation |
|---|---|
| Connect | Single nats.connect() per session |
| Register | Single AgentService on that connection |
| Heartbeat | Single heartbeat loop |
| Discover | $SRV.INFO.agents on local connection, grouped by fabric metadata |
| Roster | Per-fabric JSON file (roster.{fabric}.json) via RosterStore(fabric) |
| Queue | Per-fabric pending dir (pending/{fabric}/) via OfflineQueue(fabric) |
| Health | $SRV.PING for RTT (●/◐/○ in panel) |
Cross-Fabric Buddies
Buddies added from another project (e.g. adding project-b-d0530c from
a project-a session) are stored in the current project's roster with their
source fabric. They are preserved by stale cleanup (only same-fabric non-buddies
are pruned) and appear in the Buddies section regardless of fabric.
Agent Identity
The session name is <project-name>-<nonce> (e.g. vchat-email-a1b2c3).
The nonce is resolved in this priority order:
SYNADIA_PI_NONCEenv var — explicit override for debuggingPI_A2A_NKEY_PUBenv var — NKey-backed persistent identity. Nonce stored at~/.pi/agent/identities/<nkey_prefix>. Same NKey → same name across restarts. Different NKey → different name (safe for concurrent instances).- Random in-memory — ephemeral, changes on process restart (no NKey available)
# Stable identity across restarts
PI_A2A_NKEY_PUB=<base64url-32byte-ed25519-pubkey> pi
# Debug pinning
SYNADIA_PI_NONCE=deadbe pi
Storage Layout
~/.pi/agent/
├── ipc/ # File-based IPC (legacy bridge)
├── roster.json # Current fabric's roster
├── roster.{fabric}.json # Other fabrics' rosters
├── pending/ # Offline message queue (per-fabric)
├── identities/ # Per-NKey nonce files (`<nkey_prefix>` → nonce)
│ ├── local/
│ └── {fabric}/
└── sessions/ # pi.dev session files (JSONL)
└── --<path>--/
└── <ts>_<uuid>.jsonl
Subject Topology
agents.hb.pi.{owner}.{name} — Heartbeat (§8.3)
agents.status.pi.{owner}.{name} — Status request (§8.7)
agents.prompt.pi.{owner}.{name} — H2A prompt (§5)
agents.a2a.pi.{owner}.{name} — A2A mailbox (custom)
agents.a2a.group.{id} — Group broadcast (custom)
$SRV.INFO.agents — Peer discovery (§4)
Response Routing Priority
When pi's agent finishes processing, responses are routed in this order:
- A2A pending reply (regular A2A messages) → NATS publish to reply inbox
- H2A pending response →
PromptResponse.send() - Sync sender → Queue in
outboxfor next sync handshake
File Map
A2A/pi-a2a/
├── index.ts # Main extension entry point
├── config/
│ └── nats-ipc.conf # Local NATS server config
├── scripts/
│ └── setup.sh # Token generation, config install
├── skill/SKILL.md # A2A skill definition
├── src/a2a/
│ ├── protocol.ts # Types, subject builders, encoding
│ ├── fabric.ts # FabricRegistry — fabric tracking, discovery
│ ├── presence.ts # Heartbeat publisher with status machine
│ ├── roster.ts # File-based KV roster store (per-fabric)
│ ├── offline-queue.ts # File-based offline message queue (per-fabric)
│ ├── inbox.ts # JSONL-backed A2A inbox (via SessionManager)
│ ├── rpc.ts # a2aDeliver, a2aCollect, a2aSync, requestStatus
│ ├── group.ts # N:N group messaging
│ ├── agent-panel.ts # TUI agent list panel (multi-fabric sections)
│ ├── bridge.ts # File-based IPC bridge (legacy)
│ ├── chat-sidecar.ts # H2A chat sidecar
│ └── validate.ts # Runtime message validators
└── test/
└── a2a.test.ts # Integration tests
Quick Start
# 1. Install nats-server (if not already installed)
brew install nats-server
# 2. Generate auth token, install config, load launchd plist
cd A2A/pi-a2a
bash scripts/setup.sh
launchctl load ~/Library/LaunchAgents/io.pi.a2a.plist
# 3. Verify the NATS server is running
tail -f /tmp/pi-nats/server.log
# 4. Launch pi — the extension auto-discovers and registers
pi
# 5. Open a second terminal in a different project — both agents
# appear on the same fabric, grouped by project name
cd ../another-project && pi
Usage
Commands
| Command | Description |
|---|---|
/a2a-sync <owner> <name> |
Sync outbox with peer agent |
/a2a-sync <owner> <name> <message> |
Queue message, then sync |
/a2a-inbox |
Show pending inbox messages |
/a2a-respond <id> <response> |
Respond to an inbox entry |
/a2a-who [owner] |
List agents on fabric + roster buddies |
/a2a-add <owner> <name> |
Add as buddy (+ send friend request if online) |
/a2a-remove <owner> <name> |
Remove specific agent from roster |
/a2a-purge |
Remove dead buddies + purge outbox/inbox messages for dead agents |
/a2a-restart |
Restart the local NATS server |
/a2a-stop |
Stop the local NATS server |
/roster-list [all|buddies|recently-met] |
List roster entries |
/a2a-request-friend <owner> <name> |
Send friend request |
/a2a-accept <owner> <name> |
Accept friend request |
/a2a-reject <owner> <name> |
Reject friend request |
/a2a-join <groupId> |
Join a group |
/a2a-leave <groupId> |
Leave a group |
/a2a-broadcast <groupId> <message> |
Broadcast to group |
/a2a-groups |
List joined groups |
/a2a-panel |
Toggle agent panel visibility |
A2A Tools
| Tool | Description |
|---|---|
a2a_sync |
Sync with another agent: push queued messages AND pull pending messages |
a2a_who |
Discover other agents on the NATS fabric |
a2a_inbox_check |
Check pending A2A messages in the inbox |
Configuration
pi-a2a connects to nats://127.0.0.1:14222 by default. Override via:
# Override NATS URL
PI_A2A_NATS_URL="nats://<host>:<port>" pi
# Override auth token
PI_A2A_NATS_TOKEN="<token>" pi
# Pin agent nonce (debugging)
SYNADIA_PI_NONCE="deadbe" pi
# NKey-backed persistent identity (stable name across restarts)
PI_A2A_NKEY_PUB="<base64url-32byte-ed25519-pubkey>" pi
# Override fabric ID (default: project directory name)
PI_A2A_FABRIC="my-fabric" pi
Config Chain (highest priority first)
| Priority | Source | Field |
|---|---|---|
| 1 | PI_A2A_NATS_URL / PI_A2A_NATS_TOKEN env vars |
env var |
| 2 | .pi/a2a-config.json in project root |
project config |
| 3 | ~/.pi/agent/a2a-config.json |
global config |
| 4 | nats://127.0.0.1:14222 / ~/.config/pi-a2a/server.seed |
default |
Design Docs
| Document | Scope |
|---|---|
A2A_design.md |
Local single-user pi-a2a (this package) |
A2A_NETWORK_design.md |
Multi-tenant production with segments, billing, zero-trust |
ROSTER.md |
Roster design, WeChat comparison, implementation roadmap |
AUDIT_log.md |
Security audit findings and fixes |
CHANGELOG.md |
Build plan and release history |
protocol.ts |
Source of truth for types and wire format |
Development
# Install dependencies
bun install
# Type check
npx tsc --noEmit
# Run tests (105 pass, 0 fail)
bun test
# Start local NATS server
bun run nats:start
License
Apache-2.0 — see LICENSE.