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.
Files changed (75) hide show
  1. zenagent_py-0.1.0/PKG-INFO +253 -0
  2. zenagent_py-0.1.0/README.md +230 -0
  3. zenagent_py-0.1.0/pyproject.toml +61 -0
  4. zenagent_py-0.1.0/pyproject.toml.orig +55 -0
  5. zenagent_py-0.1.0/zenagent/__init__.py +1 -0
  6. zenagent_py-0.1.0/zenagent/agent/__init__.py +24 -0
  7. zenagent_py-0.1.0/zenagent/agent/agent.py +264 -0
  8. zenagent_py-0.1.0/zenagent/agent/loop.py +534 -0
  9. zenagent_py-0.1.0/zenagent/agent/session.py +185 -0
  10. zenagent_py-0.1.0/zenagent/agent/validate.py +85 -0
  11. zenagent_py-0.1.0/zenagent/catalog.yaml +119 -0
  12. zenagent_py-0.1.0/zenagent/cli/__init__.py +4 -0
  13. zenagent_py-0.1.0/zenagent/cli/compact.py +165 -0
  14. zenagent_py-0.1.0/zenagent/cli/completion.py +116 -0
  15. zenagent_py-0.1.0/zenagent/cli/doctor.py +97 -0
  16. zenagent_py-0.1.0/zenagent/cli/exportcmd.py +37 -0
  17. zenagent_py-0.1.0/zenagent/cli/images.py +61 -0
  18. zenagent_py-0.1.0/zenagent/cli/keybindings.py +87 -0
  19. zenagent_py-0.1.0/zenagent/cli/main.py +666 -0
  20. zenagent_py-0.1.0/zenagent/cli/permissions.py +183 -0
  21. zenagent_py-0.1.0/zenagent/cli/render.py +225 -0
  22. zenagent_py-0.1.0/zenagent/cli/repl.py +777 -0
  23. zenagent_py-0.1.0/zenagent/cli/rpc.py +224 -0
  24. zenagent_py-0.1.0/zenagent/cli/theme.py +97 -0
  25. zenagent_py-0.1.0/zenagent/cli/usagecmd.py +24 -0
  26. zenagent_py-0.1.0/zenagent/config.py +309 -0
  27. zenagent_py-0.1.0/zenagent/context_files.py +88 -0
  28. zenagent_py-0.1.0/zenagent/crash.py +96 -0
  29. zenagent_py-0.1.0/zenagent/evals_runner.py +83 -0
  30. zenagent_py-0.1.0/zenagent/events.py +107 -0
  31. zenagent_py-0.1.0/zenagent/export.py +180 -0
  32. zenagent_py-0.1.0/zenagent/extensions.py +130 -0
  33. zenagent_py-0.1.0/zenagent/frontmatter.py +39 -0
  34. zenagent_py-0.1.0/zenagent/mcp/__init__.py +29 -0
  35. zenagent_py-0.1.0/zenagent/mcp/client.py +421 -0
  36. zenagent_py-0.1.0/zenagent/mcp/config.py +102 -0
  37. zenagent_py-0.1.0/zenagent/mcp/manager.py +223 -0
  38. zenagent_py-0.1.0/zenagent/providers/__init__.py +2 -0
  39. zenagent_py-0.1.0/zenagent/providers/anthropic_messages.py +548 -0
  40. zenagent_py-0.1.0/zenagent/providers/catalog.py +400 -0
  41. zenagent_py-0.1.0/zenagent/providers/compat.py +88 -0
  42. zenagent_py-0.1.0/zenagent/providers/fixtures/anthropic-badjson.txt +17 -0
  43. zenagent_py-0.1.0/zenagent/providers/fixtures/anthropic-text.txt +20 -0
  44. zenagent_py-0.1.0/zenagent/providers/fixtures/anthropic-thinking-toolcall.txt +32 -0
  45. zenagent_py-0.1.0/zenagent/providers/fixtures/badjson-toolcall.txt +10 -0
  46. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-text.request.json +19 -0
  47. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-text.txt +58 -0
  48. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-toolcall.request.json +39 -0
  49. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-chat-live-toolcall.txt +22 -0
  50. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-reasoner-live-thinking.request.json +22 -0
  51. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-reasoner-live-thinking.txt +360 -0
  52. zenagent_py-0.1.0/zenagent/providers/fixtures/deepseek-reasoner.txt +12 -0
  53. zenagent_py-0.1.0/zenagent/providers/fixtures/google-text.txt +4 -0
  54. zenagent_py-0.1.0/zenagent/providers/fixtures/google-toolcall.txt +2 -0
  55. zenagent_py-0.1.0/zenagent/providers/fixtures/multi-tool-parallel.txt +12 -0
  56. zenagent_py-0.1.0/zenagent/providers/fixtures/openai-responses-text.txt +12 -0
  57. zenagent_py-0.1.0/zenagent/providers/fixtures/openai-responses-toolcall.txt +18 -0
  58. zenagent_py-0.1.0/zenagent/providers/fixtures/zai-glm-5.3-text+thinking.txt +14 -0
  59. zenagent_py-0.1.0/zenagent/providers/fixtures/zai-glm-5.3-toolcall.txt +12 -0
  60. zenagent_py-0.1.0/zenagent/providers/google_generative_ai.py +244 -0
  61. zenagent_py-0.1.0/zenagent/providers/openai_completions.py +634 -0
  62. zenagent_py-0.1.0/zenagent/providers/openai_responses.py +288 -0
  63. zenagent_py-0.1.0/zenagent/providers/registry.py +148 -0
  64. zenagent_py-0.1.0/zenagent/providers/transport.py +257 -0
  65. zenagent_py-0.1.0/zenagent/retry.py +112 -0
  66. zenagent_py-0.1.0/zenagent/skills.py +170 -0
  67. zenagent_py-0.1.0/zenagent/telemetry.py +89 -0
  68. zenagent_py-0.1.0/zenagent/templates.py +95 -0
  69. zenagent_py-0.1.0/zenagent/tools/__init__.py +22 -0
  70. zenagent_py-0.1.0/zenagent/tools/bash.py +225 -0
  71. zenagent_py-0.1.0/zenagent/tools/fs.py +280 -0
  72. zenagent_py-0.1.0/zenagent/tools/search.py +200 -0
  73. zenagent_py-0.1.0/zenagent/trace.py +94 -0
  74. zenagent_py-0.1.0/zenagent/types.py +346 -0
  75. 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
+ ]