| name | pi-extension |
| description | Pi extensions and tool policy: extensions/*.ts, hooks, registerTool, promptGuidelines, registerCommand, footer/status UI, tool_result, session hooks, or subprocesses. Not for slash-command placement; use pi-command. |
Pi Extension Engineering
Boundary
| Need | Use |
|---|
| Pi extension implementation, hooks, runtime behavior, subprocess use | pi-extension |
| Slash-command placement or prompt-vs-extension decisions | pi-command |
| General TypeScript patterns and package commands | typescript |
| Focused existing-code edits | least-astonishment |
Core Principle
Pi extensions run inside the interactive agent process. Treat render paths, status paths, hooks, and tool-result handlers as hot paths. Small subprocess calls can become visible CPU, process churn, or startup latency when repeated.
Pi docs, Pi examples, and local Pi source/types are authoritative for extension behavior. When local Pi source or .d.ts files are available, inspect them before hedging about extension API behavior. Use Node docs only for runtime mechanics such as child_process, streams, signals, and buffers. Do not import editor-extension rules from other ecosystems unless the user explicitly asks for that comparison.
Living Tooling Contract
Before designing or changing a Pi extension, registered tool, activation rule, prompt snippet, or prompt guideline, read references/tooling-contracts.md completely.
The tooling contract is living guidance. When the user refines or changes accepted tooling behavior:
- Update the contract in the same coherent change as the implementation.
- Record the accepted current behavior, not the discussion history.
- Remove superseded or contradictory guidance instead of appending exceptions.
- Reconcile descriptions, prompt snippets, prompt guidelines, activation, runtime gates, and operator documentation with the revised contract.
- Keep tool-specific policy in the owning extension and this contract, not
AGENTS.md.
Pi Runtime Rules
- Keep the extension factory for registration only:
pi.on, pi.registerTool, pi.registerCommand, pi.registerShortcut, pi.registerFlag, pi.registerProvider, and renderers. Runtime actions such as pi.sendMessage() belong in handlers, tools, or commands after Pi binds the session runtime.
- Keep tool-specific model instructions in the owning
registerTool() definition: use description and parameters for the callable contract, promptSnippet for one-line discovery, and promptGuidelines for behavioral guidance. Enforce mandatory behavior in execute() or tool_call; do not duplicate tool instructions in pi/AGENTS.md.
- Use
ctx.signal for nested async work during active turn events such as tool_call, tool_result, message_update, and turn_end.
- Clean up timers, intervals, file watchers, background work, and long-running subprocesses in
session_shutdown or component disposal paths.
- Use
ctx.hasUI and ctx.mode before dialogs or TUI-only behavior. ctx.hasUI includes RPC; guard direct TUI components with ctx.mode === "tui".
- For footer/status UI, prefer
footerData, ctx, and cached state over fresh discovery.
- For custom tools that mutate files, use
withFileMutationQueue() around the full read-modify-write window.
- Custom tools must truncate large output and tell the caller when full output is saved elsewhere.
- Throw from tool
execute() to mark a failed tool result. Returning isError: true in a result object does not signal failure.
- Preload and cache external autocomplete data, filter it locally, and run session-transition guards in
session_before_* rather than render paths.
- Use
StringEnum from @earendil-works/pi-ai for string enums.
- Strip a leading
@ from custom-tool path arguments and resolve extension-relative helpers from import.meta.url.
Shell-Out Rules
- Prefer
pi.exec(command, args, { cwd, timeout, signal }) for ordinary command execution. Use raw child_process only when Pi's wrapper does not fit the use case.
- Do not shell out from footer render, status render, or other UI render paths unless the result is cached by a stable key.
- Prefer computing display values once per relevant key, such as cwd, model, provider, session id, tool name, or output fingerprint.
- Avoid subprocesses in
tool_result handlers unless gated by file type, output size, command type, or another cheap deterministic check; skip small or no-op inputs before spawning.
- Cache binary availability checks such as
where.exe, which, git --version, or tool probes. Lazy cache is usually best for optional validators.
- Treat
session_start subprocesses as startup-cost risks. Network calls, git fetch, package-manager commands, and Python probes need timeouts and a clear reason.
- Prefer in-process Node APIs for filesystem, path, JSON, and config reads.
- Avoid synchronous subprocess APIs in hot paths; they block the extension event loop.
- If a subprocess is required, use explicit args, avoid
shell: true unless required, set windowsHide: true on Windows, bound it with timeout/cancellation, and either consume or ignore stdout/stderr deliberately.
- On timeout or abort, clean up the whole child process tree. On Windows, use
taskkill /PID <pid> /T /F; on Unix-like systems, spawn detached when appropriate and signal the process group.
- For Windows churn investigations, use
scripts/diagnose-windows-process-churn.ps1 before guessing. Check for hot LSM/CryptSvc, stale Git LFS/MSYS helpers, orphan-like console processes, and Tcpip event ID 4227.
State And Session Rules
- Reconstruct in-memory state on
session_start; /reload, /new, /resume, and /fork create fresh extension instances.
- Store extension-private state with
pi.appendEntry() when it must survive reload/fork. Store tool state in tool result details when reconstruction depends on branch history.
- Do not use captured old session-bound objects after
ctx.reload(), ctx.newSession(), ctx.fork(), or ctx.switchSession().
- Command-only methods such as
ctx.reload(), ctx.newSession(), ctx.fork(), and ctx.switchSession() belong in command handlers. From tools or events, queue a follow-up command with pi.sendUserMessage().
- Check model existence and the boolean result from
pi.setModel() before reporting success.
Validation
For Pi extension changes, prefer targeted validation:
cd pi && pnpm test <matching-test-file>.ts
cd pi && pnpm run typecheck