A fast, minimal AI coding agent in Rust. Any OpenAI-compatible provider, persistent sessions, skill-based subagents, and sandboxed shell execution.
Disclaimer: This project is actively developed. While it supports any OpenAI-compatible API, not all models have been thoroughly tested.
mlx-community/gemma-4-e4b-it-4bit(Gemma 4) is used as the primary model for development and testing.
# Interactive mode — just start talking
pie
# resume your last conversation in this directory
pie -r # or pie --resume
# Pipe a question
echo "what does src/main.rs do?" | pie --md
# Use a specific model
pie -m mlx-community/gemma-4-e4b-it-4bit- Persistent sessions — conversations saved per directory, resume with
pie -r - Any provider — works with OpenAI, Anthropic, Groq, Ollama, or any OpenAI-compatible API
- Skills & subagents — markdown-based skills from agentskills.io, auto-loaded from queries
- Native Sandboxing — shell commands run with built-in OS isolation (sandbox-exec on macOS, bubblewrap on Linux)
- Streaming TUI — real-time tool calls, markdown rendering, command history
- Scriptable —
--jsonand--mdflags for single-shot mode
# Interactive (default)
pie
# Resume last session
pie -r
# Single-shot output
pie --md "explain this function"
pie --json "list files" # pipe into jq, etc.
# Use a specific skill
pie "/explore summarize this repo"
# Run a custom agent (markdown definition)
pie reviewer "check src/agent for me"
pie explore # opens the TUI running as that agent| Input | Action |
|---|---|
<query> |
Ask a question (auto-detects skills) |
/<skill> <query> |
Use a specific skill |
? |
Show help |
Ctrl+C |
Abort stream / quit |
pie acp serves the Agent Client Protocol
over stdio, so any ACP client (Zed, and other editors with ACP support) can
use pie as its coding agent.
// Zed: ~/.config/zed/settings.json
{
"agent_servers": {
"pie": {
"type": "custom",
"command": "pie",
"args": ["acp"]
}
}
}What the client gets:
- Sessions —
session/newcreates a persistent pie session;session/loadresumes one and replays the history. Session ids are pie session ids, sopie -ron the same directory picks up where the editor left off. - Workspace trust — the
cwd(andadditionalDirectories) the client sends is granted read+write in the sandbox for that session, and commands run with the session directory as their working directory.~/.ssh,.envand the otherdeny_*rules still apply on top. - Modes — pie's plan/build/debug/test/review/architect modes appear as
the session's mode selector;
session/set_modeswitches between them. - Approval before anything changes — Write, Edit and Bash each wait for a
session/request_permissionanswer (allow once / always for that tool this session / reject). Shell is gated too: the sandbox draws the boundary, but inside a writable workspaceprintf 'x' > fis an edit like any other, and a client that approves edits should not be walked around. Reads (Read, Ls, Glob, Grep) never ask.
pie server runs an HTTP daemon that exposes pie to other agents over the
Agent2Agent protocol (v1.0, JSON-RPC over
streamable HTTP) — the door for a main agent that delegates work to pie as
needed, or for a UI driving pie sessions. One daemon serves many
conversations, reusing its database pool and provider config; sessions in
different workspaces run in parallel, and turns on the same session queue.
# Run in the foreground (bind defaults to 127.0.0.1:8629)
pie server
# Or install it as a login service that restarts on failure.
# A non-loopback bind generates the api key and stores it in pie.toml.
# `--host` lists the hostname remote clients will use (required: the
# server answers 403 to any non-loopback Host header that is not
# allowlisted, a DNS-rebinding guard).
pie server install --bind 0.0.0.0:8629 --host citadel.lvh.me
# The credentials, any time
pie server token
pie status # shows bind + whether auth is on (never the key)Discovery is the Agent Card (unauthenticated; it declares the auth scheme when one is configured):
GET http://127.0.0.1:8629/.well-known/agent-card.json
POST http://127.0.0.1:8629/a2a (JSON-RPC 2.0, Authorization: Bearer)
Send SendStreamingMessage and read the SSE response: a Task frame first,
then artifactUpdate per token delta and statusUpdate per tool call, ending
with a final: true status. Frames flow from the first millisecond, so no
proxy idle timer fires mid-turn. SendMessage is the blocking fallback (one
request, final task); SubscribeToTask reattaches after a dropped
connection; CancelTask aborts the in-flight turn.
Push notifications — the callback channel for callers that can't hold a
stream. Pass a pushNotificationConfig in the send request (or attach one to
a task with CreateTaskPushNotificationConfig; configs are durable and keep
firing for later turns):
{"configuration": {"pushNotificationConfig": {"url": "https://your.app/a2a-hook", "token": "verify-me"}}}The daemon POSTs a StreamResponse frame to the webhook on every status
transition — turn start, and the final snapshot with the full answer
artifact — echoing your token in x-a2a-notification-token and sending an
Authorization: Bearer header when the config carries
authentication: {scheme: "bearer", credentials}. Failed deliveries retry
three times with exponential backoff. Configs attach to the in-flight turn
only; include the config again on follow-up messages.
Task identity follows the protocol's "life of a task": a task is the whole
conversation — the canonical taskId is the pie session id, and the
contextId (always the session id) groups the tasks of one conversation.
Between turns the task sits in INPUT_REQUIRED; follow-ups resend the same
taskId. A resolved task (FAILED/CANCELED) is immutable — references to
it answer TaskNotFound; to continue after a failure, send a fresh message
with the old contextId and no taskId, which opens a new task instance on
the same conversation.
Statelessness: only running turns live in memory. Idle tasks are derived
from the session transcript on demand, so a daemon restart costs nothing —
a client just sends the next message with the same taskId (or contextId
after a failure). One running turn per conversation is enforced across
every entry point, and delegated runs serve at depth 1, so agents cannot
nest through this door (main_agent_only MCP servers stay invisible to
them).
Agents are markdown files: frontmatter for configuration, body is the
agent's system prompt. Drop a *.md file into ~/.pie/agents/ (global)
or .pie/agents/ (project-local, overrides global by name) and run it
with pie <name> [query...] — a first query token matching an agent name
selects it.
---
name: reviewer # default: file name
description: Read-only code reviewer
model: deep # a model tier from pie.toml, or a literal model id
max_steps: 50 # cap the tool-call iterations (unset: unbounded)
output_mode: md # md | json | interactive
temperature: 0.3
# Tools are opt-in for agents/ drops: without a `plugins` list the agent
# has NO tools. Known names: fs, fs-readonly, shell, websearch, skills,
# agentsmd, mcp (all configured servers) or mcp:<server> for specific
# ones — unknown names fail the run.
plugins: [fs-readonly, shell]
skills_paths: ["~/src/my-skills"] # extra skill directories (needs skills)
readonly: true # demotes fs to fs-readonly (belt and suspenders)
sandbox: # full sandbox config, merged like pie.toml's
allow_write: ["."]
grants: ["fs-read:/tmp"] # pre-granted permissions
---
You are a senior code reviewer. Be blunt and specific.
(The rest of this file is the agent's system prompt.)Built-in commands (.pie/commands/*.md, embedded and global/local
commands/ dirs) keep working unchanged and keep the full default tool
set — a plugins: list there is also honored if present. An agents/
file with the same name overrides its commands/ twin. pie skills
lists everything.
Pie is configured via environment variables, a pie.toml file, or CLI flags.
The fastest way to get started is with environment variables:
export OPENAI_API_KEY="sk-..."
export OPENAI_MODEL="mlx-community/gemma-4-e4b-it-4bit"
export OPENAI_BASE_URL="http://localhost:1234/v1"
# For providers that support Anthropic-compatible endpoints (e.g. zai)
export ANTHROPIC_BASE_URL="https://api.z.ai/api/anthropic"For managing multiple providers or project-specific settings, use pie.toml.
Pie searches for this file in:
~/.pie/pie.toml(Global configuration)./.pie/pie.toml(Project-specific configuration)
For a full list of configuration options, see .pie/pie.toml.example.
HTTP-based MCP servers are configured
under [mcp.<name>]. Default runs (and legacy commands/ agents) connect
to every configured server best-effort — a server that is down costs a
warning in the session log, never the run. An agent with an explicit
plugins: list gets exactly what it names: plugins: [mcp] (every server,
fail-loud) or plugins: ["mcp:<name>"] (specific ones, fail-loud). Their
tools appear as <name>__<tool>.
[mcp.deepwiki]
url = "https://mcp.deepwiki.com/mcp"
[mcp.context7]
url = "https://mcp.context7.com/mcp"
[mcp.context7.headers]
# a value matching a [secrets] key is replaced by that secret at load time
CONTEXT7_API_KEY = "context7_key"
# GitHub's remote MCP (needs a PAT in [secrets] as github_pat):
# [mcp.github]
# url = "https://api.githubcopilot.com/mcp/"
# [mcp.github.headers]
# Authorization = "github_pat"
[secrets]
context7_key = "..."Every interaction records its LLM usage — request count, prompt/completion tokens, cached tokens and reasoning tokens — and prints a summary line when the run finishes (non-interactive mode):
· done in 6s · 4.6k tokens · 98% cached · 1 request
--json output carries the same stats in a usage object (cache_rate
is the cached fraction of prompt tokens; cost_usd is null without
configured pricing).
For cost accounting, configure per-model rates in USD per million tokens
under [pricing.<model-id>] (exact model id match):
[pricing."glm-5.1"]
input = 0.6 # uncached input tokens
cached_input = 0.1 # cache hits; defaults to `input` when omitted
output = 2.2 # output tokensAll runs are persisted to the llm_usage table in ~/.pie/pie.db for
bookkeeping. pie usage aggregates spend per model (defaults to the last
30 days; --days 0 is all time, --json= emits machine-readable output):
pie usage
pie usage --days 7LLM usage, last 30 days
model req prompt compl total cached cache cost
@z-ai/glm-5.3-flash 18 126.8k 3.2k 130.0k 114.6k 90% —
────────────────────────────────────────────────────────────────────
total 18 126.8k 3.2k 130.0k 114.6k 90% —
Raw queries work too:
sqlite3 ~/.pie/pie.db "SELECT model, SUM(total_tokens), SUM(cost_usd) \
FROM llm_usage GROUP BY model"default_provider = "local"
[provider.local]
model = "mlx-community/gemma-4-e4b-it-4bit"
base_url = "http://localhost:1234/v1"
api_key = "sk-..."
[provider.zai]
model = "glm-5.1"
base_url = "https://api.z.ai/api/paas/v4/"
anthropic_url = "https://api.z.ai/api/anthropic"
api_key = "..."To use a specific provider from your config:
pie -p ollama "how are you?"| Flag | Description |
|---|---|
-m, --model |
Model name |
--base-url |
API base URL |
--api-key |
API key |
-p, --provider |
Config provider name |
-r, --resume |
Continue last session |
--md |
Markdown output (single-shot) |
--json |
JSON output (single-shot) |
-d, --debug |
Debug logging |
- Sandbox: Configure restrictions in
pie.tomlunder[sandbox]. - Skills: Add custom skills to
.pie/skills/<name>/SKILL.md. - Instructions: Add project-level instructions to
AGENTS.md.
brew tap dineshdb/pie https://github.com/dineshdb/pie
brew install dineshdb/pie/piecargo build --releaseMIT