Skip to main content

ZenAgent

PyPI CI Python

A minimal coding agent written from scratch in pure Python: one interface for streaming LLM calls, an agent loop that is an async generator, and a terminal harness. No vendor SDKs, no agent framework — httpx is the only runtime dependency of the kernel.

Named after the Zen of Python: explicit contracts, flat layers, and one obvious way to do things. (Renamed from pyagent; there is no compatibility shim.)

Built as a learning project and as the small, hackable Python kernel that the ecosystem is missing (in the spirit of pi-ai + pi-agent-core).

Repo: https://github.com/encyc/zen-agent · PyPI package: zenagent-py (imports as zenagent).

Docs

Full documentation — architecture, usage guides, internals (source tour), contracts and the feature map — lives in docs/. Start with docs/architecture.md. Release notes: CHANGELOG.md; the tool-kernel gap study and the next roadmap live under docs/research/omp-cli-study/.

Install

# install the published CLI
uv tool install "zenagent-py[cli]"   # or: pip install "zenagent-py[cli]"
zen --help                          # imports as `zenagent`; needs Python 3.11+

# work on the project itself
uv sync --extra cli                 # kernel + interactive extras (prompt_toolkit, rich)

The interactive TUI checks PyPI at most once an hour for a newer zenagent-py and prints one quiet line in the transcript when one exists; -p/--json never check. Disable it with ZENAGENT_NO_UPDATE_CHECK=1.

Requires Python 3.15+ (the project targets the current uv toolchain; any 3.11+ should work for the kernel itself).

API keys

Read from the environment variable named by each provider in zenagent/catalog.yaml:

provider env var
zai-coding-cn ZAI_CODING_CN_API_KEY
deepseek DEEPSEEK_API_KEY
opencode-go OPENCODE_API_KEY

Usage

# one-shot, exits 0 on success and 1 when the provider failed
uv run zen -p "explain what this repo does"

# pick a model / reasoning level; allow write + execute tools
uv run zen -p "create /tmp/x/hello.txt, then read it back" -m deepseek-flash --yes --thinking high

# attach images (needs a vision model; deepseek-flash is one)
uv run zen -p "what is in this picture?" --image shot.png

# guardrails and reproducibility
uv run zen -p "..." --max-turns 20 --max-cost 0.05 --no-context-files
uv run zen -p "analyze this" --read-only --trace run.jsonl --cwd /path/to/project

# runtime modes: plan = read-only, ask = confirm (default), full = automatic
uv run zen -p "investigate and propose a plan" --mode plan
uv run zen -p "apply the fix" --mode full

# structured output (a single JSON object; no streamed text)
uv run zen -p "say hi" --json

# interactive REPL
uv run zen

Runtime modes: plan (read-only; the agent investigates and proposes a plan), ask (default: write/execute tools need confirmation), full (run write/execute tools without asking; catastrophic commands still need --allow-dangerous). --yes means full, --read-only means plan, and /mode plan|ask|full switches mid-session. Session display names come from the first question; rename with /rename <name> (or r in the TUI session picker).

Write guards are independent of the mode: full/--yes never auto-approve a write outside the project root, a write to a credential store (~/.zenagent, ~/.ssh, …) or a destructive shell command. Interactive runs confirm them; headless runs refuse and name the flag (--allow-outside PATH, --allow-protected, --allow-destructive). Every successful write is snapshotted per project; /restore puts the last run's files back and skips anything you edited afterwards.

Diagnostics

uv run zen doctor      # keys, catalog, config, session dir, terminal

Extending the agent (file-based, no restart needed)

What Where How it shows up
Skills .zenagent/skills/<name>/SKILL.md (project) or ~/.zenagent/skills/ listed in the prompt; the model loads one with read_skill
Prompt templates .zenagent/commands/<name>.md /name args expands and sends it
Extensions extensions = ["path.py"] in config or --extension registers tools, slash commands, prompt text, event hooks
MCP servers ~/.zenagent/mcp.json tools exposed as mcp__<server>__<tool>
Custom providers/models [providers.custom.<id>] in config.toml usable with -m <model>

