Stage 1

The Foundation

2025-09-09 — 2025-11-12 v0.0.1 → v0.6.0 209 commits
tui agent-runtime cli llm-providers extension-api

Zeitgeist

Pi began in August 2025 as a monorepo with an ambitious scope: not just another coding agent, but an entire toolkit for building AI-powered developer tools. Mario Zechner set up npm workspaces on day one with four packages — pi-ai (a unified LLM API), pi-agent-core (an agent runtime), pi-tui (a custom terminal UI library), and pi-coding-agent (the interactive CLI). Later came pi-web-ui, pi-mom (a Slack bot), and pi-pods (GPU deployment tooling). The decision to build all of these in one repo, from scratch, rather than assembling existing libraries reveals the core philosophy: full-stack control over the agent experience, from the raw LLM API calls up to the pixel-level terminal rendering.

The most striking early decision was building a custom TUI library (pi-tui) with "surgical differential rendering" — a system that tracks which terminal cells actually changed and only redraws those, preserving the user's scrollback buffer. Most coding agents at this point simply dumped text to stdout. Pi wanted to be a real application with a proper UI, not a script. The TUI renders markdown, handles emoji wrapping edge cases, and supports theming — all from scratch, within the first two weeks.

The other foundational bet was the unified provider API in pi-ai. Rather than binding to one LLM vendor, pi abstracts OpenAI, Anthropic, and Google behind a single streaming interface (AsyncGenerator<StreamEvent>). This wasn't just about flexibility — it was about making the agent runtime provider-agnostic, so the same session could be replayed against different models. Reasoning token support was added across all providers immediately, including partial JSON parsing for streaming tool calls — infrastructure that would become critical as thinking-capable models became the norm.

Key Developments

The Unified LLM Provider Interface

The LLMProvider interface in packages/ai/src/ abstracts chat completions into a single streaming protocol. Each provider (OpenAI, Anthropic, Google) implements the same chat() method returning AsyncGenerator<StreamEvent>. This means the agent runtime never thinks about which model it's talking to — it just processes a stream. The design included Unicode surrogate sanitization across all providers from the start, suggesting experience with production edge cases.

Surgical Differential Rendering (pi-tui)

Rather than using an existing terminal UI library like Ink or Blessed, pi built pi-tui with a differential rendering engine inspired by React's virtual DOM, but for terminal cells. The Container component tracks child changes, and the renderer only sends ANSI escape codes for cells that actually differ from the previous frame. This preserves the terminal scrollback buffer — a detail that matters enormously for a tool you use inside tmux or a terminal multiplexer.

The Agent Runtime Loop

packages/agent/src/ implements the core agent loop: accept a user message, call the LLM, parse tool calls from the streaming response, execute tools, and loop. Tool calls are extracted from streaming JSON (via a custom partial JSON parser) so the agent can begin preparing before the model finishes generating. The runtime handles abort signals, thinking traces, and stop reasons as first-class concepts.

Mom: The Slack Bot

One of the more surprising early additions was pi-mom — a Slack bot that delegates messages to the coding agent running in a Docker sandbox. Mom got working memory (cross-session context), centralized logging, and usage tracking within the first few weeks. This reveals an early vision for pi as not just a CLI tool but a platform — the same agent runtime can power interactive terminals, Slack bots, or web interfaces.

Web UI Components

The pi-web-ui package provided Lit-based web components for AI chat interfaces, including artifact rendering (with auto-reload when dependencies change), collapsible tool renderers, and session persistence via IndexedDB. A browser extension prototype for an "AI reading assistant" appeared and was abandoned — but the web components survived and evolved into a real alternative interface.

The Four-Tool Foundation

Pi launched with exactly four tools: read, write, edit, and bash. These remain the core tools through v0.66. The tool system was designed around the AgentTool interface with typed inputs and results, custom rendering (ToolRenderResult), and an onCompleted callback for guaranteed delivery. The bash tool notably supported timeout handling and working directory tracking from the start.

