npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yiki21/pi-agent-team

v0.6.0

Published

Turn several machines running the Pi coding agent into one team over your tailnet. Three transports (broker / mesh / SWIM), one shared API and conformance suite.

Readme

pi-agent-team

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  │
 └──────────────┘                        └──────────────┘

What it does

  • 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 / ⚠️ FAIL cards in the transcript. Display only — they never enter the model's context.
  • Two entry points. /team commands 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-agent-team or A2A?

@bacnh85/pi-a2a also connects Pi to other agents, over the A2A protocol. Neither package replaces the other. The difference that decides it is where an incoming message runs.

pi-agent-team delivers a teammate's message into the Pi session that is already running. The model that reads it has that session's context: the task in progress, what it already tried, what it has changed. It answers with team_send when it chooses to.

pi-a2a starts a new, isolated Pi session for each incoming task, runs it to completion and returns the reply as a task artifact. That session works in the same repository but starts with an empty context. Anything the job needs has to be in the message or found again on disk.

So pi-agent-team is for agents that keep working and talk along the way. A2A is for handing a self-contained job to an agent and getting a result back.

| | pi-agent-team | pi-a2a | |---|---|---| | Who handles an incoming message | the running session, at its next turn | a new isolated session per task | | Who it can talk to | Pi nodes running this extension | any A2A agent: Pi, Hermes, Google ADK, LangChain, CrewAI | | Addressing | name, list, @label, * | peer name or URL; a2a_orchestrate fans out by advertised capability | | Finding peers on other machines | broker roster, mesh seeds or SWIM gossip, over the tailnet | static peers config, mDNS on one LAN, or a self-hosted a2a-switchboard gateway | | Stopping two agents looping | re marks a reply, and a reply is never answered | a turn cap per conversation (default 5) | | Long jobs | the sender does not block; the answer arrives later as a team_send | returnImmediately acks at once, a2a_status polls by task id | | Following a peer | /team watch shows its turn output without waking you | none | | Auth | one shared team token | shared or per-peer tokens, plus audit log, rate limits, inbound injection filtering and outbound redaction |

Compared against pi-a2a 0.7.13.

Use pi-agent-team when

  • every agent is Pi, on machines you control, connected by a private network.
  • the message must reach the session doing the work. "How far along is the migration?" only makes sense to the session running it.
  • you split work by role and address it by label, such as @review or @runner.
  • you want to follow another agent's conclusions with /team watch without starting a turn of your own.

Use pi-a2a when

  • the other agent is not Pi, or not yours: Hermes, Google ADK, LangChain, CrewAI, anything that speaks A2A.
  • the job is self-contained and you want a result, not a conversation.
  • the job runs longer than you want to wait, and you would rather poll it by task id.
  • callers need separate identities, an audit trail or rate limits.

Both at once

The tool names do not overlap (team_* and a2a_*), and pi-a2a's inbound server stays off until you start it. One workable split is pi-agent-team between your own Pi nodes, and pi-a2a's outbound a2a_call for agents outside the team.

Install

pi install npm:@yiki21/pi-agent-team

Or from GitHub:

pi install git:github.com/Yiki21/pi-agent-team

Requires Node 22+ and Pi. No runtime dependencies. Everything below assumes this is installed — there is no path to type.

The three modes

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.

Use it

You need a token first. Anyone can generate one; everyone shares it.

openssl rand -hex 32

Option A — broker

Run 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>

Option B — mesh or swim

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.

Then talk

/team                     # interactive menu
/team peers               # who is online
/team send other hello
/team send "@web" deploy is starting
/team send '*' maintenance in 5 minutes

Configure

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 \
  pi

Commands — 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.

Resuming a session

pi --resume reconnects to the same team with the same name and labels, and says so in a notice.

What is remembered lives in the session, not in the shared config file, so it stays per-session: join a different team in one Pi and the others are unaffected. Nothing secret goes in the session — the token is read from the team config file as usual.

Three things it deliberately does not do:

  • --fork does not reconnect. Two branches under one name would keep replacing each other. A fork starts unbound.
  • Flags and environment variables still win. --team-name or TEAM_NAME overrides the remembered name.
  • /team leave is remembered too. Resume after leaving does not join again.

Changes made with /team join --name or /team label are recorded when the connection is live, so resume picks up the latest values.

Watching a peer's answers

reply is about how you answer others. Subscribing is the inverse: it is how you follow what a specific peer concludes, without that peer doing anything differently.

/team watch add dev02      # follow dev02's answers
/team watch list
/team watch remove dev02

the model-facing equivalent is team_watch({ action, target }).

What you get: after each of dev02's turns, its final output arrives as a SUB card with the first 200 characters. What you do not get: a new turn. The card never wakes your model, so reading it is something you do when you next run. That is also what keeps two mutually-subscribed nodes from looping — the notification cannot start anything.

Things worth knowing:

  • Broker mode only. Mesh and swim have no broker to hold the subscription list, so team watch fails with a reason rather than silently doing nothing.
  • The peer is not told. It receives no receipt or acknowledgement of any kind, and a publisher that nobody is watching is told so. There is no way for a peer to detect that it is being watched.
  • At most 8. Rejected one at a time so you can choose what to drop; nothing is evicted silently.
  • Not persisted. A broker restart drops subscriptions; each subscriber re-registers on reconnect. Notifications during the gap are lost, the same way messages in flight are.
  • Summaries are bounded and merged. A peer emitting in a tight loop cannot flood a subscriber: notifications are rate-limited and the skipped ones are reported as a count on the next one, never dropped silently.

reply=off does not disable subscription publishing. The two settings are independent.

How it works

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

Requirements

  • Node 22+
  • Pi
  • Tailscale or any private network — the broker binds a single non-public address
  • Go, only for swim mode

License

MIT