@ilya-lesikov/pi-pi

Pi-Pi: a persistent, general-purpose Pi agent with adaptive tools, workers, skills, and memory

Packages

Package details

extension

Install @ilya-lesikov/pi-pi from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@ilya-lesikov/pi-pi
Package
@ilya-lesikov/pi-pi
Version
1.17.0
Published
Oct 26, 2026
Downloads
1,604/mo · 605/wk
Author
ilya-lesikov
License
Apache-2.0
Types
extension
Size
2.5 MB
Dependencies
6 dependencies · 5 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions/orchestrator"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-pi

A persistent, general-purpose agent configuration for Pi.

Install

pi install npm:@ilya-lesikov/pi-pi

Recommended optional extension:

pi install npm:pi-mcp-adapter

Start Pi and talk to it normally. The initial Pi session owns all work; pi-pi does not create a second task context or impose research, planning, implementation, or review phases. It continues interrupted work autonomously, recovers stalled turns, and asks for user input only when unavailable information or preference controls a consequential decision. A turn that ends in prose after real work is checked out of band — the turn is replayed to its own model with a yes/no question — and resumed through a hidden message only when that model says work is left, so nothing is added to the conversation on a false alarm.

/pp

/pp remains the control panel for:

  • session and context status;
  • bounded background workers;
  • reloadable skills;
  • prompt size and recall;
  • providers, model slots and family overrides;
  • main and worker models.

Reports (Status, Usage, Diagnostics, Explain routing) open as a scrollable view in the terminal. Elsewhere they go wherever the host shows pi-pi's messages: its notifications, a chat message the model never sees, or stderr in a run without a UI, where /pp prints the status and exits. The worker viewer becomes the transcript as text with Refresh, Send and Stop. Some apps (bb, and possibly ACP adapters) wait for a model run after every prompt and /pp starts none; there the menu says the app may stay busy afterwards, and on bb without pi-pi's plugin, which shows neither notifications nor chat messages outside a turn, reports open in a read-only text dialog. With the plugin, bb gets the menu through the plugin instead (see bb).

Footer

The first line says what the main agent is doing and for how long — waiting for the model, thinking, writing, running a tool (and which), waiting on workers or on your answer, waiting out a rate limit, compacting — or that it is idle and since when; it turns amber when it waits on you and red when the model has said nothing for two minutes. The directory and branch sit on the right of that line and shorten first when space runs out. The second line has the token, cache, cost and context figures and the provider, model, thinking level and pi-pi version. Statuses other extensions set get a third line when there are any. pi's own "Working" row is hidden, since the footer says more.

Memory

The prompt is kept inside the model's window by folding old tool traffic out of the copy on its way to the provider: a result becomes an [omitted: <size>B] notice, and the call that asked for it, along with everything the user and the model said, is never touched. Arguments are left exactly as they were written, so a folded call still states what it ran and on what; only the answer goes. Folding is oldest-first and never reversed, so the prompt's prefix stays stable between requests and the provider's cache survives. Calls old enough to be finished with are removed outright rather than left as husks.

The session itself is never cut. recall_tool_output hands back a folded result by the id of the call it answered — the one on the call and on the result block beside the notice — and vcc_recall searches durable session history: messages, tool calls, and tool results. Native Pi session restoration remains authoritative; pi-pi does not duplicate conversation state in task files.

Tool provisioning

The tools the agent is told to prefer are installed rather than assumed. ripgrep and the code graph are fetched at session start; a language server is fetched the first time a file of its language is touched. Everything lands in one directory that is prepended to the session's PATH, so ordinary which lookups find it and the user's shell environment is untouched.

Downloads verify what the publisher actually offers — a published digest where there is one, npm's own integrity check for npm packages, a toolchain's own verification for toolchain components — and /pp → Diagnostics reports which of those applied per tool rather than implying a uniform guarantee. A session that cannot reach the network starts anyway, degraded exactly as it would have been before.

Hung commands

pi waits on a shell command forever unless the call sets a timeout, so a server left in the foreground or a script that never exits holds the turn until someone notices. pi-pi gives every bash call that sets no timeout a default one, in the main agent and in workers alike: performance.internals.bashTimeout (10 minutes; workerBashTimeout for workers). It adds the timeout to the call's arguments rather than replacing pi's bash, so it works whichever extension's bash runs, and pi-pi can sit next to another extension that replaces bash. A call with its own timeout keeps it. The agent's tool guidance also tells it to set a timeout sized to the command on anything that takes more than a few seconds, and to start servers and watchers in a bash call of their own, in the background with their output in a file.

