@geminixiang/pi-agent-team
Rule-agnostic independent Pi agent sessions with private mailboxes and causal audit
Package details
Install @geminixiang/pi-agent-team from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@geminixiang/pi-agent-team- Package
@geminixiang/pi-agent-team- Version
0.3.0- Published
- Aug 12, 2026
- Downloads
- 536/mo · 536/wk
- Author
- geminixiang
- License
- MIT
- Types
- extension, skill
- Size
- 178.1 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/extension.ts"
],
"skills": [
"./skills"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@geminixiang/pi-agent-team
A rule-agnostic, mailbox-driven agent team runtime for Pi.
Install
Install as a pi package — this registers team_start, the retained-team control tools, and the bundled pi-agent-team operator skill in your pi sessions:
pi install npm:@geminixiang/pi-agent-team
Or try it once without installing (temporary for the current run only):
pi -e npm:@geminixiang/pi-agent-team
pi remove npm:@geminixiang/pi-agent-team uninstalls it. Use pi install -l to write to project settings (.pi/settings.json) instead of user settings.
What ships
The bundled pi-agent-team skill teaches the parent agent when to use foreground or detached mode, how to observe and intervene without polling or unsolicited guidance, and how to continue a settled retained team. It is withheld from member sessions, which lack team_start, while their other installed skills remain available.
Production code knows nothing about relay counting, werewolf, expected answers, roles, or phases. It provides only generic coordination — including a generic vote tally, not a domain-specific one: the runtime counts opaque choices and reports ties honestly, it never knows what a vote is for.
- independent member identities and unique Pi
AgentSessions with full pi-coding-agent capability (only extensions are withheld, so a member cannot recursively start teams); ids are validated unique, non-empty, and free of collisions with the runtime's own reserved principal ids ("user", "runtime") or the extension's "all" sentinel; settlement ends one coordination round but retains these sessions, soteam_promptcan address any member afterward and start a fresh round over the same first-person histories; - member sessions persisted to the project's default Pi session directory (named
agent team · <name> (<id>), linked to the parent viaparentSession), so every member's full first-person history survives the run — and even a mid-run crash — and is readable/resumable with ordinary Pi session tooling; the result carries each member'ssessionIdandsessionRef(file path); - a post-settlement report turn: the reporter — pre-designated via
reporterId, or whoever last validly held the special"reporter"claim (kept on finish, renounced by voluntary release, cleared on error) — gets one final prompt after the team settles, and its response is returned verbatim as the team's report;reportPromptpasses the caller's reporting instructions through untouched, and with no reporter the result honestly says so instead of the runtime writing one itself; - direct
team_dmand explicitteam_handoff; a recipient may be named by member id or by an unambiguous display name — a name shared by two members, or matching none, bounces rather than being guessed at; sending to yourself bounces the same way instead of silently vanishing; - public
team_say(passive by default; mentioning teammates intokeeps the message public while waking exactly the mentioned members to reply — the normal way to hold a conversation without a DM restating the public message) andteam_broadcast(interrupts everyone at once; use sparingly); - immutable restricted groups through
team_group_createandteam_group_send; - opaque atomic
team_claimwith explicitteam_release, auto-released when the owner finishes or errors; team_vote_open/team_vote_cast/team_vote_abstain/team_vote_close: a runtime-tallied poll, never self-declared by a member. Opening declares whether the initiator votes (so a moderator can stay outside the electorate), a bounded 0–3 idle-time reminder budget, and whether exhausted non-responders remain missing or become explicit abstentions. Abstention is first-class and can never win as a textual choice. Once every eligible member votes or abstains, the initiator is directly woken to close; ties are reported honestly, choices remain opaque exact-match strings, and terminal non-responders leave quorum while blocked members remain eligible. Legacy claim-then-cast callers retain the former all-member, no-reminder behavior;- creating a group requires already holding a claim on that exact id — enforced by the runtime, not left to doctrine, so two members racing to set up the same kind of thing in the same wave can't both succeed and leave a mess to reconcile; a brand-new poll id needs someone to hold that claim before the first vote lands, but any member may then cast, including the first vote — the claim only guards against a second, unrelated poll under the same name;
- a same-source wave (every member made ready by the identical envelope — the initial broadcast, or a
team_broadcast) executes sequentially instead of concurrently, so later members in that wave observe earlier members' already-applied claims/groups/polls before deciding what to do, instead of everyone independently attempting the same thing at once and reconciling duplicates afterward; when that shared envelope is the single initial post, the opening wave additionally gives every member an independent first take — a peer's openingteam_sayis held back until every member has drafted, then revealed to everyone together, so the coordination benefit of sequential wake-up doesn't come at the cost of anchoring; an explicit interrupt or mention reaching a still-undrafted member lifts its barrier early, merging the held-back public context into its mailbox in envelope order before the prompt that depends on it; - observation-queue wake-up,
team_wait,team_block, andteam_finish— each ends a member's turn immediately (the adapter self-aborts the session).team_block(reason)is an explicit non-terminal request for requester input: the reason is saved, the member stops waking, and a detached run parks without polling untilteam_promptresumes it; blocked members retain queued mail, claims, and poll eligibility; - process-local retained teams and optional detached execution:
team_start({ detached: true, ... })returns arunIdimmediately in long-lived TUI/RPC sessions, while a foreground run exposes the same handle asteamin its final manifest.team_getreads a bounded snapshot,team_waitwaits onstateChangeSeqwithout polling, andteam_promptintervenes with one member during a live round. After settlement, that same prompt starts a clean continuation round using its message as the new objective: coordination state resets, but every member's Pi session and history remain.team_cancelaborts the current round through a run-owned signal. Lightweight event history is capped at 512 entries per retained team, at most 64 teams are retained, and events exclude message bodies; live rounds are cancelled and all retained sessions disposed on parent-session shutdown; - a control-plane digest (member states, blocked reasons, held claims, own groups, open polls' live tally and missing voters) supplied on every wake;
- error degradation: a member whose turn fails becomes
errored, announced publicly, and messages to it bounce back to the sender; - a final flush wake that delivers undelivered passive observations before the team settles;
- hash-chained causal audit events with private bodies redacted;
- two live views for two audiences, both TUI-only: a Discord-style presence roster (who's speaking, who they last addressed, who's idle/finished/errored) pinned above the editor for the duration of the run, and a full blow-by-blow chat transcript in the tool-result card for debugging;
- a bounded final result: the model-visible tool content is the reporter's report plus a pointer-based manifest (settlement, per-member state/turns/session pointers, message and event counts, audit head) — never the raw transcript or event log, which once returned ~1.2M chars from a single game and forced a split-turn compaction of the parent session; the persisted display snapshot is likewise capped at the last 200 activities. The permanent record is the member session files, not the tool result.
Coordination model
The runtime follows CPU-style worker coordination rather than assigning a workflow order:
- all members start as symmetric workers when
startMemberIdis omitted; - member IDs and peer-list positions carry no scheduling meaning;
team_claimis an atomic compare-and-swap-like election/work-ownership primitive and a synchronization fence: it must be the only action in that response, and the caller may act as owner only after a later privateCLAIM_ACQUIRED;team_dmandteam_handoffare directed mailbox interrupts that wake exactly one worker;team_group_sendis restricted to an immutable group audience and wakes its other members;team_sayappends public speech without consuming an LLM turn; on its own it wakes no one, while its mentioned recipients (to) are woken like a directed interrupt;team_broadcastwakes everyone, at the cost of being the one way a member can trigger a same-source (sequential) wave;- public announcements are observed the next time a worker wakes for directed work, or immediately if sent via
team_broadcast; - the scheduler runs ready workers in concurrent waves — except a same-source wave, which runs sequentially — and reports
idle,ready,running,waiting,blocked,finished, anderroredseparately.
The parent should use opaque unrelated member IDs, send the same initial objective to all members, and let them elect coordination and task order. startMemberId remains only as an advanced directed-start escape hatch.
Local development use
From a checkout of this repo, load the extension straight from source:
pi --no-extensions -e ./packages/pi-agent-team/src/extension.ts --approve
Example prompt:
Call team_start once. Create 8 members with opaque, unrelated IDs and names. Do not encode order in IDs, names, or member array position. Objective: autonomously coordinate so every member publicly says exactly one distinct number and the public sequence is 1 through 8. Omit startMemberId and send every member the same initial message: "Elect a coordinator using team_claim. Negotiate an order using directed messages, not peer-list position. Directed handoff wakes the next worker; public speech does not wake peers. Each worker must call team_say with its number before team_finish. Complete without user help." Do not inject later messages or expected answers.
team_start accepts an objective, roster, one initial message, and either one starting member or all members; optional reporterId pre-designates who delivers the final report, and optional reportPrompt carries the caller's reporting instructions (format, files, language) verbatim into that final turn. By default it remains synchronous. With detached: true, use the returned runId with team_get, team_wait, team_prompt, and team_cancel; requester prompts are counted in userInterventions and attributed as user messages.
Honest acceptance scope
The experiments live only in test/acceptance/; production does not import them.
npm test --workspace @geminixiang/pi-agent-team
The LLM-free suite verifies transport and isolation properties reproducibly:
- a runtime-randomized, eight-member transformation chain whose operations are private to each adapter;
- unique session IDs, private canaries, causal send → delivery → wake evidence;
- removing a required stage prevents the runtime from inventing the expected result;
- an external moderator and eight opaque players complete a hidden-role game entirely through generic messages;
- game roles are shuffled and private; public evidence contains only hashes for private bodies.
- the turn-ending protocol (
TurnStateinsrc/turn-state.ts) — includingteam_blockself-abort/batch truncation — is fully covered without a live model, since it's pure: no session, no I/O; - blocked-member physics: stable quiescence, parked detached resumption, passive mailbox retention (including opening-wave reveals), claim and poll eligibility, honest intervention attribution, abort closure, digest reasons, and attention-first TUI ordering (
test/blocked.test.ts); - detached control-plane behavior: immediate start, monotonic non-polling waits, run-owned cancellation, explicit invalid/terminal errors, 512-event and 64-run caps, body-free activity summaries, and bounded report snapshots (
test/run-manager.test.ts); - poll policy and tallying — non-voting initiators, bounded reminders, explicit and automatic abstention, initiator-only completion wakes, clear winners, honest ties, terminal-aware quorum, immutable close results, claimed poll namespaces, opaque exact-match choices, legacy full-participation wakes, and zero-vote edge cases (
test/poll.test.ts). - same-source wave sequencing — a wave sharing one cause envelope runs member-by-member, each observing the prior members' committed claims; a wave from distinct envelopes, and a flush-promoted wave, are unaffected;
team_broadcastwakes every teammate from a single envelope; the opening wave specifically gives every member an independent first take (no peer speech leaks into their first observation batch) before revealing everyone's drafts together on the next turn (test/same-source-wave.test.ts). - recipient resolution — an id always resolves; a display name resolves only when it names exactly one member; two members sharing a name, or a name matching none, bounces instead of misdelivering; a self-addressed direct message or handoff bounces the same way instead of silently vanishing; PEERS never includes the waking member itself;
team_group_create's member list accepts the same id/name mix (test/recipient-resolution.test.ts). - member identity — empty, whitespace-only, and reserved-principal-id member ids are rejected by the runtime; duplicate ids and the extension's "all" sentinel are rejected before a raw roster array can silently fold into one
Mapentry (test/identity.test.ts). - the turn-ending and budget physics enforced by the core itself, not only the Pi adapter — any
TeamAgent's batch is truncated atwait/finish,maxTurnsis a real per-wave hard cap, andrun()may only be called once per instance (test/runtime.test.ts,test/coordination.test.ts). - the live prompt text (
formatTurninsrc/pi-agent.ts) — asserts what a member actually reads on wake, including that an open poll's tally and missing voters are rendered, not just carried silently in the digest object (test/pi-agent.test.ts).
These tests prove the generic runtime routes isolated adapters correctly. They do not by themselves prove that a particular LLM reasons independently. That requires a live-model run and transcript inspection. No live result should be reported as stronger evidence than its recorded session IDs, deliveries, wakes, and messages support.
Limits
- A
completedsettlement means only that all members calledteam_finish; it is not a scenario-specific correctness verdict. Aquiescentsettlement means no members are runnable while unfinished members remain —errored-members-remainis terminal failure,blocked-members-remainis an explicit requester wait (for non-detached runs), andno-runnable-membersmay be a genuine deadlock. Anexhaustedsettlement means the turn budget ran out with runnable members remaining, enforced as a real per-wave hard cap rather than only checked before a wave starts; the partial result is still returned. Inspect public speech and causal evidence before claiming that the objective succeeded. - A synchronous
team_startremains foreground until its first round settles. Its manifest'steamid is then accepted byteam_getandteam_prompt; prompting a settled member continues the retained team in a new background round. Retained teams and their controls are process/session-local: no daemon, socket API, or restart persistence. ATeamRuntimeinstance still represents exactly one round; continuation creates a clean runtime over the retained agent sessions rather than reusing settled coordination state. - A quiescent team may stop before every member calls
team_finish; this is reported asquiescent, never silently called success. - The parent supplies the initial objective/message. Runtime-generated follow-up hints and scenario-specific fallback decisions do not exist; runtime messages are limited to task-agnostic control notices (claim results, bounces, budget and error alerts).
- A command batch returned by one turn is applied in order and fail-stop, not as a transaction: commands that already committed are never rolled back, but the first rejected command halts the batch and everything after it is discarded. The rejection bounces back (
COMMAND_FAILED, plusCOMMAND_BATCH_HALTEDwhen later commands were discarded) and wakes the member to replan next turn — so a rejected send can no longer be sealed off by afinishqueued behind it in the same batch. A genuine cross-command dependency still needs a cross-turn fence (a claim, or waiting for a confirmation message), not same-batch ordering. - "Independent" bundles three separate guarantees, not all equally strong. Session independence (separate
AgentSessions, unique ids) is fully enforced. Epistemic independence (forming a view without a peer's conclusion already in hand) is enforced only for the very first turn of a single-post opening wave — after that it's a matter of doctrine and prompt framing, not runtime physics. Filesystem independence is not provided at all: every member shares the parent process'scwdand full tool access, soteam_claimarbitrates logical ownership by convention, not with an OS-level lock — two members can still physically write the same file at once if they don't cooperate. Read "independent agent team" as "separate conversations," never as "sandboxed" or "worktree-isolated."