@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.

Packages

Package details

extension

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-subagents integrating npm:pi-subagents-style JSON agent overrides — configure agents via settings.json without writing .md files.

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, Enter to open a live or read-only history viewer, and Esc to return. Long rosters reserve a right-hand track/thumb scrollbar and show ↑ N more or ↓ N more at the clipped edge. Configure via /agents → Settings → Widget: all, background, or off
  • Conversation viewer — select any agent in /agents to 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 pressing e to open a Pi-native composer, typing, then Enter to send (Esc or 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 with Esc, q, or the close control. Stop a still-running agent by pressing x (then x again 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>.md or .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_subagents gets ownership-scoped Agent, get_subagent_result, and steer_subagent tools 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 SIGKILL termination 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.md directory layout supported)
  • Tool denylist — block specific tools via disallowed_tools frontmatter
  • 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 via pi.events, enabling other extensions to react to sub-agent activity
  • Cross-extension RPC — other pi extensions can spawn and stop subagents via the pi.events event bus (subagents:rpc:ping, subagents:rpc:spawn, subagents:rpc:stop). Standardized reply envelopes with protocol versioning. Emits subagents:ready on session start
  • Schedule subagents — pass schedule to the Agent tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same subagent-notification followUp path as manual background completions; manage via /agents → Scheduled jobs
  • Model scope enforcement — opt-in validation that subagent model choices stay within your pi enabledModels allowlist (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:

  • schedule cannot be combined with inherit_context (no parent conversation exists at fire time) or resume (schedules create fresh agents).
  • run_in_background does not change a scheduled fire; scheduled agents always run detached.
  • Scheduled fires bypass the maxConcurrent queue so a 5-minute interval cannot be deferred behind long-running manual agents.
  • Headless pi -p doesn'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 declared contextWindow, 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:foo in tools: narrows what surfaces; it can't load foo on its own. Mismatches fire extension-error:… warnings.
  • Any ext: entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So tools: "*, ext:mcp/search" exposes only search from mcp, nothing from any other extension.
  • Extension names match case-insensitively ([Mcp] = [mcp]); tool names in ext:foo/bar stay 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_start or before_agent_start rather than at load. Subagent scoping is re-derived as tools appear, so these surface normally — including under ext: 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 is src/index.ts also answers to [src]). Prefer the package name — the path-derived one is incidental.
  • Plain tools: typos fail loudly: tools: reed, grep fires tools-error:… instead of silently producing an under-tooled agent.
  • exclude_extensions: wins over extensions: and over ext: selectors — an excluded extension never loads and a tools: ext: entry can't pull it back. Plain names only (no paths, no *); a name matching nothing fires an extension-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.on handlers like session_start only fire for extensions bound to the session), but not other load-time side effects — a factory that subscribes directly to the shared pi.events bus 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
  • Eject — writes the embedded default config as a .md file 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 .md file). 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:

  1. At max_turns — steering message: "Wrap up immediately — provide your final answer now."
  2. Up to 5 grace turns to finish cleanly
  3. 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 /agents menu 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