@esso0428/pi-subagents
A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.
Package details
Install @esso0428/pi-subagents from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@esso0428/pi-subagents- Package
@esso0428/pi-subagents- Version
0.18.0- Published
- Oct 21, 2026
- Downloads
- 6,015/mo · 274/wk
- Author
- esso0428
- License
- MIT
- Types
- extension
- Size
- 3.1 MB
- Dependencies
- 3 dependencies · 4 peers
Pi manifest JSON
{
"image": "https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png",
"video": "https://github.com/tintinweb/pi-subagents/raw/master/media/demo.mp4",
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@esso0428/pi-subagents
A pi extension that brings Claude Code-style autonomous sub-agents to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Agent calls block by default, can detach explicitly, support mid-run steering and session resume, and let you define your own custom agent types.
Fork of
@tintinweb/pi-subagentsintegratingnpm:pi-subagents-style JSON agent overrides — configure agents viasettings.jsonwithout writing.mdfiles.
https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
Features
- Claude Code look & feel — same tool names, calling conventions, and UI patterns (
Agent,get_subagent_result,steer_subagent) — feels native - Parallel detached agents — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and internal automatic join (consolidated notifications)
- Agents panel UI — persistent above-editor widget with animated spinners, live tool activity, token counts, colored status icons, and one focus-gated navigator. It shows every agent by default; press
↓at an empty prompt to activate the panel,↑/↓to select,Enterto open a live or read-only history viewer, andEscto return. Long rosters reserve a right-hand track/thumb scrollbar and show↑ N moreor↓ N moreat the clipped edge. Configure via/agents → Settings → Widget:all,background, oroff - Conversation viewer — select any agent in
/agentsto open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). The main viewer reserves a right-hand scrollbar rail for the transcript and exposes a[preview]header action for the focused tool. Steer a running agent inline by pressingeto open a Pi-native composer, typing, thenEnterto send (Escor an empty submit returns); the follow-up appears as a muted/gray USER message and redirects the agent after its current tool.Alt+Up(a-up/alt+up) recalls submitted follow-up drafts. Read-only tool previews replace the viewer in place, use the larger viewport with the same right-hand track/thumb scrollbar and top/bottom hidden-line affordances, and return withEsc,q, or the close control. Stop a still-running agent by pressingx(thenxagain to confirm) — both work for background agents too - Durable Agent View history — every spawned agent writes mandatory project-local history under
.pi-subagents/, keeping the conversation view reopenable after completion, cleanup, or reload - Custom agent types — define agents in
.pi/agents/<name>.mdor.agents/agents/<name>.md(project) or globally, with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions - Nested subagents — opt-in, default-off delegation: an agent whose frontmatter sets
allowed_subagentsgets ownership-scopedAgent,get_subagent_result, andsteer_subagenttools with the same blocking/detached modes as the main agent, depth-capped from the main session (default 2). Unlike upstream, nested children are not hidden — the agents roster renders them as an indented subtree you can navigate into. The allowlist is a privilege boundary, so it is set by hand in the agent file and never enabled automatically - Mid-run steering — inject messages into running agents to redirect their work without restarting
- Session resume — pick up where an agent left off, preserving full conversation context
- Durable interruption recovery — catchable shutdowns preserve running/queued metadata and partial transcripts so interrupted agents remain indexed after reload; abrupt
SIGKILLtermination cannot be checkpointed - Graceful turn limits — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
- Case-insensitive agent types —
"explore","Explore","EXPLORE"all work. Unknown types fall back to general-purpose with a note - Fuzzy model selection — specify models by name (
"haiku","sonnet") instead of full IDs, with automatic filtering to only available/configured models - Context inheritance — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
- Persistent agent memory — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
- Git worktree isolation — run agents in isolated repo copies; changes auto-committed to branches on completion
- Skill preloading — inject named skills into agent system prompts, discovered from
.pi/skills/,.agents/skills/, and global locations (Pi-standard<name>/SKILL.mddirectory layout supported) - Tool denylist — block specific tools via
disallowed_toolsfrontmatter - Styled completion notifications — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
- Event bus — lifecycle events (
subagents:created,started,completed,failed,steered,compacted) emitted viapi.events, enabling other extensions to react to sub-agent activity - Cross-extension RPC — other pi extensions can spawn and stop subagents via the
pi.eventsevent bus (subagents:rpc:ping,subagents:rpc:spawn,subagents:rpc:stop). Standardized reply envelopes with protocol versioning. Emitssubagents:readyon session start - Schedule subagents — pass
scheduleto theAgenttool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the samesubagent-notificationfollowUp path as manual background completions; manage via/agents → Scheduled jobs - Model scope enforcement — opt-in validation that subagent model choices stay within your pi
enabledModelsallowlist (sourced from/scoped-models, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via/agents → Settings → Scope models
Install
pi install npm:@esso0428/pi-subagents
Or load directly for development:
pi -e ./src/index.ts
Task tracking integration
For TaskExecute-based task tracking, install the downstream compatibility fork:
pi install npm:@esso0428/pi-tasks
@esso0428/pi-tasks@0.9.2 is a narrow downstream compatibility fork of @tintinweb/pi-tasks, not a wholesale replacement. It provides tighter integration with @esso0428/pi-subagents, initially focusing on corrected tool descriptions, prompt guidance, and compatibility rather than new task features or optimization. TaskExecute remains detached; this integration does not provide a TaskExecute resume API.
Quick Start
The parent agent spawns sub-agents using the Agent tool. Calls are blocking by default and return the completed result inline:
Agent({
subagent_type: "Explore",
prompt: "Find all files that handle authentication",
description: "Find auth files",
})
Set run_in_background: true when you explicitly want a detached call. It returns an agent ID immediately; completion arrives through the notification path, and the result can be retrieved with get_subagent_result:
Agent({
subagent_type: "Explore",
prompt: "Find all files that handle authentication",
description: "Find auth files",
run_in_background: true,
})
run_in_background: false explicitly selects blocking mode. When the field is omitted, the effective mode is resolved as tool-call value → agent frontmatter default → blocking. The same resolution applies when resuming an agent. A blocking result is consumed inline and does not produce a completion follow-up or Goal wake; a detached result remains available for a later join.
Use get_subagent_result({ agent_id, wait: true }) to join a detached agent. This is a cancellable wait that consumes the terminal result on success and cancels its pending completion notification. With wait: false or an omitted wait, the call returns immediately: a queued or running status check does not consume the result, while retrieving a terminal result consumes it and cancels its pending notification. Cancelling the join only cancels the result-tool wait — the child keeps running and can still notify normally.
Nested agents use the same Agent, get_subagent_result, and steer_subagent interface. Nested result, resume, and steering requests are ownership-scoped and fail closed for foreign IDs. There is no public wait-group API: detached completions are joined automatically within the parent turn, according to the configured join mode below.
Direct Agent calls use the blocking/detached mode described above. Scheduled agents, cross-extension RPC spawns, and TaskExecute remain upstream-style detached operations; the direct Agent default does not turn those paths into blocking calls.
Scheduling
Add a schedule field to register the agent to fire later instead of running now. Scheduled agents always run detached, regardless of the direct Agent default:
Agent({
subagent_type: "Explore",
prompt: "Look at recent commits and summarize what changed since last week",
description: "Weekly commit review",
schedule: "0 0 9 * * 1", // 9am every Monday (6-field cron)
})
Schedule formats:
- Cron — 6-field (
second minute hour day-of-month month day-of-week), e.g."0 0 9 * * 1"for 9am every Monday,"0 */15 * * * *"for every 15 minutes. - Interval —
"5m","1h","30s","2d". Fires repeatedly at that interval. - One-shot relative —
"+10m","+2h","+1d". Fires once at that future time. - One-shot absolute — full ISO timestamp, e.g.
"2026-12-25T09:00:00.000Z".
When a schedule fires, the spawn runs detached and its completion notification arrives in the conversation through the same subagent-notification followUp path as a manually-spawned detached agent — your parent agent reasons about the result the same way. Scheduling does not opt into the direct blocking Agent mode.
Schedules are session-scoped: they reset on /new and restore on /resume. List and cancel via /agents → Scheduled jobs (creation is the Agent tool's job — there is no parallel manual-create wizard). Storage at <cwd>/.pi/subagent-schedules/<sessionId>.json with PID-based file locking for cross-instance safety.
Disable the feature entirely: /agents → Settings → Scheduling → disabled removes schedule from the Agent tool spec (no LLM-context cost), hides the menu entry, and stops any active scheduler. The schema-level removal takes effect on the next pi session; the runtime kill is immediate. Re-enable from the same menu.
Restrictions:
schedulecannot be combined withinherit_context(no parent conversation exists at fire time) orresume(schedules create fresh agents).run_in_backgrounddoes not change a scheduled fire; scheduled agents always run detached.- Scheduled fires bypass the
maxConcurrentqueue so a 5-minute interval cannot be deferred behind long-running manual agents. - Headless
pi -pdoesn't wait for scheduled subagents.
UI
The extension renders a persistent Agents widget above the editor. It initializes during TUI session_start, after the session branch restores completed-agent history, so openable agents are visible immediately when a session starts or resumes. By default it shows all agents (widgetMode: all), including every managed run. Switch to background or off via /agents → Settings → Widget:
● Agents
├─ ⠹ Agent Refactor auth module · ↻5≤30 · 5 tool uses · 33.8k token (62%) · 12.3s
│ ⎿ editing 2 files…
├─ ⠹ Explore Find auth files · ↻3 · 3 tool uses · 12.4k token (8%) · 4.1s
│ ⎿ searching…
├─ ⠹ Agent Long-running task · ↻42 · 38 tool uses · 91.0k token (84% · ⇊2) · 2m17s
│ ⎿ reading…
└─ 2 queued
The token field is annotated with two optional signals inside parens:
NN%— context-window utilization (color-coded: <70% dim, 70–85% warning, ≥85% error). Omitted when the model has no declaredcontextWindow, or briefly right after compaction.⇊N— number of times the session has compacted, when > 0. Stays dim; the percent's color carries urgency.
Agents panel navigator
While agents are running or have openable history, the single Agents panel renders above the editor:
● Agents ↑↓ select · enter view · esc back
├─ ● Agent Refactor auth module · ↻5≤30 · 5 tool uses · 12.3s
│ ⎿ editing 2 files…
├─ ○ Explore Find auth files · ↻3 · 4.1s
└─ +1 more (↓ 1 more; 1 finished)
At an empty prompt, press ↓ to activate the panel. The selected row is marked ●; ↑/↓ move through the full bounded roster, Enter opens a running agent live or a terminal agent's read-only history. A queued or interrupted agent remains selectable, and an interrupted agent's partial transcript can be opened read-only after reload. The widget remembers the selected agent and bounded viewport when you return from the prompt or a viewer; if that record disappears, it falls back to the nearest valid row. Esc returns to the prompt. ↑ while the first row is selected also returns to the prompt. Pressing ↑ at an inactive prompt, typing in a non-empty prompt, or using j/k/← behaves normally; the panel only captures the focus-gated arrow sequence described above. Disable it entirely via /agents → Settings → Widget → off.
Shared Agent Tree
The Agents widget and /agents → Agent conversations this session use the same shared Agent Tree projection. The /agents entry is current-session scoped, and the widget's existing all/background/off filter remains in force. It renders eligible agents in visible preorder; workflow-owned rows appear under a synthetic workflow root, followed by real workflow agents and their nested descendants. Workflow roots are grouping-only descriptors and never enter AgentManager; real workflow agents remain first-class rows, visible, selectable, and openable in their own Agent Views. Terminal agents with durable history remain openable as read-only views.
Roots are expanded by default, and an explicit collapse remains authoritative when children appear later. ↑/↓ move through visible rows only, so collapsed descendants are skipped. ←/→ collapse or expand the selected branch and otherwise navigate the hierarchy. Enter on a workflow root opens the existing /agents → Workflows inspector; Enter on a real agent opens its Agent View. The Workflows inspector is a secondary run-progress/control surface, not a replacement for the shared Agent Tree or the real agents it contains.
Individual agent results render Claude Code-style in the conversation:
| State | Example |
|---|---|
| Running | ⠹ ↻3≤30 · 3 tool uses · 12.4k token (8%) / ⎿ searching, reading 3 files… |
| Completed | ✓ ↻8 · 5 tool uses · 33.8k token (62%) · 12.3s / ⎿ Done |
| Wrapped up | ✓ ↻50≤50 · 50 tool uses · 89.1k token (84% · ⇊2) · 45.2s / ⎿ Wrapped up (turn limit) |
| Stopped | ■ ↻3 · 3 tool uses · 12.4k token (8%) / ⎿ Stopped |
| Error | ✗ ↻3 · 3 tool uses · 12.4k token (8%) / ⎿ Error: timeout |
| Aborted | ✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3) / ⎿ Aborted (max turns exceeded) |
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline. The viewer's tool preview is read-only; its scrollbar and j/k/wheel/page/top/bottom controls never modify the agent transcript.
Every spawned agent writes its full conversation to mandatory project-local history at <cwd>/.pi-subagents/agent-transcripts/<agent-id>.jsonl. This durable history keeps the Agent View/conversation viewer reopenable after completion, cleanup, or reload. The extension creates an ignored .pi-subagents/ directory for these artifacts.
The optional OS-temp .output mirror is separate: it is written at <os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output (owner-only 0700, cleared on reboot) when enabled. Set output_transcript: false on a custom agent, or outputTranscript: false in subagents.json, to omit only this mirror; frontmatter overrides the project default. These controls affect only transcript files. They are independent of persist_session (the pi session on disk), isolation: worktree (which commits the agent's work to a git branch), and memory: (durable files).
Writing or omitting either transcript file does not alter Agent or TaskExecute results, get_subagent_result results, notifications, join/wait behavior, or what the main agent sees. Background agent completion notifications render as styled boxes:
✓ Find auth files completed
↻3 · 3 tool uses · 12.4k token · 4.1s
⎿ Found 5 files related to authentication...
transcript: .pi/output/agent-abc123.jsonl
Automatically joined completions render each agent as a separate block. The LLM receives structured <task-notification> XML for parsing, while the user sees the themed visual.
Default Agent Types
| Type | Tools | Model | Prompt Mode | Description |
|---|---|---|---|---|
general-purpose |
all 7 | inherit | append (parent twin) |
Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
Explore |
read, bash, grep, find, ls | haiku (falls back to inherit) | replace (standalone) |
Fast codebase exploration (read-only) |
Plan |
read, bash, grep, find, ls | inherit | replace (standalone) |
Software architect for implementation planning (read-only) |
The general-purpose agent is a parent twin — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
Default agents can be ejected (/agents → select agent → Eject) to export them as .md files for customization, overridden by creating a .md file with the same name (e.g. .pi/agents/general-purpose.md), or disabled per-project with enabled: false frontmatter.
Custom Agents
Define custom agent types by creating .md files. The filename becomes the agent type name. Any name is allowed — using a default agent's name overrides it.
Agents are discovered from three locations (higher priority wins):
| Priority | Location | Scope |
|---|---|---|
| 1 (highest) | .pi/agents/<name>.md |
Project — pi's config dir; authoritative, and where /agents writes |
| 2 | .agents/agents/<name>.md |
Project — the shared cross-tool .agents workspace (same convention as .agents/skills/) |
| 3 | $PI_CODING_AGENT_DIR/agents/<name>.md (default ~/.pi/agent/agents/<name>.md) |
Global — available everywhere |
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. If both project locations define the same name, .pi/agents/ wins — .pi stays the project authority; .agents/agents/ is an additional read location for projects that keep their agent assets in the .agents workspace. The global location follows the upstream PI_CODING_AGENT_DIR env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory.
Example: .pi/agents/auditor.md
---
description: Security Code Reviewer
tools: read, grep, find, bash
model: anthropic/claude-opus-4-6
thinking: high
max_turns: 30
---
You are a security auditor. Review code for vulnerabilities including:
- Injection flaws (SQL, command, XSS)
- Authentication and authorization issues
- Sensitive data exposure
- Insecure configurations
Report findings with file paths, line numbers, severity, and remediation advice.
Then spawn it like any built-in type:
Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })
Frontmatter Fields
All fields are optional — sensible defaults for everything.
| Field | Default | Description |
|---|---|---|
description |
filename | Agent description shown in tool listings |
display_name |
— | Display name for UI (e.g. widget, agent list) |
tools |
all 7 | Which tools the agent can call. Built-in names (read, grep, …), * / all (all built-ins), none, and ext:<extension> / ext:<extension>/<tool> selectors for extension tools. See Tool & extension scoping below |
extensions |
true |
Which extensions to load for the agent. true (all defaults), false (none), or an explicit list: [mcp, "/abs/path.ts", "*"]. See Tool & extension scoping below |
exclude_extensions |
— | Extension denylist applied after extensions: — exclude wins. Plain names only (case-insensitive), no paths or *. Useful with extensions: true to drop one extension (e.g. pi-notify) |
skills |
true |
Inherit skills from parent. Can be a comma-separated list of skill names to preload (see Skill Preloading for discovery locations) |
memory |
— | Persistent agent memory scope: project, local, or user. Auto-detects read-only agents |
disallowed_tools |
— | Comma-separated tools to deny even if extensions provide them |
allowed_subagents |
none | Opt in to scoped nested Agent, get_subagent_result, and steer_subagent tools. Omitted / empty / none / false = no nesting; all (or "*" / true) = any enabled agent; comma-separated list = only those agent types |
isolation |
— | Set to worktree to run in an isolated git worktree |
model |
inherit parent | Model — provider/modelId or fuzzy name ("haiku", "sonnet"). Resolved tolerantly (./- and a trailing date stamp are interchangeable) and falls back to the same model under another provider if the named one doesn't have it |
thinking |
inherit | off, minimal, low, medium, high, xhigh, max — actual availability depends on your pi version and model; pi clamps unsupported levels down |
max_turns |
unlimited | Max agentic turns before graceful shutdown. 0 or omit for unlimited |
persist_session |
false |
Persist this subagent as a normal pi session instead of keeping the session in memory only. The optional OS-temp .output mirror is controlled separately by output_transcript |
output_transcript |
true (or subagents.json outputTranscript) |
Write this subagent's optional OS-temp .output mirror; when set, overrides the subagents.json outputTranscript default. Set false to omit only the mirror; mandatory .pi-subagents history is still written and keeps Agent View reopenable |
session_dir |
pi default | Optional session directory when persist_session: true; omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
prompt_mode |
replace |
replace: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). append: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
inherit_context |
false |
Fork parent conversation into agent |
run_in_background |
false |
Invocation mode default: omitted means blocking; true selects detached mode. An explicit tool-call value overrides this frontmatter default |
isolated |
false |
Hermetic specialist mode: forces extensions: false + skills: false + drops ext: selectors. Only built-in tools. Distinct from isolation: worktree (filesystem) |
enabled |
true |
Set to false to disable an agent (useful for hiding a default agent per-project) |
Frontmatter and agentOverrides are authoritative for fields they specify: model, thinking, max_turns, inherit_context, isolated, and isolation. run_in_background is different: they supply its default, while an explicit tool-call value overrides it; if neither sets it, blocking is the default. The corresponding Agent tool parameters otherwise fill only fields the selected agent's frontmatter or agentOverrides leaves unspecified.
Forgiving model: resolution. A model: pin is matched against pi's model registry tolerantly, so cosmetic id variations don't silently drop the agent back to the parent's model: . and - are treated as equivalent in version numbers (claude-haiku-4.5 ≡ claude-haiku-4-5), a trailing -YYYYMMDD date stamp is optional (anthropic/claude-haiku-4-5-20251001 matches an undated registry id and vice-versa), and a provider/modelId whose named provider doesn't carry that model retries the bare id against every provider. Precedence is exact → fuzzy under the named provider → same model under any provider → unavailable, so an exact match always wins and dated snapshots aren't conflated. If nothing resolves, the pin can't run and the agent inherits the parent model — /agents → Agent types flags this case as (unavailable, fallback: inherit) and shows the resolved target (→ provider/id) when resolution lands on a different provider or version than configured. (This is distinct from Model Scope enforcement, which matches the enabledModels allowlist by exact entry.)
Tool & extension scoping
extensions: decides which extensions load, tools: decides which tools surface to the LLM. They compose:
# Default (both omitted): all extensions load, all 7 built-ins surface
tools: read, grep, find # narrow to listed built-ins; extensions still load
tools: "*" # all 7 built-ins (alias: `all`)
tools: none # zero built-ins (alias: `""`)
tools: "*, ext:mcp/search" # built-ins plus one extension tool
extensions: false # no extensions load
extensions: [mcp] # only mcp loads
extensions: ["*", "/abs/foo.ts"] # all defaults plus one path-loaded extension
exclude_extensions: pi-notify # everything except pi-notify (with extensions: true)
# Specialist: load one extension, expose only one of its tools, keep built-ins
extensions: [mcp]
tools: "*, ext:mcp/search"
isolated: true # hermetic: built-ins only, no extensions/skills/context
A few rules the examples don't make obvious:
extensions:is the sole loading authority.ext:foointools:narrows what surfaces; it can't loadfooon its own. Mismatches fireextension-error:…warnings.- Any
ext:entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. Sotools: "*, ext:mcp/search"exposes onlysearchfrommcp, nothing from any other extension. - Extension names match case-insensitively (
[Mcp]=[mcp]); tool names inext:foo/barstay case-sensitive. - Extensions that register tools lazily work too. MCP-backed extensions typically can't enumerate their tools until their servers connect, so they register from
session_startorbefore_agent_startrather than at load. Subagent scoping is re-derived as tools appear, so these surface normally — including underext:selectors, which keep narrowing correctly no matter when a tool shows up. - An installed package extension matches by its package short name (
@scope/pi-subagents→[pi-subagents]), in addition to its path-derived name (a package whose entry issrc/index.tsalso answers to[src]). Prefer the package name — the path-derived one is incidental. - Plain
tools:typos fail loudly:tools: reed, grepfirestools-error:…instead of silently producing an under-tooled agent. exclude_extensions:wins overextensions:and overext:selectors — an excluded extension never loads and atools: ext:entry can't pull it back. Plain names only (no paths, no*); a name matching nothing fires anextension-error:…warning.exclude_extensions:is not a sandbox: excluded extensions' factory code still executes once during loading. Exclusion suppresses their tools and their bound lifecycle hooks (pi.onhandlers likesession_startonly fire for extensions bound to the session), but not other load-time side effects — a factory that subscribes directly to the sharedpi.eventsbus stays live. Don't rely on it to contain an untrusted extension.- Array and string forms are equivalent:
[a, b]=="a, b".
Nested subagents
Nested delegation is default-off and hand-authored. An agent that owns a real
fan-out responsibility opts in by setting allowed_subagents in its own
frontmatter:
# .pi/agents/support-coordinator.md
---
name: support-coordinator
description: Coordinates support-triage work across several areas
allowed_subagents: support-file-finder, support-callsite-tracer
tools: read, grep, find, ls
---
Omitted, empty, none, or false means no nested tools are injected at all.
all (or "*" / true) allows any enabled agent; a comma-separated list
restricts nesting to exactly those types. Unknown, disabled, and out-of-list
types are rejected, never fallen back to — a configured fallback agent
cannot hand a nested caller something outside its allowlist.
The allowlist is a privilege boundary. A nested child runs with its own tool
set, so choose it as carefully as you would tools:. It is set by hand in the
agent file and is never enabled automatically; no skill or tool description
teaches a model to grant itself delegation.
A nested child receives ownership-scoped Agent, get_subagent_result, and
steer_subagent tools. Nested Agent calls use the same mode resolution as
top-level calls: they block by default, and run_in_background: true selects
detached execution. Result, resume, and steer are ownership-checked, so a
parent cannot read, steer, or resume a foreign child. maxSubagentDepth caps
how deep nesting goes (default 2: main session 0, its subagents 1, nested
children 2); an agent already at the cap receives no nested tools at all, since
it can never own a child. Change it project-wide via maxSubagentDepth in
subagents.json or /agents → Settings → Nested depth. Nested children occupy
no concurrency slot: their parent already holds one, and queueing a child
behind its own parent would deadlock.
Nested children consume no concurrency slot and are never reported to the
main session as top-level agents — their completion surfaces inside the parent,
and their token usage folds into every ancestor's total. Each still writes its
own mandatory .pi-subagents history.
How nested children are shown
Upstream hides nested children from every surface. This fork keeps the same
reporting semantics but makes the hierarchy visible: the agents roster renders
children as an indented subtree, each with its own spinner and activity line,
token counts marked (in parent) so totals are not double-counted, and a
nested blocked: … note when the allowlist or depth cap refuses a dispatch.
↑/↓ move across every visible row regardless of depth and Enter opens that
level's conversation viewer. Because the lookup is case-insensitive, an override
named Explore also changes what a lowercase explore request resolves to.
The parentAgentId and depth of a nested child are recorded in its recovery
checkpoint, so a reload rebuilds the same subtree rather than flattening it. If
a parent record is ever removed before its child, the child keeps its indent and
says its parent is gone.
subagents.agentOverrides applies to nested dispatch exactly as it does at
top level, including the project-over-global precedence described in
Priority Chain.
npm:pi-subagents-Style JSON Agent Overrides
This fork adds the ability to configure — and even create — agents entirely through JSON,
no .md files required. The syntax matches npm:pi-subagents
(also known as oh-my-opencode-agent / OMO style): put a subagents.agentOverrides block in
settings.json and it takes the highest priority.
Priority Chain
Local JSON (.pi/settings.json → subagents.agentOverrides) ← highest
Global JSON (~/.pi/agent/settings.json → subagents.agentOverrides)
───── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
Local .md (.pi/agents/*.md)
Global .md (~/.pi/agent/agents/*.md)
Built-in (general-purpose, Explore, Plan) ← lowest
JSON overrides win over everything below the line. If an agent name in the JSON doesn't
match any existing .md or built-in agent, it is auto-registered with the fields you
provide.
Global Override Example
Put this in ~/.pi/agent/settings.json to set project-wide defaults for all your agents:
{
"subagents": {
"defaultModel": "opencode/deepseek-v4-flash-free",
"agentOverrides": {
"Explore": {
"model": "github-copilot/gpt-5-mini",
"thinking": "off"
},
"researcher": {
"model": "opencode/deepseek-v4-flash-free",
"thinking": "max",
"systemPrompt": "You are an autonomous web researcher. Search for external evidence, official docs, ecosystem behavior, recent changes, benchmarks, and primary sources. Return a focused research brief with source links, confidence level, gaps, and decision implications. Do not edit project files."
},
"reviewer": {
"model": "opencode/deepseek-v4-flash-free",
"thinking": "max",
"tools": ["read", "grep", "find", "ls", "bash"]
}
}
}
}
Local Project Override Example
Put this in .pi/settings.json within a specific project. These values merge over the
global ones, so you can override only the fields you need to change for this project:
{
"subagents": {
"agentOverrides": {
"researcher": {
"model": "openai-codex/gpt-5.4",
"thinking": "high"
},
"fixer": {
"model": "openai-codex/gpt-5.4",
"thinking": "high",
"tools": ["read", "grep", "find", "ls", "bash", "edit", "write"],
"systemPrompt": "You are Fixer — a fast, focused implementation specialist. Execute code changes efficiently with complete context. NO external research, NO delegation, NO planning. Implement and report."
},
"explorer": {
"model": "openai-codex/gpt-5.4",
"thinking": "high",
"tools": ["read", "grep", "find", "ls", "bash"]
}
}
}
}
In this example, researcher gets openai-codex/gpt-5.4 from the local override (instead of
the global opencode/deepseek-v4-flash-free). fixer and explorer don't exist in global
overrides and have no .md files — they are auto-registered from this JSON alone.
Default tools
Set subagents.defaultTools to apply one final tool policy to every agent, after any
agentOverrides.<name>.tools value:
{
"subagents": {
"defaultTools": ["read", "bash", "+codemode", "-write"]
}
}
A list containing any plain entry (such as read, bash, *, all, or ext:...) is a
complete selection; * and all select all built-ins. A list containing only +name and
-name entries patches each agent's existing tool scope. For example, ["+codemode"]
adds that direct tool, while ["-write"] removes write. An empty list is an explicit
empty tool set. Global and local defaultTools merge with local-only +/- entries applied to
the inherited global list; a local list with a plain entry or an empty list replaces it.
A per-agent tools value is resolved first against that agent's original scope, then
defaultTools is resolved against the result as the final policy layer. Thus
defaultTools: ["+codemode"] adds codemode even to an agent with plain per-agent tools,
and re-adds it after tools: false without restoring any built-ins. A plain defaultTools
list replaces the per-agent result. Agents without an override receive the same final policy.
Built-in names remain a built-in allowlist, ext: entries remain extension selectors, and
other direct names (for example codemode) are passed through to matching extension tools.
Complete and empty selections also make extension tools opt-in, so tools: ["read"] does
not expose every loaded extension tool; patch-only lists keep the agent's existing extension
scope. On supported Pi hosts, selecting codemode also registers Pi's codemode built-in in
the child SDK session. Older hosts without the codemode factory cannot expose that tool and
produce a warning instead of crashing.
Supported Override Fields
| Field | Type | Description |
|---|---|---|
model |
string | false |
Model to use; false clears override and inherits parent |
thinking |
string | false |
Thinking level: off, minimal, low, medium, high, xhigh, max |
systemPrompt |
string |
Full system prompt text |
systemPromptMode |
"replace" | "append" |
Prompt mode for the JSON systemPrompt; works for both existing and auto-registered agents |
tools |
string[] | false |
Tool selection for this agent; resolved against its base scope before the final defaultTools policy |
disabled |
boolean |
true hides the agent from the type list |
skills |
string[] |
Skill list. ["*"] = all skills, [] = none, ["simplify"] = only named |
How Skills Are Mapped
Nico JSON (string[]) |
Resulting tintinweb value |
|---|---|
["*"] |
true — all skills inherited |
["simplify", "librarian"] |
["simplify", "librarian"] — only listed |
[] or false |
false — no skills |
| omitted | true — all skills (default) |
Tools
Agent
Launch a sub-agent. Calls block by default; set run_in_background: true for an explicit detached call.
| Parameter | Type | Required | Description |
|---|---|---|---|
prompt |
string | yes | The task for the agent |
description |
string | yes | Short 3-5 word summary (shown in UI) |
subagent_type |
string | yes | Agent type (built-in or custom) |
model |
string | no | Optional model — used only when the selected agent's frontmatter or agentOverrides leaves model unspecified. Accepts provider/modelId or a fuzzy name ("haiku", "sonnet"); resolution is tolerant (./- and a trailing date stamp are interchangeable) with provider fallback |
thinking |
string | no | Thinking level: off, minimal, low, medium, high, xhigh, max — used only when the selected agent's frontmatter or agentOverrides leaves thinking unspecified (availability depends on pi version and model) |
max_turns |
number | no | Max agentic turns — used only when the selected agent's frontmatter or agentOverrides leaves max_turns unspecified. Omit for unlimited |
run_in_background |
boolean | no | true returns an ID immediately and runs detached; an explicit tool-call value overrides the selected agent's frontmatter or agentOverrides default, while omission uses that default (blocking if neither is set) |
resume |
string | no | Agent ID to resume a previous session; the same blocking/detached mode resolution applies |
isolated |
boolean | no | No extension/MCP tools — used only when the selected agent's frontmatter or agentOverrides leaves isolated unspecified |
isolation |
"worktree" |
no | Run in an isolated git worktree — used only when the selected agent's frontmatter or agentOverrides leaves isolation unspecified |
inherit_context |
boolean | no | Fork parent conversation into agent — used only when the selected agent's frontmatter or agentOverrides leaves inherit_context unspecified |
get_subagent_result
Check status and retrieve results from a detached agent.
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id |
string | yes | Agent ID to check |
wait |
boolean | no | true cancellably joins until terminal; omitted or false returns immediately. Retrieving a terminal result consumes it and cancels its pending notification |
verbose |
boolean | no | Include full conversation log |
With wait omitted or false, get_subagent_result returns immediately. A queued or running status check does not consume the result; retrieving any terminal result consumes it and cancels its pending detached notification. With wait: true, the tool cancellably waits through queued and running states, then retrieves and consumes the terminal result with the same notification cancellation. Cancelling the wait does not stop the child or consume its result. Completion notifications are automatically joined internally; there are no public wait-group handles.
steer_subagent
Send a steering message to a running agent. The message interrupts after the current tool execution.
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id |
string | yes | Agent ID to steer |
message |
string | yes | Message to inject into agent conversation |
Commands
| Command | Description |
|---|---|
/agents |
Interactive agent management menu |
The /agents command opens an interactive menu:
Agent conversations this session
Agent history all sessions
Agent types
Scheduled jobs
Workflows
Create new agent
Settings
- Agent conversations this session — opens the shared Agent Tree for the current session. See Shared Agent Tree for navigation and behavior.
- Agent history all sessions — browse durable agent histories from all sessions.
- Agent types — unified list with source indicators:
•(project),◦(global),✕(disabled). Each row shows the agent's model, and the highlighted agent's full description appears below the list. The model column flags(unavailable, fallback: inherit)when a configured model can't be resolved (it would silently inherit the parent model), and shows(→ provider/id)when it resolves to a different provider or version than configured. Select an agent to manage it:- Default agents (no override): Eject (export as
.md), Disable - Default agents (ejected/overridden): Edit, Disable, Reset to default, Delete
- Custom agents: Edit, Disable, Delete
- Disabled agents: Enable, Edit, Delete
- Default agents (no override): Eject (export as
- Eject — writes the embedded default config as a
.mdfile to project or personal location, so you can customize it - Disable/Enable — toggle agent availability. Disabled agents stay visible in the list (marked
✕) and can be re-enabled - Scheduled jobs — view and manage scheduled agents.
- Workflows — inspect and control workflow runs.
- Create new agent — choose project/personal location, then manual wizard (step-by-step prompts for name, tools, model, thinking, system prompt) or AI-generated (describe what the agent should do and a sub-agent writes the
.mdfile). Any name is allowed, including default agent names (overrides them) - Settings — configure max concurrency, default max turns, grace turns, and join mode at runtime
Graceful Max Turns
Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
- At
max_turns— steering message: "Wrap up immediately — provide your final answer now." - Up to 5 grace turns to finish cleanly
- Hard abort only after the grace period
| Status | Meaning | Icon |
|---|---|---|
completed |
Finished naturally | ✓ green |
steered |
Hit limit, wrapped up in time | ✓ yellow |
aborted |
Grace period exceeded | ✗ red |
stopped |
User-initiated abort | ■ dim |
Concurrency
The configurable maxConcurrent limit (default: 4) applies to work submitted to the manager queue. Direct detached Agent calls and scheduled calls bypass that queue. Owned nested/workflow children do not consume the top-level pool; workflows have their own cap. Cross-extension/RPC top-level spawns remain subject to the manager queue unless the caller opts out. Blocking calls wait inline and do not produce detached completion notifications.
Automatic Join
Detached completions are joined internally at parent-turn boundaries. The join mode controls how these notifications are delivered and applies only to detached agents; blocking results are already inline and are never added to the outbox or joined.
| Mode | Behavior |
|---|---|
smart (default) |
2+ detached agents spawned in the same parent turn are automatically grouped into a single consolidated notification. Solo agents notify individually. |
async |
Each detached agent sends its own notification on completion (compatibility alias for ungrouped delivery). Best when results need incremental processing. |
group |
Force automatic grouping even when spawning a single detached agent. Useful when more agents will follow. |
Timeout behavior: When detached agents are automatically grouped, a 30-second timeout starts after the first agent completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers. Group membership is internal and no group ID is exposed.
There is no public create/update/seal wait-group API. Use blocking Agent calls when the result is needed inline, or explicitly detach and join with get_subagent_result({ wait: true }).
Configuration:
- Configure join mode in
/agents→ Settings → Join mode
Model Scope
Opt-in: off by default. Enable via /agents → Settings → Scope models.
When on, each subagent spawn's effective model is validated against pi's own enabledModels list (configured via pi's /scoped-models UI). pi-subagents reads that list; it doesn't manage it. Both of pi's settings files are honored: global ~/.pi/agent/settings.json and project-local <cwd>/.pi/settings.json. Project overrides global — mirrors pi's SettingsManager deep-merge, so a tighter per-project scope (hand-edited into the project settings) is respected.
Out-of-scope handling depends on source:
| Model source | Out-of-scope behavior |
|---|---|
Caller-supplied via Agent({ model: "..." }) |
Hard error returned to the orchestrator, listing allowed models |
| Pinned in agent frontmatter | Warning toast + the pinned model runs (frontmatter is authoritative) |
| Parent-inherited (neither set) | Warning toast + parent's model runs |
Design: scopeModels is a guardrail against the orchestrator picking unexpected models at runtime, not a hard policy against user-level config. The "frontmatter is authoritative" guarantee from v0.5.1 still holds for model: — caller params can't override frontmatter, and frontmatter pins run even when out of scope (with a visible warning).
Pattern format: only exact provider/modelId entries are honored (e.g. anthropic/claude-haiku-4-5-20251001). Glob patterns (*sonnet*), bare model IDs, and :thinking suffixes — which pi itself supports — are silently dropped here. pi's /scoped-models picker writes exact entries, so the limitation is invisible if you configure scope through the UI. Hand-edited globs produce an empty allowed set (scope check becomes a no-op).
No-op safety: if enabledModels is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.
Persistent Settings
Runtime tuning values set via /agents → Settings (max concurrency, default max turns, grace turns, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off) persist across pi restarts. Two files, merged on load:
- Global:
~/.pi/agent/subagents.json— your machine-wide defaults. Edit by hand; the/agentsmenu never writes here. - Project:
<cwd>/.pi/subagents.json— per-project overrides. Written by/agents→ Settings.
Precedence: project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency 4, default max turns unlimited, grace turns 5, join mode smart, defaults enabled).
Disable defaults (disableDefaultAgents, default false): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your project/global custom agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).
Output transcript (outputTranscript, default true): the project/global default for writing each subagent's optional OS-temp .output mirror. Toggle via /agents → Settings → Output transcript, or set false in subagents.json to omit only that mirror project-wide — useful when temporary run artifacts should not sit on disk for backup or DLP tooling to pick up. A custom agent's output_transcript frontmatter overrides this per agent. Applied live at spawn time. Mandatory project-local .pi-subagents history is always written and remains available to reopen Agent View. This setting does not alter Agent/TaskExecute results, get_subagent_result, notifications, join/wait behavior, or what the main agent sees; it also does not affect persist_session, worktree commits, or memory files.
Tool description (toolDescriptionMode, default "full"): which Agent tool description the LLM sees. "full" is the rich Claude Code-style prompt (~1,400 tokens with the default agents); "compact" is ~75% smaller — one-line agent type list, terse usage notes — for small/local models where tool-spec tokens are expensive. Per-option details stay in the parameter descriptions in every mode (the parameter schema is never customizable). Applies on the next pi session.
"custom" registers your own description from <cwd>/.pi/agent-tool-description.md (project) or <agentDir>/agent-tool-description.md (global; project wins). The file is read once at tool registration, so edits also apply on the next pi session. Dynamic parts stay live via placeholders — a static agent list would go stale the moment you add a custom agent:
Launch an autonomous agent. Available types:
{{typeList}}
Custom agents live in .pi/agents/ or {{agentDir}}/agents/.
Placeholders: {{typeList}} (full per-agent descriptions), {{compactTypeList}} (first sentence each), {{agentDir}}, {{scheduleGuideline}} (expands with its own leading newline + - bullet when scheduling is on — place it directly after your last rule line; empty when scheduling is off). Unknown placeholders are left verbatim with a stderr warning; a missing or empty file falls back to "full" with a warning. Note the usual trust umbrella: a project-level file shapes the orchestrator's prompt, same as project agents and extensions do.
Starting point: copy examples/agent-tool-description.md — it reproduces the default full description exactly (a CI test keeps it in sync), so you can trim from a known-good baseline instead of writing from scratch.
Example — global defaults for a beefy machine:
mkdir -p ~/.pi/agent
cat > ~/.pi/agent/subagents.json <<'EOF'
{
"maxConcurrent": 16,
"graceTurns": 10
}
EOF
Every project now starts with concurrency 16 and grace 10, without ever touching the menu. Individual projects can still override via /agents → Settings.
Failure behavior: missing file is silent; malformed JSON logs a [pi-subagents] Ignoring malformed settings at … warning to stderr; invalid/out-of-range field values are dropped per-field; write failures downgrade the /agents toast to a warning with (session only; failed to persist).
Events
Agent lifecycle events are emitted via pi.events.emit() so other extensions can react:
| Event | When | Key fields |
|---|---|---|
subagents:created |
Background agent registered | id, type, description, isBackground |
subagents:started |
Agent transitions to running (including queued→running) | id, type, description |
subagents:completed |
Agent finished successfully (background and foreground) | id, type, durationMs, tokens (lifetime { input, output, total }), toolUses, result |
subagents:failed |
Agent errored, stopped, or aborted (background and foreground) | same as completed + error, status |
subagents:steered |
Steering message sent | id, message |
subagents:compacted |
Agent's session successfully compacted | id, type, description, reason ("manual" / "threshold" / "overflow"), tokensBefore, compactionCount |
subagents:scheduled |
Schedule lifecycle change | { type: "added" | "removed" | "updated" | "fired" | "error", … } (job/agentId/error fields per type) |
subagents:scheduler_ready |
Scheduler bound to session, enabled jobs armed | sessionId, jobCount |
subagents:ready |
RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | — |
subagents:settings_loaded |
Persisted settings applied at extension init | settings (merged global + project) |
subagents:settings_changed |
/agents → Settings mutation was applied |
settings, persisted (boolean — false on write failure) |
tokens.total = input + output + cacheWrite. cacheRead is excluded — each turn's cacheRead is the cumulative cached prefix re-read on that one API call, so summing per-message would over-count it. Use contextUsage.percent (surfaced as (NN%) in the widget) for current context size.
Cross-Extension RPC
Other pi extensions can spawn and stop subagents programmatically via the pi.events event bus, without importing this package directly.
All RPC replies use a standardized envelope: { success: true, data?: T } on success, { success: false, error: string } on failure.
Discovery
Listen for subagents:ready to know when RPC handlers are available:
pi.events.on("subagents:ready", () => {
// RPC handlers are registered — safe to call ping/spawn/stop
});
subagents:ready fires only when pi-subagents is actually loaded and bound in the current session. A session that excludes it (via an agent's extensions:) emits no subagents:ready and does not answer the RPC channels — exactly as if pi-subagents were not installed. Treat "no subagents:ready" as "not available here" and give discovery a timeout rather than waiting indefinitely.
Ping
Check if the subagents extension is loaded and get the protocol version:
const requestId = crypto.randomUUID();
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
unsub();
if (reply.success) console.log("Protocol version:", reply.data.version);
});
pi.events.emit("subagents:rpc:ping", { requestId });
Spawn
Spawn a subagent and receive its ID:
const requestId = crypto.randomUUID();
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
unsub();
if (!reply.success) {
console.error("Spawn failed:", reply.error);
} else {
console.log("Agent ID:", reply.data.id);
}
});
pi.events.emit("subagents:rpc:spawn", {
requestId,
type: "general-purpose",
prompt: "Do something useful",
options: { description: "My task", run_in_background: true },
});
options.model accepts either a Model object (e.g. ctx.model) or a "provider/modelId" string — strings are resolved against ctx.modelRegistry at the RPC boundary, so cross-extension callers can forward serializable values without losing auth context.
options.cwd (absolute path to an existing directory — anything else returns an error envelope; null means unset) runs the agent in a different working directory than the parent session. Its tools operate there and the prompt's environment block describes it, but .pi config still loads from the parent session's project — the target directory's .pi extensions never execute, and its agents/skills/settings are not picked up. RPC spawns remain detached and ID-first; they do not inherit the direct Agent tool's blocking default. Combined with isolation: "worktree", the worktree is created from the target directory's repo, the agent works at the equivalent subdirectory inside the copy (a monorepo-package cwd stays scoped to that package), and the resulting pi-agent-* branch lands in that repo — the completion message names it. On session end, worktree registrations are pruned in every repo that received one; only a hard crash can leave a stale entry (then: git worktree prune in the target repo). Agents with memory: keep reading/writing the parent project's memory.
Stop
Stop a running agent by ID:
const requestId = crypto.randomUUID();
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
unsub();
if (!reply.success) console.error("Stop failed:", reply.error);
});
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
Reply channels are scoped per requestId, so concurrent requests don't interfere.
Persistent Agent Memory
Agents can have persistent memory across sessions. Set memory in frontmatter to enable:
---
memory: project # project | local | user
---
| Scope | Location | Use case |
|---|---|---|
project |
.pi/agent-memory/<name>/ |
Shared across the team (committed) |
local |
.pi/agent-memory-local/<name>/ |
Machine-specific (gitignored) |
user |
<agentDir>/agent-memory/<name>/ (default ~/.pi/agent/agent-memory/, honors PI_CODING_AGENT_DIR) |
Global personal memory |
The user scope previously hardcoded ~/.pi/agent-memory/. If that legacy directory exists for an agent and the new location doesn't, it keeps being used — existing memories aren't orphaned.
Memory uses a MEMORY.md index file and individual memory files with frontmatter. Agents with write tools get full read-write access. Read-only agents (no write/edit tools) automatically get read-only memory — they can consume memories written by other agents but cannot modify them. This prevents unintended tool escalation.
The disallowed_tools field is respected when determining write capability — an agent with tools: write + disallowed_tools: write correctly gets read-only memory.
Worktree Isolation
Set isolation: worktree to run an agent in a temporary git worktree:
Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
The agent gets a full, isolated copy of the repository. On completion:
- No changes: worktree is cleaned up automatically
- Changes made: changes are committed to a new branch (
pi-agent-<id>) and returned in the result - Agent committed its own work: the branch is created at the agent's HEAD, preserving its commits (uncommitted leftovers are committed on top first)
The automatic preservation commit uses --no-verify, so local pre-commit hooks can't block it — the commit is local-only and never pushed, and pre-push/server-side hooks still apply.
If the worktree cannot be created (not a git repo, no commits, or git worktree add fails), the Agent tool returns a clear error instead of running unisolated — isolation: "worktree" is a strict guarantee, not a hint. Initialize git and commit at least once, or omit isolation.
Skill Preloading
Skills can be preloaded by name and injected into the agent's system prompt:
---
skills: api-conventions, error-handling
---
Discovery roots (checked in this order, first match wins):
| Scope | Path | Source |
|---|---|---|
| Project | <cwd>/.pi/skills/ |
Pi-standard |
| Project | <cwd>/.agents/skills/ |
Agent Skills spec |
| User | $PI_CODING_AGENT_DIR/skills/ (default ~/.pi/agent/skills/) |
Pi-standard |
| User | ~/.agents/skills/ |
Agent Skills spec |
| User | ~/.pi/skills/ |
Legacy (pre-Pi) |
Per root, a skill named foo resolves to the first of:
<root>/foo.md— flat file at the top level<root>/foo/SKILL.md— directory skill (top-level)<root>/*/.../foo/SKILL.md— directory skill, found by recursive descent
Recursion skips dotfile directories and node_modules. A directory that itself contains a SKILL.md is treated as a single skill — we don't descend into it. Traversal is byte-order sorted for deterministic resolution across filesystems.
Security: symlinks are rejected at every layer (root, flat file, skill directory, SKILL.md inside a skill directory) — intentional deviation from Pi, which follows symlinks. Skill names with path-traversal characters (.., /, \, spaces, leading dot, >128 chars) are rejected.
Tool Denylist
Block specific tools from an agent even if extensions provide them:
---
tools: read, bash, grep, write
disallowed_tools: write, edit
---
This is useful for creating agents that inherit extension tools but should not have write access.
Architecture
src/
index.ts # Extension entry: tool/command registration, rendering
types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
agent-types.ts # Unified agent registry (defaults + user), tool name resolution
agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
cross-extension-rpc.ts # RPC handlers for cross-extension spawn/ping via pi.events
group-join.ts # Group join manager: batched completion notifications with timeout
custom-agents.ts # Load user-defined agents from .pi/agents/, .agents/agents/, and global agents
memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
output-file.ts # Streaming output file transcripts for agent sessions
worktree.ts # Git worktree isolation (create, cleanup, prune)
prompts.ts # Config-driven system prompt builder
context.ts # Parent conversation context for inherit_context
env.ts # Environment detection (git, platform)
ui/
agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
conversation-viewer.ts # Live conversation overlay for viewing agent sessions
License
MIT — tintinweb