Language servers

The lsp tool gives the agent and its workers definitions, references, hover, diagnostics, symbols, call hierarchy and code actions from real language servers. Fourteen are built in (TypeScript/JavaScript, Go, Rust, Python, C/C++, Bash, Lua, Zig, Kotlin, Ruby, C#, Swift, Elixir, Java); five of them are installed on demand, the rest are used when found on PATH. A server starts on first use, once per project root — the nearest directory with one of its markers (go.mod, Cargo.toml, tsconfig.json, …) — so a monorepo gets one server per project. Queries that span the project wait for the server to finish indexing; a server that keeps failing to start is retried with a growing delay rather than on every call.

After each successful edit or write, the errors and warnings the file's already-running servers report are appended to the result, at most 20, inside <diagnostics file="…">. It never starts a server or waits more than a moment for one; reading a file starts its servers in the background so the next edit finds them ready. Turn it off with lsp.diagnosticsAfterEdit: false or /pp → Settings → LSP.

lsp.servers in the global or project config overrides a built-in field by field or adds a server:

{
  "lsp": {
    "servers": {
      "typescript": { "command": ["/opt/tsls/bin/typescript-language-server", "--stdio"] },
      "go": { "enabled": false },
      "nix": { "command": ["nil"], "extensions": [".nix"], "rootMarkers": ["flake.nix"], "settings": { "nil": { "formatting": { "command": ["nixfmt"] } } } }
    }
  }
}

settings answers the server's workspace/configuration requests; initializationOptions and env are passed as given. A project config can only choose the command and env a server runs once pi trusts the project; until then they are ignored and /pp → Diagnostics says so.

Skills

The main prompt contains a compact catalog. Full skill guidance is loaded through load_skill and appears as a tagged tool result, so it remains searchable and can be loaded again when no longer salient.

pi-pi reads the same skill folders as pi, in this order of precedence when two skills share a name:

  1. the project: .pi/skills, then .agents/skills in the working directory and each parent up to the repository root (outside a repository, the working directory only). Only while pi trusts the project, as pi does; in bb and in print, JSON or RPC runs, which never ask, trust a project once from a terminal pi or with pi's defaultProjectTrust.
  2. global: pi's agent folder skills (~/.pi/agent/skills), ~/.agents/skills, and the older ~/.pi/skills. PI_SKILLS_DIR replaces all three.
  3. pi-pi's own skills.

A skill may be <name>/SKILL.md anywhere below a skills folder, or a Markdown file directly inside one; hidden folders and node_modules are skipped, and symlinked skills are followed, with their references read where they really live. It needs frontmatter with name and description; disable-model-invocation: true keeps it out of the catalog while it stays loadable by name. Subagents cannot load skills themselves; a skill whose frontmatter also sets audience: any can be named in an Agent call's skills parameter and is preloaded into that subagent's prompt. Other skills are written for the main agent and are refused there. pi's own /skill:name commands and skills added through pi's settings or packages are pi's, not part of pi-pi's catalog.

pi-pi's skills cover feature work, bug work, investigation, git operations, operations on real machines and clusters, review, skill authoring, and the code rules shared with subagents. Their names start with pp- (pp-review, pp-code-rules, …), which is reserved: a skill of yours never takes their place by its name. The old names (review, code-rules, …) still load them until a skill of yours uses one.

/pp → Skills lists every skill found and sets how yours relate to pi-pi's, saved per skill under skills.entries in the global or project config (a project's entry for a skill replaces the global one whole; null there cancels it):

"skills": { "entries": {
  "deep-review": { "extends": "pp-review" },
  "my-git":      { "replaces": "pp-git-work" },
  "review":      { "standalone": true },
  "pp-ops-work": { "enabled": false }
} }
  • extends: loading the pi-pi skill returns its text followed by yours, marked as the user's and winning where they disagree; yours leaves the catalog. Extensions set globally come before those set in the project.
  • replaces: yours is listed in its place, and loading the pi-pi skill — as pi-pi's other skills tell the agent to — returns yours, with any extensions after it. A project's replacement wins over a global one.
  • standalone: keep both; overlaps with pi-pi's skills are no longer reported for it.
  • enabled: false: turns one skill off. audience overrides whether a skill of yours that extends or replaces nothing can go to subagents; one that does goes as the pi-pi skill it extends or replaces.

