Turn several machines running the Pi coding agent into one team. Each node keeps its own filesystem, session and context; they discover each other and exchange messages over your tailnet.
No public relay, no phone app, no account.
laptop dev01
┌──────────────┐ ┌──────────────┐
│ pi dev01-web│◄──── messages ────────►│ pi srv-api │
└──────────────┘ └──────────────┘
- Nodes find each other. Every node appears in a roster with its machine and labels.
- Send to one, several, a label group, or everyone.
other,a,b,@web,*. - A teammate's message reaches the model without interrupting it. It is delivered as a custom message: while the agent is mid-tool-call it queues, and the model sees it at the next turn instead of having its work cut short.
- The model answers with
team_send. No output is mirrored automatically, so a run that was doing something else cannot have its tail sent to the wrong peer as a "reply". - The conversation ends. A request is answered; a reply is not. Two agents cannot talk in circles.
- You can see it.
📥 RECV/📤 SEND/🔁 REPLY/⚠️ FAILcards in the transcript. Display only — they never enter the model's context. - Two entry points.
/teamcommands for you,team_*tools for the model. Both share one implementation, so they cannot behave differently. - An agent skill ships with it, so the model knows the commands without being told.
pi install npm:@yiki21/pi-agent-teamOr from GitHub:
pi install git:github.com/Yiki21/pi-agent-teamRequires Node 22+ and Pi. No runtime dependencies. Everything below assumes this is installed — there is no path to type.
All three share one API and pass one conformance suite, so they behave the same. Pick one at a time.
| mode | how nodes find each other | how messages travel | needs |
|---|---|---|---|
broker (default) |
a broker you run pushes the roster | through the broker | a reachable broker URL |
mesh |
nodes exchange member tables directly | direct node-to-node | one seed address |
swim |
SWIM gossip (Go sidecar) | direct node-to-node | a seed, plus the sidecar built |
Start with broker. It is one process, it survives nodes coming and going,
and nothing needs to know anyone else's address in advance.
Use mesh when you want no centre and every node can reach every other. Use
swim when nodes actually die without saying goodbye and you need the roster to
notice.
You need a token first. Anyone can generate one; everyone shares it.
openssl rand -hex 32Run the broker on one always-on machine. It does not need Pi, only Node:
TEAM_TOKEN=<token> npx -y -p @yiki21/pi-agent-team pi-agent-team-broker --bind "$(tailscale ip -4)"pi install puts the package somewhere that is not on your PATH, so the
broker command is run through npx rather than by name.
For a permanent broker, use systemd — a broker started from a terminal dies when you close it, and one in systemd comes back after a reboot. A unit file plus the reasoning is in docs/systemd.md.
Then each node:
/team join dev --url http://<tailnet-ip>:8787 --token <token>
No broker. The first node starts, everyone else points at it:
# first node
/team join dev --mode mesh --token <token>
# the address to hand out is printed by:
/team status
# every other node
/team join dev --mode mesh --seeds 100.64.0.1:19801 --token <token> --name dev01-web
For swim, add --mode swim. It also needs a sidecar, which you build once:
cd ~/.pi/agent/npm/node_modules/@yiki21/pi-agent-team/swim
go build -o ../swim-sidecar .swim seeds use the gossip port, not the delivery port. /team status
prints the right one.
/team # interactive menu
/team peers # who is online
/team send other hello
/team send "@web" deploy is starting
/team send '*' maintenance in 5 minutes
Every option works three ways. They share one implementation, so what you can set one way you can set every way.
Startup — environment variables. For scripts, containers, systemd:
TEAM_NAME=dev01-web \
TEAM_MODE=broker \
TEAM_URL=http://100.64.0.1:8787 \
TEAM_TOKEN=<token> \
TEAM_LABELS=web \
piCommands — for you. Options are the same names:
/team join dev --url http://100.64.0.1:8787 --token <token> --name dev01-web
/team mode mesh
/team join dev --mode mesh --seeds 100.64.0.1:19801
Tools — for the model and automation. team_join, team_info,
team_roster, team_send, team_label, team_leave. team_join takes the
same options as /team join.
| Option | Meaning | Saved? |
|---|---|---|
url |
broker address (broker mode) | yes |
token |
team token | yes |
mode |
broker / mesh / swim |
yes |
seeds |
mesh/swim seed addresses | yes |
name |
this node's name | no |
labels |
this node's labels, for @label sends |
no |
port |
mesh/swim listening port (0 = pick one) | no |
listen |
mesh/swim listening address | no |
Reply behaviour is a separate setting, not a connection option:
reply |
What it does |
|---|---|
off |
Never reminds. Messages still arrive; answering is the model's call |
remind (default) |
Reminds once when a request goes unanswered |
mirror |
Mirrors every turn's output to all nodes as fyi (both sides on means both keep posting) |
Set it with reply= / TEAM_REPLY / --team-reply, or /team reply <mode> at
runtime. The older names still work: TEAM_ANNOUNCE, --team-announce,
/team announce, and the values auto (now remind) and always (now
mirror). Using one prints a note saying what it is called now — auto and
always no longer describe what the mode does, which is why they were renamed.
The last four are not saved because one machine can run several Pi agents and they share one config file. Saving the name would have the second agent overwrite the first; saving the port would have it try to listen on a port already in use. Pass them on each run.
/team join dev --mode mesh --seeds 100.64.0.1:19801 --name dev01-web --port 19801
/team mode switches mode without retyping url or token.
docs/how-it-works.md covers:
- the three transports in detail, and which to pick
- what a message flows through, and the rules that end a conversation
- SWIM mode: why the sidecar is Go, and its two ports
- the security model, and what a stolen token gets an attacker
- design notes that were measured rather than assumed
- how to run the tests
- known limitations
- Node 22+
- Pi
- Tailscale or any private network — the broker binds a single non-public address
- Go, only for
swimmode
MIT