@youngjurry/pi-agents
Persistent in-process Codex-style multi-agent collaboration for Pi
Package details
Install @youngjurry/pi-agents from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@youngjurry/pi-agents- Package
@youngjurry/pi-agents- Version
0.10.0- Published
- Sep 10, 2026
- Downloads
- 545/mo · 545/wk
- Author
- youngjurry
- License
- MIT
- Types
- extension
- Size
- 144.2 KB
- Dependencies
- 0 dependencies · 5 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Codex-style Agents for Pi
In-process persistent multi-agent collaboration modeled after OpenAI Codex MultiAgentV2.
Install
Install the public npm package:
pi install npm:@youngjurry/pi-agents
Or install directly from GitHub:
pi install git:github.com/YoungJurry/pi-agents
Choose only one source to avoid loading the extension twice. Then start Pi or run /reload in an existing session.
This extension runs child agents with the same OS user and filesystem permissions as the root Pi process. Review SECURITY.md before using it on untrusted work.
Quick start
Ask Pi to delegate work:
Spawn two agents to research independent parts of this task, wait for completion,
then pull and summarize their results.
The model queries list_agents(view="tools") for the action catalog and, when needed, list_agents(view="roles") for role-specific configuration. It executes catalog actions through the compact agent_action dispatcher.
Completion notices are intentionally small. Full answers enter the parent context only when explicitly requested with list_agents(view="results").
Inspect sub-agent sessions
Use the user-only /agents command to browse the current root session's sub-agents and open a read-only transcript:
/agents
/agents /root/api_research
The picker orders agents by latest task assignment, newest first; a successful followup_task moves a reused agent back to the top without letting ordinary streaming or tool activity reshuffle the list. It shows lifecycle status, model, effective thinking level, role, nickname, and residency. Selecting an agent opens its active session branch with normal Pi-style assistant messages, thinking, tool calls, tool results, and collaboration messages. Custom tools reuse their original Pi renderers, so tools such as web_search and fetch stay as compact as they are in the main transcript. Running sessions refresh while the viewer is open. Child permission dialogs wait behind the inspector and appear only after the user fully exits /agents, so they cannot steal focus from or hide behind its overlay.
Viewer controls:
↑/↓: move one line←/→: move one page up or downt/b: jump to the top or bottomCtrl+O: expand or collapse tool outputCtrl+T: show or hide thinkingr: refresh immediatelyEscape: return to the agent picker
This inspector is implemented only as a slash command and TUI overlay. It does not register an LLM tool, alter tool schemas or system prompts, add messages to the root context, switch sessions, wake agents, or expose child sessions through the normal /resume picker.
Usage accounting
Pi's built-in /session remains the authoritative view of the main Agent and its cache behavior. Use the user-only command below for separately calculated main, sub-agent, and combined totals:
/agent-usage
The command writes a detailed report directly into the normal TUI transcript, like /session; it does not open an overlay. The report includes main, sub-agent, and combined input/output/cache/token/cost totals, followed by separate main-model and sub-agent-model breakdowns. Each model row shows its tokens, prompt/output/cache details, number of contributing Agent sessions, and number of usage records. Non-model tool and summary usage is kept in an explicit Tools/summaries bucket rather than being misattributed to a model.
The report is stored as a TUI-only custom entry so it remains outside LLM context and does not affect /session message or token accounting. Reading the report does not load or wake child AgentSessions. Pi currently has no extension hook that can add child tokens to built-in /session while excluding them from that command's cache statistics.
Tools
Only two compact collaboration schemas remain active:
list_agents: query one catalog or data viewroles: available roles and their tool accesstools: action descriptions and parameter schemasstatus: canonical agent-tree statusresults: stored final answers
agent_action: execute a catalog action by name with its matching arguments
list_agents(view="tools") returns these actions as ordinary tool-result content instead of activating more provider tool schemas:
spawn_agents: submit one or many persistent child tasks in a single action; overflow waits in the visible FIFO queuesend_message: non-waking mailbox messagefollowup_task: assign more work and trigger processingwait_agent: event-driven mailbox waitinterrupt_agent: abort a run without deleting context
Example dispatcher call:
{
"action": "spawn_agents",
"arguments": {
"agents": [
{
"message": "Research the API implementation",
"task_name": "api_research",
"agent_type": "explorer"
},
{
"message": "Inspect the test strategy",
"task_name": "test_research",
"agent_type": "explorer"
}
]
}
}
The five action implementations stay registered locally, but their individual provider schemas are never added to later requests. spawn_agents replaces the old single-task action rather than adding a redundant sixth action; an array with one item performs a single spawn. Root and child agents use the same stable list_agents + agent_action surface. Agents use canonical paths such as /root/api_research and can recursively spawn children.
Context inheritance
Each item in the spawn_agents action accepts a fork_turns argument:
none: fresh contextall: sanitized semantic parent context (default)- a positive integer string: the most recent N task/user turns
Tool calls, tool results, thinking, shell transcripts, and previous collaboration mail are removed from inherited context.
Roles
Built-ins: default, explorer, awaiter.
Additional roles are read from:
~/.pi/agent/agents/*.md- nearest trusted
.pi/agents/*.md
Role format:
---
name: reviewer
description: Review code without editing
tools: read, grep, find, ls, bash
skills: [document]
model: openai/gpt-5.4
thinking: high
nickname_candidates: [Ada, Grace]
---
Review carefully and return findings with exact paths.
skills optionally filters Pi's normal progressive Skill disclosure. Omit the field to inherit all discoverable Skills, specify names such as skills: [document] to expose only those Skill names/descriptions/paths, or use skills: [] to expose none. The child still reads a matching SKILL.md on demand rather than placing complete Skill instructions in every prompt. As in Pi's main Agent, the catalog is shown only when read or bash is active. Unknown explicitly selected Skill names fail clearly instead of being silently ignored.
Model configuration
Global sub-agent settings live outside the installed package so updates cannot overwrite them:
~/.pi/agent/pi-agents/settings.json
{
"defaultModel": "opencode-go/ox-alpha-free",
"defaultThinkingLevel": "medium",
"maxConcurrentSubagents": 3,
"maxResidentSubagents": 3
}
Model and thinking configuration uses task fields first, then Role frontmatter, then global settings. Parent Agent model and thinking state are never used as implicit fallbacks.
| Task override | Effective selection |
|---|---|
model and reasoning_effort |
Use both explicit values |
model only |
Use that model and its highest supported thinking level |
reasoning_effort only |
Use the Role/global model with the explicit thinking level |
| neither | Use Role values where present, then defaultModel and defaultThinkingLevel |
Unsupported thinking values are clamped through Pi's model-specific thinkingLevelMap, and the effective level is persisted and displayed. If defaultThinkingLevel is omitted, the plugin-level default is medium. Supported configured values are off, minimal, low, medium, high, xhigh, and max.
The settings file is optional, but spawning requires a model from either the task, selected Role, or defaultModel; it no longer inherits the parent model. Limits must be positive integers, and maxResidentSubagents cannot be smaller than maxConcurrentSubagents. If concurrency is configured while residency is omitted, residency automatically expands to at least the concurrency limit. Setting changes take effect after /reload. Invalid JSON, an invalid defaultThinkingLevel, an empty defaultModel, or an unavailable configured model produces an explicit error. fork_turns controls inherited messages independently of model and thinking selection.
Lifecycle
- Default child execution slots: 3 (4 active agents including root), configurable with
maxConcurrentSubagents - Default resident child sessions: 3, configurable with
maxResidentSubagents spawn_agentsstarts tasks until all execution slots are occupied and records the remainder asqueued- Queued tasks are lightweight, persistent, FIFO ordered, and do not occupy resident-session capacity
- Queued session identity, child ownership metadata, and sanitized fork context are durably written before the batch spawn returns
list_agents(view="status")reports each waiting task's queue position plus current running/queued capacity- The live widget shows
Agents active: <running>/<limit> · queued: <waiting>and labels waiting paths explicitly - Completed/interrupted sessions are unloaded by LRU when residency is full
- Child sessions persist under
~/.pi/agent/pi-agents/roots/<root-session-id>/sessions/and reload lazily - Full final answers persist under
~/.pi/agent/pi-agents/roots/<root-session-id>/results/ - Each root storage group records its owning main-session file in
owner.json - The extension reads and writes only
~/.pi/agent/pi-agents/andsettings.json; it contains no automatic legacy migration, archival, or deletion logic - Resuming an existing main session removes groups whose owning main-session file has been deleted; new sessions and
/reloaddo not trigger grouped cleanup - Parents receive a compact completion notice instead of the full answer; use
list_agents(view="results")or read the result file on demand - Notices to a busy agent are queued safely:
wait_agentreturns them in its own result, and any leftovers are delivered right after a successful recipient turn wait_agentsends only newly queued mailbox notices to the model; its child status tree excludes the active caller, is folded in the TUI by default, and can be toggled withCtrl+O- Failed notice delivery is re-queued instead of silently discarded
- Notices pending when a turn is aborted or errors are deferred to the next explicit turn without restarting the interrupted agent
- The extension never inserts messages between an assistant tool call and its tool result, keeping session history protocol-valid for strict gateways
- Child sessions are kept out of Pi's normal
/resumepicker - Agent-tree metadata persists in root session custom entries
- Child extension approval dialogs are serialized and forwarded to the root TUI with the agent path
- While
/agentsis open, forwarded permission/custom dialogs wait without taking keyboard focus and appear only after the inspector closes - In non-interactive modes, permission extensions such as
permission-gate.tsfail closed - All agents share the same cwd and filesystem
Use /agents to browse the tree and inspect read-only child transcripts. A compact live tree appears below the editor while child agents exist and shows each active agent's provider/model identifier and effective thinking level.
Manual migration from versions before 0.10.0
Version 0.10.0 removes all runtime compatibility code for the former codex-agents names. Existing users who cannot see an old Agent tree, or who still have ~/.pi/agent/codex-agents/ or agents-setting.json, should close every Pi process first and run the following command once. It renames the storage/settings paths and updates the old custom-entry identifiers and persisted file paths in main and child session JSONL files. It refuses to merge conflicting old and new paths automatically.
python3 - <<'PY'
from pathlib import Path
import json
import os
import stat
agent_dir = Path.home() / ".pi" / "agent"
old_root = agent_dir / "codex-agents"
new_root = agent_dir / "pi-agents"
if old_root.exists():
if new_root.exists():
raise SystemExit(
f"Refusing to merge because both {old_root} and {new_root} exist. "
"Back them up and reconcile them manually first."
)
old_root.rename(new_root)
old_settings = new_root / "agents-setting.json"
new_settings = new_root / "settings.json"
if old_settings.exists():
if new_settings.exists():
raise SystemExit(
f"Refusing to overwrite {new_settings}; reconcile it with {old_settings} manually."
)
old_settings.rename(new_settings)
custom_types = {
"codex-agents": "pi-agents",
"codex-agents-state": "pi-agents-state",
"codex-agents-child-meta": "pi-agents-child-meta",
"codex-agents-fork-context": "pi-agents-fork-context",
}
old_prefix = str(old_root)
new_prefix = str(new_root)
def migrate(value):
changed = False
if isinstance(value, dict):
output = {}
for key, child in value.items():
if key == "customType" and isinstance(child, str) and child in custom_types:
output[key] = custom_types[child]
changed = True
elif key in {"sessionFile", "resultFile"} and isinstance(child, str) and (
child == old_prefix or child.startswith(old_prefix + os.sep)
):
output[key] = new_prefix + child[len(old_prefix):]
changed = True
else:
output[key], child_changed = migrate(child)
changed |= child_changed
return output, changed
if isinstance(value, list):
output = []
for child in value:
migrated, child_changed = migrate(child)
output.append(migrated)
changed |= child_changed
return output, changed
return value, False
files = set((agent_dir / "sessions").rglob("*.jsonl"))
if new_root.exists():
files.update(new_root.rglob("*.jsonl"))
changed_files = 0
for file in sorted(files):
temporary = file.with_name(file.name + ".pi-agents-migrate")
touched = False
try:
with file.open("r", encoding="utf-8") as source, temporary.open("w", encoding="utf-8") as target:
for line in source:
try:
value = json.loads(line)
except json.JSONDecodeError:
target.write(line)
continue
value, line_changed = migrate(value)
target.write(
json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n"
if line_changed else line
)
touched |= line_changed
if touched:
os.chmod(temporary, stat.S_IMODE(file.stat().st_mode))
os.replace(temporary, file)
changed_files += 1
else:
temporary.unlink()
except BaseException:
temporary.unlink(missing_ok=True)
raise
print(f"Migration complete: updated {changed_files} JSONL file(s). Restart Pi or run /reload.")
PY
Fresh installations do not need this command.