Stage 10

Extension Renaissance

2026-03-04 — 2026-03-23 v0.56.0 → v0.62.0 273 commits 37 contributors
agent-runtime tui llm-providers cli devex

Zeitgeist

The 273 commits between v0.56.0 and v0.62.0 (March 3-23, 2026) represent pi's most architecturally ambitious stage since the Great Restructuring. Where Stage 9 was defined by volume and community breadth, Stage 10 was defined by depth: the extension system was rebuilt from hooks into a composable architecture, the provider registration API matured into a first-class feature, the keybinding system was overhauled, and the session management layer gained capabilities that turned pi from a single-conversation tool into a multi-session workspace.

The spirit of this stage is captured in the --fork CLI flag (#2290): you can now fork an existing session directly from the command line, copying its conversation history into a new project context. This is not a UI feature; it is a workflow primitive. Combined with JSONL session export/import (#2356), named sessions, and the session runtime API that was forming in the background, pi was building the infrastructure for conversations as first-class, portable, composable artifacts.

The community continued contributing, with @Perlence, @aliou, @mrexodia, @haoqixu, @markusylisiurunen, @hjanuschka, and @dmmulroy landing significant features. But the stage was also marked by a return of the core maintainer's architectural hand: the keybinding namespace migration, the steering message timing fix, the lazy provider loading optimization, and the built-in tool extensibility redesign all required deep understanding of pi's internals and could not be delegated.

Key Developments

Steering Messages and Agent Coordination

Steering messages -- user messages queued while the agent is still generating -- had a subtle timing bug: they would skip pending tool calls from the current assistant message, causing the agent to miss tool results. The fix in v0.58.4 (#2330) ensured steering waits until the current tool-call batch fully finishes before injecting the user's follow-up. This seemingly minor fix reflects a deeper architectural question about how user intent interacts with in-progress agent work. The agent package's deferred steering model now treats steering as a queue that drains between complete agent turns, not between individual tool calls.

Keybinding Namespace Migration

Every interactive keybinding was migrated from flat names like "expandTools" and "interrupt" to namespaced identifiers like "app.tools.expand" and "app.interrupt" (#2391). The keybindings.json format was updated to use these canonical IDs, with automatic migration on startup. Extension authors needed to update keyHint(), keyText(), and keybindings.matches() calls. This was a significant breaking change, but it solved a real problem: as the number of keybindings grew past 50, flat names collided and the precedence system broke. User overrides now correctly shadow conflicting defaults globally (#2455).

Fork Flag and Session Portability

The --fork <path|id> CLI flag (#2290) let users fork an existing session file or partial UUID into a new session in the current directory. Combined with JSONL export via /export <path.jsonl> and import via /import <path.jsonl> (#2356 by @hjanuschka), sessions became portable across machines and projects. The resizable sidebar in HTML share and export views (#2435 by @dmmulroy) improved the read-only sharing experience. These features turned sessions from implementation details into user-facing data objects.

Lazy Provider Loading for Faster Startup

Startup time had grown as more providers were added, because every provider SDK was imported eagerly. The fix (#2297) lazy-loaded @mariozechner/pi-ai provider modules so they were only imported on first use. The Bun binary builds were updated to support lazy registration that survives provider resets and session reloads (#2314). OAuth callback flows were aligned across Anthropic, Gemini CLI, Antigravity, and OpenAI Codex (#2316). The result was noticeably faster startup, especially for users who only used one or two providers.

Provider Registration Lifecycle

pi.registerProvider() was enhanced to take effect immediately when called outside the initial extension load phase, removing the need for /reload after late registrations (#1669 by @aliou). The complementary pi.unregisterProvider(name) method was added, allowing dynamic provider teardown that restores any built-in models that had been overridden. This completed the provider lifecycle: register, use, unregister, restore defaults. The GitLab Duo provider extension example demonstrated the pattern for custom OAuth-authenticated provider integrations.

Platform-Specific Bug Crushing

Windows received focused attention: bash execution hanging for commands that spawn detached descendants was fixed (#2389 by @mrexodia), shell handling for external editors was corrected (#1925), and cmd.exe launch in the /diff and /files extensions was hardened (#2329). Terminal compatibility expanded with digit keybindings (#1905), tmux xterm extended keys handling (#1872), Kitty CSI-u printable decoding (#1857), and raw backspace disambiguation for Windows Terminal (#2293). The Mistral provider was migrated to the official conversations SDK. The GPT-5.4 model family was added with context windows capped at 272k.

Philosophy Shifts

AGENTS.md gained the OSS Weekend management section, turning maintainer availability into a version-controlled process. The instruction to "write the full comment to a temp file and use gh issue comment --body-file" was added alongside "Post exactly one final comment unless the user explicitly asks for multiple comments" and "If a comment is malformed, delete it immediately, then post one corrected comment." These rules read like lessons learned from actual incidents where automated PR comments went wrong.

The extensions documentation added the placement guidance: "Put extensions in ~/.pi/agent/extensions/ (global) or .pi/extensions/ (project-local) for auto-discovery. Use pi -e ./path.ts only for quick tests." This reflected a shift from treating -e as the primary extension loading mechanism to treating auto-discovery as the default, with -e reserved for development. The disable-model-invocation frontmatter field for skills (added in Stage 8) was now documented as the recommended way to create skills that should only be invoked explicitly, not automatically by the model.

The system prompt changed to keep only the ISO date (#2131), removing the time component. The prompt snippets system became opt-in (#2285): extension tools are only included in the system prompt's "Available tools" section when they provide a promptSnippet, rather than falling back to their description. This prevents the system prompt from growing unboundedly as extensions add tools.

Looking Forward

The keybinding namespace migration and the steering message fix established patterns that the Runtime Overhaul would build on: namespaced identifiers for all configurable behaviors, and careful sequencing of agent events. The --fork flag and JSONL import/export were precursors to the full session runtime API that would arrive in Stage 11. The lazy provider loading optimization was a prerequisite for the startup profiling work that followed. And the built-in tool extensibility -- overriding rendering of read/write/edit/bash -- would be completed with the ToolDefinition-based built-in tools in v0.62.0.

Key changes

Built-in tools as extensible ToolDefinitions

Extension authors can now override rendering of built-in read/write/edit/bash/grep/find/ls tools with custom `renderCall`/`renderResult` components. See [docs/extensions.md](docs/extensions.md).

From the changelog

Built-in tools as extensible ToolDefinitions. Extension authors can now override rendering of built-in read/write/edit/bash/grep/find/ls tools with custom `renderCall`/`renderResult` components. See [docs/extensions.md](docs/extensions.md).

Unified source provenance via `sourceInfo`

All resources, commands, tools, skills, and prompt templates now carry structured `sourceInfo` with path, scope, and source metadata. Visible in autocomplete, RPC discovery, and SDK introspection. See [docs/extensions.md](docs/extensions.md).

From the changelog

Unified source provenance via `sourceInfo`. All resources, commands, tools, skills, and prompt templates now carry structured `sourceInfo` with path, scope, and source metadata. Visible in autocomplete, RPC discovery, and SDK introspection. See [docs/extensions.md](docs/extensions.md).

AWS Bedrock cost allocation tagging

New `requestMetadata` option on `BedrockOptions` forwards key-value pairs to the Bedrock Converse API for AWS Cost Explorer split cost allocation.

From the changelog

AWS Bedrock cost allocation tagging. New `requestMetadata` option on `BedrockOptions` forwards key-value pairs to the Bedrock Converse API for AWS Cost Explorer split cost allocation.

Changed `ToolDefinition.renderCall` and `renderResult` semantics

Fallback rendering now happens only when a renderer is not defined for that slot. If `renderCall` or `renderResult` is defined, it must return a `Component`.

From the changelog

Changed `ToolDefinition.renderCall` and `renderResult` semantics. Fallback rendering now happens only when a renderer is not defined for that slot. If `renderCall` or `renderResult` is defined, it must return a `Component`.

Changed slash command provenance to use `sourceInfo` consistently

cli

RPC `get_commands`, `RpcSlashCommand`, and SDK `SlashCommandInfo` no longer expose `location` or `path`. Use `sourceInfo` instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

From the changelog

Changed slash command provenance to use `sourceInfo` consistently. RPC `get_commands`, `RpcSlashCommand`, and SDK `SlashCommandInfo` no longer expose `location` or `path`. Use `sourceInfo` instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

packages/coding-agent/examples/extensions/commands.ts L15–29
export default function commandsExtension(pi: ExtensionAPI) {
	pi.registerCommand("commands", {
		description: "List available slash commands",
		getArgumentCompletions: (prefix) => {
			const sources = ["extension", "prompt", "skill"];
			const filtered = sources.filter((s) => s.startsWith(prefix));
			return filtered.length > 0 ? filtered.map((s) => ({ value: s, label: s })) : null;
		},
		handler: async (args, ctx) => {
			const commands = pi.getCommands();
			const sourceFilter = args.trim() as "extension" | "prompt" | "skill" | "";

			// Filter by source if specified
			const filtered = sourceFilter ? commands.filter((c) => c.source === sourceFilter) : commands;
default export in commands.ts
packages/coding-agent/src/core/agent-session.ts L89–94
export interface ParsedSkillBlock {
	name: string;
	location: string;
	content: string;
	userMessage: string | undefined;
}
definition in agent-session.ts

Removed legacy `source` fields from `Skill` and `PromptTemplate`

Use `sourceInfo.source` for provenance instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

From the changelog

Removed legacy `source` fields from `Skill` and `PromptTemplate`. Use `sourceInfo.source` for provenance instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

packages/coding-agent/examples/extensions/commands.ts L15–29
export default function commandsExtension(pi: ExtensionAPI) {
	pi.registerCommand("commands", {
		description: "List available slash commands",
		getArgumentCompletions: (prefix) => {
			const sources = ["extension", "prompt", "skill"];
			const filtered = sources.filter((s) => s.startsWith(prefix));
			return filtered.length > 0 ? filtered.map((s) => ({ value: s, label: s })) : null;
		},
		handler: async (args, ctx) => {
			const commands = pi.getCommands();
			const sourceFilter = args.trim() as "extension" | "prompt" | "skill" | "";

			// Filter by source if specified
			const filtered = sourceFilter ? commands.filter((c) => c.source === sourceFilter) : commands;
default export in commands.ts
packages/coding-agent/src/core/agent-session.ts L89–94
export interface ParsedSkillBlock {
	name: string;
	location: string;
	content: string;
	userMessage: string | undefined;
}
definition in agent-session.ts

Removed `ResourceLoader.getPathMetadata()`

Resource provenance is now attached directly to loaded resources via `sourceInfo` ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

From the changelog

Removed `ResourceLoader.getPathMetadata()`. Resource provenance is now attached directly to loaded resources via `sourceInfo` ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

packages/coding-agent/examples/extensions/commands.ts L15–29
export default function commandsExtension(pi: ExtensionAPI) {
	pi.registerCommand("commands", {
		description: "List available slash commands",
		getArgumentCompletions: (prefix) => {
			const sources = ["extension", "prompt", "skill"];
			const filtered = sources.filter((s) => s.startsWith(prefix));
			return filtered.length > 0 ? filtered.map((s) => ({ value: s, label: s })) : null;
		},
		handler: async (args, ctx) => {
			const commands = pi.getCommands();
			const sourceFilter = args.trim() as "extension" | "prompt" | "skill" | "";

			// Filter by source if specified
			const filtered = sourceFilter ? commands.filter((c) => c.source === sourceFilter) : commands;
default export in commands.ts
packages/coding-agent/src/core/agent-session.ts L89–94
export interface ParsedSkillBlock {
	name: string;
	location: string;
	content: string;
	userMessage: string | undefined;
}
definition in agent-session.ts

Removed `extensionPath` from `RegisteredCommand` and `RegisteredTool`

extension-api agent-runtime cli

Use `sourceInfo.path` for provenance instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

From the changelog

Removed `extensionPath` from `RegisteredCommand` and `RegisteredTool`. Use `sourceInfo.path` for provenance instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734))

packages/coding-agent/examples/extensions/commands.ts L15–29
export default function commandsExtension(pi: ExtensionAPI) {
	pi.registerCommand("commands", {
		description: "List available slash commands",
		getArgumentCompletions: (prefix) => {
			const sources = ["extension", "prompt", "skill"];
			const filtered = sources.filter((s) => s.startsWith(prefix));
			return filtered.length > 0 ? filtered.map((s) => ({ value: s, label: s })) : null;
		},
		handler: async (args, ctx) => {
			const commands = pi.getCommands();
			const sourceFilter = args.trim() as "extension" | "prompt" | "skill" | "";

			// Filter by source if specified
			const filtered = sourceFilter ? commands.filter((c) => c.source === sourceFilter) : commands;
default export in commands.ts
packages/coding-agent/src/core/agent-session.ts L89–94
export interface ParsedSkillBlock {
	name: string;
	location: string;
	content: string;
	userMessage: string | undefined;
}
definition in agent-session.ts

Issues closed (10)