earendil-works/pi
Minimal TypeScript coding agent built on its own multi-provider LLM API and agent loop, extended through in-process TypeScript extensions.
Overview
Pi is a terminal coding agent that sells itself on what it leaves out. The README calls it “a minimal, extensible agent harness” and says plainly that it skips sub-agents, plan mode and a permission system. What it ships instead is a small core and a large extension surface. Users and packages add the missing pieces in TypeScript.
The repository is a layered monorepo. pi-ai is a unified streaming LLM API with a generated model catalog. pi-agent-core is a generic agent loop with hooks for steering, follow-ups, request preparation and tool interception. pi-tui is a differential-rendering terminal UI library. pi-coding-agent is the pi CLI, which puts sessions, compaction, tools, extensions, skills and MCP on top of those layers. Newer packages (codemode, mcp, durable, protocol, server, chord) add sandboxed tool scripting, a standalone MCP client and early remote-session work.
Pi suits developers who want a hackable harness more than a finished product. Out of the box the model gets four tools (read, bash, edit, write). Sessions are append-only JSONL trees that you can branch. Everything else is opt-in.
Architecture
flowchart LR
U["User"] --> MODE["Mode: TUI / print / json / rpc"]
SDK["SDK: createAgentSession"] --> SESS
MODE --> SESS["AgentSession"]
SESS --> AG["Agent (pi-agent-core)"]
AG --> LOOP["runLoop"]
LOOP --> AI["pi-ai streamSimple"]
AI --> PROV["Provider APIs"]
LOOP --> TOOLS["Tools: read/bash/edit/write"]
SESS --> EXT["ExtensionRunner"]
EXT --> TOOLS
SESS --> SM["SessionManager (JSONL tree)"]
SESS --> CMP["Compaction"]
SESS --> MCP["MCP servers / codemode"]
| Component | Path | Role |
|---|---|---|
| LLM API | packages/ai/ |
stream/streamSimple, about 20 wire APIs under src/api/, many provider catalogs under src/providers/ |
| Agent core | packages/agent/src/agent-loop.ts, agent.ts |
Turn loop, tool execution, before/after tool hooks, steering and follow-up queues |
| Coding agent session | packages/coding-agent/src/core/agent-session.ts |
Wires the Agent to sessions, compaction, extensions, system prompt |
| Tools | packages/coding-agent/src/core/tools/ |
read, bash, edit, write, grep, find, ls, powershell |
| Sessions | packages/coding-agent/src/core/session-manager.ts |
Append-only JSONL tree with branching |
| Compaction | packages/coding-agent/src/core/compaction/ |
Threshold check, cut point, structured summaries |
| Extensions | packages/coding-agent/src/core/extensions/ |
jiti-loaded TypeScript extensions and their API |
| Modes | packages/coding-agent/src/modes/ |
Interactive TUI, print, JSON event stream, RPC |
| MCP | packages/mcp/, packages/coding-agent/src/core/mcp-servers.ts |
MCP client and exposure modes |
| Codemode | packages/codemode/ |
Sandboxed JS (QuickJS/WASI) whose only capability is calling tools |
| TUI | packages/tui/ |
Terminal rendering library |
How a request flows
- Mode.
main.tsresolves the app mode:rpcorjsonif requested,printif-pis set or stdio is not a TTY, otherwise the interactive TUI (main.ts). - Prompt.
AgentSession.promptfirst offers/commandsto extensions, refuses input while compaction runs, then expands templates and skills and hands the message to theAgent(agent-session.ts). - Turn start.
runLoopdrains queued steering messages, callsprepareNextTurn(where the session compacts if over threshold), and letsprepareRequestswap the context, model or thinking level (agent-loop.ts, agent-session.ts). - Stream.
streamAssistantResponseappliestransformContext, converts agent messages to LLM messages, resolves the API key per call, and relaystext_delta,thinking_deltaandtoolcall_deltaevents (agent-loop.ts). The stream function ispi-ai’sstreamSimple, which dispatches to a built-in provider or an API registered by an extension (compat.ts). - Tools. An
errororabortedstop ends the run. Alengthstop fails every tool call, because the arguments may be truncated. OtherwiseexecuteToolCallsruns the batch in parallel, or sequentially when the config or any tool asks for it (agent-loop.ts, L508-L523). Each call passes throughbeforeToolCallandafterToolCall, whichAgentSessionroutes to extensiontool_callandtool_resulthandlers (agent-session.ts). - Continue or stop.
finishTurncan end or force another turn. The loop keeps going while there are tool results or steering messages, then checks the follow-up queue before it emitsagent_end(agent-loop.ts). There is no turn cap in the loop.
Key components
Agent core
pi-agent-core knows nothing about coding. It is a loop with clear seams: getSteeringMessages (input typed while the agent works), getFollowUpMessages, prepareNextTurn, prepareRequest, transformContext, convertToLlm, finishTurn and the two tool hooks. beforeToolCall can return { block: true, reason }, and afterToolCall can replace content, details, error flags or request termination (types.ts). Changes to the tool set are announced to the model as toolsAdded/toolsRemoved system messages, so a replayed transcript always matches the executable tools.
Editing
edit takes a path and an edits array of {oldText, newText}. Each edit is matched against the original file, and overlapping edits are rejected (edit.ts). Execution is serialised per file by a mutation queue. The tool strips the BOM, normalises line endings to LF, applies the edits, restores the original endings, and returns a display diff and a unified patch (edit.ts). If there is no exact match, fuzzyFindText retries after normalising trailing whitespace, smart quotes and dashes (edit-diff.ts). The tool asks for JSON-schema constrained sampling where the provider supports it. Pi has no git integration, linting or test loop. The model uses bash for those.
Sessions and compaction
SessionManager stores each session as an append-only tree in JSONL. Branching moves a leaf pointer instead of rewriting history (session-manager.ts). Compaction triggers when context tokens exceed contextWindow - reserveTokens. The defaults reserve 16,384 tokens and keep the most recent 20,000 (compaction.ts, L267-L270). Older entries are summarised by the model into a structured note and stay in the tree. Pi also compacts and retries after a provider rejects a request as a context overflow. There is no embedding index. Project context comes from AGENTS.override.md, AGENTS.md or CLAUDE.md files (resource-loader.ts) and from skill descriptions.
Tools and safety
The default active set is read, bash, edit and write (agent-session.ts). grep, find, ls and powershell are available on request. bash spawns the user’s shell with the process’s own rights. There is no allow-list, approval prompt or sandbox in the core. The README points to containers, OpenShell, or the Gondolin extension, which routes tools into a micro-VM. Every tool takes a pluggable Operations object, so file and shell access can be redirected to SSH or a container. A project-trust prompt gates loading of project-local settings, packages and extensions.
MCP and codemode
MCP servers connect over stdio or HTTP. Their tools are named mcp__<server>__<tool>, and each server has an exposure mode. direct declares the tools to the model. deferred hides them until a tool_search tool loads them. codemode makes them callable only from sandboxed scripts. hidden registers them without making them reachable (mcp-servers.ts). Codemode runs model-written JavaScript in QuickJS on WASI, and calling injected tools is its only capability. This keeps large tool catalogues out of the prompt.
Extending it
- Extensions. TypeScript modules loaded through jiti, with a default export that receives the extension API. They can register tools, commands, providers and MCP servers, subscribe to events (
tool_call,tool_result, input and more), send messages and draw UI. Extensions can be hot-reloaded. - Skills. Directories containing
SKILL.md. Only the name and description go into the prompt, and the body loads on demand. - Prompt templates and themes. Reusable
/templatetext and TUI colour themes. - Pi packages. Bundles of the above, shared through npm or git and installed with the built-in package manager.
- Custom models.
models.jsonpoints existing wire APIs (for exampleopenai-completions) at Ollama, vLLM, LM Studio or any compatible endpoint. - Embedding.
createAgentSessioninsdk.ts, RPC mode (JSONL over stdio) or JSON event output.
Running it
- Install. Use the install script,
npm install -g @earendil-works/pi-coding-agent, ornix run. Node.js 22.19 or newer is required. - Start. Run
piin a project directory and/loginto connect a subscription or API key. Usepi -p "..."for one-shot output or--mode rpc/--mode jsonfor automation. - From source.
npm install --ignore-scripts,npm run build, then./pi-test.sh.
Strengths and caveats
- Strength: clean layering. The LLM API, agent loop, TUI and coding agent are separate packages with small, documented seams. Each is useful on its own.
- Strength: extension depth. Extensions run in-process with access to tool interception, providers, commands and UI. Features other agents hard-code (sub-agents, plan mode, permissions) can be built as packages.
- Strength: careful edit tool. Multi-edit against the original file, per-file locking, line-ending and BOM preservation, and a narrow fuzzy fallback.
- Strength: branchable sessions and a structured, threshold-driven compaction.
- Caveat: no guardrails by default. No approvals, sandbox or checkpoints.
bashruns with your full rights unless you containerise pi or add an extension. - Caveat: minimal on purpose. No sub-agents, no plan mode, no repo index. Teams that want those must assemble them.
- Caveat: a moving target. The
durable,server,protocolandchordpackages are new and partly experimental.agent-session.tsalone is about 4,400 lines.
Sources: code at 636703a, verified Q&A.
How it answers the Open-source coding agents questions
Each answer was drafted by a code-reading agent at commit 636703a. Its citations were checked mechanically. Compare with the other open-source coding agents →
How is the agent loop implemented?
answeredThe agent loop lives in packages/agent/ — a single-threaded, turn-based loop with no separate planner component. Single loop, no planner. The runLoop() function in packages/agent/src/agent-loop.ts (line 163) runs an inner loop processing assistant responses and tool calls, and an outer loop that checks for follow-up messages. There is no planning phase: every turn the model produces a response (text + optional tool calls) and the loop executes them. Tool-call schema. Tools are defined via AgentTool<T> in packages/agent/src/types.ts (line 464) with a name, parameters (typebox schema), execute() function, and optional prepareArguments/beforeToolCall/afterToolCall hooks. Tool calls from the model are validated against their schemas before execution; invalid calls produce error results. Execution can be "parallel" (default — concurrent) or "sequential" — see executeToolCalls() at line 508. Turn structure. Each turn: (1) pending messages (steering/follow-up) are emitted, (2) prepareRequest hook runs, (3) streamAssistantResponse() at line 381 converts AgentMessage[] to Message[] via convertToLlm, calls the LLM stream function, and emits streaming events (start, text_delta, toolcall_start, etc.), (4) any tool calls are executed with results pushed back to context, (5) finishTurn decides continuation. Stop conditions. A turn stops on stopReason === "error" or "aborted" (hard stop, line 245-256). Otherwise, if no tool calls remain, no steering/follow-up messages are queued, and finishTurn doesn't request continuation, the loop ends (line 310-318). Sub-agents. Pi explicitly chooses not to implement sub-agents (README line 19). Instead it supports nested tool calls via ctx.executeTool() in extensions (see packages/coding-agent/src/core/extensions/types.ts line 394), codemode scripts, and MCP tool delegation. Each model request is self-contained; the model may call tools that internally call other tools, but no child agent loops exist.
How is repository context gathered and kept within the context window?
answeredPi gathers repo context from multiple sources and uses compaction (LLM-generated summaries) to stay within context windows. System prompt assembly. buildSystemPrompt() in packages/coding-agent/src/core/system-prompt.ts (line 88) constructs the system prompt from multiple sections: tool snippets, tool guidelines, skill descriptions, context files (.claude/ files), and an appendSystemPrompt setting. Context files are read via the resource loader as {path, content} pairs and wrapped in <project_instructions> tags. Tools for context gathering. Pi's built-in tools (read, grep, find, ls, bash) let the model explore the codebase. The read tool (packages/coding-agent/src/core/tools/read.ts) supports path, offset, and limit params. The grep tool provides regex search. Files are truncated to DEFAULT_MAX_BYTES / DEFAULT_MAX_LINES to avoid overflow. Compaction (summarization). When the estimated context tokens exceed contextWindow - reserveTokens (default 16k reserve), shouldCompact() in packages/coding-agent/src/core/compaction/compaction.ts (line 267) triggers compaction. The algorithm walks session entries from newest to oldest, accumulating tokens until keepRecentTokens (default 20k) are kept, then generates an LLM summary of the older conversation (line 645, generateSummary()). The summary uses a structured format (Goal, Progress, Key Decisions, Next Steps, Critical Context) and is inserted as a compactionSummary message in the session. After compaction, the older messages are hidden from the LLM context but remain in the session tree. No embeddings or vector store. Pi has no embeddings or RAG. All context comes from the system prompt, tool results, and conversation history. Context transform hook. Extensions can inject/modify context via the transformContext hook (defined in AgentLoopConfig, packages/agent/src/agent-loop.ts line 390), and Pi's session uses this to apply hidden tool declarations and forced prompts. Context overflow recovery. isContextOverflow() from the AI package detects when the provider rejects a request as too large; Pi then triggers _compactBeforeNextAssistantResponse() (packages/coding-agent/src/core/agent-session.ts around line 776) and retries.
.claude/ directory.How are code edits applied?
answeredPi provides two file-editing tools — edit and write — plus a file-mutation-queue for concurrency safety. Edit tool (exact search/replace). The edit tool in packages/coding-agent/src/core/tools/edit.ts uses an edits array of {oldText, newText} pairs. Edits are matched against the original file content, not incrementally, and overlapping edits are rejected. Matching is exact by default but falls back to fuzzy matching (edit-diff.ts line 207): Unicode normalization (smart quotes to ASCII, dashes to hyphens, spaces normalized) and trailing-whitespace stripping. The original file is read, the BOM is stripped, line endings are detected, content is normalized to LF, edits are applied, and native line endings are restored before writing. A unified diff and display diff with line numbers are returned in the result. Write tool (whole file). The write tool (packages/coding-agent/src/core/tools/write.ts) creates or overwrites a file, auto-creating parent directories. It uses a simple {path, content} schema. Validation. Tools use typebox schemas for argument validation (e.g., editSchema at line 32 of edit.ts). The validateToolArguments() function from @earendil-works/pi-ai rejects invalid arguments before execution. Truncated responses with stopReason === "length" have all tool calls failed immediately (agent-loop.ts line 267) because their arguments may be incomplete. No linter or test runners. Pi does not automatically run linters or tests after edits. The model is expected to verify its work via bash commands. Git integration. Pi does not auto-commit or branch before edits. The hosted-git-info package is a dependency for repository detection, but git operations are manual via bash. File mutation queue. Both edit and write tools use withFileMutationQueue() in packages/coding-agent/src/core/tools/file-mutation-queue.ts (referenced at edit.ts line 163) to serialize operations on the same file, preventing concurrent edits from different tool calls from racing. Pluggable operations. Both tools accept custom ReadOperations/EditOperations/WriteOperations interfaces, so file editing can be delegated (e.g., to SSH or containers).
How are shell commands and file writes kept safe?
answeredPi explicitly states it "does not include a built-in permission system" (README line 90). Safety is provided through extensible checkpoints rather than built-in sandboxing. No built-in permission manager. By default, the bash tool (packages/coding-agent/src/core/tools/bash.ts) spawns child processes via spawn() with the same OS-level permissions as the Pi process. The filesystem tools (read, write, edit, grep, find) all operate with the process's user permissions. There is no allow/deny list, container, or landlock built in. Pluggable tool operations. Every built-in tool (bash, read, write, edit, grep, find, ls, powershell) defines an Operations interface (e.g., BashOperations at bash.ts line 75, EditOperations at edit.ts line 83). These can be overridden to delegate execution to remote systems, containers, or sandboxes. Containerization patterns are documented in packages/coding-agent/docs/containerization.md: (1) plain Docker runs the whole Pi process in a container, (2) Docker Sandboxes uses managed sandbox with credential proxy, (3) OpenShell provides policy-controlled sandboxes, (4) the Gondolin extension routes built-in tools into a local Linux micro-VM while keeping Pi and provider auth on the host. Abort signals. All tools accept an AbortSignal and check signal.aborted between operations, enabling responsive cancellation of long-running commands. The bash tool also kills the process tree on abort. Tool execution hooks. beforeToolCall and afterToolCall hooks (packages/agent/src/types.ts lines 66-104) allow extensions to block, modify, or monitor tool execution — effectively a permission callback. Bash output truncation. The bash executor (packages/coding-agent/src/core/bash-executor.ts) truncates output beyond DEFAULT_MAX_BYTES and writes full output to a temp file, preventing context overflow. Binary output is sanitized via sanitizeBinaryOutput(). Project trust. packages/coding-agent/src/core/project-trust.ts gates whether project-level settings and MCP server configurations are loaded, preventing untrusted projects from injecting tool configurations.
Which models are supported and how are they called?
answeredPi supports many providers through the @earendil-works/pi-ai package. Providers. The AI package at packages/ai/src/ has provider implementations for Anthropic (anthropic-messages.ts), OpenAI (openai-completions.ts, openai-responses.ts), Google (google-generative-ai.ts), Mistral (mistral-conversations.ts), Bedrock (bedrock-converse-stream.ts), Azure OpenAI (azure-openai-responses.ts), Cloudflare Workers AI, llama.cpp, and a generic pi-messages.ts proxy. Each provider implements a streamSimple() function matching the StreamFn signature. Model catalog. Models are defined in a generated models.generated.ts and in model-catalog.ts, organized by provider with metadata (context window, cost, reasoning support, input types). The ModelRuntime class in packages/coding-agent/src/core/model-runtime.ts orchestrates providers, credentials, and model refresh. Local models. Pi directly integrates with llama.cpp via the router (/llama command). Compatible endpoints (Ollama, vLLM, SGLang, LM Studio) can be configured via models.json using existing API adapters (e.g., "api": "openai-completions"). Tool-calling vs text formats. Pi uses native tool-calling (function/tool schema) with all providers that support it. The constrainedSampling config on tools (edit.ts line 156) requests JSON Schema-guided generation when the provider supports it. For providers without native tool support, tools can be serialized as text instructions. Per-model prompt tuning. Tools contribute promptSnippet and promptGuidelines (e.g., editToolSystemPromptContribution at edit.ts line 43) that feed into the system prompt per active tool, allowing the model-specific prompt to be tuned. Cost tracking. Every assistant message carries a usage object with token counts and cost (packages/agent/src/agent.ts line 48-55). The usage-totals.ts module aggregates costs across the session. Default model IDs per provider are in model-resolver.ts (e.g., claude-opus-4-8, gpt-5.5, gemini-3.1-pro-preview).
How can it be extended and customised?
answeredPi has four major extension mechanisms: Extensions (TypeScript). Extensions are TypeScript modules that export a default factory receiving ExtensionAPI (see docs at packages/coding-agent/docs/extensions.md and types at packages/coding-agent/src/core/extensions/types.ts). They can registerTool() (line 1634), registerCommand() (line 1643), registerProvider() (line 1821), registerMcpServer() (line 1857), subscribe to lifecycle events via pi.on(), send messages, and access UI. Extensions run inside the Pi process and can inspect all state. Reload support means they can be hot-replaced. MCP Protocol. Pi connects to MCP servers over stdio or streamable HTTP (packages/coding-agent/docs/mcp.md). Tools from MCP servers are surfaced to the model as mcp__<server>__<tool> with configurable exposure (direct, deferred, codemode, hidden). Resource tools (read_mcp_resource, list_mcp_resources) are also available. Skills. Per the Agent Skills specification, skills are directories with SKILD.md files. Pi scans configured skill locations and adds name/description to the system prompt; full instructions load on demand when the model invokes them. Pi Packages. Bundles of extensions, skills, prompt templates, themes, and context files distributed via npm or git (packages/coding-agent/docs/packages.md). Config/Instruction files. Pi reads settings.json for tool config, models.json for custom provider endpoints, mcp.json for MCP servers, and context files (.claude/ directory) as project instructions. Headless/SDK use. The @earendil-works/pi-coding-agent package exports a TypeScript SDK (packages/coding-agent/src/core/sdk.ts). The CreateAgentSessionOptions interface (line 48) allows programmatic session creation with custom tools, models, and configuration. There's also RPC mode accepting JSONL commands on stdin/stdout, JSON mode, and print mode. Prompt templates allow reusable message text expansion. Themes customize terminal colors.
SKILL.md file (not SKILD.md).