@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

Packages

Package details

extensionskill

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-a1b2c3 vs project-a-d4e5f6). Set SYNADIA_PI_NONCE env 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_requestfriend_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 supportsensitivity: "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:

  1. SYNADIA_PI_NONCE env var — explicit override for debugging
  2. PI_A2A_NKEY_PUB env 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).
  3. 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:

  1. A2A pending reply (regular A2A messages) → NATS publish to reply inbox
  2. H2A pending responsePromptResponse.send()
  3. Sync sender → Queue in outbox for 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.