Session Management Basics

Even in this foundational stage, sessions were persistent — saved as JSONL files with full message history, model state, and thinking traces. The --resume flag with a session selector appeared early, along with model cycling (switching models mid-conversation) and thinking level controls.

Philosophy Shifts

The README at this stage described pi simply as "A collection of tools for managing LLM deployments and building AI agents." No AGENTS.md existed yet — the project's development conventions were implicit, not codified. The lack of formal rules reflects a single-developer project moving fast.

The most revealing philosophy is in what pi didn't do: it didn't use LangChain, didn't use an existing TUI library, didn't abstract away the streaming protocol. Every layer was built from scratch with specific opinions about how an agent should work. The presence of pi-pods (vLLM deployment tooling) alongside the coding agent suggests the author was thinking about the full lifecycle: deploy your own models, build agents that use them, run those agents everywhere.

Looking Forward

Several seeds planted here bloom in later stages: the ToolRenderResult interface foreshadows the extension system's custom renderers. The provider abstraction enables the model catalog and auto-detection that come in the Provider Ecosystem stage. The session JSONL format becomes the foundation for branching, forking, and the session tree. And the Slack bot's "delegate to a subprocess" pattern eventually inspires how extensions and sub-agents work. The missing piece is extensibility — at this point, all tools and behaviors are hardcoded. That changes dramatically starting in Stage 5.

Key changes

Tui

tui agent-runtime reliability

Add model selector TUI and update session management; Improve tool execution rendering and error handling; Improve TUI instructions and thinking selector styling; Add /thinking command and improve TUI UX

