Pila
A minimal, auditable terminal coding agent harness in Python.
The same agent loop backs the interactive TUI, headless CLI modes and an embeddable SDK.
| Package | Responsibility |
|---|---|
pila.ai |
Providers, streaming, model catalogs, abort signals |
pila.agent |
Agent loop, tools, permissions, budgets |
pila.coding |
Coding tools, sessions, checkpoints, MCP, prompts, CLI |
pila.tui |
Interactive terminal UI |
pila.sdk |
Agent and @tool embedding API |
Features
- Streaming agent loop — text, thinking, tool calls, tool results, usage and errors are emitted as events.
- Providers — Anthropic, OpenAI-compatible (OpenAI, xAI, Groq, DeepSeek,
Mistral, OpenRouter, Ollama, …), Google Gemini, GitHub Copilot, and an offline
fauxprovider for tests and demos. - Tools —
read,bash/powershell,edit,write,grep,find,lsandwebsearch. Searches use the in-processripgrep-rsengine and respect.gitignore. - Permissions —
plan,ask,accept-edits(default) andbypass, refined by glob rules inpermissions.toml. Every decision is recorded in the session and explained to the model when a call is blocked. - Budgets — opt-in caps on total tokens or cost:
--budget cost=2.50or--budget tokens=250000. - Sessions — JSONL session tree with resume, fork, clone, export, labels and automatic compaction.
- Checkpoints —
write/editsnapshot the files they change;/undorestores them. - MCP — stdio and streamable-HTTP servers from
settings.json, plus the Context7 documentation server (on by default). - Context and skills —
AGENTS.md,SYSTEM.md, project and built-inSKILL.mdskills, prompt templates, and extensions. - Code review skill — a built-in
SKILL.mdthat runs ruff, sqlfluff, hadolint, tofu and tflint only for the file types in scope, then writesreport.md. - Four run modes — interactive TUI, print, JSON and RPC over stdio.
- SDK — typed
@toolfunctions and a sync/asyncAgentwith events, approvals, MCP and budgets. - Cross-platform — Windows, macOS and Linux. File edits preserve line endings; the shell resolves to bash/sh when available, otherwise PowerShell.
Install
Requires Python 3.12 or newer.
pip install 'pila-ai[cli]'
pila login # API key, OAuth, or a custom provider
pila
The base package is the embedding SDK: pip install pila-ai has two direct
dependencies — httpx2 (HTTP, keyless web search and page reading) and
ripgrep-rs (the engine behind grep/find/ls). click, rich and
prompt_toolkit live in the cli extra; a missing extra reports
pip install 'pila-ai[cli]' instead of failing with an import error.
From a checkout:
uv sync # installs the CLI and development dependencies
uv run pila
Quick start
pila "explain @src/pila/cli.py" # TUI with an initial prompt
pila -p "list the TODO comments" # print the final response
pila --json "summarize main.py" > events.jsonl
pila -c # continue the latest session
pila -r <id-prefix> # resume a specific session
pila models [provider] # provider catalogs
pila sessions # sessions for this directory
pila --help # every flag and subcommand
TUI essentials:
/opens the command menu;@completes files.Tabaccepts a completion;Enterfills it so you can keep typing, then submits on the nextEnter./model,/thinking,/permissions,/resume,/fork,/compactand/undocover the common workflows.!commandruns a shell command and adds its output to the conversation;!!commandruns it without adding the output to the model context.EscorCtrl+Cinterrupts a turn. Text submitted during a turn is queued as steering;/commandsand!shelllines return to the editor./thinkingchanges are stored in the session and insettings.json; they are restored on resume.
Permissions
Every tool declares a kind — read, web, edit, shell or mcp — and the
mode decides what runs without asking:
| Mode | Reads & web | File changes | Shell & MCP |
|---|---|---|---|
plan |
allow | deny | deny |
ask |
allow | ask | ask |
accept-edits (default) |
allow | allow | ask |
bypass |
allow | allow | allow |
In the TUI an ask opens an approval panel (y allow once, a allow for the
session, n deny). Headless modes cannot prompt, so ask becomes a denial with
an actionable message.
Rules refine the mode. They live in two files, merged global first and project last; inside a file the last matching rule wins:
# ~/.pila/agent/permissions.toml or <project>/.pila/permissions.toml
mode = "accept-edits"
[[rules]]
tool = "bash"
match = "rm -rf *"
action = "deny"
[[rules]]
tool = "bash"
match = "git *"
action = "allow"
allow skips the prompt, deny blocks the call, and plan remains read-only:
rules may tighten it but not widen it. The effective mode comes from
--permission-mode, then permissionMode in settings.json, then mode in a
permissions file, then accept-edits. /permissions shows the mode, rules and
session grants; /permissions mode ask switches mid-session.
The workspace guard confines read, write, edit, grep, find and ls to
the directory Pila was started in: absolute paths, .. segments and escaping
symlinks are rejected, and @ completion only lists workspace files. Shell
commands are not OS-sandboxed — treat the guard as protection against
mistakes and prompt injection, not as a security boundary for untrusted code.
Budgets
Budgets are opt-in and cap only total tokens or cost:
pila -p --budget cost=2.50 "fix the build"
pila -p --budget tokens=200000 "fix the build"
pila -p --budget cost=2.50,tokens=200000 "fix the build"
Tokens count input + output + cache reads. Without --budget nothing is
enforced. When a limit is reached the turn ends normally, a limit_reached
event is emitted, the transcript records it, and print mode exits 1.
/budget shows the counters for the limits you set.
MCP servers
Pila speaks the MCP 2025-06-18 protocol directly, over stdio and streamable
HTTP, with no extra dependency. Context7 is the shipped built-in and is enabled
by default. It is an ordinary mcpServers entry in settings.json and provides
up-to-date, version-specific library documentation without a key. Set
"enabled": false in the global or project settings to turn it off
persistently, or use --no-mcp for a single run.
pila mcp list # built-ins, runtimes, configured servers
pila mcp add context7 --project # pin it on for this project
pila mcp remove context7 # remove a project/global override
| Built-in | Runtime | Adds |
|---|---|---|
context7 |
http | Version-specific library and framework documentation |
Custom servers use the same JSON shape:
pila mcp add github --command uvx --arg mcp-server-github --env GITHUB_TOKEN=…
pila mcp add wiki --url https://mcp.deepwiki.com/mcp --header "Authorization=Bearer …"
{
"mcpServers": {
"local": { "command": "uvx", "args": ["some-mcp-server"], "env": { "TOKEN": "…" } },
"remote": { "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer …" } }
}
}
Settings are deep-merged in three layers: shipped defaults, the global
~/.pila/agent/settings.json (or $PILA_CONFIG_DIR/settings.json), then the
project's .pila/settings.json. ${workspaceFolder} (or {cwd}) in an
argument expands to the session directory. Servers connect in parallel with an
8-second budget; failures are warnings and do not stop the run. Tools appear as
mcp__<server>__<tool> and go through the normal permission policy. /mcp
shows live state; --no-mcp disables the integration for a run.
Checkpoints and undo
Before write or edit changes a file, Pila copies the previous version into
~/.pila/agent/checkpoints/<session>/ (5 MB per file; larger files are recorded
as skipped). /undo [count] restores the last count write/edit groups
newest-first, deletes files that did not exist before, and adds a note to the
conversation. Disable checkpoints with "checkpoints": false in
settings.json.
Providers
| Provider | Credentials |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY |
GEMINI_API_KEY |
|
| xAI, Groq, DeepSeek, Mistral, OpenRouter | XAI_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY, OPENROUTER_API_KEY |
| Ollama | none (local) |
| GitHub Copilot | pila login github-copilot, or GITHUB_COPILOT_TOKEN |
Every provider also accepts settings.apiKeys. Sign-in stores credentials in
~/.pila/agent/auth.json
pila login # pick OAuth, API key, or custom provider
pila login anthropic # prompt for the API key
pila login github-copilot # device flow (asks for the GitHub host)
pila logout [provider]
Pila ships no hard-coded model names. Catalogs are fetched from each provider's
models API, cached in ~/.pila/agent/models.json, refreshed after 24 hours, and
fall back to the cache when the provider is unreachable. Model selection at
startup is: --model, the model stored in a resumed session, model in
settings.json, the newest sign-in in auth.json, then a provider environment
variable.
Custom OpenAI-compatible providers can be registered through pila login or
manually:
{
"providers": {
"my-llm": {
"api": "openai-completions",
"baseUrl": "https://my-llm.example.com/v1",
"models": [
{"id": "gpt-oss-120b", "name": "gpt-oss-120b", "contextWindow": 200000, "maxTokens": 16384}
]
}
}
}
GitHub Copilot. The device flow works with github.com, GitHub Enterprise
Cloud (*.ghe.com) and GitHub Enterprise Server. The catalog is fetched from
your account at login. Optional settings: providers.github-copilot.enterpriseUrl
and providers.github-copilot.clientId. Environment overrides:
GITHUB_COPILOT_TOKEN, GITHUB_COPILOT_ENTERPRISE_URL,
GITHUB_COPILOT_CLIENT_ID, GITHUB_COPILOT_API_BASE.
Web search. websearch needs no API key. It tries optional Firecrawl
(FIRECRAWL_API_KEY) and Brave (BRAVE_SEARCH_API_KEY) first, then DuckDuckGo,
Bing RSS and Wikipedia. A url argument reads one page. Proxy environment
variables and the OS certificate store apply through httpx2.
Python SDK
pila.sdk embeds the same agent loop without the CLI or TUI:
from pila.sdk import Agent, tool
@tool
def search_orders(customer: str, limit: int = 10) -> list[dict]:
"""Find recent orders for a customer.
Args:
customer: Customer name or id.
limit: Maximum number of orders to return.
"""
return db.search(customer, limit)
agent = Agent(model="anthropic/claude-sonnet-4-5", tools=[search_orders])
result = agent.run("What did Acme order last week?")
print(result.text)
@toolderives the JSON schema from type hints (str,int,float,bool,list[…],dict[…],Literal[…],Optional[…]) and reads descriptions from the Google-styleArgs:section. Async functions are supported.Agent(...)acceptssystem,append_system,context,extra_guidelines,thinking,temperature,max_tokens,cwd,tools,coding_tools,budget,permission_mode,permission_rules,approval,checkpoints,mcp/mcp_servers,load_project,sessionandon_event.mcp_servers=[McpServerConfig(...)]connects at construction and exposesmcp__<server>__<tool>tools under the normal permission policy.agent.mcp_warningsreports servers that failed to start.- SDK defaults:
permission_mode="bypass", no checkpoints, no MCP, no project discovery (load_project=Trueopts in) and no session file (session="agent.jsonl"persists). run()returnsRunResultwithtext,messages,usage,cost,steps,tool_calls,session_id,stop_reasonandlimit_reason;okisFalseon an error, abort or budget cap.stream()yields the same events as the CLI;arun()/astream()are async equivalents;add_tool()adds tools after construction.
MCP, context and built-in tools combine freely:
from pila.sdk import Agent, McpServerConfig
agent = Agent(
model="anthropic/claude-sonnet-4-5",
mcp_servers=[McpServerConfig(name="time", command="uvx", args=["mcp-server-time"])],
context=["Team conventions live in docs/style.md."],
coding_tools=["read", "grep", "ls"],
)
An approval callback makes graded modes useful in a service:
from pila.agent.permissions import PermissionRequest
def approve(request: PermissionRequest) -> str: # "allow", "session" or "deny"
return "allow" if request.subject.startswith("git ") else "deny"
agent = Agent(
model="anthropic/claude-sonnet-4-5",
coding_tools=["read", "bash"],
permission_mode="ask",
approval=approve,
)
examples/sdk_agent.py combines a custom tool, a budget, streaming and an
approval callback:
uv run python examples/sdk_agent.py --budget cost=0.50 "tour this repository"
Interactive commands
| Command | Action |
|---|---|
/help |
show commands |
/model [pattern] |
switch model; no argument opens a picker |
/thinking [level] |
set the thinking level |
/new |
start a new session |
/resume, /sessions [id] |
browse and switch sessions |
/fork [id] |
fork before an earlier user message |
/clone |
duplicate the current branch |
/export [path] |
export JSONL branch or static HTML |
/tree [id] |
inspect the session tree |
/compact [instructions] |
compact the conversation |
/permissions [mode <mode>] |
show or switch the approval policy |
/budget |
show token/cost budget and usage |
/undo [count] |
restore files changed by the last write/edit calls |
/mcp |
list configured MCP servers and tools |
/name [label] |
get or set the session label |
/session, /stats |
session statistics |
/cwd |
print the working directory |
/login [provider], /logout [provider] |
manage credentials |
/skills, /skill:<name> [args] |
list or load a skill |
/exit, /quit, /q |
leave |
!command, !!command |
run a shell command with or without model-visible output |
Sessions
Sessions are JSONL files under ~/.pila/agent/sessions/ (override with
PILA_CONFIG_DIR). Each line is an entry in a session tree: messages, tool
results, shell executions, model and thinking changes, labels, and compaction
markers. Compaction summarizes older messages and records a firstKeptEntryId,
so resume rebuilds the context without resending summarized history.
/clone copies the active branch into a new session file; /fork stops before
a chosen user message and returns that message to the editor. /export path.jsonl writes the active branch; any other path writes a self-contained
static HTML transcript.
Development
uv sync # install/lock dependencies (uv.lock is committed)
uv run pytest # full suite, fully offline
uv run ruff check .
uv run ruff format --check .
uv run coverage run -m pytest
The suite uses the faux provider (PILA_FAUX_SCRIPT replays a JSON scenario)
and mock HTTP transports, so it needs no API keys or network. CI runs lint,
format check and tests on Python 3.12, 3.13 and 3.14 across Ubuntu, macOS and
Windows (.github/workflows/ci.yml).
Infrastructure is wrapped once: pila/http.py owns the single pooled httpx2
client (keep-alive, per-request timeouts, SSE streams, and a set_client() hook
for tests). Providers, catalogs and web search reuse it.
Layout
src/pila/
ai/ types, models, model API clients, abort, providers
agent/ agent loop, messages/events, permissions, budgets
agent/skills/ built-in skills shipped in the package
coding/ tools, web search, sessions, compaction, checkpoints, MCP, prompts
sdk/ embeddable Agent, @tool, RunResult
tui/ interactive app, renderer, completion, selector
cli.py Click CLI and headless modes
http.py pooled HTTP adapter
examples/ runnable SDK example
tests/ offline pytest suite
License
Apache License 2.0. See the LICENSE and NOTICE files in the distribution.