Servers that are down at start are retried before each prompt (/mcp shows state, /mcp retry forces it), and /reload re-discovers skills, templates and extensions mid-session.

Library / SDK

from zenagent.agent import Agent
from zenagent.tools import ALL_TOOLS

agent = Agent(
    model=model,                 # a zenagent.providers.catalog.Model
    system_prompt="be brief",
    tools=list(ALL_TOOLS),       # or your own AgentTool objects
    api_key="...",
)
await agent.prompt("explain this repo")   # events via agent.subscribe(fn)

See examples/README.md for a headless run, a custom tool with a fake stream (offline), a direct MCP call, and a zen rpc client. For embedding with the full app layer (MCP, skills, sessions), use Runtime + ChatController — see examples/embed.py.

REPL commands

command effect
/help list commands
/exit leave (Ctrl+D also works)
/clear forget the conversation
/model [id] show / switch model (next turn)
/thinking [level] off / low / medium / high / max
/mode [plan|ask|full] show or switch the runtime permission mode
/compact summarize the session into a new session file
/undo drop the last turn (writes a new session file)
/restore [list|n] restore files changed by the last run (or list recent runs)
/sessions [all] list this project's sessions (or all projects)
/rename <name> rename the current session
/fork <n> branch a new session at user message n
/import <path> load a session file and continue it
/image <path> attach an image for the next prompt
/skills list available skills
/mcp MCP server status; /mcp retry reconnects
/reload re-discover skills, templates, extensions
/theme <name> default / mono / solarized / a JSON file
/hotkeys show active keybindings
/pin [on|off] pin/unpin the current session
/new start a new session
/settings show model, prefs and paths
/search [query] web search, or show the provider
/doctor run environment checks
/todo print the session todo list
/jobs [kill <id>] list or kill background jobs
/copy [text] copy to the system clipboard
/btw <question> ask a side question without touching the transcript
/shake [tools|images|thinking|all] drop heavy content into a new session
/handoff write a handoff markdown, then compact
/add-dir /remove-dir /dirs track extra workspace roots
/fresh reset display state
/trace show the trace path (when started with --trace)

Command arguments complete too (Tab): /mode → plan/ask/full, /thinking → levels, /model → catalog ids. In the TUI the same values appear as you type, and a bare /mode or /thinking opens a selector.

While the agent is working, anything you type is queued as steering input rather than starting a second run. Ctrl+C aborts the current run cooperatively.

Configuration

Optional ~/.zenagent/config.toml, plus a committable <project>/.zenagent/config.toml that layers on top (themes, keybindings and MCP live in their own files):

default_model = "deepseek-flash"

[providers.deepseek]
base_url = "https://mirror.example/v1"   # optional
api_key_env = "MY_DEEPSEEK_KEY"          # optional

# define a provider from scratch
[providers.custom.local]
base_url = "http://127.0.0.1:8080/v1"
api_key_env = "LOCAL_API_KEY"
adapter = "openai-completions"
[[providers.custom.local.models]]
id = "my-model"
context_window = 32768
max_tokens = 4096
reasoning = false

extensions = ["~/work/my-extension.py"]

[tools]
allow = ["read_*", "grep"]     # never ask for these
deny = ["write_file"]          # always refuse

# global layer only (a project-layer [task]/[safety]/[backup] is rejected)
[safety]
allow_outside = ["~/scratch"]  # writes allowed outside the workspace
allow_destructive = false
allow_protected = false

[backup]
enabled = true
max_file_bytes = 2097152       # larger files are not snapshotted
keep_runs = 50

[task]
max_turns = 30                   # subagent model turns before it is cut off
keep_transcripts = 64            # per-session subagent transcripts retained
max_transcript_bytes = 33554432  # total bytes of retained transcripts per session
max_transcript_file_bytes = 8388608  # one transcript past this is cut and marked

Themes: ~/.zenagent/keybindings.json ({"newline": "escape,enter"}) and --theme mono. MCP servers: ~/.zenagent/mcp.json with {"servers": {"name": {"command": [...]} | {"url": "...", "headers": {...}}}}.

