pi-lazy-extensions
Lazy-load pi extensions on demand via a ToolSearch-style proxy tool
Package details
Install pi-lazy-extensions from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-lazy-extensions- Package
pi-lazy-extensions- Version
0.1.1- Published
- Jun 15, 2026
- Downloads
- not available
- Author
- monotykamary
- License
- MIT
- Types
- extension
- Size
- 58 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
🦥 pi-lazy-extensions
Lazy-load pi extensions on demand
ToolSearch-style proxy tool — extensions only load when you need them.
The Problem
Every extension in ~/.pi/agent/extensions/ loads at startup. If you have many extensions, they all register their tools, commands, and event handlers immediately — even if you rarely use them. This clutters the tool list and wastes resources.
The Solution
pi-lazy-extensions registers a single ext proxy tool. The LLM discovers and activates extensions on demand, just like Anthropic's ToolSearch for MCP tools. Extensions stay unloaded until needed.
Startup: only eager extensions load
│
▼
┌─────────────┐
│ ext tool │ ← always available
│ (pi-lazy) │
└──────┬──────┘
│
│ LLM calls ext({ activate: "todo" })
▼
┌─────────────┐
│ todo ext │ ← loaded on demand
│ (lazy) │ tools appear instantly
└─────────────┘
Install
pi install npm:pi-lazy-extensions
Setup
Create a lazy-extensions.json manifest in your project root or .pi/ directory:
{
"version": 1,
"extensions": [
{
"name": "todo",
"path": "~/.pi/agent/extensions/todo.ts",
"lifecycle": "lazy",
"description": "Task management - create, list, and track todos",
"toolSummary": ["todo_list", "todo_add", "todo_complete"],
"tags": ["productivity", "tasks"]
},
{
"name": "snake",
"path": "~/.pi/agent/extensions/snake.ts",
"lifecycle": "lazy",
"description": "Snake game while you wait",
"tags": ["fun"]
},
{
"name": "git-checkpoint",
"path": "~/.pi/agent/extensions/git-checkpoint.ts",
"lifecycle": "eager",
"description": "Auto git stash/restore on each turn"
}
],
"settings": {
"idleTimeout": 10,
"eagerOverrides": ""
}
}
Manifest Location
The manifest is searched in this order:
LAZY_EXTENSIONS_CONFIGenvironment variable (explicit path)<cwd>/.pi/lazy-extensions.json(project.pidir)~/.pi/agent/lazy-extensions.json(global)<cwd>/lazy-extensions.json(project root)
Extension Config
| Field | Type | Description |
|---|---|---|
name |
string | Unique identifier for the extension |
path |
string | Path to the extension's entry point (.ts or .js) |
lifecycle |
"lazy" | "eager" | "keep-alive" |
Default: "lazy". Eager loads at startup, keep-alive never unloads |
description |
string? | What the extension does (shown in search results) |
toolSummary |
string[]? | Names of tools this extension registers (for discovery before load) |
tags |
string[]? | Search/filter tags |
Settings
| Setting | Type | Default | Description |
|---|---|---|---|
disableProxyTool |
boolean | false | If true, don't register the ext proxy tool |
idleTimeout |
number | 10 | Minutes before unloading idle lazy extensions (0 = never) |
eagerOverrides |
string | "" | Comma-separated extension names to force eager loading |
Usage
Via the ext tool (LLM calls)
ext({}) → Status: list all extensions
ext({ search: "todo" }) → Search extensions matching "todo"
ext({ activate: "todo" }) → Load and activate the "todo" extension
ext({ tools: "todo" }) → List tools registered by "todo"
After ext({ activate: "todo" }), the todo extension's tools become directly available to the LLM.
Via the /ext command (user calls)
/ext → Show status
/ext activate todo → Activate an extension
/ext search productivity → Search extensions
/ext tools todo → List extension tools
How It Works
- At startup,
pi-lazy-extensionsreads the manifest and registers theextproxy tool - Eager and keep-alive extensions are loaded immediately during
session_start - Lazy extensions stay unloaded — their metadata is available for search/discovery
- When the LLM (or user) calls
ext({ activate: "name" }), the extension is dynamically loaded via jiti (with the same module alias map that pi's own loader uses, soimport { Type } from "typebox"andimport type { ExtensionAPI } from "@earendil-works/pi-coding-agent"work correctly), and its factory function is called with the sharedExtensionAPI - New tools registered by the activated extension appear immediately — no
/reloadneeded - After an idle timeout, lazy extensions are "soft unloaded" (their tools are deactivated)
Limitations
- No full unloading: Once an extension's factory runs, its event handlers are permanent. Idle unloading only removes tools from the active set.
- Shortcuts, flags, and message renderers persist: These registrations have no deactivate/remove API in the current ExtensionAPI. They remain active even after idle-unload. The
extstatus display shows counts of these for awareness. sourceInfoattribution: Tools registered by lazy-loaded extensions will show thepi-lazy-extensionsextension'ssourceInfo, not the original extension's. This is becausepi.registerTool()stamps each tool with thesourceInfoof the extension that made the call — and since the lazy extension's factory receives the sameExtensionAPIaspi-lazy-extensions, all registered tools are attributed topi-lazy-extensions. There is no SDK API to override this./extcommand vsexttool: The/extcommand usesctx.ui.notify()for output, which may truncate large results. For rich output (search results, detailed status), prefer theexttool interface which renders properly in the TUI.session_starthandlers never fire for the current session: When a lazy extension is activated mid-session, anysession_starthandlers it registers will not fire until the next session or/reload. Extensions that depend onsession_startfor initialization (reading config, setting up state) may not work correctly when lazy-loaded. Theexttool displays a warning when this is detected.- Duplicate tool names: If a lazy extension registers a tool with the same name as an already-registered tool, pi's "first registration wins" policy silently skips it. The
exttool displays a warning when this is detected during activation.
Example: Converting an Existing Extension
Before (always loaded):
~/.pi/agent/extensions/my-heavy-ext.ts
After (lazy loaded):
- Move or keep the extension in its existing path
- Add to
lazy-extensions.json:
{
"version": 1,
"extensions": [
{
"name": "my-heavy-ext",
"path": "~/.pi/agent/extensions/my-heavy-ext.ts",
"lifecycle": "lazy",
"description": "My heavy extension with 5 tools",
"toolSummary": ["heavy_tool_1", "heavy_tool_2", "heavy_tool_3", "heavy_tool_4", "heavy_tool_5"]
}
]
}
- Rename or remove the original from auto-discovery (add a
.baksuffix or move it out of~/.pi/agent/extensions/) so it isn't loaded eagerly by pi itself
Motivation
This is an experiment. If the pattern proves useful, the goal is to propose a pi.loadExtension(path) method to the pi SDK that handles proper sourceInfo attribution, deduplication, and full lifecycle management. This package serves as a working prototype to validate the approach.
Module Resolution
Lazy extensions are loaded via jiti — the same TypeScript/ESM transpiler that pi uses for its own extension loader. The jiti instance is configured with the same alias map that pi builds internally, which resolves:
typebox(andtypebox/compile,typebox/value)@earendil-works/pi-coding-agent@earendil-works/pi-agent-core@earendil-works/pi-tui@earendil-works/pi-ai(and@earendil-works/pi-ai/oauth)@sinclair/typebox(legacy alias)
This ensures lazy extensions can use the same imports as normally-loaded extensions. If jiti is unavailable (e.g. stripped from the runtime), the loader falls back to raw import(), which only works for .js files without SDK imports.
License
MIT