pi-submarine

A no-nonsense Pi extension for focused foreground child-session delegation.

Packages

Package details

extension

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, and skills.
  • 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_list for 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-submarine trusts 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 nor agentsMd: "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:

  • task is required. It is the task and expected output for the child.
  • title is 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 from task. 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.
  • agent is optional. Omit it for an unnamed child. Pass an existing filename stem such as "reviewer" to use reviewer.md, not a role name. A literal agent: "subagent" loads subagent.md.
  • model is optional. Pass comma-separated model@effort or provider/model@effort candidates in fallback order. This replaces the named agent's model chain for the call.
  • agentsMd is optional and valid only with fresh runs. none skips automatic context-file loading, including AGENTS.md and CLAUDE.md. auto enables normal discovery. Omission uses the named agent's choice, or none for an unnamed child. An explicit choice persists on resume.
  • context is 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 both cwd and agentsMd.
  • cwd is 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:

  • sessionId is required. Use the child Pi session ID from an earlier subagent or subagent_resume result, including recovery text after an interrupted or failed child run.
  • message is 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:

  • cwd is optional. Usually omit it to list the caller project's agents. An explicit cwd lists 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: true results 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-submarine does 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-submarine rejects concurrent attempts to append to the same child session, including subagent_resume while the original subagent is 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-submarine does not add a wall-clock timeout around Pi's session.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.