Costs are estimated from the catalog's unit prices (input, output, and cache read/write separately). DeepSeek bills peak/off-peak; the catalog has no time dimension, so peak rates are used — treat usage.cost as a conservative upper bound. Providers with subscription pricing (zai, opencode-go) carry 0.

Sessions are append-only JSONL scoped to the project they were started in: ~/.zenagent/projects/<project-key>/sessions/. /sessions, --resume latest, --list-sessions and zen export latest default to the current project; add all/--all-projects for a cross-project view. Sessions from before project scoping stay in ~/.zenagent/sessions/ as read-only legacy entries and can be moved with zen sessions adopt PATH. --resume continues the most recent one. Destructive-looking operations never rewrite a file: /undo, /fork and /compact all write a new session and leave the old one intact. The file format is exactly the public message model, so it can be inspected with jq or replayed elsewhere.

Architecture

cli/        REPL, headless entry point, rendering, permission gate, /compact   ← harness
agent/      loop (pure function) + Agent (state, events, steering, abort)      ← runtime
providers/  openai-completions (+ zai/deepseek dialects), anthropic-messages   ← client
            shared SSE transport, compat flags, packaged model catalog
types.py    messages, content blocks, usage                     ← vocabulary
events.py   stream events, agent events, abort token

Dependencies point strictly downward: providers knows nothing about agent, agent knows nothing about cli. The kernel (types, events, providers, agent, tools) imports only httpx; prompt_toolkit/rich live in the CLI extra.

Contracts worth knowing

  • StreamFn never raises. Every failure (network, 4xx/5xx, parse error, cancellation) becomes a terminal done/error event. safe_stream enforces this for any stream function, including injected fakes.
  • Cancellation is a protocol state, not an exception. The transport raises Aborted for cooperative cancellation; asyncio.CancelledError is reserved for real task cancellation.
  • Tools cannot crash the loop. Unknown tools, invalid arguments, blocked calls, exceptions, and truncated (stop_reason == "length") calls all become is_error tool results. Truncated calls are never executed and always get a result, so the transcript never carries a dangling tool_call.
  • Every event carries a partial snapshot, so the UI never assembles deltas itself; assembly belongs to the adapter and happens once.
  • Tools declare their approval tier (read / write / exec); the kernel does not enforce it — the harness permission gate does. Plan/--read-only therefore allow web_search/fetch and MCP tools with readOnlyHint: true, while the kernel stays policy-free.
  • Every tool result goes through one budget. Over 40 KB keeps the head and tail with a uniform truncation notice; overlong lines are middle-truncated. Per-tool caps remain the first line of defense, MCP/extension tools get the same safety net.

Tools

read_file (paged: 400 lines per call by default, offset/limit for the next page, line_numbers for inspection), write_file (refuses writing back a truncated read page, preserves the executable bit), edit_file (exactly-one-match replacement, replace_all), list_dir, bash (merged output, 30 KB head+tail truncation, process-group kill on timeout/abort, non-interactive environment, optional cwd), glob (root .gitignore aware, sort=mtime), grep (ripgrep when available, Python fallback), web_search (provider chain), fetch (URL → readable text), wait (background-job results), todo (session-scoped checklist with a TUI HUD and /todo editor), ask (structured questions to the user; TUI dialog / REPL prompt / graceful headless degrade), task (read-only subagent delegation with its own transcript and cost), checkpoint/rewind (the model compacts its own exploration into a report), lsp (diagnostics/symbols/references/definition/rename through pyright or typescript-language-server), and github (read-only gh ops; only registered when gh is installed). Oversized results spill to the session artifacts/ directory. bash supports async background jobs; job results are collected with wait and managed with /jobs. Write tools are fenced to the project root by default (outside/protected writes need confirmation); successful writes are recorded in a project-scoped snapshot log and can be put back with /restore.

Tests

bash scripts/check.sh     # the gate: sync + ruff + pytest (CI runs exactly this)
uv run pytest -q          # fully offline: fake stream fns + replayed SSE fixtures
./scripts/eval.sh         # behavioral evals against a real model (opt-in, costs money)