Changes apply to the next skill load and the next worker spawned. Diagnostics, and Check overlaps… in the Skills menu, ask the session's model which of your skills compete with pi-pi's — names and descriptions only — and say so when the check could not run.

Workers

The main session owns long-running and interactive work. Functional workers are optional and bounded:

  • explore for local retrieval and mapping;
  • librarian for external sources;
  • advisor_* for independent judgment;
  • deep-debugger_* for difficult diagnosis;
  • reviewer_* for fresh read-only review;
  • task for a self-contained parallel slice.

Workers receive no duplicated task artifact bundle. They can search the owning session with vcc_recall when prior decisions or tool results matter.

Workers never block the main session. Agent returns a worker id at once, and the result arrives on its own: at the main agent's next tool-call boundary while it is busy, or as a new turn once it is idle and nobody is typing or answering a dialog. Outside the terminal, where an app expects one turn per prompt, a run that ends while workers are busy stays open until their results are in it instead of the results starting an unprompted turn later; you can stop it or send a message at any time, and after 30 minutes without any worker activity it ends and the results wait for your next message. A run without a UI waits up to two hours of silence, then stops the workers and says in its final answer what they did not finish. pi-pi's own automatic continuations likewise continue the run that is ending rather than starting a new one; in the terminal they keep the short pause in which you can step in. The main agent controls what it started with agent_wait (bounded, returns on the first result or question), agent_send (steer a running worker, answer its question, or give a finished one follow-up work in the same context), agent_status, and agent_stop (graceful, kill even when stuck, or tool to cancel only the call it is stuck in). A worker can report progress and ask its parent a question, and finishes with a structured result; reviewers return their findings with severity and location.

A worker has no run, turn or silence limit: the parent agent watches it with agent_status and stops it when it judges the work stuck or done. A worker that stays silent is flagged to the parent once after performance.internals.workerStallWarn (3 minutes); a run whose session produces nothing at all within workerStartTimeout (1 minute) fails with the startup step it was stuck in; a model request that goes workerModelStale (10 minutes) without a response and with no tool running is abandoned and the worker continues in place, failing after the third such hang in a run; workerQuestionTimeout bounds how long a question waits, and workerBashTimeout is the default timeout for a worker's shell commands (see Hung commands). Workers end with the session that started them.

The panel above the editor lists the workers under a header that counts them by state, the ones that need you first: a question (with its text), a worker gone silent (for how long, in which tool), a failure (with its error, kept until you open it); then running workers with their current step, time and tokens, then queued ones; a stopped worker shows as stopped. It shows three at a time and sums up the rest; a finished worker stays a few seconds. alt+w makes the panel itself selectable, so you pick one with the arrow keys and act on it while the editor, your draft and the footer stay where they are: Enter opens its live transcript, m messages or answers it, i interrupts its current tool, s stops and k k kills it, Esc goes back. agent_status gives the agent the same view, plus the tail of what a worker is writing; /pp → Workers lists them too.

Every worker's conversation is written as it happens to .pp/workers/<session>/<worker>.jsonl, in pi's session format, readable only by you and deleted after a week. A missing .pp/.gitignore is created ignoring everything; when an existing one does not ignore workers/, transcripts go to ~/.cache/pi-pi/workers instead. While the main agent waits with agent_wait, the call's progress shows each worker's current step, so apps that show a running tool's progress show the workers too. Under Paseo, workers also appear as Paseo's own sub-agent cards following that transcript.

bb

bb does not show pi's panels, so pi-pi ships a bb plugin in bb/ that shows them in the thread instead, above the composer: a status line with what the main agent is doing and a pi-pi menu button, the task checklist as a card, and a worker card (running · total · asking, open while any worker runs). The worker card lists each worker's state, current step, question, result or error; clicking a worker shows its task, latest output and result, with Send (a steer, an answer, or more work for a finished one), Stop and Kill (asks to confirm). Install it from npm, alongside the pi package:

