@nklisch/pi-subagents
A maintained MIT fork of pi-subagents with ordered lifecycle interception for Pi extensions.
Package details
Install @nklisch/pi-subagents from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@nklisch/pi-subagents- Package
@nklisch/pi-subagents- Version
18.2.0-nklisch.2- Published
- Sep 8, 2026
- Downloads
- 1,195/mo · 320/wk
- Author
- nklisch
- License
- MIT
- Types
- extension
- Size
- 654.3 KB
- Dependencies
- 1 dependency · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
],
"video": "https://github.com/gotgenes/pi-subagents/raw/main/media/demo.mp4",
"image": "https://github.com/gotgenes/pi-subagents/raw/main/media/screenshot.png"
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@nklisch/pi-subagents
A pi extension that gives pi a focused, in-process sub-agent core — autonomous agents that run inside the same pi runtime (no spawned subprocesses), plus a typed API and lifecycle events other extensions build on. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in joined or detached mode, steer them while active, resume retained sessions, and define your own custom agent types.
A maintained MIT fork of
@gotgenes/pi-subagents, preserving its upstream execution core while adding a generic ordered lifecycle-interceptor provider seam. See Fork maintenance for provenance, qualification, and upstream-return policy.
https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
Features
- In-process & native — agents run inside the same pi runtime (no spawned subprocesses), sharing tool names, calling conventions, and UI patterns (
subagent,resume_subagent,stop_subagent,list_subagents,get_subagent_result,steer_subagent) — feels native - Joined and detached delivery — join a child result into the current parent turn or detach it for a completion notification; every run shares FIFO admission (configurable concurrency limit, default 4)
- Live widget UI — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
- Session transcripts — open any subagent's full session transcript, including records whose heavy live session has been released, in pi's native read-only viewer via
/subagents:sessions; search literal text and cycle matches without steering the child - Custom agent types — define project agents in
.pi/agents/<name>.mdor the shared.agents/agents/<name>.mdconvention, with YAML frontmatter for prompts, models, thinking, and built-in tools - 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
- Graceful turn limits — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
- Policy-aware agent types — unambiguous names resolve case-insensitively. Unknown names default to
general-purpose, can target another enabled fallback, or can fail closed viafallbackSubagent - 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
- Styled completion notifications — detached agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show the bounded preview; use the transcript path for the full session
- Event bus — lifecycle events (
subagents:created,started,completed,failed,resumed,steered,compacted) emitted viapi.events, enabling other extensions to react to sub-agent activity
Install
This branch is locally qualified but unpublished, so it cannot be installed from npm yet.
The published package is @nklisch/pi-subagents. Fork maintenance records its provenance, release policy, and compatibility commitments.
Load it directly for development:
pi -e ./src/index.ts
Quick Start
The parent agent spawns sub-agents using the subagent tool:
subagent({
subagent_type: "Explore",
prompt: "Find all files that handle authentication",
description: "Find auth files",
mode: "joined",
})
Joined agents return their settled result inline. Detached agents return an ID immediately and notify you once on completion (the default).
UI
The extension renders a persistent widget above the editor showing active detached agents (joined runs are rendered inline by the subagent tool's progress stream):
● 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.
Individual agent results render inline 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 (explicit stop) |
| Error | ✗ ⟳3 · 3 tool uses · 12.4k token (8%) / ⎿ Error: timeout |
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
Detached 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
The LLM receives structured <task-notification> XML for parsing, while the user sees the themed visual. The notification contains a bounded preview; use get_subagent_result or the transcript path for longer output.
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 |
Fast codebase exploration (read-only); inherits the parent prompt as a base |
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 uses replace mode: the parent prompt is the cacheable base and its specialist read-only instructions are appended last, giving those instructions the final say.
Default agents can be 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 |
Pi project authority |
| 2 | .agents/agents/<name>.md |
Shared cross-tool project definitions |
| 3 | $PI_CODING_AGENT_DIR/agents/<name>.md (default ~/.pi/agent/agents/<name>.md) |
Global — available everywhere |
.agents/agents is read-only to this extension. .pi/agents overrides it and the global location on name collisions.
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:
subagent({ 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 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. none denies all built-ins. Parent extension tools remain inheritable and are narrowed by extension policy |
model |
inherit parent | Model — provider/modelId or fuzzy name ("haiku", "sonnet") |
thinking |
inherit | off, minimal, low, medium, high, xhigh, max (actual support depends on host and model) |
max_turns |
unlimited | Max agentic turns before graceful shutdown. 0 or omit for unlimited |
prompt_mode |
append |
replace: parent prompt is the cacheable base; body is appended last with full control (no <sub_agent_context> bridge, no <agent_instructions> wrapper). append: parent prompt is the base; body is wrapped in <agent_instructions> and a sub-agent context bridge is injected (agent acts as a "parent twin") |
inherit_context |
false |
Fork parent conversation into agent |
mode |
detached |
Default result delivery: detached or joined |
timeout_seconds |
— | Positive active-runtime deadline; queued time is excluded |
enabled |
true |
Set to false to disable an agent (useful for hiding a default agent per-project) |
Frontmatter is authoritative.
If an agent file sets model, thinking, max_turns, inherit_context, or mode, those values are used as defaults for that agent.
subagent tool parameters only fill fields the agent config leaves unspecified. Legacy run_in_background and foreground fields are rejected with a diagnostic; they are never silently remapped.
Migration is direct:
# Before # After
run_in_background: true mode: detached
run_in_background: false mode: joined
Tools
subagent
Launch a sub-agent.
| 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 | Model — provider/modelId or fuzzy name ("haiku", "sonnet") |
thinking |
string | no | Thinking level: off, minimal, low, medium, high, xhigh, max |
max_turns |
number | no | Max agentic turns. Omit for unlimited (default) |
mode |
joined or detached |
no | Result delivery mode; defaults to detached |
timeout_seconds |
integer | no | Positive active-runtime deadline; queued time is excluded |
inherit_context |
boolean | no | Fork parent conversation into agent |
Choosing the next action
- Use
mode: "joined"when this parent turn needs the settled result; use the defaultmode: "detached"for independent work. - Let detached agents finish normally. Completion automatically wakes the parent with one bounded result preview; do not poll.
- Use
steer_subagentto redirect an active agent andstop_subagentfor cooperative cancellation. - Use
resume_subagentafter an agent finishes when it should continue with the same retained conversation history. - Use
get_subagent_resultfor a bounded status/result check or recovery after a missed notification; it never waits or dumps the full conversation.
get_subagent_result
Inspect a bounded status or final result from a subagent. It is not the normal completion path because detached completion notifications wake the parent automatically.
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id |
string | yes | Agent ID to check |
The result is nonblocking and bounded. Live records report current status and activity; terminal records include a transcript pointer when output is truncated. Use /subagents:sessions for the full retained transcript.
query_subagent_session
Search a child transcript without steering or changing the child. The tool reads the live in-memory session when available and falls back to the retained JSONL transcript after release.
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id |
string | yes | Agent ID to inspect |
query |
string | no | Case-insensitive literal text; omit for recent entries |
kind |
all, messages, tool_calls, or tool_results |
no | Search scope; defaults to all |
order |
newest or oldest |
no | Result order; defaults to newest |
limit |
integer 1–50 | no | Maximum entries; defaults to 20 |
offset |
non-negative safe integer | no | Offset into the ordered matching entries; defaults to 0 |
Matching is complete and case-insensitive literal across visible message text, tool names and IDs, arguments, and correlated results. Returned excerpts are bounded and center the first match with omission markers and source-range metadata; an omitted or empty query keeps beginning-of-entry previews. Tool names and call IDs are searched in full but exposed as bounded display previews, while stable entry IDs remain intact for overlay correlation. Output is also byte- and line-bounded, and every non-empty matching page emits at least one bounded entry. Details report the total, returned, before, and after counts plus copyable next/previous offsets when applicable. Use the next offset only from the returned details; it advances by entries actually emitted when output bounds shorten a page and is always greater than the requested offset. A zero-match query reports no_matches, while an offset beyond a non-empty result set reports page_out_of_range. Results identify the source and retained transcript path when one exists; use /subagents:sessions for the complete native transcript and get_subagent_result for status or final output.
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 |
|---|---|
/subagents:settings |
Configure concurrency, turn limits, retention, interrupt behavior, and unknown-type fallback |
/subagents:sessions |
View a subagent's session transcript (read-only) |
/subagents:settings
Interactive list to tune max concurrency, default/grace turns, consumed and unconsumed live-session retention, abort-all-on-ESC behavior, and unknown-agent fallback. Changes persist across pi restarts (see Persistent Settings).
/subagents:sessions
Pick any subagent — running, completed, or retained after its live session was released — and read its full session transcript in pi's native per-entry viewer.
Read-only: no steering, no session takeover (steering lives in the steer_subagent tool and the detached widget).
In the viewer, press / to enter literal search, type a query, and press Enter
(or n) for the next match. Press N or Shift+Enter for the previous match.
Press Tab to toggle the tool-only filter without discarding the query. While
search is active, the first Esc clears the search and returns to the transcript;
a second Esc closes the viewer. Outside search, q or Esc closes it directly.
Ordinary ↑/↓ (or j/k) and Page Up/Page Down scrolling remain available.
Creating and editing agent definitions is not a command — write an agent .md file in your editor, or ask a pi session to generate one (see Custom Agents).
Graceful Max Turns
Instead of immediately 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 or within the grace window | ✓ green |
stopped |
Explicit stop, parent cancellation, timeout, hard limit, or lifecycle abort | ■ dim |
error |
Provider, execution, or workspace teardown failure | ✗ red |
Concurrency
Joined and detached agents share a configurable FIFO concurrency limit (default: 4). Excess agents are automatically queued and start as running agents complete. The widget shows each queued detached agent with its effective model and queued state. Stopping queued work settles it immediately and reports that no work started.
Joined agents do not bypass the queue: sibling joined tool calls use Pi's native parallel execution and share the same admission limit. Detached completion nudges are held while the parent is running and flushed at the parent run boundary, suppressing stale or consumed notifications.
Resume requests are serialized per retained session. A second request while a turn is genuinely active gets a deterministic busy result; after an abort or extension-driven continuation, resume waits for both the prior invocation and Pi's authoritative idle boundary instead of racing AgentSession.prompt(). Session teardown likewise emits and awaits session_shutdown before Pi revokes child extension contexts, allowing child-owned processes and timers to stop cleanly.
Persistent Settings
Runtime tuning values set via /subagents:settings persist across pi restarts. Terminal records remain available for the whole parent session. Their heavy live sessions are released after the consumed or unconsumed retention window; the result and persisted transcript pointer remain available, but a released session cannot resume without starting a new subagent. A completion notification, joined result, or get_subagent_result collection marks a result consumed, so the shorter consumed window normally governs after delivery.
Two files, merged on load:
- Global:
~/.pi/agent/subagents.json— your machine-wide defaults. Edit by hand; the/subagents:settingscommand never writes here. - Project:
<cwd>/.pi/subagents.json— per-project overrides. Written by/subagents:settings.
Precedence: project overrides global on any field present in both.
Missing fields use these defaults: max concurrency 4, max turns unlimited, grace turns 5, consumed-session retention 10 minutes, unconsumed-session retention 720 minutes, abort all on parent ESC enabled, and unknown-agent fallback general-purpose.
Example — global defaults for a beefy machine:
mkdir -p ~/.pi/agent
cat > ~/.pi/agent/subagents.json <<'EOF'
{
"maxConcurrent": 16,
"graceTurns": 10,
"unconsumedSessionRetentionMinutes": 1440,
"abortAllOnInterrupt": false,
"fallbackSubagent": false
}
EOF
Every project now starts with concurrency 16, grace 10, a one-day unconsumed retention cap, detached agents surviving parent ESC, and unknown agent types failing closed.
Individual projects can still override via /subagents: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 /subagents:settings 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 |
Agent record registered | id, type, description, runId, mode |
subagents:started |
Agent transitions to running (including queued→running) | id, type, description, runId, mode |
subagents:completed |
Agent reaches a terminal state | id, type, runId, mode, status, terminalReason, activeRuntimeMs, tokens, toolUses, result |
subagents:resumed |
A resumed turn reaches a terminal state | completed-event shape plus runId, status, terminalReason, and error |
subagents:failed |
Agent ends in an error or stopped state | same as completed + error, status, terminalReason |
subagents:steered |
Steering message sent | id, message |
subagents:compacted |
Agent's session successfully compacted | id, type, description, reason ("manual" / "threshold" / "overflow"), tokensBefore, compactionCount |
subagents:settings_loaded |
Persisted settings applied at extension init | settings (merged global + project) |
subagents:settings_changed |
/subagents: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.
Worktree Isolation
Worktree isolation lives in a companion package, not this core.
Install @gotgenes/pi-subagents-worktrees and list the agent types you want isolated in its worktreeAgents config — opted-in agents run in a temporary git worktree, and their changes are saved to a branch on completion.
The earlier isolation: "worktree" spawn flag and isolation: frontmatter key were removed from the core.
Removed: agent memory and skill preloading
Persistent agent memory (the memory: frontmatter key) and skill preloading (the skills: frontmatter key) were removed when the core was slimmed down.
Children always inherit the parent's skills and extensions, so the isolated, extensions, and skills frontmatter keys no longer exist. Child creation uses a denylist rather than a registration-time allowlist: extension tools—including tools registered during lifecycle hooks—remain available, while disallowed built-ins and the three recursive orchestration tools stay excluded.
Migrating from disallowed_tools
The disallowed_tools frontmatter field has been removed.
Use @gotgenes/pi-permission-system's permission: frontmatter instead — it provides richer semantics (allow/ask/deny vs. binary hide):
# Before (no longer supported)
disallowed_tools: bash
# After
permission:
bash: deny
Permission System Integration
When @gotgenes/pi-permission-system is installed, this extension integrates automatically:
- Per-agent permission policies — define
permission:in agent YAML frontmatter to set allow/ask/deny rules per agent type. The permission system resolves the agent name from the<active_agent>tag in the child system prompt. - Tool filtering — the permission system's
before_agent_starthandler removes denied tools from the child session before the agent starts. ask-state forwarding — when a child session triggers anaskpermission, the prompt forwards to the parent session's UI. The parent approves or denies, and the child resumes.- Deterministic child detection — this extension publishes
subagents:child:session-createdbeforebindExtensions()fires; the permission system subscribes and registers the child session synchronously, so detection does not rely on env vars or filesystem heuristics.
No configuration is required.
When @gotgenes/pi-permission-system is not installed, the lifecycle events have no subscriber — a harmless no-op.
For Extension Authors
This package exposes two public subpath exports for companion extensions to import from the published tarball.
@nklisch/pi-subagents — cross-extension service contract
Access the subagent service from another extension at runtime:
const { getSubagentsService } = await import("@nklisch/pi-subagents");
const svc = getSubagentsService();
void svc?.launch("Explore", "Check for stale TODOs", { mode: "detached" });
Declare this package as an optional peer dependency.
See src/service/service.ts for the full SubagentsService interface, the single WorkspaceProvider, and ordered lifecycle registration.
Ordered lifecycle interception
registerLifecycleInterceptor() is a generative provider seam for extensions that must replace or deny the exact child prompt, or accept, replace, abort, or continue a proposed child result before finalization.
Callbacks receive immutable agent/session/run/parent identity and execution-path facts, never a manager or live session.
Registrations are awaited in order; prompt and result replacements pipe to later providers; continuations stay in the same session and stop after three rounds.
Existing lifecycle events remain observational and cannot substitute for this registration API.
const registration = svc?.registerLifecycleInterceptor({
beforeStart: async ({ prompt }) => ({ action: "continue", prompt: `Context:\n${prompt}` }),
beforeComplete: async ({ proposedResult }) => ({ action: "complete", result: proposedResult }),
});
await registration?.dispose();
@nklisch/pi-subagents/settings — layered config loader
Extensions that store configuration in JSON files can use the shared layered loader, which reads a global file (<agentDir>/<filename>) and a project file (<cwd>/.pi/<filename>) and merges them — project wins on conflicts, missing files are silent, malformed files warn and fall back:
import { loadLayeredSettings, type LayeredSettingsSource } from "@nklisch/pi-subagents/settings";
interface MyConfig { enabled?: boolean; limit?: number }
function sanitize(raw: unknown): Partial<MyConfig> {
if (!raw || typeof raw !== "object") return {};
const r = raw as Record<string, unknown>;
const out: Partial<MyConfig> = {};
if (typeof r.enabled === "boolean") out.enabled = r.enabled;
if (typeof r.limit === "number") out.limit = r.limit;
return out;
}
const config = loadLayeredSettings<MyConfig>({
agentDir, // Pi runtime agent home directory
cwd, // project root — project file lives at <cwd>/.pi/<filename>
filename: "my-extension.json",
sanitize,
warnLabel: "my-extension", // prefix for the malformed-file stderr warning
});
loadLayeredSettings returns Partial<T> (all fields optional); apply your defaults after the call.
It never throws — all error conditions produce a console.warn and return {}.
Architecture
This extension is a minimal, composable core: it owns agent spawning, execution, and result retrieval, and exposes a typed SubagentsService plus lifecycle events that other extensions build on.
See docs/architecture/architecture.md for design principles, domain ownership, lifecycle and service contracts, and module dependency diagrams.
Relationship to upstream
This package is a maintained fork of @gotgenes/pi-subagents, itself an independently maintained hard fork of tintinweb/pi-subagents.
It retains the minimal core and adds only the generic lifecycle-provider seam described above.
The fork follows the rebase, security, and return-to-upstream policy in Fork maintenance.
License
MIT — tintinweb (upstream) and Chris Lasher (fork)