Behavioral evals (real model, small cost) check the agent rather than the code; they are separate from the offline gate:

./scripts/eval.sh --list                      # case ids
./scripts/eval.sh --model deepseek-flash --repeats 3

scripts/capture_sse.py records real provider traffic into zenagent/providers/fixtures/ so the parser can be checked against the wire; the replay tests skip when a capture is absent.

Releasing

zenagent-py ships from a tag. Everything before the tag runs offline:

bash scripts/check.sh            # lint + tests
bash scripts/release-check.sh    # build sdist/wheel, install both ways, run the CLI
git tag vX.Y.Z
git push origin main vX.Y.Z      # the publish workflow re-runs the gate, then uploads

release-check.sh is the packaging gate: it asserts the version is consistent (pyproject.toml, __version__, and the tag on HEAD), that the sdist and wheel carry no dev or stray files, that a kernel-only install works (pip install zenagent-py), and that a [cli] install starts (zen --version, TUI imports, search providers, a settings round-trip in an isolated $HOME). It tests the requires-python floor by default; pass --python 3.13 for another interpreter, or --skip-build to verify the existing dist/. It never uploads and writes only under a temp dir.

.github/workflows/publish.yml runs the same gate before uploading. A manual run is always a dry run — it builds and verifies but cannot publish; only a v* tag push reaches PyPI:

gh workflow run publish.yml     # optional dry run on CI (build + verify only)

Status

v1 (milestones 1–4), S1–S3 and the Textual TUI are complete; the offline suite and recorded live captures are the gate (docs/testing.md). Implemented: interactive REPL + TUI, MCP, skills/templates/extensions, RPC/SDK, export/usage/telemetry, four protocol adapters, runtime modes (plan/ask/full), session display names + rename, and TUI session management (new/fork/delete/export/rename), hand-slip guardrails (workspace fence, protected paths, destructive-command confirmations, write snapshots + /restore). The TUI sidebar (ctrl+b) is a drag-resizable work panel: the full todo checklist, running subagents and background jobs (click one for its log), plus a context-composition section with a per-slice breakdown overlay. A subagent row opens its read-only transcript, which survives --resume. The P1/P2 completion slice (see docs/superpowers/plans/2026-11-01-p1-p2-completion.md) added the tool registry, LSP/GitHub/checkpoint-rewind tools, /plan-review, /hotkeys, /usage, /mcp add, the /todo editor, read-file grouping, clipboard/editor keys, and the command batch listed above. Not yet: OS-level sandboxing. The fence and snapshots protect against agent mistakes, not against malicious repositories or prompt injection (threat model #2).

Metadata

Release files for zenagent-py 0.7.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zenagent-py 0.7.1
File Size Uploaded Provenance Actions
zenagent_py-0.7.1.tar.gz 373.9 kB Without provenance Details for zenagent_py-0.7.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for zenagent-py 0.7.1
File Interpreter ABI Platform Actions
zenagent_py-0.7.1-py3-none-any.whl Python 3 none any Details for zenagent_py-0.7.1-py3-none-any.whl

Total release size: 825.6 kB

Release files / zenagent_py-0.7.1.tar.gz

Download URL zenagent_py-0.7.1.tar.gz
Size 373.9 kB
Tags Source
SHA-256 checksum
How to use checksums
aab10fbed1c7afae333c4e68726386c1eeb5ae071bebb2db798be37af5430995
BLAKE2b-256 checksum
How to use checksums
af731c6d0a9672d85b4e6963b9a909b2434720ad58429fc1bc920f58eb89daf5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / zenagent_py-0.7.1-py3-none-any.whl

Download URL zenagent_py-0.7.1-py3-none-any.whl
Size 451.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bd99cbaa30157ff418f2d85ea85988238c9d40bd3d975b0ece9e2917805b52f8
BLAKE2b-256 checksum
How to use checksums
6098d9155a2e7d98c41cbb60e1e5cee9db69e5837dc63a7d288a02f191fa885b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for zenagent-py 0.7.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.7.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page