packages/coding-agent/src/session-manager.ts L15–23
export interface SessionHeader {
	type: "session";
	id: string;
	timestamp: string;
	cwd: string;
	systemPrompt: string;
	model: string;
	thinkingLevel: string;
}
definition in session-manager.ts
packages/coding-agent/src/tools/bash.ts L9–23
export const bashTool: AgentTool<typeof bashSchema> = {
	name: "bash",
	label: "bash",
	description:
		"Execute a bash command in the current working directory. Returns stdout and stderr. Commands run with a 30 second timeout.",
	parameters: bashSchema,
	execute: async (_toolCallId: string, { command }: { command: string }, signal?: AbortSignal) => {
		return new Promise((resolve, reject) => {
			const child = spawn("sh", ["-c", command], {
				detached: true,
				stdio: ["ignore", "pipe", "pipe"],
			});

			let stdout = "";
			let stderr = "";
definition in bash.ts
packages/coding-agent/src/tools/edit.ts L26–40
export const editTool: AgentTool<typeof editSchema> = {
	name: "edit",
	label: "edit",
	description:
		"Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
	parameters: editSchema,
	execute: async (
		_toolCallId: string,
		{ path, oldText, newText }: { path: string; oldText: string; newText: string },
	) => {
		const absolutePath = resolve(expandPath(path));

		if (!existsSync(absolutePath)) {
			throw new Error(`File not found: ${path}`);
		}
definition in edit.ts

Agent Runtime

agent-runtime cli

Add search, scrolling, spacing, and Ctrl+C exit to session selector; Add --resume flag with session selector; Add custom session events for thinking level and model changes; Add artifact message persistence for session reconstruction

packages/coding-agent/src/tui/session-selector.ts L175–189
export class SessionSelectorComponent extends Container {
	private sessionList: SessionList;

	constructor(sessionManager: SessionManager, onSelect: (sessionPath: string) => void, onCancel: () => void) {
		super();

		// Load all sessions
		const sessions = sessionManager.loadAllSessions();

		// Add header
		this.addChild(new Spacer(1));
		this.addChild(new Text(chalk.bold("Resume Session"), 1, 0));
		this.addChild(new Spacer(1));
		this.addChild(new DynamicBorder());
		this.addChild(new Spacer(1));
definition in session-selector.ts
packages/coding-agent/src/session-manager.ts L15–23
export interface SessionHeader {
	type: "session";
	id: string;
	timestamp: string;
	cwd: string;
	systemPrompt: string;
	model: string;
	thinkingLevel: string;
}
definition in session-manager.ts
packages/coding-agent/src/session-manager.ts L15–23
export interface SessionHeader {
	type: "session";
	id: string;
	timestamp: string;
	cwd: string;
	systemPrompt: string;
	model: string;
	thinkingLevel: string;
}
definition in session-manager.ts

Llm Providers

llm-providers

Add image support in tool results across all providers; Add Unicode surrogate sanitization for all providers; Add ollama dependency and dialog backdrop blur; Add Anthropic prompt caching, pluggable storage, and CORS proxy support

packages/ai/src/agent/agent-loop.ts L8–9
export function agentLoop(
	prompt: UserMessage,
definition in agent-loop.ts
packages/ai/src/agent/tools/calculate.ts L4–7
export interface CalculateResult extends AgentToolResult<undefined> {
	content: Array<{ type: "text"; text: string }>;
	details: undefined;
}
definition in calculate.ts
packages/ai/src/providers/anthropic.ts L29–33
export interface AnthropicOptions extends StreamOptions {
	thinkingEnabled?: boolean;
	thinkingBudgetTokens?: number;
	toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
}
definition in anthropic.ts

Extension Api

extension-api tui cli

Add navigation message tracking to browser extension; Add custom message extension system with typed renderers and message transformer; More browser extension work, disable ajv validation in browser extensions, it us; add cross-browser extension with AI reading assistant

packages/browser-extension/src/message-transformer.ts L7–26
export function browserMessageTransformer(messages: AppMessage[]): Message[] {
	return messages
		.filter((m) => {
			// Keep LLM-compatible messages + navigation messages
			return m.role === "user" || m.role === "assistant" || m.role === "toolResult" || m.role === "navigation";
		})
		.map((m) => {
			// Transform navigation messages to user messages with <system> tags
			if (m.role === "navigation") {
				const nav = m as NavigationMessage;
				const tabInfo = nav.tabIndex !== undefined ? ` (tab ${nav.tabIndex})` : "";
				return {
					role: "user",
					content: `<system>Navigated to ${nav.title}${tabInfo}: ${nav.url}</system>`,
				} as Message;
			}

			// Strip attachments from user messages
			if (m.role === "user") {
				const { attachments, ...rest } = m as any;
definition in message-transformer.ts
packages/browser-extension/src/messages/NavigationMessage.ts L9–15
export interface NavigationMessage {
	role: "navigation";
	url: string;
	title: string;
	favicon?: string;
	tabIndex?: number;
}
definition in NavigationMessage.ts
packages/web-ui/example/src/custom-messages.ts L12–17
export interface SystemNotificationMessage {
	role: "system-notification";
	message: string;
	variant: "default" | "destructive";
	timestamp: string;
}
definition in custom-messages.ts

Web Ui

web-ui cli devex

Improve web-ui message and tool UX; Update web-ui example to use javascript-repl instead of calculate/getCurrentTime; Add JavaScript REPL tool to web-ui package

packages/web-ui/src/components/MessageList.ts L13–27
export class MessageList extends LitElement {
	@property({ type: Array }) messages: AppMessage[] = [];
	@property({ type: Array }) tools: AgentTool[] = [];
	@property({ type: Object }) pendingToolCalls?: Set<string>;
	@property({ type: Boolean }) isStreaming: boolean = false;
	@property({ attribute: false }) onCostClick?: () => void;

	protected override createRenderRoot(): HTMLElement | DocumentFragment {
		return this;
	}

	override connectedCallback(): void {
		super.connectedCallback();
		this.style.display = "block";
	}
definition in MessageList.ts
packages/web-ui/src/components/Messages.ts L17–18
export type UserMessageWithAttachments = UserMessageType & { attachments?: Attachment[] };
definition in Messages.ts
packages/browser-extension/src/tools/javascript-repl.ts L81–89
export type JavaScriptReplToolResult = {
	files?:
		| {
				fileName: string;
				contentBase64: any;
				mimeType: string;
		  }[]
		| undefined;
};
definition in javascript-repl.ts

Architecture

architecture tui reliability

Add minimal TUI rewrite with differential rendering; improve error handling and stop reason types

packages/tui/src/components-new/editor.ts L18–20
export interface TextEditorConfig {
	// Configuration options for text editor (none currently)
}
definition in editor.ts
packages/tui/src/components-new/loader.ts L7–21
export class Loader extends Text {
	private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
	private currentFrame = 0;
	private intervalId: NodeJS.Timeout | null = null;
	private ui: TUI | null = null;

	constructor(
		ui: TUI,
		private message: string = "Loading...",
	) {
		super("");
		this.ui = ui;
		this.start();
	}
definition in loader.ts
packages/ai/src/agent/agent-loop.ts L8–9
export function agentLoop(
	prompt: UserMessage,
definition in agent-loop.ts

Add interactive UI selector for /thinking command

cli
packages/coding-agent/src/tui-renderer.ts L283–297
export class TuiRenderer {
	private ui: TUI;
	private chatContainer: Container;
	private statusContainer: Container;
	private editor: CustomEditor;
	private editorContainer: Container; // Container to swap between editor and selector
	private footer: FooterComponent;
	private agent: Agent;
	private version: string;
	private isInitialized = false;
	private onInputCallback?: (text: string) => void;
	private loadingAnimation: Loader | null = null;
	private onInterruptCallback?: () => void;
	private lastSigintTime = 0;
definition in tui-renderer.ts

Add getAllFromIndex method for efficient sorted queries

performance
packages/web-ui/src/storage/backends/indexeddb-storage-backend.ts L7–21
export class IndexedDBStorageBackend implements StorageBackend {
	private dbPromise: Promise<IDBDatabase> | null = null;

	constructor(private config: IndexedDBConfig) {}

	private async getDB(): Promise<IDBDatabase> {
		if (!this.dbPromise) {
			this.dbPromise = new Promise((resolve, reject) => {
				const request = indexedDB.open(this.config.dbName, this.config.version);

				request.onerror = () => reject(request.error);
				request.onsuccess = () => resolve(request.result);

				request.onupgradeneeded = (_event) => {
					const db = request.result;
definition in indexeddb-storage-backend.ts
packages/web-ui/src/storage/stores/sessions-store.ts L8–22
export class SessionsStore extends Store {
	getConfig(): StoreConfig {
		return {
			name: "sessions",
			keyPath: "id",
			indices: [{ name: "lastModified", keyPath: "lastModified" }],
		};
	}

	/**
	 * Additional config for sessions-metadata store.
	 * Must be included when creating the backend.
	 */
	static getMetadataConfig(): StoreConfig {
		return {
definition in sessions-store.ts

Add model name to footer stats line (right-aligned)

llm-providers cli
packages/coding-agent/src/tui/footer.ts L9–23
export class FooterComponent {
	private state: AgentState;

	constructor(state: AgentState) {
		this.state = state;
	}

	updateState(state: AgentState): void {
		this.state = state;
	}

	render(width: number): string[] {
		// Calculate cumulative usage from all assistant messages
		let totalInput = 0;
		let totalOutput = 0;
definition in footer.ts

Add padding to Aborted text for consistent spacing

packages/coding-agent/src/tui/assistant-message.ts L8–22
export class AssistantMessageComponent extends Container {
	private contentContainer: Container;

	constructor(message?: AssistantMessage) {
		super();

		// Container for text/thinking content
		this.contentContainer = new Container();
		this.addChild(this.contentContainer);

		if (message) {
			this.updateContent(message);
		}
	}
definition in assistant-message.ts

Add abort signal handling to read, write, and edit tools

agent-runtime
packages/coding-agent/src/tools/edit.ts L26–40
export const editTool: AgentTool<typeof editSchema> = {
	name: "edit",
	label: "edit",
	description:
		"Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
	parameters: editSchema,
	execute: async (
		_toolCallId: string,
		{ path, oldText, newText }: { path: string; oldText: string; newText: string },
		signal?: AbortSignal,
	) => {
		// Check if already aborted
		if (signal?.aborted) {
			throw new Error("Operation aborted");
		}
definition in edit.ts
packages/coding-agent/src/tools/read.ts L24–43
export const readTool: AgentTool<typeof readSchema> = {
	name: "read",
	label: "read",
	description: "Read the contents of a file. Returns the full file content as text.",
	parameters: readSchema,
	execute: async (_toolCallId: string, { path }: { path: string }, signal?: AbortSignal) => {
		// Check if already aborted
		if (signal?.aborted) {
			throw new Error("Operation aborted");
		}

		const absolutePath = resolve(expandPath(path));

		if (!existsSync(absolutePath)) {
			throw new Error(`File not found: ${path}`);
		}

		const content = readFileSync(absolutePath, "utf-8");
		return { output: content, details: undefined };
	},
definition in read.ts

Add thinking level persistence and fix UI issues

reliability
packages/coding-agent/src/session-manager.ts L15–23
export interface SessionHeader {
	type: "session";
	id: string;
	timestamp: string;
	cwd: string;
	systemPrompt: string;
	model: string;
	thinkingLevel: string;
}
definition in session-manager.ts

Simplify assistant message spacing - just add spacer to components

packages/coding-agent/src/tui/streaming-message.ts L8–22
export class StreamingMessageComponent extends Container {
	private spacer: Spacer;
	private markdown: Markdown;
	private statsText: Text;

	constructor() {
		super();
		this.spacer = new Spacer(1);
		this.markdown = new Markdown("");
		this.statsText = new Text("", 1, 0);
		this.addChild(this.spacer);
		this.addChild(this.markdown);
		this.addChild(this.statsText);
	}
definition in streaming-message.ts
packages/coding-agent/src/tui/tui-renderer.ts L25–39
export class TuiRenderer {
	private ui: TUI;
	private chatContainer: Container;
	private statusContainer: Container;
	private editor: CustomEditor;
	private editorContainer: Container; // Container to swap between editor and selector
	private footer: FooterComponent;
	private agent: Agent;
	private version: string;
	private isInitialized = false;
	private onInputCallback?: (text: string) => void;
	private loadingAnimation: Loader | null = null;
	private onInterruptCallback?: () => void;
	private lastSigintTime = 0;
definition in tui-renderer.ts

Add spacer above user messages (except first one)

packages/coding-agent/src/tui/tui-renderer.ts L25–39
export class TuiRenderer {
	private ui: TUI;
	private chatContainer: Container;
	private statusContainer: Container;
	private editor: CustomEditor;
	private editorContainer: Container; // Container to swap between editor and selector
	private footer: FooterComponent;
	private agent: Agent;
	private version: string;
	private isInitialized = false;
	private onInputCallback?: (text: string) => void;
	private loadingAnimation: Loader | null = null;
	private onInterruptCallback?: () => void;
	private lastSigintTime = 0;
definition in tui-renderer.ts

Add thinking trace support and improve bash output formatting

packages/coding-agent/src/tui-renderer.ts L297–311
export class TuiRenderer {
	private ui: TUI;
	private chatContainer: Container;
	private statusContainer: Container;
	private editor: CustomEditor;
	private editorContainer: Container; // Container to swap between editor and selector
	private footer: FooterComponent;
	private agent: Agent;
	private version: string;
	private isInitialized = false;
	private onInputCallback?: (text: string) => void;
	private loadingAnimation: Loader | null = null;
	private onInterruptCallback?: () => void;
	private lastSigintTime = 0;
definition in tui-renderer.ts