pi-submarine
A no-nonsense Pi extension for focused foreground child-session delegation.
Package details
Install pi-submarine from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-submarine- Package
pi-submarine- Version
0.7.0- Published
- Nov 1, 2026
- Downloads
- 333/mo · 247/wk
- Author
- dnouri
- License
- MIT
- Types
- extension
- Size
- 145.9 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/dnouri/pi-submarine/0.7/media/pi-submarine.jpg",
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-submarine
And now for something completely delegated.
pi-submarine sends tasks to Pi child sessions and
brings back their answers. The child's transcript stays out of the parent's
context.
Install
pi install npm:pi-submarine
Requires Pi 1.3.0 or newer in the 1.x series. Both Pi peer dependency ranges
are ^1.3.0, and the tests run against SDK 1.3.0 only, so a later 1.x
release is not covered.
Upgrading to 0.7. Tool results now publish version 2 saved-conversation declarations with every descendant. Consumers that support only version 1 must add version 2 support before upgrading. Older nested version 1 declarations remain readable as degraded leaves. See Saved-conversation references.
Features
- Fresh children that start without the parent conversation, and forked children that copy the current branch.
- Named agents from markdown files with four frontmatter keys:
description,model,agentsMd, andskills. - Model chains on a named agent, with explicit effort levels and fallback on the first turn.
- Call-level overrides for the model and context files that keep the named prompt and skills.
subagent_listfor the agents and models visible from a directory.- Progress updates with activity, turn count, nested children, and context usage.
- A depth limit for nested subagents, and parallel calls through
multi_tool_use.parallel. - Recursive saved-conversation references that let a viewer open every descendant transcript.
- Child sessions that resume after a failure, an interruption, or an explicit user request to continue that exact child.
Each subagent call waits for one foreground child. The extension provides
no built-in agent roles, background jobs, or workflow engine.
Usage
[!IMPORTANT]
pi-submarinetrusts project-local Pi inputs for the child cwd. Review unfamiliar.pi/resources and project.agents/skills before you run subagents. Children run with the Pi process's permissions. Neither fresh context noragentsMd: "none"is a sandbox.
The extension registers three tools:
subagent({ task, title, agent?, model?, agentsMd?, context?, cwd? })
subagent_resume({ sessionId, message, title })
subagent_list({ cwd? })
All three tools reject unknown arguments. For example:
subagent({ task: "Inspect src/runner.ts and summarize the control flow. Do not edit files." })
subagent({ agentsMd: "auto", task: "Review src/runner.ts using the project instructions. Do not edit files." })
subagent({ model: "acme/acme-5v-turbo@low", task: "Read screenshots/login.png and return its contents." })
subagent({ agent: "reviewer", model: "acme/fast@low,acme/steady@high", task: "Review src/runner.ts for regressions. Do not edit files." })
subagent({ context: "fork", task: "Use the parent conversation to check my last plan. Do not edit files." })
Model names in the examples are placeholders. Call subagent_list({}) only
when agent names, models, or supported effort levels are unknown. Use known
choices directly. One comma-separated model chain is one child with
fallback, not parallel children.
Fresh children can use files and tools in their cwd. They do not see the
parent conversation or its attached images. Include the needed background,
paths, constraints, checks, and expected output in task. Use
context: "fork" when the child needs the parent conversation.
Use multi_tool_use.parallel to run independent subagent calls together.
Start dependent calls after their prerequisites finish.
subagent({ task, title, agent?, model?, agentsMd?, context?, cwd? })
Runs one focused task in a child Pi session and returns the child's answer and session ID. The arguments are:
taskis required. It is the task and expected output for the child.titleis required, with a maximum of 120 characters. Use three to five words to name the task for the user; it appears while the child runs, in the result, and in saved-chat viewers. Titles are never derived fromtask. Whitespace collapses to single spaces, and a title that is still empty or holds control or formatting characters is rejected before any child work starts.agentis optional. Omit it for an unnamed child. Pass an existing filename stem such as"reviewer"to usereviewer.md, not a role name. A literalagent: "subagent"loadssubagent.md.modelis optional. Pass comma-separatedmodel@effortorprovider/model@effortcandidates in fallback order. This replaces the named agent's model chain for the call.agentsMdis optional and valid only with fresh runs.noneskips automatic context-file loading, includingAGENTS.mdandCLAUDE.md.autoenables normal discovery. Omission uses the named agent's choice, ornonefor an unnamed child. An explicit choice persists on resume.contextis optional and defaults to"fresh". Use"fresh"for a new child session. Use"fork"only when the child must inherit the current conversation branch. A fork rejects bothcwdandagentsMd.cwdis optional and valid only with fresh runs. Omit it to use the caller cwd. Relative values resolve from the caller cwd. Pi discovers project agents, context files, skills, and other resources there. Relative tool paths and bash commands use that directory.
Effort levels are off, minimal, low, medium, high, xhigh, and
max. Support depends on the model and the installed Pi version.
Non-reasoning models support @off.
For a model ID without a provider, pi-submarine prefers a single
authenticated match from the parent's provider. Otherwise, the ID must have
exactly one authenticated match from any provider. Ambiguous IDs require
provider/model@effort.
When neither the call nor the agent file selects a model chain, fresh and forked children inherit the parent's current model and effort.
Fallback advances only on an initial provider error before assistant success or tool progress. Prompt rejection, abort, later failures, and weak answers do not trigger fallback. If all candidates fail, the call returns an error. Each fallback attempt uses the same child session ID. The extension keeps failed attempts in the child's JSONL file but excludes them from the active conversation.
Fresh runs create a new child session in the current root session's
.subagents/ directory. Nested subagents share that directory. Fork runs
branch from the current conversation leaf into the same directory. A fork
can see the current parent turn and the parent's instructions. Phrase the
task so a parent-only final-answer marker stays out of the child's output.
An explicit cwd can hide a named agent that the caller cwd would find.
In that case, subagent fails with a hint to omit cwd and put external
paths in task.
A successful initial result looks like this:
## Subagent reviewer result
Subagent session ID: 019...
Task: Check catalog conflicts
Ran 3 turns at 42% context
Model: acme/fast
<child assistant answer>
The Subagent session ID identifies the child for recovery or an explicit
request to resume that exact child session. Initial results name the serving
model when known and summarize earlier failed attempts with truncated
provider errors. The task and per-episode turn count appear after the
session ID. Unknown or unavailable context usage appears as ? context.
The model note follows those lines, and the child's answer comes last.
subagent_resume({ sessionId, message, title })
Continues a child Pi session within the current root session. It opens the
recorded child JSONL session, appends message, and waits for the child's
answer. It returns the answer and the same session ID, without the initial
Model line. title is required and names this continuation; it is never
derived from message. Resume results include the same Task and Ran
lines under that title. The arguments are:
sessionIdis required. Use the child Pi session ID from an earliersubagentorsubagent_resumeresult, including recovery text after an interrupted or failed child run.messageis required. The extension appends it to that same child conversation.
For example, recover an interrupted task with:
subagent_resume({
sessionId: "<returned-child-id>",
message: "Continue the interrupted task from where you left off.",
title: "Continue the interrupted review",
})
Resume requires a persisted parent Pi session. It looks up the child in
the current root session's manifest. It does not search other roots or
copy the child session. Use a new subagent for ordinary follow-up work.
Resume is for recovery or an explicit user request to continue that exact
child.
For a child that started fresh, resume reloads current resources from the
recorded cwd. If the original call set agentsMd, resume keeps that choice
even if the agent file changes. Otherwise, resume uses the current agent
file's settings. An unnamed child uses agentsMd: none and skills: auto.
Resume reloads skills and files rather than using snapshots. Resuming a
named fresh child requires a valid current agent file.
For a child that started as a fork, resume loads normal Pi resources from
the recorded cwd. It ignores the agent file's agentsMd and skills
controls. Both modes restore the child's recorded model and effort rather
than reapplying current defaults. Resume sends message as plain user text.
It does not repeat the initial prompt envelope or add new parent history.
subagent_list({ cwd? })
Lists the markdown agents visible from a working directory, plus the unnamed child mode and models with configured authentication. The argument is:
cwdis optional. Usually omit it to list the caller project's agents. An explicitcwdlists another directory's agents. Relative values resolve from the caller cwd.
The text result shows each agent's name, description, path, and source.
The source is user or project. The Callable models section lists
supported effort levels and marks the current session model.
Structured details include the resolved cwd, source counts, user and
project agent directories, the callable model catalog, and the current
model. If an explicit cwd hides project agents visible from the
caller cwd, the result includes a warning.
Child configuration
Children load their own Pi settings and resources for the child cwd and
agent directory. The agent directory is $PI_CODING_AGENT_DIR when set,
otherwise ~/.pi/agent. To give a child provider access, configure
auth.json, models.json, the environment, or a startup extension that
loads in the child.
The extension does not copy temporary API keys, provider registrations, or model-header overrides from the parent. Give a child equivalent files or startup extensions when it needs them.
These choices control each initial call:
| Choice | Default | Override |
|---|---|---|
| Model and effort | The named agent's chain, otherwise the parent's current model and effort. | Call-level model replaces the whole chain for named or unnamed children. |
| Context files | The agent file's agentsMd, otherwise none. |
Fresh-only agentsMd: "auto" loads normal personal and project instructions. "none" skips them. |
| Skills | auto for unnamed children and agent files that omit skills. |
An agent file can set skills: none or a comma-separated list. |
| Workspace | The caller cwd for fresh children. | Fresh-only cwd changes project discovery and tool paths. |
Forks copy the current conversation branch and load normal Pi resources.
They reject call-level agentsMd and cwd. See Markdown agents
for the agent file format.
Markdown agents
Review unfamiliar project .pi/agents/*.md files before running them.
Their prompts can instruct the child to use tools and change files.
User agents live in agents/*.md under the Pi agent directory. Project
agents live in the nearest ancestor .pi/agents/*.md from the effective
cwd. Project agents override user agents with the same filename stem.
Discovery reads only the top level of each directory. It ignores hidden
files, uppercase .MD, and *.chain.md files.
For a project agent, normally pass the agent name and omit cwd. For
example, create .pi/agents/reviewer.md:
---
description: Reviews code for concrete regressions
agentsMd: auto
skills: auto
---
Inspect the requested changes. Separate verified failures from concerns.
This agent inherits the parent's model and effort unless the call selects a model chain. Select it by filename stem:
subagent({ agent: "reviewer", task: "Review src/runner.ts for regressions. Do not edit files." })
Call-level model and agentsMd overrides preserve the named prompt and
skills. If the task needs files outside the project, put their paths in
task unless that directory should define the child's project context.
Agent files use the frontmatter block shown above, not YAML. description
is required and nonempty. The optional model supplies the default chain
when the call omits it. The value uses the same format as the call argument,
with an @effort suffix on every candidate. For example:
---
description: Completes bounded development tasks
model: acme/fast@low,acme/steady@high,acme/backup@low
---
Complete the task and report the result.
The parser accepts only description, model, agentsMd, and skills.
It rejects duplicate keys, blank frontmatter lines, comments, quoted values,
arrays, and block scalars. Files must have both frontmatter delimiters and a
nonempty body.
agentsMd may be none or auto and defaults to none. skills may be
auto, none, or a comma-separated list of skill names and defaults to
auto. A fresh run fails before execution if a named skill is missing or
hidden from automatic model invocation.
Every initial subagent call sends the child a user prompt envelope with
subagent context and the task. Named agents add their markdown body in an
XML-style block, not in the system prompt.
Unnamed fresh children retain normal skills. Named fresh children follow
their agent file's agentsMd and skills controls unless the call overrides
agentsMd. The agentsMd choice controls automatic context-file loading.
It does not disable extensions, settings, SYSTEM.md, APPEND_SYSTEM.md,
or the child's ability to read files.
A fork ignores the agent file's agentsMd and skills controls but still
applies its prompt body and model chain. It also copies the parent
conversation, instructions included, so the child is not isolated from them.
Artifacts and progress
subagent and subagent_resume require a persisted parent Pi session.
Once a child episode starts, pi-submarine writes artifacts beside the
root parent session when possible, even if the episode later fails or is
aborted:
<parent-session>.jsonl
<parent-session>.jsonl.subagents/
manifest.jsonl
<child-session>.jsonl
manifest.jsonl records lifecycle data used for resume and nesting.
Full child messages remain in the child session JSONL.
While a child runs, Pi frontends receive partial text updates like this:
- Review catalog records (subagent, 6% ctx, 1 turn, 15s) -> Check catalog conflicts (reviewer, ? ctx, 2 turns, 8s): using read
Each update also carries structured details.run data:
type SubagentRunView = {
episodeId: string
sessionId: string
agent: string
title: string
status: "running" | "completed" | "failed" | "aborted"
turnCount: number
lastActivityAt: string
activity: string
contextUsage?: {
tokens: number | null
contextWindow: number
percent: number | null
}
children: SubagentRunView[]
}
sessionId is the ID to use for continuation. episodeId identifies one
call to subagent or subagent_resume. An initial call and its later
resume calls are separate episodes that share one child sessionId. Nested
child run metadata stays in structured run details. The extension leaves
nested transcripts and final answers in child sessions instead of pasting
them into the parent model context.
Context usage comes from Pi's AgentSession.getContextUsage(). If Pi
reports unknown usage, the progress text shows ? ctx. If usage is
unavailable, the segment omits it. The extension uses Pi's standard
tool update callback, so it needs no custom TUI renderer.
Each progress segment is the caller's title followed by the agent name and
ends with whole seconds since that episode's last activity, with a minimum
of 0s and no maximum. Nested segments have their own titles and ages. While an episode runs, the runner
emits an update every 15 seconds even if the activity text has not changed.
It refreshes context usage before each heartbeat without changing the last
activity timestamp. The heartbeat stops on completion, failure, or abort.
Saved-conversation references
A saved-conversation reference is a pointer to a child transcript. A viewer
that renders tool details can use it to open the transcript. The references
appear in details.savedConversations, beside details.run. The field does
not depend on the tool name. It is a viewer convention, not an upstream Pi
API. Version 2 has this shape:
// Shape of details.savedConversations
type SavedConversations = {
version: 2
root: string
coverage: "observed" | "complete" | "partial"
items: SavedConversationOccurrence[]
}
type SavedConversationOccurrence = {
id: string
parentId: string | null
kind: "start" | "resume" | "unknown"
sessionId: string
label?: string
title?: string
stats?: { turns: number; contextPercent: number | null }
save:
| { kind: "pi-v3"; file: string; readiness: "planned" | "checked" }
| { kind: "unavailable"; reason: string }
}
root is an absolute directory, not a filesystem root. items is an
ordered occurrence ledger for the immediate child and every declared
descendant. The immediate child is the single root occurrence with
parentId: null. Other rows use parentId to describe invocation ancestry.
Each row has the exact session ID from its child's Pi v3 header. Fresh and
fork runs label the child with its agent name. Duplicate labels are valid.
title is display text, not chat identity. stats.turns is the owning
episode's turn count, and stats.contextPercent is its context percentage
or null when unknown or unavailable. The runner snapshots these fields
from its run view on each declaration. Nested occurrences retain the title
and stats declared by their producer. Stats permit integer turn counts
from 0 to 1,000,000 and context percentages from 0 to 100.
Normal id values are pi-submarine episode IDs. A resume gets a new episode
ID and keeps the same child session ID and file. Treat every id as opaque.
Legacy imports and conflicting sources can use synthetic IDs to preserve
their declarations without borrowing identity from run metadata. Use the root and
session ID to identify a chat, not the label or occurrence ID.
coverage describes declaration completeness, not task success or file
availability. Running results use observed. Terminal results use
complete when every nested declaration is represented and partial when
a declaration is missing, malformed, conflicted, or over a limit. An
unavailable save does not by itself make coverage partial.
A pi-v3 save contains a safe path relative to root. A planned save can
appear before Pi writes the session file. A viewer that opens it too early
should retry after the child finishes. Each invocation validates only its
own file at completion and changes that save to checked. The file must be
readable, sit inside root, and have a header that matches the recorded
session ID and cwd. A failed check produces an unavailable row without
changing the tool's outcome or hiding valid descendants. Any published file
can still disappear or change later.
The collector accepts version 1 declarations from older nested producers.
It imports every item as an unknown leaf and marks coverage partial. It
does not infer missing descendants or episode IDs. Consumers should retain
version 1 support for historical results.
A result retains at most 1,024 occurrence rows and 1,024 nested invocation
slots for its lifetime. Each validated string can contain at most 4 KiB of
UTF-8. Overflow marks coverage partial. Row overflow keeps the direct child
and an ancestor-closed selection instead of emitting dangling descendants.
A result without this field remains readable text. See Pilish's extension author guide for the consumer contract.
Errors and limits
- Once a child episode starts, failures and interruptions return native
isError: trueresults with error text and terminal details. The extension throws preflight validation and lookup errors instead. Pi turns both kinds into failed tool results, and a parent that was not aborted can continue. - The extension forwards parent abort signals to the child session. If a child session already exists, interruption and failure errors include its session ID for possible recovery. Validation and lookup failures do not return a session ID.
- Resume requires the recorded model to be available and authenticated. If Pi reports an SDK model substitution on any run, the run fails before prompting instead of using a substitute.
- Pi persists final details for registered tools and standalone SDK
customTools. If the Pi process dies before it finalizes the parent result, those details are not saved. A later extension can replace result details. pi-submarinedoes not add session-file paths, stack traces, or child transcripts to the text a model sees in a success, interruption, or recovery result. It preserves the original provider or extension error message, which may contain its own details.- The extension logs manifest append failures but does not fail an otherwise successful child episode. Recovery is not guaranteed if the original manifest write failed.
- In one Pi process,
pi-submarinerejects concurrent attempts to append to the same child session, includingsubagent_resumewhile the originalsubagentis still active. This is not a cross-process lock. - Nested subagents share the Node.js event loop. Each child has its own Pi extension runtime. The extension rejects nesting deeper than 4 to stop accidental circular delegation.
pi-submarinedoes not add a wall-clock timeout around Pi'ssession.prompt().
Development
For local development, load the extension directly:
pi -e ./src/index.ts
Run the checks with:
npm install --ignore-scripts --no-package-lock
npm run typecheck
npm test -- --no-cache
npm run build
npm run smoke:registration -- --no-cache
The offline SDK 1.3.0 checks in test/sdk-seam.test.ts and
test/sdk-auth.test.ts cover registered and standalone tools, persistence,
failures, recovery, and child authentication without real provider requests.
To keep the saved conversations and raw results from a consumer check, run:
SUBMARINE_ARTIFACT_OUTPUT="$PWD/tmp/saved-conversation-artifacts" \
npx vitest run test/sdk-seam.test.ts test/sdk-auth.test.ts --no-cache
Pi loads ./src/index.ts through its TypeScript extension loader. The npm
package contains src/, README.md, and package.json. Model-facing labels,
descriptions, guidelines, and prompt envelopes live in src/tool-prompts.ts.
Keep that file, src/index.ts, and this README in sync when the tool
contract changes.
