ollama-coding-agent 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.
- ollama_coding_agent-0.1.0/LICENSE +21 -0
- ollama_coding_agent-0.1.0/PKG-INFO +222 -0
- ollama_coding_agent-0.1.0/README.md +204 -0
- ollama_coding_agent-0.1.0/coding_agent/__init__.py +4 -0
- ollama_coding_agent-0.1.0/coding_agent/__main__.py +6 -0
- ollama_coding_agent-0.1.0/coding_agent/agent.py +316 -0
- ollama_coding_agent-0.1.0/coding_agent/cli.py +234 -0
- ollama_coding_agent-0.1.0/coding_agent/config.py +40 -0
- ollama_coding_agent-0.1.0/coding_agent/intent.py +138 -0
- ollama_coding_agent-0.1.0/coding_agent/mcp_client.py +88 -0
- ollama_coding_agent-0.1.0/coding_agent/mcp_server.py +109 -0
- ollama_coding_agent-0.1.0/coding_agent/ollama_client.py +73 -0
- ollama_coding_agent-0.1.0/coding_agent/session_store.py +141 -0
- ollama_coding_agent-0.1.0/coding_agent/tools.py +249 -0
- ollama_coding_agent-0.1.0/coding_agent/ui.py +224 -0
- ollama_coding_agent-0.1.0/ollama_coding_agent.egg-info/PKG-INFO +222 -0
- ollama_coding_agent-0.1.0/ollama_coding_agent.egg-info/SOURCES.txt +21 -0
- ollama_coding_agent-0.1.0/ollama_coding_agent.egg-info/dependency_links.txt +1 -0
- ollama_coding_agent-0.1.0/ollama_coding_agent.egg-info/entry_points.txt +2 -0
- ollama_coding_agent-0.1.0/ollama_coding_agent.egg-info/requires.txt +4 -0
- ollama_coding_agent-0.1.0/ollama_coding_agent.egg-info/top_level.txt +1 -0
- ollama_coding_agent-0.1.0/pyproject.toml +29 -0
- ollama_coding_agent-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hanlin Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ollama-coding-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local coding agent driving Qwen Coder (or any Ollama-compatible model) through a sandboxed toolset via MCP
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Environment :: Console
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: httpx>=0.28
|
|
14
|
+
Requires-Dist: rich>=15.0
|
|
15
|
+
Requires-Dist: typer>=0.27
|
|
16
|
+
Requires-Dist: mcp>=1.27
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Local Coding Agent (Ollama + Qwen Coder)
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
```bash
|
|
23
|
+
ollama pull qwen3-coder:30b
|
|
24
|
+
pip install -r requirements.txt
|
|
25
|
+
```
|
|
26
|
+
No `ollama` python package required — the agent talks to an OpenAI-compatible
|
|
27
|
+
chat-completions endpoint (`/api/v1/chat/completions`) directly over HTTP via
|
|
28
|
+
`httpx`. This works against Ollama itself or a gateway in front of it (e.g.
|
|
29
|
+
Open WebUI) — point `--ollama-host` at whichever one you're running.
|
|
30
|
+
|
|
31
|
+
## Run
|
|
32
|
+
```bash
|
|
33
|
+
python cli.py "Add type hints to utils.py, then run the test suite" \
|
|
34
|
+
--project-root ./myrepo
|
|
35
|
+
```
|
|
36
|
+
Add `--auto-approve` to skip confirmation prompts (only in an already-sandboxed
|
|
37
|
+
environment, e.g. a container you're fine getting wiped). Run `python cli.py --help`
|
|
38
|
+
for the full option list — it's a Typer app, so `--help` is auto-generated and
|
|
39
|
+
kept in sync with the code.
|
|
40
|
+
|
|
41
|
+
Omit the task string to drop into an interactive session instead of a
|
|
42
|
+
one-shot run — see [Session management](#session-management) below.
|
|
43
|
+
|
|
44
|
+
## What makes this "production grade" vs. the first draft
|
|
45
|
+
|
|
46
|
+
| Concern | First draft | This version |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Editing existing files | Only full overwrite via `write_file` | `edit_file` does exact unique-match replace + shows a unified diff, `write_file` refuses to clobber existing files |
|
|
49
|
+
| Path safety | None — agent could read/write anywhere | Every path resolved and checked against `project_root`; escapes raise `SandboxError` |
|
|
50
|
+
| Shell safety | Ran anything, unbounded | Denylist for destructive patterns (`rm -rf /`, `sudo`, fork bombs, etc.), timeout, output truncation |
|
|
51
|
+
| Human oversight | None | Write/edit/shell calls pause for approval unless the tool is in `safe_tools` or `auto_approve=True` |
|
|
52
|
+
| Model reliability | Assumed clean tool-call JSON | Retries with backoff on API errors; malformed tool-call args are caught and reported back to the model instead of crashing |
|
|
53
|
+
| Context window | Unbounded growth | Char-budget trimming keeps the running conversation under `context_char_budget` |
|
|
54
|
+
| Observability | `print()` only | Structured log file (`agent_run.log`) recording every model call, tool call, args, and result |
|
|
55
|
+
| Config | Hardcoded constants | `AgentConfig` dataclass — one place to tune model, sandbox root, limits, policy |
|
|
56
|
+
| Sessions | Each run started from a blank conversation | Every message is persisted to SQLite (`session_store.py`); resume by id or name, or run interactively |
|
|
57
|
+
| Codebase search | `grep` piped through a subprocess | Pure-Python `search_files` (regex + glob filter, skips `.git`/`node_modules`/etc.) and a `glob_files` tool for pattern-based file discovery |
|
|
58
|
+
|
|
59
|
+
## Still recommended before real production use
|
|
60
|
+
|
|
61
|
+
1. **Run it in a container**, not on your host. The path sandbox and shell
|
|
62
|
+
denylist reduce risk but are not a substitute for OS-level isolation —
|
|
63
|
+
treat `run_shell` as "can execute arbitrary code" and contain the blast
|
|
64
|
+
radius accordingly (Docker, gVisor, a disposable VM).
|
|
65
|
+
2. **Version control everything.** Require the project root to be a git repo
|
|
66
|
+
and commit before each run, so any agent change is a reviewable diff you
|
|
67
|
+
can revert.
|
|
68
|
+
3. **Rate/step limits per user** if this is exposed to a team, not just you.
|
|
69
|
+
4. **Swap the char-based context trimming for a real tokenizer** if you hit
|
|
70
|
+
context issues in practice — it's a rough approximation.
|
|
71
|
+
5. **Add tests for the tools module** (`tools.py`) in your CI — the sandbox
|
|
72
|
+
check is the one thing you really don't want to regress silently.
|
|
73
|
+
6. Qwen3-Coder's native tool-calling is solid but not perfect at this size —
|
|
74
|
+
watch the log for `BAD ARGS` entries; if they're frequent, consider a
|
|
75
|
+
larger quant or `qwen2.5-coder:32b` (dense, less agentic-tuned but very
|
|
76
|
+
reliable on straightforward edits).
|
|
77
|
+
|
|
78
|
+
## Intent parsing
|
|
79
|
+
|
|
80
|
+
Before the agent takes any action, the raw task string is parsed by the model
|
|
81
|
+
(in strict JSON mode, no tools) into structured intent:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"task_type": "bugfix",
|
|
86
|
+
"summary": "Fix add() which subtracts instead of adding",
|
|
87
|
+
"target_files": ["math_utils.py"],
|
|
88
|
+
"constraints": [],
|
|
89
|
+
"risk_level": "low"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This gets injected into the conversation as a system message (with each
|
|
94
|
+
target file tagged `exists` or `new` in the sandbox), so the model starts
|
|
95
|
+
with grounded structure instead of just the raw sentence. Two things follow
|
|
96
|
+
from this automatically:
|
|
97
|
+
|
|
98
|
+
- **High-risk tasks force approval**, even if you ran with `--auto-approve`.
|
|
99
|
+
Detected via `risk_level: "high"` (deletion, deploys, migrations, etc.).
|
|
100
|
+
- **Malformed or failed parsing degrades gracefully** — after retries, it
|
|
101
|
+
falls back to `task_type: "other"` with `confident=False` logged, and the
|
|
102
|
+
agent still runs on the raw task text rather than blocking.
|
|
103
|
+
|
|
104
|
+
Skip it with `--skip-intent-parsing` if you want lower latency on simple
|
|
105
|
+
tasks, or point it at a smaller/faster model with `--intent-model`.
|
|
106
|
+
|
|
107
|
+
## Session management
|
|
108
|
+
|
|
109
|
+
Every message in the conversation — system, user, assistant, tool results —
|
|
110
|
+
is persisted to a SQLite file (`agent_sessions.db` by default, `--db-path` to
|
|
111
|
+
change it) as the run happens, via `session_store.py`. A session is done the
|
|
112
|
+
moment it's created; nothing extra to opt into.
|
|
113
|
+
|
|
114
|
+
**Resume a previous run:**
|
|
115
|
+
```bash
|
|
116
|
+
python cli.py "Add type hints to utils.py" --session-name utils-typing
|
|
117
|
+
# ...later, in the same or a different terminal...
|
|
118
|
+
python cli.py --resume utils-typing "Also add docstrings"
|
|
119
|
+
```
|
|
120
|
+
`--resume` accepts either the session id it printed at the end of a run, or
|
|
121
|
+
the `--session-name` you gave it. `--session-name` is optional — without it
|
|
122
|
+
you just get an 8-character id. When you resume, the prior conversation is
|
|
123
|
+
printed before the run continues (assistant replies rendered the same
|
|
124
|
+
Markdown-panel way they looked the first time), so it's visibly clear that
|
|
125
|
+
context carried over rather than just trusting it happened in the background.
|
|
126
|
+
|
|
127
|
+
**Browse saved sessions:**
|
|
128
|
+
```bash
|
|
129
|
+
python cli.py --list-sessions
|
|
130
|
+
```
|
|
131
|
+
Shows id, name, status (`running` / `done` / `max_steps` / `error`), last
|
|
132
|
+
updated time, model, and the original task for each session.
|
|
133
|
+
|
|
134
|
+
**Delete a session:**
|
|
135
|
+
```bash
|
|
136
|
+
python cli.py --delete-session utils-typing
|
|
137
|
+
```
|
|
138
|
+
Removes the session and its full message history. Also available as
|
|
139
|
+
`/delete <id-or-name>` from inside interactive mode.
|
|
140
|
+
|
|
141
|
+
**Interactive mode** — omit the task argument entirely to get a REPL instead
|
|
142
|
+
of a one-shot run:
|
|
143
|
+
```bash
|
|
144
|
+
python cli.py --project-root ./myrepo # fresh session, prompts for input
|
|
145
|
+
python cli.py --resume utils-typing # resumes and prompts for input
|
|
146
|
+
```
|
|
147
|
+
Type a task and press enter to run it; the conversation (and the MCP tool
|
|
148
|
+
connection) stays alive between turns, so follow-ups don't pay the cost of
|
|
149
|
+
re-parsing intent or re-spawning the tool server. Special inputs:
|
|
150
|
+
- `/sessions` — list saved sessions without leaving the REPL
|
|
151
|
+
- `/delete <id-or-name>` — delete a saved session without leaving the REPL
|
|
152
|
+
- `/exit` or `/quit` (or Ctrl-D / Ctrl-C) — leave
|
|
153
|
+
|
|
154
|
+
A spinner shows while waiting on the model (initial intent parsing and every
|
|
155
|
+
turn), including a live retry counter if a call fails transiently and gets
|
|
156
|
+
retried — so a slow or cold-loading model doesn't look like it's hung.
|
|
157
|
+
|
|
158
|
+
## Terminal UI
|
|
159
|
+
|
|
160
|
+
`ui.py` renders everything through [rich](https://github.com/Textualize/rich):
|
|
161
|
+
banner + parsed intent as a panel, each step with a colored ✓/✗, `edit_file`
|
|
162
|
+
diffs and `write_file` new-file content syntax-highlighted by extension,
|
|
163
|
+
approval prompts that show the actual diff/command *before* you approve —
|
|
164
|
+
not just the raw args — and the final response rendered as Markdown (headers,
|
|
165
|
+
lists, code blocks) rather than literal text.
|
|
166
|
+
|
|
167
|
+
If `rich` isn't installed, `agent.py` and `cli.py` both detect the missing
|
|
168
|
+
import and fall back to plain `print()` — nothing breaks, it just looks
|
|
169
|
+
like the original CLI.
|
|
170
|
+
|
|
171
|
+
## Tools as an MCP server
|
|
172
|
+
|
|
173
|
+
The sandboxed tools now live behind a real MCP server (`mcp_server.py`),
|
|
174
|
+
not inline in the agent. The agent is an MCP *client* — it spawns the
|
|
175
|
+
server as a subprocess (stdio transport) scoped to `--project-root`, fetches
|
|
176
|
+
the tool list, converts it to Ollama's function-calling schema, and calls
|
|
177
|
+
tools through the MCP session instead of Python function calls directly.
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
cli.py → agent.py → mcp_client.py ⇄ (stdio) ⇄ mcp_server.py → tools.py
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
What this buys you:
|
|
184
|
+
- **Any MCP client can use the same tools** — Claude Desktop, another agent
|
|
185
|
+
framework, a different model entirely — all sharing the identical
|
|
186
|
+
sandbox, denylist, and diff-preview logic in `tools.py`.
|
|
187
|
+
- **The server is independently runnable and testable**:
|
|
188
|
+
```bash
|
|
189
|
+
AGENT_PROJECT_ROOT=/path/to/repo python mcp_server.py
|
|
190
|
+
```
|
|
191
|
+
- Internal tools (`_preview_edit`, `_preview_write`, `_file_exists`) are
|
|
192
|
+
underscore-prefixed and filtered out of what's shown to the LLM in
|
|
193
|
+
`list_llm_tools()` — the agent still calls them directly for approval
|
|
194
|
+
previews and intent validation, the model never sees them.
|
|
195
|
+
|
|
196
|
+
The agent loop is now `async` end-to-end (an MCP session requires it);
|
|
197
|
+
`cli.py` runs it via `asyncio.run()`. Nothing about the approval flow,
|
|
198
|
+
retries, logging, or intent parsing changed — they just go through
|
|
199
|
+
`MCPToolClient` now instead of a local `Tools` instance.
|
|
200
|
+
|
|
201
|
+
## Files
|
|
202
|
+
- `config.py` — all tunables in one dataclass
|
|
203
|
+
- `intent.py` — parses the freeform task into structured intent (task_type, target_files, constraints, risk_level)
|
|
204
|
+
- `tools.py` — sandboxed tool implementations (used by `mcp_server.py`, not called directly by the agent anymore)
|
|
205
|
+
- `mcp_server.py` — MCP server exposing those tools over stdio
|
|
206
|
+
- `mcp_client.py` — async MCP client the agent uses to reach the server
|
|
207
|
+
- `ollama_client.py` — raw `httpx` client for the model's OpenAI-compatible chat-completions endpoint — no `ollama` package dependency
|
|
208
|
+
- `session_store.py` — SQLite persistence for sessions and their full message history (resume/list/interactive mode)
|
|
209
|
+
- `ui.py` — rich terminal rendering (diffs, panels, approval prompts, session tables) — purely presentational
|
|
210
|
+
- `agent.py` — the loop: parse intent, call model, approve, execute via MCP, persist, repeat
|
|
211
|
+
- `cli.py` — command-line entry point (Typer — `python cli.py --help` for auto-generated, always-in-sync docs)
|
|
212
|
+
|
|
213
|
+
Point at a non-default Ollama host with `--ollama-host http://some-host:11434`
|
|
214
|
+
or the `OLLAMA_HOST` env var (checked in that order).
|
|
215
|
+
|
|
216
|
+
If your Ollama endpoint sits behind an authenticated proxy, set the key via
|
|
217
|
+
environment variable rather than the CLI flag — it avoids the token landing
|
|
218
|
+
in your shell history:
|
|
219
|
+
```bash
|
|
220
|
+
export OLLAMA_API_KEY="sk-..."
|
|
221
|
+
python cli.py "task" --project-root ./repo --ollama-host http://your-host:8080
|
|
222
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Local Coding Agent (Ollama + Qwen Coder)
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
```bash
|
|
5
|
+
ollama pull qwen3-coder:30b
|
|
6
|
+
pip install -r requirements.txt
|
|
7
|
+
```
|
|
8
|
+
No `ollama` python package required — the agent talks to an OpenAI-compatible
|
|
9
|
+
chat-completions endpoint (`/api/v1/chat/completions`) directly over HTTP via
|
|
10
|
+
`httpx`. This works against Ollama itself or a gateway in front of it (e.g.
|
|
11
|
+
Open WebUI) — point `--ollama-host` at whichever one you're running.
|
|
12
|
+
|
|
13
|
+
## Run
|
|
14
|
+
```bash
|
|
15
|
+
python cli.py "Add type hints to utils.py, then run the test suite" \
|
|
16
|
+
--project-root ./myrepo
|
|
17
|
+
```
|
|
18
|
+
Add `--auto-approve` to skip confirmation prompts (only in an already-sandboxed
|
|
19
|
+
environment, e.g. a container you're fine getting wiped). Run `python cli.py --help`
|
|
20
|
+
for the full option list — it's a Typer app, so `--help` is auto-generated and
|
|
21
|
+
kept in sync with the code.
|
|
22
|
+
|
|
23
|
+
Omit the task string to drop into an interactive session instead of a
|
|
24
|
+
one-shot run — see [Session management](#session-management) below.
|
|
25
|
+
|
|
26
|
+
## What makes this "production grade" vs. the first draft
|
|
27
|
+
|
|
28
|
+
| Concern | First draft | This version |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Editing existing files | Only full overwrite via `write_file` | `edit_file` does exact unique-match replace + shows a unified diff, `write_file` refuses to clobber existing files |
|
|
31
|
+
| Path safety | None — agent could read/write anywhere | Every path resolved and checked against `project_root`; escapes raise `SandboxError` |
|
|
32
|
+
| Shell safety | Ran anything, unbounded | Denylist for destructive patterns (`rm -rf /`, `sudo`, fork bombs, etc.), timeout, output truncation |
|
|
33
|
+
| Human oversight | None | Write/edit/shell calls pause for approval unless the tool is in `safe_tools` or `auto_approve=True` |
|
|
34
|
+
| Model reliability | Assumed clean tool-call JSON | Retries with backoff on API errors; malformed tool-call args are caught and reported back to the model instead of crashing |
|
|
35
|
+
| Context window | Unbounded growth | Char-budget trimming keeps the running conversation under `context_char_budget` |
|
|
36
|
+
| Observability | `print()` only | Structured log file (`agent_run.log`) recording every model call, tool call, args, and result |
|
|
37
|
+
| Config | Hardcoded constants | `AgentConfig` dataclass — one place to tune model, sandbox root, limits, policy |
|
|
38
|
+
| Sessions | Each run started from a blank conversation | Every message is persisted to SQLite (`session_store.py`); resume by id or name, or run interactively |
|
|
39
|
+
| Codebase search | `grep` piped through a subprocess | Pure-Python `search_files` (regex + glob filter, skips `.git`/`node_modules`/etc.) and a `glob_files` tool for pattern-based file discovery |
|
|
40
|
+
|
|
41
|
+
## Still recommended before real production use
|
|
42
|
+
|
|
43
|
+
1. **Run it in a container**, not on your host. The path sandbox and shell
|
|
44
|
+
denylist reduce risk but are not a substitute for OS-level isolation —
|
|
45
|
+
treat `run_shell` as "can execute arbitrary code" and contain the blast
|
|
46
|
+
radius accordingly (Docker, gVisor, a disposable VM).
|
|
47
|
+
2. **Version control everything.** Require the project root to be a git repo
|
|
48
|
+
and commit before each run, so any agent change is a reviewable diff you
|
|
49
|
+
can revert.
|
|
50
|
+
3. **Rate/step limits per user** if this is exposed to a team, not just you.
|
|
51
|
+
4. **Swap the char-based context trimming for a real tokenizer** if you hit
|
|
52
|
+
context issues in practice — it's a rough approximation.
|
|
53
|
+
5. **Add tests for the tools module** (`tools.py`) in your CI — the sandbox
|
|
54
|
+
check is the one thing you really don't want to regress silently.
|
|
55
|
+
6. Qwen3-Coder's native tool-calling is solid but not perfect at this size —
|
|
56
|
+
watch the log for `BAD ARGS` entries; if they're frequent, consider a
|
|
57
|
+
larger quant or `qwen2.5-coder:32b` (dense, less agentic-tuned but very
|
|
58
|
+
reliable on straightforward edits).
|
|
59
|
+
|
|
60
|
+
## Intent parsing
|
|
61
|
+
|
|
62
|
+
Before the agent takes any action, the raw task string is parsed by the model
|
|
63
|
+
(in strict JSON mode, no tools) into structured intent:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"task_type": "bugfix",
|
|
68
|
+
"summary": "Fix add() which subtracts instead of adding",
|
|
69
|
+
"target_files": ["math_utils.py"],
|
|
70
|
+
"constraints": [],
|
|
71
|
+
"risk_level": "low"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This gets injected into the conversation as a system message (with each
|
|
76
|
+
target file tagged `exists` or `new` in the sandbox), so the model starts
|
|
77
|
+
with grounded structure instead of just the raw sentence. Two things follow
|
|
78
|
+
from this automatically:
|
|
79
|
+
|
|
80
|
+
- **High-risk tasks force approval**, even if you ran with `--auto-approve`.
|
|
81
|
+
Detected via `risk_level: "high"` (deletion, deploys, migrations, etc.).
|
|
82
|
+
- **Malformed or failed parsing degrades gracefully** — after retries, it
|
|
83
|
+
falls back to `task_type: "other"` with `confident=False` logged, and the
|
|
84
|
+
agent still runs on the raw task text rather than blocking.
|
|
85
|
+
|
|
86
|
+
Skip it with `--skip-intent-parsing` if you want lower latency on simple
|
|
87
|
+
tasks, or point it at a smaller/faster model with `--intent-model`.
|
|
88
|
+
|
|
89
|
+
## Session management
|
|
90
|
+
|
|
91
|
+
Every message in the conversation — system, user, assistant, tool results —
|
|
92
|
+
is persisted to a SQLite file (`agent_sessions.db` by default, `--db-path` to
|
|
93
|
+
change it) as the run happens, via `session_store.py`. A session is done the
|
|
94
|
+
moment it's created; nothing extra to opt into.
|
|
95
|
+
|
|
96
|
+
**Resume a previous run:**
|
|
97
|
+
```bash
|
|
98
|
+
python cli.py "Add type hints to utils.py" --session-name utils-typing
|
|
99
|
+
# ...later, in the same or a different terminal...
|
|
100
|
+
python cli.py --resume utils-typing "Also add docstrings"
|
|
101
|
+
```
|
|
102
|
+
`--resume` accepts either the session id it printed at the end of a run, or
|
|
103
|
+
the `--session-name` you gave it. `--session-name` is optional — without it
|
|
104
|
+
you just get an 8-character id. When you resume, the prior conversation is
|
|
105
|
+
printed before the run continues (assistant replies rendered the same
|
|
106
|
+
Markdown-panel way they looked the first time), so it's visibly clear that
|
|
107
|
+
context carried over rather than just trusting it happened in the background.
|
|
108
|
+
|
|
109
|
+
**Browse saved sessions:**
|
|
110
|
+
```bash
|
|
111
|
+
python cli.py --list-sessions
|
|
112
|
+
```
|
|
113
|
+
Shows id, name, status (`running` / `done` / `max_steps` / `error`), last
|
|
114
|
+
updated time, model, and the original task for each session.
|
|
115
|
+
|
|
116
|
+
**Delete a session:**
|
|
117
|
+
```bash
|
|
118
|
+
python cli.py --delete-session utils-typing
|
|
119
|
+
```
|
|
120
|
+
Removes the session and its full message history. Also available as
|
|
121
|
+
`/delete <id-or-name>` from inside interactive mode.
|
|
122
|
+
|
|
123
|
+
**Interactive mode** — omit the task argument entirely to get a REPL instead
|
|
124
|
+
of a one-shot run:
|
|
125
|
+
```bash
|
|
126
|
+
python cli.py --project-root ./myrepo # fresh session, prompts for input
|
|
127
|
+
python cli.py --resume utils-typing # resumes and prompts for input
|
|
128
|
+
```
|
|
129
|
+
Type a task and press enter to run it; the conversation (and the MCP tool
|
|
130
|
+
connection) stays alive between turns, so follow-ups don't pay the cost of
|
|
131
|
+
re-parsing intent or re-spawning the tool server. Special inputs:
|
|
132
|
+
- `/sessions` — list saved sessions without leaving the REPL
|
|
133
|
+
- `/delete <id-or-name>` — delete a saved session without leaving the REPL
|
|
134
|
+
- `/exit` or `/quit` (or Ctrl-D / Ctrl-C) — leave
|
|
135
|
+
|
|
136
|
+
A spinner shows while waiting on the model (initial intent parsing and every
|
|
137
|
+
turn), including a live retry counter if a call fails transiently and gets
|
|
138
|
+
retried — so a slow or cold-loading model doesn't look like it's hung.
|
|
139
|
+
|
|
140
|
+
## Terminal UI
|
|
141
|
+
|
|
142
|
+
`ui.py` renders everything through [rich](https://github.com/Textualize/rich):
|
|
143
|
+
banner + parsed intent as a panel, each step with a colored ✓/✗, `edit_file`
|
|
144
|
+
diffs and `write_file` new-file content syntax-highlighted by extension,
|
|
145
|
+
approval prompts that show the actual diff/command *before* you approve —
|
|
146
|
+
not just the raw args — and the final response rendered as Markdown (headers,
|
|
147
|
+
lists, code blocks) rather than literal text.
|
|
148
|
+
|
|
149
|
+
If `rich` isn't installed, `agent.py` and `cli.py` both detect the missing
|
|
150
|
+
import and fall back to plain `print()` — nothing breaks, it just looks
|
|
151
|
+
like the original CLI.
|
|
152
|
+
|
|
153
|
+
## Tools as an MCP server
|
|
154
|
+
|
|
155
|
+
The sandboxed tools now live behind a real MCP server (`mcp_server.py`),
|
|
156
|
+
not inline in the agent. The agent is an MCP *client* — it spawns the
|
|
157
|
+
server as a subprocess (stdio transport) scoped to `--project-root`, fetches
|
|
158
|
+
the tool list, converts it to Ollama's function-calling schema, and calls
|
|
159
|
+
tools through the MCP session instead of Python function calls directly.
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
cli.py → agent.py → mcp_client.py ⇄ (stdio) ⇄ mcp_server.py → tools.py
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
What this buys you:
|
|
166
|
+
- **Any MCP client can use the same tools** — Claude Desktop, another agent
|
|
167
|
+
framework, a different model entirely — all sharing the identical
|
|
168
|
+
sandbox, denylist, and diff-preview logic in `tools.py`.
|
|
169
|
+
- **The server is independently runnable and testable**:
|
|
170
|
+
```bash
|
|
171
|
+
AGENT_PROJECT_ROOT=/path/to/repo python mcp_server.py
|
|
172
|
+
```
|
|
173
|
+
- Internal tools (`_preview_edit`, `_preview_write`, `_file_exists`) are
|
|
174
|
+
underscore-prefixed and filtered out of what's shown to the LLM in
|
|
175
|
+
`list_llm_tools()` — the agent still calls them directly for approval
|
|
176
|
+
previews and intent validation, the model never sees them.
|
|
177
|
+
|
|
178
|
+
The agent loop is now `async` end-to-end (an MCP session requires it);
|
|
179
|
+
`cli.py` runs it via `asyncio.run()`. Nothing about the approval flow,
|
|
180
|
+
retries, logging, or intent parsing changed — they just go through
|
|
181
|
+
`MCPToolClient` now instead of a local `Tools` instance.
|
|
182
|
+
|
|
183
|
+
## Files
|
|
184
|
+
- `config.py` — all tunables in one dataclass
|
|
185
|
+
- `intent.py` — parses the freeform task into structured intent (task_type, target_files, constraints, risk_level)
|
|
186
|
+
- `tools.py` — sandboxed tool implementations (used by `mcp_server.py`, not called directly by the agent anymore)
|
|
187
|
+
- `mcp_server.py` — MCP server exposing those tools over stdio
|
|
188
|
+
- `mcp_client.py` — async MCP client the agent uses to reach the server
|
|
189
|
+
- `ollama_client.py` — raw `httpx` client for the model's OpenAI-compatible chat-completions endpoint — no `ollama` package dependency
|
|
190
|
+
- `session_store.py` — SQLite persistence for sessions and their full message history (resume/list/interactive mode)
|
|
191
|
+
- `ui.py` — rich terminal rendering (diffs, panels, approval prompts, session tables) — purely presentational
|
|
192
|
+
- `agent.py` — the loop: parse intent, call model, approve, execute via MCP, persist, repeat
|
|
193
|
+
- `cli.py` — command-line entry point (Typer — `python cli.py --help` for auto-generated, always-in-sync docs)
|
|
194
|
+
|
|
195
|
+
Point at a non-default Ollama host with `--ollama-host http://some-host:11434`
|
|
196
|
+
or the `OLLAMA_HOST` env var (checked in that order).
|
|
197
|
+
|
|
198
|
+
If your Ollama endpoint sits behind an authenticated proxy, set the key via
|
|
199
|
+
environment variable rather than the CLI flag — it avoids the token landing
|
|
200
|
+
in your shell history:
|
|
201
|
+
```bash
|
|
202
|
+
export OLLAMA_API_KEY="sk-..."
|
|
203
|
+
python cli.py "task" --project-root ./repo --ollama-host http://your-host:8080
|
|
204
|
+
```
|