bb plugin install npm:@ilya-lesikov/bb-plugin-pi-pi

bb plugin outdated and bb plugin update pi-pi pick up new releases. To run it from a checkout instead, for development: cd bb && npm install && bb plugin build . && bb plugin install ..

npm run release releases it together with pi-pi whenever bb/ changed (see Releasing). To release the plugin alone, bump version in bb/package.json, commit, and run npm run release:bb-plugin (-- --dry-run to stop short of publishing). It builds the committed plugin from a fresh clone, installs the packed package the way bb does and checks it, publishes it, and tags the commit bb-plugin-vX.Y.Z; then push the commit and the tag.

Under bb, pi-pi writes the list and its workers to pi-pi/state.json in the thread's storage directory ($BB_THREAD_STORAGE), and the plugin's buttons come back as files in pi-pi/commands/ next to it, which pi-pi runs within half a second. bb stops pi when you press Stop and a while after a run ends, and starts it again with your next message; while pi is not running the status line says so, the task list stops showing work in progress, workers show as stopped, and their buttons go away. When pi stops updating the file without ending, the status line says it is not responding.

The pi-pi menu button opens /pp in the composer as the plugin's forms: the same screens as in the terminal, with reports in a scrollable form and pi-pi's notices on the status line while it is open. It opens while the agent works too, as /pp does in the terminal, but only while pi is running: after Stop, send a message first. Worker updates reach the model as usual but are not shown in bb's chat, which would show their raw text; the worker card shows the workers.

The plugin also draws ask_user, confirm and workers' questions as forms in the composer, instead of bb's bare dialogs: every question of an ask_user in one form with tabs, option descriptions, the recommended option marked, checkboxes for a multi-select, an Other field and a comment; confirm with Yes and No (No first and Yes in red when it is dangerous) and its detail as markdown; number keys pick an option, and Cancel with a reason passes your reason on. A reply typed in the chat while the agent's form is open after its run goes to the agent as your message and takes the form down; a form shown inside the run (when pi-pi could not ask the plugin what it shows) takes the reply as its answer instead. A worker's form shows its question as markdown and takes no typed reply; the message goes to the agent. A worker's question opens a form of its own once the agent has not answered it for twelve seconds, or at once when the agent is idle, and not while the agent has a form of its own open, which takes a worker's form down until it closes; your answer goes to the worker, and the agent is told. pi-pi reaches the plugin through bb's local server ($BB_SERVER_URL); without the plugin, or when bb refuses to show the form, it asks with bb's dialogs as before. bb shows a turn's final message only once the run has ended, so the agent's form opens after the run, under that message, and your answer starts the next turn; bb's dialogs still ask inside the run. Pressing Stop while such a form is open, or bb taking it down otherwise, starts nothing: the agent hears of it with your next message. A form left unanswered until bb stops an idle pi (after 30 minutes by default) goes away with it: answer in the chat instead.

The plugin also offers pi-pi's quick agent to bb as an AI service, so thread titles (and the branch names that follow them) and commit messages come from the model configured as agents.quick. Pick it with bb settings ai-services set thread-title pi-pi --plugin pi-pi (likewise commit-message), or leave Automatic, which tries it after bb cloud; bb settings ai-services test thread-title tries it. Each request runs pi -p --pp-agent quick once on the machine of bb's server, with only pi-pi loaded and from an empty directory, so the global pi-pi config applies, not a project's. The plugin's settings name the pi command (pi by default) and, when pi list does not show it, pi-pi's directory. bb gives a title a few seconds (eight in bb 0.46) and keeps the prompt's own text when they run out, and titles no prompt shorter than five words.

Task checklist

The main agent keeps a checklist with TaskCreate, TaskUpdate, TaskList and TaskGet, shown above the editor as counts plus the running and next pending tasks (five rows at most). Each create or update result repeats that short list, so apps that do not show pi-pi's panels show the tasks there; under Paseo the four tools are one todo tool, which Paseo draws as its own todo card. The list lives in the session itself: every task tool result carries it, so /resume, forks and /tree restore the list as it stood at that point, and it survives compaction. Once every task is completed, the next one starts a fresh list. When edits, commits or turns pile up without the list changing, the agent gets a reminder that names what has gone stale. Workers have no task tools.

Asking the user

