zenagent-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zenagent_py-0.1.0/PKG-INFO +253 -0
- zenagent_py-0.1.0/README.md +230 -0
- zenagent_py-0.1.0/pyproject.toml +61 -0
- zenagent_py-0.1.0/pyproject.toml.orig +55 -0
- zenagent_py-0.1.0/zenagent/__init__.py +1 -0
- zenagent_py-0.1.0/zenagent/agent/__init__.py +24 -0
- zenagent_py-0.1.0/zenagent/agent/agent.py +264 -0
- zenagent_py-0.1.0/zenagent/agent/loop.py +534 -0
- zenagent_py-0.1.0/zenagent/agent/session.py +185 -0
- zenagent_py-0.1.0/zenagent/agent/validate.py +85 -0
- zenagent_py-0.1.0/zenagent/catalog.yaml +119 -0
- zenagent_py-0.1.0/zenagent/cli/__init__.py +4 -0
- zenagent_py-0.1.0/zenagent/cli/compact.py +165 -0
- zenagent_py-0.1.0/zenagent/cli/completion.py +116 -0
- zenagent_py-0.1.0/zenagent/cli/doctor.py +97 -0
- zenagent_py-0.1.0/zenagent/cli/exportcmd.py +37 -0
- zenagent_py-0.1.0/zenagent/cli/images.py +61 -0
- zenagent_py-0.1.0/zenagent/cli/keybindings.py +87 -0
- zenagent_py-0.1.0/zenagent/cli/main.py +666 -0
- zenagent_py-0.1.0/zenagent/cli/permissions.py +183 -0
- zenagent_py-0.1.0/zenagent/cli/render.py +225 -0
- zenagent_py-0.1.0/zenagent/cli/repl.py +777 -0
- zenagent_py-0.1.0/zenagent/cli/rpc.py +224 -0
- zenagent_py-0.1.0/zenagent/cli/theme.py +97 -0
- zenagent_py-0.1.0/zenagent/cli/usagecmd.py +24 -0
- zenagent_py-0.1.0/zenagent/config.py +309 -0
- zenagent_py-0.1.0/zenagent/context_files.py +88 -0
- zenagent_py-0.1.0/zenagent/crash.py +96 -0
- zenagent_py-0.1.0/zenagent/evals_runner.py +83 -0
- zenagent_py-0.1.0/zenagent/events.py +107 -0
- zenagent_py-0.1.0/zenagent/export.py +180 -0
- zenagent_py-0.1.0/zenagent/extensions.py +130 -0
- zenagent_py-0.1.0/zenagent/frontmatter.py +39 -0
- zenagent_py-0.1.0/zenagent/mcp/__init__.py +29 -0
- zenagent_py-0.1.0/zenagent/mcp/client.py +421 -0
- zenagent_py-0.1.0/zenagent/mcp/config.py +102 -0
- zenagent_py-0.1.0/zenagent/mcp/manager.py +223 -0
- zenagent_py-0.1.0/zenagent/providers/__init__.py +2 -0
- zenagent_py-0.1.0/zenagent/providers/anthropic_messages.py +548 -0
- zenagent_py-0.1.0/zenagent/providers/catalog.py +400 -0
- zenagent_py-0.1.0/zenagent/providers/compat.py +88 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/anthropic-badjson.txt +17 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/anthropic-text.txt +20 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/anthropic-thinking-toolcall.txt +32 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/badjson-toolcall.txt +10 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-text.request.json +19 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-text.txt +58 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-toolcall.request.json +39 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-toolcall.txt +22 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-reasoner-live-thinking.request.json +22 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-reasoner-live-thinking.txt +360 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-reasoner.txt +12 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/google-text.txt +4 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/google-toolcall.txt +2 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/multi-tool-parallel.txt +12 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/openai-responses-text.txt +12 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/openai-responses-toolcall.txt +18 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/zai-glm-5.3-text+thinking.txt +14 -0
- zenagent_py-0.1.0/zenagent/providers/fixtures/zai-glm-5.3-toolcall.txt +12 -0
- zenagent_py-0.1.0/zenagent/providers/google_generative_ai.py +244 -0
- zenagent_py-0.1.0/zenagent/providers/openai_completions.py +634 -0
- zenagent_py-0.1.0/zenagent/providers/openai_responses.py +288 -0
- zenagent_py-0.1.0/zenagent/providers/registry.py +148 -0
- zenagent_py-0.1.0/zenagent/providers/transport.py +257 -0
- zenagent_py-0.1.0/zenagent/retry.py +112 -0
- zenagent_py-0.1.0/zenagent/skills.py +170 -0
- zenagent_py-0.1.0/zenagent/telemetry.py +89 -0
- zenagent_py-0.1.0/zenagent/templates.py +95 -0
- zenagent_py-0.1.0/zenagent/tools/__init__.py +22 -0
- zenagent_py-0.1.0/zenagent/tools/bash.py +225 -0
- zenagent_py-0.1.0/zenagent/tools/fs.py +280 -0
- zenagent_py-0.1.0/zenagent/tools/search.py +200 -0
- zenagent_py-0.1.0/zenagent/trace.py +94 -0
- zenagent_py-0.1.0/zenagent/types.py +346 -0
- zenagent_py-0.1.0/zenagent/usage.py +142 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: zenagent-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal, Pythonic coding agent: streaming LLM client, async-generator agent loop, and terminal harness.
|
|
5
|
+
Keywords: agent,llm,ai,cli,async,coding-agent
|
|
6
|
+
Author: Justin Gao
|
|
7
|
+
Author-email: Justin Gao <atomyuangao@gmail.com>
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Dist: httpx>=0.28.1
|
|
15
|
+
Requires-Dist: prompt-toolkit>=3.0.53 ; extra == 'cli'
|
|
16
|
+
Requires-Dist: rich>=15.0.0 ; extra == 'cli'
|
|
17
|
+
Requires-Python: >=3.15
|
|
18
|
+
Project-URL: Homepage, https://github.com/encyc/zen-agent
|
|
19
|
+
Project-URL: Repository, https://github.com/encyc/zen-agent
|
|
20
|
+
Project-URL: Changelog, https://github.com/encyc/zen-agent/commits/main
|
|
21
|
+
Provides-Extra: cli
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# ZenAgent
|
|
25
|
+
|
|
26
|
+
A minimal coding agent written from scratch in pure Python: one interface for
|
|
27
|
+
streaming LLM calls, an agent loop that is an async generator, and a terminal
|
|
28
|
+
harness. No vendor SDKs, no agent framework — `httpx` is the only runtime
|
|
29
|
+
dependency of the kernel.
|
|
30
|
+
|
|
31
|
+
Named after the Zen of Python: explicit contracts, flat layers, and one obvious
|
|
32
|
+
way to do things. (Renamed from `pyagent`; there is no compatibility shim.)
|
|
33
|
+
|
|
34
|
+
Built as a learning project and as the small, hackable Python kernel that the
|
|
35
|
+
ecosystem is missing (in the spirit of `pi-ai` + `pi-agent-core`).
|
|
36
|
+
|
|
37
|
+
Repo: <https://github.com/encyc/zen-agent> · PyPI package: `zenagent-py` (imports as `zenagent`).
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv sync --extra cli # kernel + interactive extras (prompt_toolkit, rich)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.15+ (the project targets the current `uv` toolchain; any
|
|
46
|
+
3.11+ should work for the kernel itself).
|
|
47
|
+
|
|
48
|
+
## API keys
|
|
49
|
+
|
|
50
|
+
Read from the environment variable named by each provider in
|
|
51
|
+
`zenagent/catalog.yaml`:
|
|
52
|
+
|
|
53
|
+
| provider | env var |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `zai-coding-cn` | `ZAI_CODING_CN_API_KEY` |
|
|
56
|
+
| `deepseek` | `DEEPSEEK_API_KEY` |
|
|
57
|
+
| `opencode-go` | `OPENCODE_API_KEY` |
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# one-shot, exits 0 on success and 1 when the provider failed
|
|
63
|
+
uv run zen -p "explain what this repo does"
|
|
64
|
+
|
|
65
|
+
# pick a model / reasoning level; allow write + execute tools
|
|
66
|
+
uv run zen -p "create /tmp/x/hello.txt, then read it back" -m deepseek-flash --yes --thinking high
|
|
67
|
+
|
|
68
|
+
# attach images (needs a vision model; deepseek-flash is one)
|
|
69
|
+
uv run zen -p "what is in this picture?" --image shot.png
|
|
70
|
+
|
|
71
|
+
# guardrails and reproducibility
|
|
72
|
+
uv run zen -p "..." --max-turns 20 --max-cost 0.05 --no-context-files
|
|
73
|
+
uv run zen -p "analyze this" --read-only --trace run.jsonl --cwd /path/to/project
|
|
74
|
+
|
|
75
|
+
# structured output (a single JSON object; no streamed text)
|
|
76
|
+
uv run zen -p "say hi" --json
|
|
77
|
+
|
|
78
|
+
# interactive REPL
|
|
79
|
+
uv run zen
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Read-only tools (`read_file`, `list_dir`, `glob`, `grep`) run without asking.
|
|
83
|
+
Write/execute tools (`write_file`, `edit_file`, `bash`) need `--yes`, an
|
|
84
|
+
interactive "yes", or an "always" answer for that tool in the session.
|
|
85
|
+
|
|
86
|
+
### Diagnostics
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run zen doctor # keys, catalog, config, session dir, terminal
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Extending the agent (file-based, no restart needed)
|
|
93
|
+
|
|
94
|
+
| What | Where | How it shows up |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| **Skills** | `.zenagent/skills/<name>/SKILL.md` (project) or `~/.zenagent/skills/` | listed in the prompt; the model loads one with `read_skill` |
|
|
97
|
+
| **Prompt templates** | `.zenagent/commands/<name>.md` | `/name args` expands and sends it |
|
|
98
|
+
| **Extensions** | `extensions = ["path.py"]` in config or `--extension` | registers tools, slash commands, prompt text, event hooks |
|
|
99
|
+
| **MCP servers** | `~/.zenagent/mcp.json` | tools exposed as `mcp__<server>__<tool>` |
|
|
100
|
+
| **Custom providers/models** | `[providers.custom.<id>]` in config.toml | usable with `-m <model>` |
|
|
101
|
+
|
|
102
|
+
Servers that are down at start are retried before each prompt (`/mcp` shows
|
|
103
|
+
state, `/mcp retry` forces it), and `/reload` re-discovers skills, templates and
|
|
104
|
+
extensions mid-session.
|
|
105
|
+
|
|
106
|
+
### Library / SDK
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from zenagent.agent import Agent
|
|
110
|
+
from zenagent.tools import ALL_TOOLS
|
|
111
|
+
|
|
112
|
+
agent = Agent(
|
|
113
|
+
model=model, # a zenagent.providers.catalog.Model
|
|
114
|
+
system_prompt="be brief",
|
|
115
|
+
tools=list(ALL_TOOLS), # or your own AgentTool objects
|
|
116
|
+
api_key="...",
|
|
117
|
+
)
|
|
118
|
+
await agent.prompt("explain this repo") # events via agent.subscribe(fn)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See `examples/` for a headless run, a custom tool with a fake stream (offline),
|
|
122
|
+
a direct MCP call, and a `zen rpc` client.
|
|
123
|
+
|
|
124
|
+
### REPL commands
|
|
125
|
+
|
|
126
|
+
| command | effect |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `/help` | list commands |
|
|
129
|
+
| `/exit` | leave (Ctrl+D also works) |
|
|
130
|
+
| `/clear` | forget the conversation |
|
|
131
|
+
| `/model [id]` | show / switch model (next turn) |
|
|
132
|
+
| `/thinking [level]` | `off` / `low` / `medium` / `high` / `max` |
|
|
133
|
+
| `/compact` | summarize the session into a new session file |
|
|
134
|
+
| `/undo` | drop the last turn (writes a new session file) |
|
|
135
|
+
| `/sessions` | list saved sessions |
|
|
136
|
+
| `/fork <n>` | branch a new session at user message n |
|
|
137
|
+
| `/import <path>` | load a session file and continue it |
|
|
138
|
+
| `/image <path>` | attach an image for the next prompt |
|
|
139
|
+
| `/skills` | list available skills |
|
|
140
|
+
| `/mcp` | MCP server status; `/mcp retry` reconnects |
|
|
141
|
+
| `/reload` | re-discover skills, templates, extensions |
|
|
142
|
+
| `/theme <name>` | `default` / `mono` / `solarized` / a JSON file |
|
|
143
|
+
| `/hotkeys` | show active keybindings |
|
|
144
|
+
|
|
145
|
+
While the agent is working, anything you type is queued as steering input
|
|
146
|
+
rather than starting a second run. Ctrl+C aborts the current run cooperatively.
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
Optional `~/.zenagent/config.toml` (themes, keybindings and MCP live in their own files):
|
|
151
|
+
|
|
152
|
+
```toml
|
|
153
|
+
default_model = "deepseek-flash"
|
|
154
|
+
|
|
155
|
+
[providers.deepseek]
|
|
156
|
+
base_url = "https://mirror.example/v1" # optional
|
|
157
|
+
api_key_env = "MY_DEEPSEEK_KEY" # optional
|
|
158
|
+
|
|
159
|
+
# define a provider from scratch
|
|
160
|
+
[providers.custom.local]
|
|
161
|
+
base_url = "http://127.0.0.1:8080/v1"
|
|
162
|
+
api_key_env = "LOCAL_API_KEY"
|
|
163
|
+
adapter = "openai-completions"
|
|
164
|
+
[[providers.custom.local.models]]
|
|
165
|
+
id = "my-model"
|
|
166
|
+
context_window = 32768
|
|
167
|
+
max_tokens = 4096
|
|
168
|
+
reasoning = false
|
|
169
|
+
|
|
170
|
+
extensions = ["~/work/my-extension.py"]
|
|
171
|
+
|
|
172
|
+
[tools]
|
|
173
|
+
allow = ["read_*", "grep"] # never ask for these
|
|
174
|
+
deny = ["write_file"] # always refuse
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Themes: `~/.zenagent/keybindings.json` (`{"newline": "escape,enter"}`) and
|
|
178
|
+
`--theme mono`. MCP servers: `~/.zenagent/mcp.json` with
|
|
179
|
+
`{"servers": {"name": {"command": [...]} | {"url": "...", "headers": {...}}}}`.
|
|
180
|
+
|
|
181
|
+
Costs are estimated from the catalog's unit prices (input, output, and cache
|
|
182
|
+
read/write separately). DeepSeek bills peak/off-peak; the catalog has no time
|
|
183
|
+
dimension, so peak rates are used — treat `usage.cost` as a conservative upper
|
|
184
|
+
bound. Providers with subscription pricing (zai, opencode-go) carry 0.
|
|
185
|
+
|
|
186
|
+
Sessions are append-only JSONL under `~/.zenagent/sessions/`; `--resume` continues
|
|
187
|
+
the most recent one. Destructive-looking operations never rewrite a file: `/undo`,
|
|
188
|
+
`/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
|
|
189
|
+
can be inspected with `jq` or replayed elsewhere.
|
|
190
|
+
|
|
191
|
+
## Architecture
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
cli/ REPL, headless entry point, rendering, permission gate, /compact ← harness
|
|
195
|
+
agent/ loop (pure function) + Agent (state, events, steering, abort) ← runtime
|
|
196
|
+
providers/ openai-completions (+ zai/deepseek dialects), anthropic-messages ← client
|
|
197
|
+
shared SSE transport, compat flags, packaged model catalog
|
|
198
|
+
types.py messages, content blocks, usage ← vocabulary
|
|
199
|
+
events.py stream events, agent events, abort token
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Dependencies point strictly downward: `providers` knows nothing about `agent`,
|
|
203
|
+
`agent` knows nothing about `cli`. The kernel (`types`, `events`, `providers`,
|
|
204
|
+
`agent`, `tools`) imports only `httpx`; `prompt_toolkit`/`rich` live in the CLI
|
|
205
|
+
extra.
|
|
206
|
+
|
|
207
|
+
### Contracts worth knowing
|
|
208
|
+
|
|
209
|
+
- **`StreamFn` never raises.** Every failure (network, 4xx/5xx, parse error,
|
|
210
|
+
cancellation) becomes a terminal `done`/`error` event. `safe_stream` enforces
|
|
211
|
+
this for any stream function, including injected fakes.
|
|
212
|
+
- **Cancellation is a protocol state, not an exception.** The transport raises
|
|
213
|
+
`Aborted` for cooperative cancellation; `asyncio.CancelledError` is reserved
|
|
214
|
+
for real task cancellation.
|
|
215
|
+
- **Tools cannot crash the loop.** Unknown tools, invalid arguments, blocked
|
|
216
|
+
calls, exceptions, and truncated (`stop_reason == "length"`) calls all become
|
|
217
|
+
`is_error` tool results. Truncated calls are never executed and always get a
|
|
218
|
+
result, so the transcript never carries a dangling `tool_call`.
|
|
219
|
+
- **Every event carries a `partial` snapshot**, so the UI never assembles deltas
|
|
220
|
+
itself; assembly belongs to the adapter and happens once.
|
|
221
|
+
|
|
222
|
+
## Tools
|
|
223
|
+
|
|
224
|
+
`read_file`, `write_file`, `edit_file` (exactly-one-match replacement),
|
|
225
|
+
`list_dir`, `bash` (merged output, 30 KB head+tail truncation, process-group
|
|
226
|
+
kill on timeout/abort), `glob`, `grep` (ripgrep when available, Python fallback).
|
|
227
|
+
|
|
228
|
+
## Tests
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
bash scripts/check.sh # the gate: sync + ruff + pytest (CI runs exactly this)
|
|
232
|
+
bash scripts/check.sh # the gate: sync + ruff + pytest (CI runs exactly this)
|
|
233
|
+
uv run pytest -q # fully offline: fake stream fns + replayed SSE fixtures
|
|
234
|
+
./scripts/eval.sh # behavioral evals against a real model (opt-in, costs money)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Behavioral evals (real model, small cost) check the agent rather than the
|
|
238
|
+
code; they are separate from the offline gate:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
./scripts/eval.sh --list # case ids
|
|
242
|
+
./scripts/eval.sh --model deepseek-flash --repeats 3
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`scripts/capture_sse.py` records real provider traffic into
|
|
246
|
+
`zenagent/providers/fixtures/` so the parser can be checked against the wire; the
|
|
247
|
+
replay tests skip when a capture is absent.
|
|
248
|
+
|
|
249
|
+
## Status
|
|
250
|
+
|
|
251
|
+
Milestones 1–2 are complete and verified against a live provider; the
|
|
252
|
+
interactive harness and the second protocol adapter are implemented. Not in
|
|
253
|
+
scope (v1): MCP, subagents, sandboxing, a full-screen TUI, custom providers.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# ZenAgent
|
|
2
|
+
|
|
3
|
+
A minimal coding agent written from scratch in pure Python: one interface for
|
|
4
|
+
streaming LLM calls, an agent loop that is an async generator, and a terminal
|
|
5
|
+
harness. No vendor SDKs, no agent framework — `httpx` is the only runtime
|
|
6
|
+
dependency of the kernel.
|
|
7
|
+
|
|
8
|
+
Named after the Zen of Python: explicit contracts, flat layers, and one obvious
|
|
9
|
+
way to do things. (Renamed from `pyagent`; there is no compatibility shim.)
|
|
10
|
+
|
|
11
|
+
Built as a learning project and as the small, hackable Python kernel that the
|
|
12
|
+
ecosystem is missing (in the spirit of `pi-ai` + `pi-agent-core`).
|
|
13
|
+
|
|
14
|
+
Repo: <https://github.com/encyc/zen-agent> · PyPI package: `zenagent-py` (imports as `zenagent`).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv sync --extra cli # kernel + interactive extras (prompt_toolkit, rich)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Python 3.15+ (the project targets the current `uv` toolchain; any
|
|
23
|
+
3.11+ should work for the kernel itself).
|
|
24
|
+
|
|
25
|
+
## API keys
|
|
26
|
+
|
|
27
|
+
Read from the environment variable named by each provider in
|
|
28
|
+
`zenagent/catalog.yaml`:
|
|
29
|
+
|
|
30
|
+
| provider | env var |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `zai-coding-cn` | `ZAI_CODING_CN_API_KEY` |
|
|
33
|
+
| `deepseek` | `DEEPSEEK_API_KEY` |
|
|
34
|
+
| `opencode-go` | `OPENCODE_API_KEY` |
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# one-shot, exits 0 on success and 1 when the provider failed
|
|
40
|
+
uv run zen -p "explain what this repo does"
|
|
41
|
+
|
|
42
|
+
# pick a model / reasoning level; allow write + execute tools
|
|
43
|
+
uv run zen -p "create /tmp/x/hello.txt, then read it back" -m deepseek-flash --yes --thinking high
|
|
44
|
+
|
|
45
|
+
# attach images (needs a vision model; deepseek-flash is one)
|
|
46
|
+
uv run zen -p "what is in this picture?" --image shot.png
|
|
47
|
+
|
|
48
|
+
# guardrails and reproducibility
|
|
49
|
+
uv run zen -p "..." --max-turns 20 --max-cost 0.05 --no-context-files
|
|
50
|
+
uv run zen -p "analyze this" --read-only --trace run.jsonl --cwd /path/to/project
|
|
51
|
+
|
|
52
|
+
# structured output (a single JSON object; no streamed text)
|
|
53
|
+
uv run zen -p "say hi" --json
|
|
54
|
+
|
|
55
|
+
# interactive REPL
|
|
56
|
+
uv run zen
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Read-only tools (`read_file`, `list_dir`, `glob`, `grep`) run without asking.
|
|
60
|
+
Write/execute tools (`write_file`, `edit_file`, `bash`) need `--yes`, an
|
|
61
|
+
interactive "yes", or an "always" answer for that tool in the session.
|
|
62
|
+
|
|
63
|
+
### Diagnostics
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run zen doctor # keys, catalog, config, session dir, terminal
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Extending the agent (file-based, no restart needed)
|
|
70
|
+
|
|
71
|
+
| What | Where | How it shows up |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| **Skills** | `.zenagent/skills/<name>/SKILL.md` (project) or `~/.zenagent/skills/` | listed in the prompt; the model loads one with `read_skill` |
|
|
74
|
+
| **Prompt templates** | `.zenagent/commands/<name>.md` | `/name args` expands and sends it |
|
|
75
|
+
| **Extensions** | `extensions = ["path.py"]` in config or `--extension` | registers tools, slash commands, prompt text, event hooks |
|
|
76
|
+
| **MCP servers** | `~/.zenagent/mcp.json` | tools exposed as `mcp__<server>__<tool>` |
|
|
77
|
+
| **Custom providers/models** | `[providers.custom.<id>]` in config.toml | usable with `-m <model>` |
|
|
78
|
+
|
|
79
|
+
Servers that are down at start are retried before each prompt (`/mcp` shows
|
|
80
|
+
state, `/mcp retry` forces it), and `/reload` re-discovers skills, templates and
|
|
81
|
+
extensions mid-session.
|
|
82
|
+
|
|
83
|
+
### Library / SDK
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from zenagent.agent import Agent
|
|
87
|
+
from zenagent.tools import ALL_TOOLS
|
|
88
|
+
|
|
89
|
+
agent = Agent(
|
|
90
|
+
model=model, # a zenagent.providers.catalog.Model
|
|
91
|
+
system_prompt="be brief",
|
|
92
|
+
tools=list(ALL_TOOLS), # or your own AgentTool objects
|
|
93
|
+
api_key="...",
|
|
94
|
+
)
|
|
95
|
+
await agent.prompt("explain this repo") # events via agent.subscribe(fn)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
See `examples/` for a headless run, a custom tool with a fake stream (offline),
|
|
99
|
+
a direct MCP call, and a `zen rpc` client.
|
|
100
|
+
|
|
101
|
+
### REPL commands
|
|
102
|
+
|
|
103
|
+
| command | effect |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `/help` | list commands |
|
|
106
|
+
| `/exit` | leave (Ctrl+D also works) |
|
|
107
|
+
| `/clear` | forget the conversation |
|
|
108
|
+
| `/model [id]` | show / switch model (next turn) |
|
|
109
|
+
| `/thinking [level]` | `off` / `low` / `medium` / `high` / `max` |
|
|
110
|
+
| `/compact` | summarize the session into a new session file |
|
|
111
|
+
| `/undo` | drop the last turn (writes a new session file) |
|
|
112
|
+
| `/sessions` | list saved sessions |
|
|
113
|
+
| `/fork <n>` | branch a new session at user message n |
|
|
114
|
+
| `/import <path>` | load a session file and continue it |
|
|
115
|
+
| `/image <path>` | attach an image for the next prompt |
|
|
116
|
+
| `/skills` | list available skills |
|
|
117
|
+
| `/mcp` | MCP server status; `/mcp retry` reconnects |
|
|
118
|
+
| `/reload` | re-discover skills, templates, extensions |
|
|
119
|
+
| `/theme <name>` | `default` / `mono` / `solarized` / a JSON file |
|
|
120
|
+
| `/hotkeys` | show active keybindings |
|
|
121
|
+
|
|
122
|
+
While the agent is working, anything you type is queued as steering input
|
|
123
|
+
rather than starting a second run. Ctrl+C aborts the current run cooperatively.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
Optional `~/.zenagent/config.toml` (themes, keybindings and MCP live in their own files):
|
|
128
|
+
|
|
129
|
+
```toml
|
|
130
|
+
default_model = "deepseek-flash"
|
|
131
|
+
|
|
132
|
+
[providers.deepseek]
|
|
133
|
+
base_url = "https://mirror.example/v1" # optional
|
|
134
|
+
api_key_env = "MY_DEEPSEEK_KEY" # optional
|
|
135
|
+
|
|
136
|
+
# define a provider from scratch
|
|
137
|
+
[providers.custom.local]
|
|
138
|
+
base_url = "http://127.0.0.1:8080/v1"
|
|
139
|
+
api_key_env = "LOCAL_API_KEY"
|
|
140
|
+
adapter = "openai-completions"
|
|
141
|
+
[[providers.custom.local.models]]
|
|
142
|
+
id = "my-model"
|
|
143
|
+
context_window = 32768
|
|
144
|
+
max_tokens = 4096
|
|
145
|
+
reasoning = false
|
|
146
|
+
|
|
147
|
+
extensions = ["~/work/my-extension.py"]
|
|
148
|
+
|
|
149
|
+
[tools]
|
|
150
|
+
allow = ["read_*", "grep"] # never ask for these
|
|
151
|
+
deny = ["write_file"] # always refuse
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Themes: `~/.zenagent/keybindings.json` (`{"newline": "escape,enter"}`) and
|
|
155
|
+
`--theme mono`. MCP servers: `~/.zenagent/mcp.json` with
|
|
156
|
+
`{"servers": {"name": {"command": [...]} | {"url": "...", "headers": {...}}}}`.
|
|
157
|
+
|
|
158
|
+
Costs are estimated from the catalog's unit prices (input, output, and cache
|
|
159
|
+
read/write separately). DeepSeek bills peak/off-peak; the catalog has no time
|
|
160
|
+
dimension, so peak rates are used — treat `usage.cost` as a conservative upper
|
|
161
|
+
bound. Providers with subscription pricing (zai, opencode-go) carry 0.
|
|
162
|
+
|
|
163
|
+
Sessions are append-only JSONL under `~/.zenagent/sessions/`; `--resume` continues
|
|
164
|
+
the most recent one. Destructive-looking operations never rewrite a file: `/undo`,
|
|
165
|
+
`/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
|
|
166
|
+
can be inspected with `jq` or replayed elsewhere.
|
|
167
|
+
|
|
168
|
+
## Architecture
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
cli/ REPL, headless entry point, rendering, permission gate, /compact ← harness
|
|
172
|
+
agent/ loop (pure function) + Agent (state, events, steering, abort) ← runtime
|
|
173
|
+
providers/ openai-completions (+ zai/deepseek dialects), anthropic-messages ← client
|
|
174
|
+
shared SSE transport, compat flags, packaged model catalog
|
|
175
|
+
types.py messages, content blocks, usage ← vocabulary
|
|
176
|
+
events.py stream events, agent events, abort token
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Dependencies point strictly downward: `providers` knows nothing about `agent`,
|
|
180
|
+
`agent` knows nothing about `cli`. The kernel (`types`, `events`, `providers`,
|
|
181
|
+
`agent`, `tools`) imports only `httpx`; `prompt_toolkit`/`rich` live in the CLI
|
|
182
|
+
extra.
|
|
183
|
+
|
|
184
|
+
### Contracts worth knowing
|
|
185
|
+
|
|
186
|
+
- **`StreamFn` never raises.** Every failure (network, 4xx/5xx, parse error,
|
|
187
|
+
cancellation) becomes a terminal `done`/`error` event. `safe_stream` enforces
|
|
188
|
+
this for any stream function, including injected fakes.
|
|
189
|
+
- **Cancellation is a protocol state, not an exception.** The transport raises
|
|
190
|
+
`Aborted` for cooperative cancellation; `asyncio.CancelledError` is reserved
|
|
191
|
+
for real task cancellation.
|
|
192
|
+
- **Tools cannot crash the loop.** Unknown tools, invalid arguments, blocked
|
|
193
|
+
calls, exceptions, and truncated (`stop_reason == "length"`) calls all become
|
|
194
|
+
`is_error` tool results. Truncated calls are never executed and always get a
|
|
195
|
+
result, so the transcript never carries a dangling `tool_call`.
|
|
196
|
+
- **Every event carries a `partial` snapshot**, so the UI never assembles deltas
|
|
197
|
+
itself; assembly belongs to the adapter and happens once.
|
|
198
|
+
|
|
199
|
+
## Tools
|
|
200
|
+
|
|
201
|
+
`read_file`, `write_file`, `edit_file` (exactly-one-match replacement),
|
|
202
|
+
`list_dir`, `bash` (merged output, 30 KB head+tail truncation, process-group
|
|
203
|
+
kill on timeout/abort), `glob`, `grep` (ripgrep when available, Python fallback).
|
|
204
|
+
|
|
205
|
+
## Tests
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bash scripts/check.sh # the gate: sync + ruff + pytest (CI runs exactly this)
|
|
209
|
+
bash scripts/check.sh # the gate: sync + ruff + pytest (CI runs exactly this)
|
|
210
|
+
uv run pytest -q # fully offline: fake stream fns + replayed SSE fixtures
|
|
211
|
+
./scripts/eval.sh # behavioral evals against a real model (opt-in, costs money)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Behavioral evals (real model, small cost) check the agent rather than the
|
|
215
|
+
code; they are separate from the offline gate:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
./scripts/eval.sh --list # case ids
|
|
219
|
+
./scripts/eval.sh --model deepseek-flash --repeats 3
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`scripts/capture_sse.py` records real provider traffic into
|
|
223
|
+
`zenagent/providers/fixtures/` so the parser can be checked against the wire; the
|
|
224
|
+
replay tests skip when a capture is absent.
|
|
225
|
+
|
|
226
|
+
## Status
|
|
227
|
+
|
|
228
|
+
Milestones 1–2 are complete and verified against a live provider; the
|
|
229
|
+
interactive harness and the second protocol adapter are implemented. Not in
|
|
230
|
+
scope (v1): MCP, subagents, sandboxing, a full-screen TUI, custom providers.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zenagent-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A minimal, Pythonic coding agent: streaming LLM client, async-generator agent loop, and terminal harness."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
keywords = [
|
|
7
|
+
"agent",
|
|
8
|
+
"llm",
|
|
9
|
+
"ai",
|
|
10
|
+
"cli",
|
|
11
|
+
"async",
|
|
12
|
+
"coding-agent",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.15"
|
|
23
|
+
dependencies = ["httpx>=0.28.1"]
|
|
24
|
+
|
|
25
|
+
[[project.authors]]
|
|
26
|
+
name = "Justin Gao"
|
|
27
|
+
email = "atomyuangao@gmail.com"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
zen = "zenagent.cli.main:main"
|
|
31
|
+
zenagent = "zenagent.cli.main:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
cli = [
|
|
35
|
+
"prompt-toolkit>=3.0.53",
|
|
36
|
+
"rich>=15.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/encyc/zen-agent"
|
|
41
|
+
Repository = "https://github.com/encyc/zen-agent"
|
|
42
|
+
Changelog = "https://github.com/encyc/zen-agent/commits/main"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
[tool.uv.build-backend]
|
|
49
|
+
module-root = ""
|
|
50
|
+
module-name = "zenagent"
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
55
|
+
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
dev = [
|
|
58
|
+
"pytest>=9.2.0",
|
|
59
|
+
"pytest-asyncio>=1.4.1",
|
|
60
|
+
"ruff>=0.17.3",
|
|
61
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zenagent-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A minimal, Pythonic coding agent: streaming LLM client, async-generator agent loop, and terminal harness."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
keywords = ["agent", "llm", "ai", "cli", "async", "coding-agent"]
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Intended Audience :: Developers",
|
|
9
|
+
"Environment :: Console",
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
12
|
+
"Topic :: Software Development :: Libraries",
|
|
13
|
+
"Typing :: Typed",
|
|
14
|
+
]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Justin Gao", email = "atomyuangao@gmail.com" }
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.15"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.28.1",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
zen = "zenagent.cli.main:main"
|
|
25
|
+
zenagent = "zenagent.cli.main:main"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
cli = [
|
|
29
|
+
"prompt-toolkit>=3.0.53",
|
|
30
|
+
"rich>=15.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/encyc/zen-agent"
|
|
35
|
+
Repository = "https://github.com/encyc/zen-agent"
|
|
36
|
+
Changelog = "https://github.com/encyc/zen-agent/commits/main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
[tool.uv.build-backend]
|
|
43
|
+
module-root = ""
|
|
44
|
+
module-name = "zenagent"
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=9.2.0",
|
|
49
|
+
"pytest-asyncio>=1.4.1",
|
|
50
|
+
"ruff>=0.17.3",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Agent package: the loop, its config/state/tool surface, and the Agent shell.
|
|
2
|
+
|
|
3
|
+
``zenagent.agent.loop`` is the pure core; ``zenagent.agent.agent.Agent`` is the
|
|
4
|
+
stateful wrapper harnesses drive. Tool packages (later tasks) import
|
|
5
|
+
``AgentTool``/``ToolResultAgent`` from here to build instances.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from zenagent.agent.agent import Agent
|
|
9
|
+
from zenagent.agent.loop import (
|
|
10
|
+
AgentConfig,
|
|
11
|
+
AgentState,
|
|
12
|
+
AgentTool,
|
|
13
|
+
ToolResultAgent,
|
|
14
|
+
agent_loop,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Agent",
|
|
19
|
+
"AgentConfig",
|
|
20
|
+
"AgentState",
|
|
21
|
+
"AgentTool",
|
|
22
|
+
"ToolResultAgent",
|
|
23
|
+
"agent_loop",
|
|
24
|
+
]
|