PDO (Python Do) — a terminal-first AI agent that reasons, plans, and safely executes real tasks.
Project description
PDO — Python Do
████████ ██████ ████████ ██ ██ ██ ██ ██ ██ ████████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ████████
Think. Plan. Do.
The same pixel-art logo greets you on every launch.
PDO is free and open source (MIT licensed). Contributions are welcome — see CONTRIBUTING.md. Star the repo if you find it useful! ⭐
PDO is a terminal-first AI agent that completes real tasks — it doesn't just answer questions. Give it a goal and it reasons about it, plans the steps, decides whether tools are needed, executes them safely, reviews the result, and replies clearly. When a plain answer is enough, it just answers; it never reaches for a tool it doesn't need.
you ▸ list all markdown files in this repo and summarise the README
🔧 run_shell(command='find . -name "*.md"')
🔧 read_file(path='README.md')
PDO Here are the Markdown files… and a three-line summary of the README…
Features
- ReAct-style agent loop built on the LLM's native function/tool calling — the model picks tools and arguments; PDO executes them and feeds results back until the task is done. Tool calls are never parsed out of free text.
- Many providers — OpenAI, Anthropic, OpenRouter, local Ollama, or any
OpenAI-compatible endpoint. Switch provider and model at runtime with
/models(live model listing). The core depends only on anLLMClientinterface. - 18 built-in tools — filesystem (read / write / append / edit / list / mkdir), shell (dangerous-command guard), code search (glob / grep), git, web search & fetch, HTTP, Python exec, SQLite, and long-term memory (save / search / delete).
- Extensible three ways without touching the core:
- Plugins — drop a
Toolsubclass in<PDO_HOME>/plugins/(or ship one via thepdo.pluginsentry-point group). - Skills — Markdown prompt recipes become slash commands (
/review, …). - MCP — connect any Model Context Protocol server; its tools appear as
mcp__<server>__<tool>.
- Plugins — drop a
- Conversation management — named sessions (
/new,/resume), automatic summarisation of long history,@filereferences (text and images for vision models), and/export. - Sub-agents — a
delegate_tasktool spawns a fresh child agent for self-contained subtasks, keeping the main context small on big jobs. - Codebase search —
/indexbuilds a local BM25 index of your project; the agent then usescodebase_searchto find relevant code withpath:linerefs (no embeddings API needed — works fully offline). - Safety & control — typed confirmation for destructive commands, working-dir write sandbox, per-tool permission policies, and a structured audit log.
- Polished terminal UX — pixel-art splash, a bordered input box with slash autocomplete, Markdown rendering, a thinking spinner, color themes, and a live token-usage footer.
- Scriptable — one-shot mode (
pdo "prompt"),--jsonoutput, and a Docker image. - Tested & CI-ready — a
pytestsuite that mocks the LLM (no API key needed) and a GitHub Actions workflow.
Installation
[!IMPORTANT] PDO requires Python 3.12+. Create the virtual environment with a 3.12 interpreter explicitly — don't rely on the system
python3(macOS ships 3.9, which will not work).
# 1. Clone
git clone https://github.com/uaedoom/pdo.git
cd pdo
# 2. Create a virtual environment WITH Python 3.12+
python3.12 -m venv .venv # macOS (Homebrew): brew install python@3.12
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. Upgrade pip, then install (editable, with dev extras)
python -m pip install --upgrade pip
pip install -e ".[dev]"
This installs the pdo console command. Verify with python --version
(should be 3.12.x) and pdo --version.
Notes for users / troubleshooting
ERROR: ... Directory cannot be installed in editable mode/ "requires a setuptools-based build" — your virtual environment is on an old Python (and old pip). Recreate it with Python 3.12 and upgrade pip:deactivate; rm -rf .venv python3.12 -m venv .venv && source .venv/bin/activate python -m pip install --upgrade pip pip install -e ".[dev]"
- No
python3.12? Install it first: macOSbrew install python@3.12, Ubuntusudo apt install python3.12 python3.12-venv. - Not yet on PyPI — install by cloning as above (
pip install pdoisn't available yet). - Tested on macOS and Linux. Windows should work but is less tested.
- Runtime data (memory, sessions, logs) lives in
~/.pdoif you setPDO_HOME=~/.pdo; otherwise it defaults to the package directory.
Quick Start
# Set your API key (or copy .env.example to .env and fill it in)
export OPENAI_API_KEY=sk-...
# Optionally choose a model (gpt-4.1-mini is the default)
export OPENAI_MODEL=gpt-4.1-mini
# Run it interactively
pdo
# …or one-shot (great for scripts / pipes)
pdo "list all markdown files and summarise the README"
pdo --json "what is 2+2" # machine-readable output
pdo --version
Interactive mode gives you a prompt; type a goal, or a slash command (/help).
Replies render as Markdown, with a thinking spinner and a Codex-style activity
log. Switch colors live with /theme green (or set PDO_THEME).
Configuration
All configuration is read from the environment (a .env file is auto-loaded):
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
(required) | Your API key (OpenAI or OpenRouter, etc.). |
OPENAI_MODEL |
gpt-4.1-mini |
Model to use. |
OPENAI_BASE_URL |
(OpenAI) | API endpoint. Set to use an OpenAI-compatible provider. |
TEMPERATURE |
0.2 |
Sampling temperature (0–2). |
PDO_HOME |
package dir | Where memory and logs are stored (e.g. ~/.pdo). |
PDO fails fast with a friendly message if OPENAI_API_KEY is missing.
Using OpenRouter (or other OpenAI-compatible APIs)
PDO talks to any OpenAI-compatible endpoint — just point OPENAI_BASE_URL at it
and use that provider's key and model names. For OpenRouter:
export OPENAI_API_KEY=sk-or-... # your OpenRouter key
export OPENAI_BASE_URL=https://openrouter.ai/api/v1
export OPENAI_MODEL=openai/gpt-4.1-mini # any OpenRouter model id
pdo
The same pattern works for local servers (e.g. Ollama/LM Studio at
http://localhost:11434/v1). The model must support tool/function calling
for PDO's agent loop to use tools.
Example Usage
you ▸ build a minimal Flask API in ./hello-api
you ▸ explain this repository
you ▸ fix this Python error: <paste traceback>
you ▸ list all markdown files
you ▸ create a README for this project
Terminal commands
| Command | What it does |
|---|---|
/help |
Show available commands |
/models |
Switch provider & model (OpenAI / Anthropic / OpenRouter / Ollama) |
/tools |
List registered tools |
/mcp |
Show connected MCP servers and their tools |
/theme |
Change the color theme (e.g. /theme green) |
/export |
Save the conversation to a Markdown file |
/sessions |
List saved conversation sessions |
/new |
Start a new session (e.g. /new feature-x) |
/resume |
Switch to another session (e.g. /resume default) |
/memory |
Show saved facts and preferences |
/history |
Show recent conversation history |
/clear |
Clear the current session's history |
/version |
Show the PDO version |
/exit |
Quit |
Switching models at runtime
Type /models to pick a provider (OpenAI, Anthropic, or OpenRouter) and a model
interactively — the change applies immediately for the rest of the session. If a
key for the chosen provider isn't in your environment, PDO prompts for one and
keeps it in memory for the session only (never written to disk). Set
ANTHROPIC_API_KEY / OPENROUTER_API_KEY in your .env to skip the prompt.
Project Structure
pdo/
├─ pyproject.toml # Packaging + console script (pdo = pdo.main:main)
├─ requirements.txt # Convenience mirror of runtime deps
├─ .env.example # Sample configuration
├─ .github/workflows/ci.yml
├─ src/pdo/
│ ├─ main.py # Terminal entry point + REPL + slash commands
│ ├─ config.py # Env-based config, validated with pydantic
│ ├─ llm.py # LLMClient interface + OpenAI implementation
│ ├─ logging_setup.py # Rotating file logging for the `pdo` namespace
│ ├─ agent/
│ │ ├─ core.py # Coordinates components; runs the ReAct loop
│ │ ├─ planner.py # Breaks a goal into steps (thin, advisory)
│ │ ├─ router.py # Plain chat vs. tool use (thin; model decides)
│ │ ├─ executor.py # Runs approved tool calls, safely
│ │ ├─ reviewer.py # Sanity-checks the final answer
│ │ ├─ memory.py # Local JSON memory store
│ │ └─ messages.py # Message/ToolCall dataclasses
│ ├─ tools/
│ │ ├─ base.py # Tool base class + confirmation helper
│ │ ├─ registry.py # The single tool registry + auto-registration
│ │ ├─ filesystem.py # read / write / append / list / mkdir
│ │ ├─ shell.py # run command + dangerous-command detector
│ │ └─ memory.py # save / search / delete memory tools
│ ├─ prompts/system.md # The system prompt
│ ├─ data/ # Runtime JSON state (memory.json, history.json)
│ └─ logs/ # Rotating logs (pdo.log)
├─ tests/ # pytest suite (LLM is mocked)
└─ docs/ # Architecture notes
Where is my data? By default PDO stores
memory.json,history.jsonand logs inside the installed package directory so a fresh clone works immediately. SetPDO_HOME=~/.pdoto keep that state in your home directory instead.
Adding New Tools
A tool is a small class. Subclass Tool, declare a JSON parameter schema, and
decorate it with @register_tool — that's it. The agent picks it up
automatically; you never touch the core.
# src/pdo/tools/clock.py
from datetime import datetime
from typing import Any
from .base import Tool
from .registry import register_tool
@register_tool
class CurrentTimeTool(Tool):
name = "current_time"
description = "Return the current local date and time."
parameters = {"type": "object", "properties": {}}
def run(self, **_: Any) -> str:
return datetime.now().isoformat(timespec="seconds")
For a built-in tool, add the module to the lazy import in
tools/registry.py:get_registry. For tools that perform sensitive actions,
accept an injectable confirm callback (see tools/filesystem.py) so they can
be tested deterministically and prompt the user when needed.
Plugins (no fork required)
You don't have to edit PDO to add a tool. PDO auto-discovers plugins on startup from two places:
-
A plugins directory — drop a
.pyfile defining aToolsubclass into your plugins folder (run/toolsto see its path; default<PDO_HOME>/plugins). The@register_tooldecorator is optional there — PDO findsToolsubclasses automatically. A ready-made example lives inexamples/plugins/current_time_tool.py:mkdir -p ~/.pdo/plugins # if you run with PDO_HOME=~/.pdo cp examples/plugins/current_time_tool.py ~/.pdo/plugins/ pdo # the `current_time` tool is now available
-
Installed packages — a third-party package can advertise tools via the
pdo.pluginsentry-point group. Each entry point may resolve to aToolsubclass or aregister(registry)callable:# in the plugin package's pyproject.toml [project.entry-points."pdo.plugins"] my_tool = "my_package.tools:MyTool"
A broken plugin is logged and skipped — it never crashes PDO.
Skills (reusable prompt commands)
Drop a Markdown file in your skills directory (<PDO_HOME>/skills/) and it
becomes a slash command named after the file. An optional first line
description: … (or # Title) sets the menu text, and {{args}} interpolates
whatever you type after the command. Example
examples/skills/review.md becomes /review:
mkdir -p ~/.pdo/skills
cp examples/skills/review.md ~/.pdo/skills/
pdo # now type: /review the auth module
Referencing files with @
In any message, mention a file with @path and PDO inlines its contents for the
model — e.g. explain @src/pdo/main.py or fix the bug in @app.py.
Images too: @screenshot.png (png/jpg/gif/webp) attaches the image itself,
so vision-capable models can see it — e.g. what's wrong in this UI? @shot.png.
Multi-line input
Press Enter to send. Press Option/Alt+Enter (⌥⏎) to insert a newline and compose a multi-line message before sending.
MCP servers (Model Context Protocol)
PDO is an MCP client: connect any MCP server and its tools become available to
the agent automatically (named mcp__<server>__<tool>). Declare servers in
<PDO_HOME>/mcp.json using the standard format (see
examples/mcp.json):
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
}
}
}
Servers start on launch (over the stdio transport); run /mcp to see what's
connected. A server that fails to start is reported and skipped — it never
crashes PDO. No extra Python dependencies are required.
PDO as an MCP server / SDK
It works both ways — pdo --serve exposes the whole agent as an MCP server
over stdio, so Claude Desktop, Claude Code, or any MCP client can call its
run_task tool and get back the agent's final answer (with all of PDO's tools,
sub-agents, and codebase search behind it). For example, in a client's
mcp.json:
{ "mcpServers": { "pdo": { "command": "pdo", "args": ["--serve"] } } }
In serve mode nothing is printed to stdout and interactive confirmations are auto-denied (dangerous commands are refused rather than prompted).
And from Python scripts, embed the agent directly:
from pdo import run_agent
answer = run_agent("list the markdown files here and summarise the README")
run_agent accepts overrides (model=, base_url=, api_key=,
temperature=, or a custom llm= client) and uses an ephemeral memory so it
never touches your interactive sessions.
Roadmap
v1 — done
- Native tool-calling agent loop; multi-provider (OpenAI / Anthropic / OpenRouter / Ollama); 18 built-in tools; plugins, skills, and MCP client; named sessions + auto-summary; permission policies + audit log; themed TUI with one-shot/JSON modes; tests + CI; Docker.
v2 (this release) — done
- Multi-line input and image/vision attachments (
@image.png). - Sub-agents (
delegate_task) and codebase retrieval (/index+codebase_search, BM25 — fully offline). pdo --serve(PDO as an MCP server) and therun_agentPython API.
Next
- Embedding-based retrieval as an optional upgrade to the BM25 index.
- Streamed responses inside serve mode; richer sub-agent orchestration.
- PyPI publication and a Homebrew formula.
Each of these arrives as a new Tool (or LLMClient) — by design, none require
changes to the core.
Contributing
Contributions are welcome! See CONTRIBUTING.md for setup,
coding standards, and how to add tools. Please run ruff check . and pytest
before opening a pull request.
License
MIT © PDO Contributors
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pdo_agent-2.0.0.tar.gz.
File metadata
- Download URL: pdo_agent-2.0.0.tar.gz
- Upload date:
- Size: 76.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee2b28d9d0934f50fb3f60f321c54c53a6c9250c0b53f53f72fac7eabcba1b01
|
|
| MD5 |
7b4418225c65f5d90a8668138af5c215
|
|
| BLAKE2b-256 |
355fc278378bdb01be4906b0d95b718ca2c5f0272978ac0e571e78f04eebf1d6
|
File details
Details for the file pdo_agent-2.0.0-py3-none-any.whl.
File metadata
- Download URL: pdo_agent-2.0.0-py3-none-any.whl
- Upload date:
- Size: 72.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a7a6465f3399d65f1e43ab967d5ee85b587569f2f57a0d39d3deb02304e5a8a
|
|
| MD5 |
134509ce0b8134416a69279d4f57a93a
|
|
| BLAKE2b-256 |
88186af06704bcfa3f9ce2a4e8949fd845eff58e65bd35bc7caec7d50fe5cf25
|