ask_user and confirm ask at the end of the turn. The call registers the question and returns at once, the agent writes its explanation as the turn's final message, and the question opens under that message; every other tool call is refused until the user answers. The answer comes back as a message that continues the run; a question dismissed without a reason starts nothing, and the agent reads the dismissal with your next message. A message you send while the explanation is still being written joins the run first, and the question opens after the agent's reply to it; one you send once the run has ended, before the question opened, replaces it. The explanation comes last because text a model writes between tool calls can come back from the provider as nothing but a summarized thinking block, while a turn's final message arrives as written.

ask_user asks one short question inline, above the editor: the question as the main line, a short header in the frame, options with optional descriptions, an optional recommended option focused and marked, multi-select, and an "Other…" row for a typed answer. Digits answer at once, ctrl+e attaches a comment to the choice, and Esc cancels after an optional reason; ctrl+c never cancels a question. Several decisions go in one call as questions, shown one at a time; ← and → move between them. Calls with one option, duplicate options or an option that only declines are refused before anything is shown.

confirm puts a single yes/no decision: Yes, No, or dismissed, with danger focusing No for destructive actions.

Outside the terminal both use the host's own dialogs, kept within the limits of the strictest host: one-line titles, Title — description options, multi-select as a plain list whose pick is the answer, with a "Choose several…" entry that switches to ☐/☑ toggles and a "Done" entry, a Back entry and a final review for several questions, and "Other…" or "Add a comment…" only where the host can collect typed text. confirm becomes the host's native approval. Without any UI, ask_user lists the questions for the model to answer on stated assumptions, and confirm reports that nothing was confirmed.

Models and providers

Roles name a model slot, not a provider: Claude Haiku, Sonnet, Opus and Fable, and GPT Luna, Terra, Sol and Astra (GPT also with a costlier Pro variant), written claude/opus or gpt/astra:pro in config. Each slot runs on the newest version the first provider in a priority list serves:

  • Claude subscription — reached directly or through the LLM API gateway, in the order you choose;
  • ChatGPT subscription;
  • GitHub Copilot;
  • LLM API — paid per token.

A provider without credentials serves nothing and is passed over. On a rate limit the provider steps aside — a subscription for the whole account, the others for that one model — and work moves to the next one that serves the slot, paid LLM API included; it comes back once the limit clears. A slot can be locked to one version, and a role can name an exact provider/model, which routing then never moves.

The quick agent (agents.quick, Claude Haiku without thinking by default) answers one prompt for programs outside a session: pi -p --pp-agent quick reads the prompt from stdin, runs it on that model with no tools and no orchestrator prompt, prints the answer and exits; it exits with code 2 and the reason on stderr when the model has nothing to run on, and --pp-agent-check only prints the model it would use. Add --no-extensions -e <pi-pi directory> to keep other extensions from starting.

A model pi is started with (--model, --provider, --thinking, as apps like bb pass it) is kept instead of being replaced by the configured main model, and provider or routing changes leave it alone (changing the main model itself in /pp still applies); workers keep their own routing.

A whole family can be replaced by another provider's models, for example every Claude slot served by a cheaper model from a provider pi already has credentials for. It still reads as "Claude Opus · override zai/glm-5.3" everywhere; an overridden family does not fall back to another provider.

All of this is under /pp → Settings → Providers and Models, scoped globally or per project; Explain routing shows what each slot runs on and why.

Releasing

npm run release -- minor (or patch, major) releases pi-pi and, when bb/ changed since the last bb-plugin-v* tag, the bb plugin, both bumped by that level. From a clean main that has everything in origin/main, logged in to npm, it runs npm run test:all, commits the bumps and tags them vX.Y.Z and bb-plugin-vX.Y.Z, builds and checks the plugin, publishes both to npm, and pushes main with those tags. npm run release:dry -- minor does the same in a throwaway clone and stops before publishing and pushing.

It refuses to start when a version it would create is already tagged or on npm. When it stops before publishing, it prints how to undo the local bump. When it stops after pi-pi X.Y.Z was published, finish by hand: if the plugin was bumped to A.B.C and npm view @ilya-lesikov/bb-plugin-pi-pi@A.B.C shows nothing, run npm run release:bb-plugin; then push main with the new tags, git push --atomic origin main vX.Y.Z plus bb-plugin-vA.B.C when the plugin was released.