agent-mini 0.2.0__tar.gz → 0.3.1__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.
- {agent_mini-0.2.0 → agent_mini-0.3.1}/.gitignore +1 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/PKG-INFO +27 -5
- {agent_mini-0.2.0 → agent_mini-0.3.1}/README.md +26 -4
- agent_mini-0.3.1/docs/TECHNICAL_IMPLEMENTATION.md +496 -0
- agent_mini-0.3.1/evals/README.md +154 -0
- agent_mini-0.3.1/evals/fixtures/buggy_stack/pytest.ini +2 -0
- agent_mini-0.3.1/evals/fixtures/buggy_stack/src/stack.py +22 -0
- agent_mini-0.3.1/evals/fixtures/buggy_stack/tests/test_stack.py +24 -0
- agent_mini-0.3.1/evals/fixtures/py_project/pytest.ini +2 -0
- agent_mini-0.3.1/evals/fixtures/py_project/src/mymath.py +11 -0
- agent_mini-0.3.1/evals/fixtures/py_project/tests/test_mymath.py +16 -0
- agent_mini-0.3.1/evals/fixtures/small_repo/README.md +5 -0
- agent_mini-0.3.1/evals/fixtures/small_repo/src/config.py +2 -0
- agent_mini-0.3.1/evals/fixtures/small_repo/src/server.py +8 -0
- agent_mini-0.3.1/evals/run.py +512 -0
- agent_mini-0.3.1/evals/tasks/codebase_qa.yaml +14 -0
- agent_mini-0.3.1/evals/tasks/error_recovery.yaml +13 -0
- agent_mini-0.3.1/evals/tasks/fix_failing_test.yaml +14 -0
- agent_mini-0.3.1/evals/tasks/list_directory_basic.yaml +14 -0
- agent_mini-0.3.1/evals/tasks/multi_file_edit.yaml +16 -0
- agent_mini-0.3.1/evals/tasks/refactor_rename.yaml +15 -0
- agent_mini-0.3.1/evals/tasks/search_and_summarize.yaml +14 -0
- agent_mini-0.3.1/evals/tasks/shell_pipeline.yaml +14 -0
- agent_mini-0.3.1/evals/tasks/write_file_from_scratch.yaml +15 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/pyproject.toml +1 -1
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/__init__.py +1 -1
- agent_mini-0.3.1/src/agent_mini/agent/context.py +112 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/agent/loop.py +44 -36
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/agent/memory.py +25 -9
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/agent/token_estimator.py +11 -4
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/agent/tools.py +213 -84
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/channels/telegram.py +10 -9
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/cli.py +120 -20
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/providers/local.py +4 -1
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/providers/ollama.py +34 -3
- agent_mini-0.3.1/tests/test_context.py +120 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_loop.py +24 -0
- agent_mini-0.3.1/tests/test_providers.py +86 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_token_estimator.py +4 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_tools.py +86 -6
- {agent_mini-0.2.0 → agent_mini-0.3.1}/uv.lock +1 -1
- agent_mini-0.2.0/src/agent_mini/agent/context.py +0 -48
- agent_mini-0.2.0/tests/test_context.py +0 -66
- {agent_mini-0.2.0 → agent_mini-0.3.1}/.github/workflows/ci.yml +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/.github/workflows/publish.yml +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/CONTRIBUTING.md +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/LICENSE +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/SECURITY.md +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/config.example.json +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/__main__.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/agent/__init__.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/agent/vision.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/bus.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/channels/__init__.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/channels/base.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/config.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/providers/__init__.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/providers/base.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/providers/openai.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/src/agent_mini/sessions.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/__init__.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_config.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_json_repair.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_memory.py +0 -0
- {agent_mini-0.2.0 → agent_mini-0.3.1}/tests/test_phases.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-mini
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Ultra-lightweight personal AI agent — shell, files, web, memory — powered by local or cloud LLMs
|
|
5
5
|
Project-URL: Homepage, https://github.com/mohsinkaleem/agent-mini
|
|
6
6
|
Project-URL: Repository, https://github.com/mohsinkaleem/agent-mini
|
|
@@ -46,8 +46,9 @@ A minimal, local-first AI agent you can actually understand and extend.
|
|
|
46
46
|
- **Local-first** — Ollama as default, OpenAI if you want cloud, or any OpenAI-compatible server
|
|
47
47
|
- **Zero frameworks** — pure `httpx` + `asyncio`, no LangChain, no LiteLLM
|
|
48
48
|
- **Built-in tools** — shell, files, web search, persistent memory
|
|
49
|
+
- **Vision** — drop an image path or URL into any message; works on Ollama, OpenAI, and OpenAI-compatible providers
|
|
49
50
|
- **Extensible** — drop a Python file in `~/.agent-mini/plugins/` and it's a tool
|
|
50
|
-
- **Small-model optimized** — token-aware context pruning, tool
|
|
51
|
+
- **Small-model optimized** — tier-scaled system prompt, token-aware context pruning, tool-call repair, and a [task-eval harness](evals/) to measure whether the tuning actually pays off
|
|
51
52
|
|
|
52
53
|
## Quick Start
|
|
53
54
|
|
|
@@ -133,7 +134,7 @@ Available out of the box — no API keys needed:
|
|
|
133
134
|
| `list_directory` | Browse filesystem |
|
|
134
135
|
| `search_files` | Grep / ripgrep across files |
|
|
135
136
|
| `web_search` | DuckDuckGo search (free, no key) |
|
|
136
|
-
| `web_fetch` | Fetch
|
|
137
|
+
| `web_fetch` | Fetch a public URL as plain text (private/loopback hosts blocked) |
|
|
137
138
|
| `memory_store` | Save to persistent memory |
|
|
138
139
|
| `memory_recall` | Fuzzy search memory (TF-IDF) |
|
|
139
140
|
|
|
@@ -214,7 +215,9 @@ Control tool access:
|
|
|
214
215
|
{ "tools": { "sandboxLevel": "readonly" } }
|
|
215
216
|
```
|
|
216
217
|
|
|
217
|
-
Dangerous shell commands (`rm -rf`, `sudo`, `mkfs`, etc.) are blocked by default.
|
|
218
|
+
Dangerous shell commands (`rm -rf`, `sudo`, `mkfs`, etc.) are blocked by default. Treat the shell blocklist as **friction, not a boundary** — a determined caller can bypass it, so don't run the agent in an untrusted context and expect the blocklist to save you.
|
|
219
|
+
|
|
220
|
+
`web_fetch` is guarded against basic SSRF: requests to `localhost`, loopback (`::1`), RFC-1918 private ranges, and link-local (including the cloud metadata endpoint `169.254.169.254`) are refused, and the final URL is re-checked after every redirect. DNS rebinding can still bypass — use `sandboxLevel: readonly` if you need a stronger guarantee.
|
|
218
221
|
|
|
219
222
|
---
|
|
220
223
|
|
|
@@ -236,11 +239,14 @@ Agent Mini is a **ReAct loop** — the LLM reasons, picks a tool, observes the r
|
|
|
236
239
|
|
|
237
240
|
Key design choices for small/local models:
|
|
238
241
|
|
|
242
|
+
- **Tier-scaled system prompt** — tiny models get a compact rules block and no memory recall; larger tiers get the full descriptive prompt and recent context
|
|
243
|
+
- **Inline tool list** — `<available_tools>` block in the system prompt so small models can see tool names at a glance without inferring from the JSON schema
|
|
239
244
|
- **Token-aware context** — estimates token usage and prunes old tool results when approaching the model's effective context window
|
|
240
|
-
- **Model tier classification** — auto-detects tiny/small/medium/cloud
|
|
245
|
+
- **Model tier classification** — auto-detects tiny/small/medium/cloud (including large open-weight sizes like `:32b`, `:70b`, `8x7b`) and adjusts context budgets, iteration limits, and output caps
|
|
241
246
|
- **Tool call repair** — fixes malformed JSON from small models (trailing commas, single quotes, unquoted keys)
|
|
242
247
|
- **Loop detection** — catches repeated identical tool calls and nudges the LLM to try a different approach
|
|
243
248
|
- **History summarization** — compresses long conversations to stay within context
|
|
249
|
+
- **[Task-eval harness](evals/)** — measures success rate, iterations, tokens, and JSON-repair fire-rate across model tiers so the tuning above is testable, not hand-waved
|
|
244
250
|
|
|
245
251
|
---
|
|
246
252
|
|
|
@@ -284,6 +290,7 @@ Key paths:
|
|
|
284
290
|
| `agent-mini init` | Interactive setup wizard |
|
|
285
291
|
| `agent-mini chat` | Interactive chat |
|
|
286
292
|
| `agent-mini chat -m "..."` | Single message |
|
|
293
|
+
| `agent-mini chat --workspace <dir>` | Override the workspace for a single run (also honours `AGENT_MINI_WORKSPACE`) |
|
|
287
294
|
| `agent-mini gateway` | Start Telegram bot |
|
|
288
295
|
| `agent-mini status` | Show config status |
|
|
289
296
|
|
|
@@ -324,6 +331,21 @@ uv run pytest tests/ -v
|
|
|
324
331
|
uv run ruff check src/ tests/
|
|
325
332
|
```
|
|
326
333
|
|
|
334
|
+
### Task evals
|
|
335
|
+
|
|
336
|
+
The [`evals/`](evals/) directory contains a small, framework-free task-eval
|
|
337
|
+
harness that runs the agent end-to-end against real fixtures (refactor a
|
|
338
|
+
codebase, fix a failing test, answer a codebase question, etc.). Use it to
|
|
339
|
+
measure whether the small-model optimizations actually pay off:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
python evals/run.py # run all tasks against config model
|
|
343
|
+
python evals/run.py --task refactor_rename # single task
|
|
344
|
+
python evals/run.py --compare results/*.json # cross-tier comparison table
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
See [evals/README.md](evals/README.md) for the full workflow.
|
|
348
|
+
|
|
327
349
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
328
350
|
|
|
329
351
|
## License
|
|
@@ -11,8 +11,9 @@ A minimal, local-first AI agent you can actually understand and extend.
|
|
|
11
11
|
- **Local-first** — Ollama as default, OpenAI if you want cloud, or any OpenAI-compatible server
|
|
12
12
|
- **Zero frameworks** — pure `httpx` + `asyncio`, no LangChain, no LiteLLM
|
|
13
13
|
- **Built-in tools** — shell, files, web search, persistent memory
|
|
14
|
+
- **Vision** — drop an image path or URL into any message; works on Ollama, OpenAI, and OpenAI-compatible providers
|
|
14
15
|
- **Extensible** — drop a Python file in `~/.agent-mini/plugins/` and it's a tool
|
|
15
|
-
- **Small-model optimized** — token-aware context pruning, tool
|
|
16
|
+
- **Small-model optimized** — tier-scaled system prompt, token-aware context pruning, tool-call repair, and a [task-eval harness](evals/) to measure whether the tuning actually pays off
|
|
16
17
|
|
|
17
18
|
## Quick Start
|
|
18
19
|
|
|
@@ -98,7 +99,7 @@ Available out of the box — no API keys needed:
|
|
|
98
99
|
| `list_directory` | Browse filesystem |
|
|
99
100
|
| `search_files` | Grep / ripgrep across files |
|
|
100
101
|
| `web_search` | DuckDuckGo search (free, no key) |
|
|
101
|
-
| `web_fetch` | Fetch
|
|
102
|
+
| `web_fetch` | Fetch a public URL as plain text (private/loopback hosts blocked) |
|
|
102
103
|
| `memory_store` | Save to persistent memory |
|
|
103
104
|
| `memory_recall` | Fuzzy search memory (TF-IDF) |
|
|
104
105
|
|
|
@@ -179,7 +180,9 @@ Control tool access:
|
|
|
179
180
|
{ "tools": { "sandboxLevel": "readonly" } }
|
|
180
181
|
```
|
|
181
182
|
|
|
182
|
-
Dangerous shell commands (`rm -rf`, `sudo`, `mkfs`, etc.) are blocked by default.
|
|
183
|
+
Dangerous shell commands (`rm -rf`, `sudo`, `mkfs`, etc.) are blocked by default. Treat the shell blocklist as **friction, not a boundary** — a determined caller can bypass it, so don't run the agent in an untrusted context and expect the blocklist to save you.
|
|
184
|
+
|
|
185
|
+
`web_fetch` is guarded against basic SSRF: requests to `localhost`, loopback (`::1`), RFC-1918 private ranges, and link-local (including the cloud metadata endpoint `169.254.169.254`) are refused, and the final URL is re-checked after every redirect. DNS rebinding can still bypass — use `sandboxLevel: readonly` if you need a stronger guarantee.
|
|
183
186
|
|
|
184
187
|
---
|
|
185
188
|
|
|
@@ -201,11 +204,14 @@ Agent Mini is a **ReAct loop** — the LLM reasons, picks a tool, observes the r
|
|
|
201
204
|
|
|
202
205
|
Key design choices for small/local models:
|
|
203
206
|
|
|
207
|
+
- **Tier-scaled system prompt** — tiny models get a compact rules block and no memory recall; larger tiers get the full descriptive prompt and recent context
|
|
208
|
+
- **Inline tool list** — `<available_tools>` block in the system prompt so small models can see tool names at a glance without inferring from the JSON schema
|
|
204
209
|
- **Token-aware context** — estimates token usage and prunes old tool results when approaching the model's effective context window
|
|
205
|
-
- **Model tier classification** — auto-detects tiny/small/medium/cloud
|
|
210
|
+
- **Model tier classification** — auto-detects tiny/small/medium/cloud (including large open-weight sizes like `:32b`, `:70b`, `8x7b`) and adjusts context budgets, iteration limits, and output caps
|
|
206
211
|
- **Tool call repair** — fixes malformed JSON from small models (trailing commas, single quotes, unquoted keys)
|
|
207
212
|
- **Loop detection** — catches repeated identical tool calls and nudges the LLM to try a different approach
|
|
208
213
|
- **History summarization** — compresses long conversations to stay within context
|
|
214
|
+
- **[Task-eval harness](evals/)** — measures success rate, iterations, tokens, and JSON-repair fire-rate across model tiers so the tuning above is testable, not hand-waved
|
|
209
215
|
|
|
210
216
|
---
|
|
211
217
|
|
|
@@ -249,6 +255,7 @@ Key paths:
|
|
|
249
255
|
| `agent-mini init` | Interactive setup wizard |
|
|
250
256
|
| `agent-mini chat` | Interactive chat |
|
|
251
257
|
| `agent-mini chat -m "..."` | Single message |
|
|
258
|
+
| `agent-mini chat --workspace <dir>` | Override the workspace for a single run (also honours `AGENT_MINI_WORKSPACE`) |
|
|
252
259
|
| `agent-mini gateway` | Start Telegram bot |
|
|
253
260
|
| `agent-mini status` | Show config status |
|
|
254
261
|
|
|
@@ -289,6 +296,21 @@ uv run pytest tests/ -v
|
|
|
289
296
|
uv run ruff check src/ tests/
|
|
290
297
|
```
|
|
291
298
|
|
|
299
|
+
### Task evals
|
|
300
|
+
|
|
301
|
+
The [`evals/`](evals/) directory contains a small, framework-free task-eval
|
|
302
|
+
harness that runs the agent end-to-end against real fixtures (refactor a
|
|
303
|
+
codebase, fix a failing test, answer a codebase question, etc.). Use it to
|
|
304
|
+
measure whether the small-model optimizations actually pay off:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
python evals/run.py # run all tasks against config model
|
|
308
|
+
python evals/run.py --task refactor_rename # single task
|
|
309
|
+
python evals/run.py --compare results/*.json # cross-tier comparison table
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
See [evals/README.md](evals/README.md) for the full workflow.
|
|
313
|
+
|
|
292
314
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
293
315
|
|
|
294
316
|
## License
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# Agent Mini — Technical Implementation
|
|
2
|
+
|
|
3
|
+
A deep dive into how Agent Mini is built. The whole codebase is ~3,000 lines of Python, no frameworks (LangChain, LiteLLM, etc.), just `httpx` + `asyncio` + `click` + `rich`. This document explains the moving parts and the design decisions behind each one.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. High-Level Architecture
|
|
8
|
+
|
|
9
|
+
Agent Mini is a **ReAct-style loop** — the LLM thinks, picks a tool, observes the result, and repeats until it returns a text answer. Around that loop we have:
|
|
10
|
+
|
|
11
|
+
- **Providers** — pluggable LLM backends (Ollama, OpenAI, any OpenAI-compatible server).
|
|
12
|
+
- **Tools** — shell, files, web, memory. Extensible via plugin drop-ins.
|
|
13
|
+
- **Channels** — how the user talks to the agent (CLI REPL, Telegram).
|
|
14
|
+
- **Bus** — routes a `(channel, user_id)` pair to its own conversation history.
|
|
15
|
+
- **Sessions** — JSON-backed conversation persistence, auto-saved every turn.
|
|
16
|
+
- **Config / Memory** — JSON files under `~/.agent-mini/`.
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart LR
|
|
20
|
+
U[User] -->|CLI or Telegram| CH[Channel]
|
|
21
|
+
CH --> BUS[MessageBus]
|
|
22
|
+
BUS --> LOOP[AgentLoop]
|
|
23
|
+
LOOP --> CTX[Context builder<br/>system prompt]
|
|
24
|
+
LOOP --> PROV[Provider<br/>ollama / openai / local]
|
|
25
|
+
LOOP --> TE[ToolExecutor]
|
|
26
|
+
TE --> FS[File system]
|
|
27
|
+
TE --> SH[Shell]
|
|
28
|
+
TE --> WEB[Web]
|
|
29
|
+
TE --> MEM[Memory JSON]
|
|
30
|
+
LOOP --> SES[Sessions JSON]
|
|
31
|
+
PROV -.HTTP.-> LLM((LLM))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Source map:
|
|
35
|
+
|
|
36
|
+
- Loop and prompt: [src/agent_mini/agent/loop.py](../src/agent_mini/agent/loop.py), [src/agent_mini/agent/context.py](../src/agent_mini/agent/context.py)
|
|
37
|
+
- Tools: [src/agent_mini/agent/tools.py](../src/agent_mini/agent/tools.py)
|
|
38
|
+
- Providers: [src/agent_mini/providers/base.py](../src/agent_mini/providers/base.py), [ollama.py](../src/agent_mini/providers/ollama.py), [local.py](../src/agent_mini/providers/local.py), [openai.py](../src/agent_mini/providers/openai.py)
|
|
39
|
+
- Channels: [src/agent_mini/channels/telegram.py](../src/agent_mini/channels/telegram.py)
|
|
40
|
+
- Bus + sessions: [src/agent_mini/bus.py](../src/agent_mini/bus.py), [src/agent_mini/sessions.py](../src/agent_mini/sessions.py)
|
|
41
|
+
- CLI: [src/agent_mini/cli.py](../src/agent_mini/cli.py)
|
|
42
|
+
- Config: [src/agent_mini/config.py](../src/agent_mini/config.py)
|
|
43
|
+
- Memory: [src/agent_mini/agent/memory.py](../src/agent_mini/agent/memory.py)
|
|
44
|
+
- Vision: [src/agent_mini/agent/vision.py](../src/agent_mini/agent/vision.py)
|
|
45
|
+
- Model tiers / token estimator: [src/agent_mini/agent/token_estimator.py](../src/agent_mini/agent/token_estimator.py)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Startup & Wiring
|
|
50
|
+
|
|
51
|
+
The `agent-mini` command is a Click group defined in [src/agent_mini/cli.py](../src/agent_mini/cli.py). Sub-commands:
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `init` | Interactive wizard → writes `~/.agent-mini/config.json` |
|
|
56
|
+
| `chat [-m MSG] [-s SESSION] [--workspace DIR]` | Single-shot or REPL |
|
|
57
|
+
| `gateway` | Starts the Telegram bot |
|
|
58
|
+
| `status` | Prints active provider, channels, sandbox, memory |
|
|
59
|
+
|
|
60
|
+
The chat command wires everything together in `_chat()`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
load_config → create_provider → Memory(...) → AgentLoop(provider, config, memory)
|
|
64
|
+
→ resume/create session → REPL loop → agent.run(...) → save_session(...)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `--workspace` flag (and `AGENT_MINI_WORKSPACE` env var) overrides `config["workspace"]` and forces `restrictToWorkspace: true`. This is what the [evals harness](../evals/README.md) uses to sandbox each task into a temp dir without touching the user's real config.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. The Agent Loop
|
|
72
|
+
|
|
73
|
+
Everything runs through `AgentLoop.run()` in [loop.py](../src/agent_mini/agent/loop.py).
|
|
74
|
+
|
|
75
|
+
### 3.1 Per-turn flow
|
|
76
|
+
|
|
77
|
+
1. Rebuild the system prompt every turn (date, workspace, recent memories, model tier are all live).
|
|
78
|
+
2. Prepend system prompt to the persisted `conversation`, append the new user turn.
|
|
79
|
+
3. If the user message contains image references (paths or URLs), the user turn becomes an OpenAI-style multi-part `content` list — see [vision.py](../src/agent_mini/agent/vision.py).
|
|
80
|
+
4. Enter an iteration budget (default from tier; user override in `config["agent"]["maxIterations"]`).
|
|
81
|
+
|
|
82
|
+
Inside each iteration:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
prune old tool results → call provider (with retry+jitter)
|
|
86
|
+
→ if response has tool_calls: exec in parallel, append tool msgs, continue
|
|
87
|
+
→ else: return text, persist turn, maybe summarize history, done
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Relevant code: [loop.py L92-L235](../src/agent_mini/agent/loop.py#L92-L235).
|
|
91
|
+
|
|
92
|
+
### 3.2 Retry with jitter
|
|
93
|
+
|
|
94
|
+
`_call_provider_with_retry` retries on transient errors (HTTP 429/500/502/503/504, `ConnectError`, `ReadTimeout`, `OSError`) up to 3 times with exponential backoff plus random jitter: `wait = 2**attempt + random.uniform(0, 1)`. Non-transient errors fail through as an error string. See [loop.py L306-L336](../src/agent_mini/agent/loop.py#L306-L336).
|
|
95
|
+
|
|
96
|
+
### 3.3 Parallel tool execution
|
|
97
|
+
|
|
98
|
+
When the LLM returns multiple tool calls in one response, they're dispatched concurrently with `asyncio.gather`, tagged with `tool_call_id`, and appended in order:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
results = await asyncio.gather(*[_exec(tc) for tc in response.tool_calls])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Each result is truncated to the **tier-specific output limit** (tiny 2 KB, small 4 KB, medium 8 KB, cloud 50 KB) with a head+tail split so the model sees both ends. If a tool returned `Error: ...`, the message is postfixed with a self-reflection nudge:
|
|
105
|
+
|
|
106
|
+
> [The tool call failed. Analyze what went wrong and try a different approach.]
|
|
107
|
+
|
|
108
|
+
### 3.4 Loop / repetition detection
|
|
109
|
+
|
|
110
|
+
Every tool call is hashed with MD5 over `{name, args}` and pushed onto a sliding window of the last 6 signatures. If the last **four** are identical, a synthetic `user` message is appended:
|
|
111
|
+
|
|
112
|
+
> You have repeated the same tool call multiple times with the same result. Try a completely different approach.
|
|
113
|
+
|
|
114
|
+
This catches small models that get stuck retrying the same failing shell command. See [loop.py L213-L227](../src/agent_mini/agent/loop.py#L213-L227).
|
|
115
|
+
|
|
116
|
+
### 3.5 Context pruning
|
|
117
|
+
|
|
118
|
+
`_prune_tool_results` is called every iteration on a **copy** of `messages` (never mutates history). It:
|
|
119
|
+
|
|
120
|
+
- protects the last 3 assistant turns and their tool results,
|
|
121
|
+
- soft-trims older large tool messages to head 1500 + tail 1500,
|
|
122
|
+
- hard-clears very old ones with `[Old tool result cleared to save context]`.
|
|
123
|
+
|
|
124
|
+
This keeps the request payload small without losing recent reasoning steps.
|
|
125
|
+
|
|
126
|
+
### 3.6 History summarization
|
|
127
|
+
|
|
128
|
+
After a text-only response, if the persisted conversation exceeds 75% of the model's effective context, `_summarize_history` fires. It:
|
|
129
|
+
|
|
130
|
+
1. Finds the cut point that would bring us back below 50%.
|
|
131
|
+
2. Calls the provider with a tight summarization prompt (`temperature=0.3`, no tools).
|
|
132
|
+
3. Replaces the pruned tail with a single **`role: "user"`** turn containing `[Previous context summary]\n...`.
|
|
133
|
+
|
|
134
|
+
Why `user`, not `system`? The real system prompt is re-prepended fresh every turn in `run()`, so a stored `system` entry would give small models two leading system messages and dilute the constraint-first anchoring they rely on. See [loop.py L268-L304](../src/agent_mini/agent/loop.py#L268-L304).
|
|
135
|
+
|
|
136
|
+
### 3.7 Token / cost tracking
|
|
137
|
+
|
|
138
|
+
Two counters live on the loop: `session_usage` and per-turn `turn_usage`, both `{prompt_tokens, completion_tokens, total_tokens}`. Providers surface `usage` on `ChatResponse` when the backend reports it (Ollama's `prompt_eval_count`/`eval_count`; OpenAI's `usage`).
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 4. Model-Tier-Aware Tuning
|
|
143
|
+
|
|
144
|
+
[token_estimator.py](../src/agent_mini/agent/token_estimator.py) is the small-model spine of the project. Model name → tier → different behavior everywhere.
|
|
145
|
+
|
|
146
|
+
### 4.1 Classification
|
|
147
|
+
|
|
148
|
+
Regex patterns match from most-specific to least. Cloud models (`gpt-4`, `claude`, `gemini`, `deepseek-v2/v3`) win first, then large open-weight sizes (32B, 34B, 40B, 65B, 70B, 72B, 8x7B, 8x22B) also map to `cloud`. The `(?<![\d.])` lookbehind is important — it prevents `1.5b` (a tiny model) matching the `5b` in the small-tier regex.
|
|
149
|
+
|
|
150
|
+
| Tier | Effective context | Max iterations | Tool output cap |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| tiny (1–3B) | 3 000 | 10 | 2 000 chars |
|
|
153
|
+
| small (4–8B) | 6 000 | 15 | 4 000 chars |
|
|
154
|
+
| medium (9–14B) | 12 000 | 20 | 8 000 chars |
|
|
155
|
+
| cloud / large open-weight | 32 000 | 25 | 50 000 chars |
|
|
156
|
+
|
|
157
|
+
### 4.2 What changes per tier
|
|
158
|
+
|
|
159
|
+
- **System prompt** (`context.py`): tiny models get a stripped `_TINY_RULES` block instead of the full `_SYSTEM_PROMPT_TEMPLATE` — every token trades against reasoning budget.
|
|
160
|
+
- **Memory recall in prompt**: tiny=0, small=3, medium=5, cloud=5 recent memories. Tiny models get confused by unrelated context.
|
|
161
|
+
- **Inline tool listing**: an `<available_tools>` block is always emitted so small models can eyeball tool names without reasoning over the JSON schema.
|
|
162
|
+
- **Max iterations, context budget, output cap**: all pulled from the tier tables above unless the user pins a value in config.
|
|
163
|
+
|
|
164
|
+
### 4.3 Token estimation
|
|
165
|
+
|
|
166
|
+
Cheap: `~4 chars per token`. Vision image parts count as ~85 tokens each. Tool-call arguments are counted separately. Precise enough to drive the 75% / 50% summarization thresholds.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 5. System Prompt Construction
|
|
171
|
+
|
|
172
|
+
[context.py](../src/agent_mini/agent/context.py):
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
[tier-scaled preamble + rules]
|
|
176
|
+
[<available_tools> ... </available_tools>]
|
|
177
|
+
[## User instructions <- from config.agent.systemPrompt]
|
|
178
|
+
[## Recent memories <- top-N by tier]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Two anti-injection details worth noting:
|
|
182
|
+
|
|
183
|
+
- The template uses `str.format(date=..., workspace=...)` — user-controlled fields never go through `.format()`. Custom `systemPrompt` and memory values are string-concatenated, so a stray `{}` from the user won't blow up formatting.
|
|
184
|
+
- Memory content is appended raw — treat it as effectively user-provided when auditing.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. Providers
|
|
189
|
+
|
|
190
|
+
### 6.1 The abstraction
|
|
191
|
+
|
|
192
|
+
[providers/base.py](../src/agent_mini/providers/base.py) defines:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
@dataclass
|
|
196
|
+
class ToolCall: id: str; name: str; arguments: dict
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class ChatResponse: content, tool_calls, thinking, usage
|
|
200
|
+
|
|
201
|
+
class BaseProvider(ABC):
|
|
202
|
+
async def chat(messages, tools, temperature) -> ChatResponse
|
|
203
|
+
async def chat_stream(messages, on_delta, tools, temperature, on_thinking) -> ChatResponse
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Providers normalize to `ChatResponse`; the loop is provider-agnostic.
|
|
207
|
+
|
|
208
|
+
### 6.2 JSON repair for tool arguments
|
|
209
|
+
|
|
210
|
+
Small models emit malformed JSON. `parse_arguments` first tries `json.loads`, then `_repair_json` which:
|
|
211
|
+
|
|
212
|
+
1. Strips ``` fences.
|
|
213
|
+
2. Drops trailing commas before `}` / `]`.
|
|
214
|
+
3. Swaps single→double quotes when there are no doubles.
|
|
215
|
+
4. Quotes unquoted keys (`{foo: 1}` → `{"foo": 1}`).
|
|
216
|
+
|
|
217
|
+
Only successful `dict` results are returned; anything else logs a warning and drops the call. See [providers/base.py L37-L88](../src/agent_mini/providers/base.py#L37-L88).
|
|
218
|
+
|
|
219
|
+
### 6.3 Ollama
|
|
220
|
+
|
|
221
|
+
[ollama.py](../src/agent_mini/providers/ollama.py) — POSTs to `/api/chat`. Streams NDJSON.
|
|
222
|
+
|
|
223
|
+
Notable transforms in `_clean_messages`:
|
|
224
|
+
|
|
225
|
+
- `role: "tool"` messages are rewritten to `role: "user"` with a `[Tool result for ...]:` prefix, for compatibility with older Ollama builds that don't fully speak the tool-role protocol.
|
|
226
|
+
- OpenAI-style multi-part vision content (`[{type: "image_url", ...}]`) is flattened to Ollama's `{content, images: ["<base64>"]}` shape, stripping `data:...;base64,` prefixes when present.
|
|
227
|
+
- `think` (`false | true | "low" | "medium" | "high"`) is passed through unchanged; thinking deltas are streamed via a separate `on_thinking` callback.
|
|
228
|
+
|
|
229
|
+
### 6.4 OpenAI-compatible (`LocalProvider`)
|
|
230
|
+
|
|
231
|
+
[local.py](../src/agent_mini/providers/local.py) — plain `/chat/completions` client. Two subtleties:
|
|
232
|
+
|
|
233
|
+
- Streaming tool calls arrive in fragments indexed by `tc["index"]`. We accumulate them in a dict, then sort by that **integer** index — not by `id` (opaque string) which would sort `"call_10"` before `"call_2"` lexicographically.
|
|
234
|
+
- Arguments arrive as string fragments concatenated across chunks; we run the full string through `parse_arguments` (repair-aware) at the end.
|
|
235
|
+
|
|
236
|
+
### 6.5 OpenAI
|
|
237
|
+
|
|
238
|
+
[openai.py](../src/agent_mini/providers/openai.py) is a 20-line subclass of `LocalProvider` with `base_url = https://api.openai.com/v1` and `name = "openai"`. That's the whole thing — OpenAI *is* the reference implementation of its own protocol.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 7. Tools
|
|
243
|
+
|
|
244
|
+
All in [tools.py](../src/agent_mini/agent/tools.py). Tool definitions use the OpenAI function-calling JSON shape:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
{"type": "function", "function": {"name": ..., "description": ..., "parameters": {...}}}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 7.1 Built-in tools
|
|
251
|
+
|
|
252
|
+
Registered in `_CORE_TOOLS` and `_WEB_TOOLS`:
|
|
253
|
+
|
|
254
|
+
| Tool | Notes |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `shell_exec` | Runs via `asyncio.create_subprocess_shell` inside workspace `cwd`, 120 s timeout, output capped at 50 KB. Blocklist regex enforced. |
|
|
257
|
+
| `read_file` / `write_file` / `append_file` / `code_edit` | Text I/O. `code_edit` requires **exact single-match** find-and-replace and returns a specific error for 0 or >1 matches so the model can adjust. `read_file` caps content at 100 KB. |
|
|
258
|
+
| `list_directory` | Sorted, directories first, capped at 200 entries. |
|
|
259
|
+
| `search_files` | Prefers `rg` (ripgrep) with `--hidden --glob '!.git'`; falls back to `grep -rn`. Output capped at 100 KB. `returncode` 0 or 1 both count as success (1 = no matches). |
|
|
260
|
+
| `web_search` | POSTs to `https://html.duckduckgo.com/html/`; parses result blocks with regex; unwraps DuckDuckGo redirect `uddg` param; returns top 8 as Markdown. |
|
|
261
|
+
| `web_fetch` | SSRF-guarded, HTML → plain text via a stdlib `HTMLParser` subclass. |
|
|
262
|
+
| `memory_store` / `memory_recall` | See section 8. |
|
|
263
|
+
|
|
264
|
+
### 7.2 SSRF guard
|
|
265
|
+
|
|
266
|
+
`_check_url_ssrf` enforces:
|
|
267
|
+
|
|
268
|
+
- scheme in `{http, https}`,
|
|
269
|
+
- literal IPs must not be private/loopback/link-local/reserved,
|
|
270
|
+
- host names are resolved via `getaddrinfo` and every returned `A`/`AAAA` record is checked — catches DNS names pointing into RFC-1918.
|
|
271
|
+
|
|
272
|
+
After the request, the **final** URL (post-redirect) is re-checked so an attacker can't 302 into `169.254.169.254`. Documented caveat: DNS rebinding can still bypass — `sandboxLevel: readonly` is the stronger guarantee.
|
|
273
|
+
|
|
274
|
+
### 7.3 Shell command blocklist
|
|
275
|
+
|
|
276
|
+
Default patterns:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
\brm\s+-[^\s]*r[^\s]*f rm -rf variants
|
|
280
|
+
\bsudo\b privilege escalation
|
|
281
|
+
\bmkfs\b filesystem format
|
|
282
|
+
\bdd\s+if= raw disk write
|
|
283
|
+
:\(\)\s*\{ fork bomb
|
|
284
|
+
\b(chmod|chown)\s+...\s+/$ root permission change
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`tools.blockedCommands` extends (not replaces) the defaults. README calls this out honestly: **friction, not a boundary** — a determined shell caller can bypass. Don't run the agent against untrusted input and expect the blocklist to save you.
|
|
288
|
+
|
|
289
|
+
### 7.4 Sandbox levels
|
|
290
|
+
|
|
291
|
+
`config.tools.sandboxLevel`:
|
|
292
|
+
|
|
293
|
+
- `unrestricted` — all tools, all paths.
|
|
294
|
+
- `workspace` (default) — all tools, but `_resolve_path` refuses anything outside `config.workspace`.
|
|
295
|
+
- `readonly` — hard-blocks `shell_exec`, `write_file`, `append_file`, `code_edit` at the `execute()` gate. Read, search, memory, and web tools still work.
|
|
296
|
+
|
|
297
|
+
Path resolution: `_resolve_path` expands `~`, resolves absolute paths, joins relative paths to the workspace, and in restricted mode raises `PermissionError` if the resolved path is not `is_relative_to(workspace)`. The tool exception handler catches this and returns `Error: PermissionError: ...` to the model.
|
|
298
|
+
|
|
299
|
+
### 7.5 Plugins
|
|
300
|
+
|
|
301
|
+
`_load_plugins()` scans `~/.agent-mini/plugins/*.py`, `importlib.util.spec_from_file_location` each one, and looks for a `TOOL_DEF` dict and `handler` callable (sync or async — the executor `await`s it if it's a coroutine). Plugin definitions get concatenated onto `get_tool_defs()` and dispatched from the `_` case of the `execute()` match.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
# ~/.agent-mini/plugins/timestamp.py
|
|
305
|
+
TOOL_DEF = {
|
|
306
|
+
"type": "function",
|
|
307
|
+
"function": {
|
|
308
|
+
"name": "get_timestamp",
|
|
309
|
+
"description": "Get current UTC timestamp.",
|
|
310
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
async def handler(arguments: dict) -> str:
|
|
314
|
+
from datetime import datetime, timezone
|
|
315
|
+
return datetime.now(timezone.utc).isoformat()
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
No registration, no manifest — drop the file, restart the agent.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 8. Persistent Memory
|
|
323
|
+
|
|
324
|
+
[memory.py](../src/agent_mini/agent/memory.py) — a JSON file at `~/.agent-mini/memory.json`, list of `{key, value, timestamp}`.
|
|
325
|
+
|
|
326
|
+
### 8.1 Storage
|
|
327
|
+
|
|
328
|
+
`store()` appends a dict, trims to `maxEntries` from the tail, and rewrites the file. Corrupted JSON → warn, start fresh (never fatal).
|
|
329
|
+
|
|
330
|
+
### 8.2 Recall — hand-rolled TF-IDF
|
|
331
|
+
|
|
332
|
+
No embeddings, no vector DB. `recall(query)` does:
|
|
333
|
+
|
|
334
|
+
1. **Tokenize + stem** — regex `[a-z0-9]+`, then a compact Porter-style suffix stripper that catches common inflections (`-ing`, `-ment`, `-ness`, `-ies`→`y`, `-es`, `-ed`, `-ly`, `-s`, plus ~15 more complex rules like `-ational` → `-ate`).
|
|
335
|
+
2. **IDF**: `log((N+1)/(df+1)) + 1` per query term.
|
|
336
|
+
3. **Score each doc**: sum of `(tf/doc_len) * idf` for exact term hits, plus a substring-fallback bonus of `0.3 * idf/doc_len` for partial matches (so `python` matches `pythonic`).
|
|
337
|
+
4. Return top 10 sorted by score, formatted as `[timestamp] key: value`.
|
|
338
|
+
|
|
339
|
+
That's it — no external deps, works fine for the hundreds-of-entries scale a personal agent hits.
|
|
340
|
+
|
|
341
|
+
### 8.3 Prompt integration
|
|
342
|
+
|
|
343
|
+
`Memory.get_recent(n)` returns the tail of the store; the context builder inlines them into the system prompt under `## Recent memories` (0/3/5/5 by tier). Manual recall via the `memory_recall` tool is the fuzzy-search path.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 9. Sessions
|
|
348
|
+
|
|
349
|
+
[sessions.py](../src/agent_mini/sessions.py) — JSON files at `~/.agent-mini/sessions/<id>.json`:
|
|
350
|
+
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"id": "20260728_143022",
|
|
354
|
+
"updated": "2026-07-28T14:30:22.123456",
|
|
355
|
+
"metadata": {},
|
|
356
|
+
"conversation": [ ...role/content dicts... ]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`save_session` is called after every REPL turn (see `_chat` in cli.py). IDs default to `YYYYMMDD_HHMMSS`. `list_sessions` returns all files sorted newest-first with a computed preview of the first user message. `/load <id>` mutates the in-memory `conversation` list; `/save` exports the conversation to Markdown (renders tool calls / results in collapsible `<details>` blocks).
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## 10. Channels & Bus
|
|
365
|
+
|
|
366
|
+
### 10.1 MessageBus
|
|
367
|
+
|
|
368
|
+
[bus.py](../src/agent_mini/bus.py) is thin. It maintains an in-memory dict keyed by `f"{channel}:{user_id}"` mapping to the conversation list, and calls `agent.run()`. This is what isolates two Telegram users from each other, or the CLI from the bot.
|
|
369
|
+
|
|
370
|
+
### 10.2 Telegram channel
|
|
371
|
+
|
|
372
|
+
[telegram.py](../src/agent_mini/channels/telegram.py) uses `python-telegram-bot ≥ 21`. Highlights:
|
|
373
|
+
|
|
374
|
+
- **Allowlist**: `allowFrom` is a set of user IDs or usernames; `"*"` disables the check.
|
|
375
|
+
- **Typing indicator** via `send_action("typing")` at the start of each message.
|
|
376
|
+
- **Streaming**: sends an initial `"..."` placeholder and edits it in place. `_flush` debounces edits (min 0.7 s between updates unless ≥80 new chars). It also short-circuits when the preview is unchanged, avoiding Telegram's `message is not modified` error.
|
|
377
|
+
- **Chunking**: replies longer than 4 000 chars are split (Telegram's message limit is 4 096).
|
|
378
|
+
|
|
379
|
+
`agent-mini gateway` iterates over enabled channels and calls `start(bus.handle_message)`. Currently only Telegram is implemented, but the `BaseChannel` interface makes adding new ones cheap.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## 11. Vision
|
|
384
|
+
|
|
385
|
+
[vision.py](../src/agent_mini/agent/vision.py) scans the user message for words that look like image paths (`.png/.jpg/.jpeg/.gif/.webp/.bmp`) or `https?://…\.png` URLs. If any are found, the user message becomes an OpenAI-style multi-part list:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
[
|
|
389
|
+
{"type": "text", "text": "what is in this image?"},
|
|
390
|
+
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
|
|
391
|
+
]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- Local files are base64-encoded with the correct MIME type.
|
|
395
|
+
- Remote URLs are passed through unmodified.
|
|
396
|
+
- The Ollama provider translates this shape into its native `{content, images: [...]}` in `_clean_messages`, so the same message works on all three providers.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 12. Configuration
|
|
401
|
+
|
|
402
|
+
[config.py](../src/agent_mini/config.py) mirrors the JSON config as typed dataclasses (`AppConfig`, `ProvidersConfig`, `AgentConfig`, `ToolsConfig`, etc.). The `AppConfig.from_dict` helper does key-filtered construction with `_pick`, so unknown keys in the JSON don't blow up instantiation — forward-compatible reads.
|
|
403
|
+
|
|
404
|
+
Key paths (all under `~/.agent-mini/` by default):
|
|
405
|
+
|
|
406
|
+
| Path | Purpose |
|
|
407
|
+
|---|---|
|
|
408
|
+
| `config.json` | User config |
|
|
409
|
+
| `workspace/` | Default tool sandbox root |
|
|
410
|
+
| `memory.json` | Persistent memory store |
|
|
411
|
+
| `plugins/*.py` | User tool plugins |
|
|
412
|
+
| `sessions/*.json` | Saved conversations |
|
|
413
|
+
|
|
414
|
+
Runtime overrides: `--workspace <dir>` and `AGENT_MINI_WORKSPACE` env var (used by the evals runner to isolate each task).
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## 13. Evals Harness
|
|
419
|
+
|
|
420
|
+
[evals/](../evals/) is a framework-free task runner. Each YAML task under `evals/tasks/` describes:
|
|
421
|
+
|
|
422
|
+
- fixtures to copy into a temp workspace,
|
|
423
|
+
- a prompt to send to the agent,
|
|
424
|
+
- success predicates (file exists, `pytest` passes, regex in output, etc.),
|
|
425
|
+
- caps on iterations / tokens.
|
|
426
|
+
|
|
427
|
+
`evals/run.py` invokes the same `AgentLoop` with `AGENT_MINI_WORKSPACE` pointing at that temp dir, records success/iterations/tokens/tool-call counts, and writes a JSON report under `evals/results/`. `--compare results/*.json` prints a cross-model table. This is what lets tier-scaling changes be measured rather than intuited.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## 14. Error Handling & Observability
|
|
432
|
+
|
|
433
|
+
- **Provider errors**: transient → retry with jitter; permanent → the loop returns `f"Error communicating with LLM: {e}"` as the final text (still gets saved to conversation).
|
|
434
|
+
- **Tool errors**: caught in `execute()`, returned as `Error: {type}: {msg}`. The loop postfixes the self-reflection nudge so the model retries with a different approach.
|
|
435
|
+
- **Max iteration exhaustion**: the user turn is still appended with an assistant message `[Stopped: reached max iterations without completing.]` — so the next turn doesn't have amnesia about what happened.
|
|
436
|
+
- **Logging**: `logging.getLogger("agent-mini")`. `-v/--verbose` on `chat` / `gateway` flips to DEBUG. Tool calls log at INFO with a 200-char argument preview and 🔧 emoji marker; tool results at DEBUG with a 300-char preview.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## 15. Design Decisions Worth Calling Out
|
|
441
|
+
|
|
442
|
+
- **No agent framework.** No LangChain, no LiteLLM, no Semantic Kernel. Every abstraction is one file long, and there's nothing under the hood that isn't in the repo. If a tuning knob matters, it's a constant at the top of a Python file, not a nested YAML key you have to grep an SDK for.
|
|
443
|
+
- **Tier-scaled everything.** The same code runs a `qwen2.5:1.5b` and a `gpt-4o` — but the prompt, iteration cap, output cap, memory budget, and context budget all shift by tier. This is measured, not vibed: [evals/results/](../evals/results/) has runs across `qwen3.5:4b`, `9b`, `27b`.
|
|
444
|
+
- **JSON-only persistence.** `memory.json`, `sessions/*.json`, `config.json`. No SQLite, no vector DB. You can `cat`, `jq`, diff, and back up your entire agent state.
|
|
445
|
+
- **Provider ↔ loop contract is one dataclass** (`ChatResponse`). Adding a new backend is 100 lines of HTTP client plus a mapping function.
|
|
446
|
+
- **Streaming is optional at the loop level** but pushed all the way to the Telegram edit-in-place flow so long tool-heavy responses feel responsive.
|
|
447
|
+
- **SSRF + shell blocklist are documented as friction, not boundaries.** Honest security posture — the sandbox level is the real control.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## 16. End-to-End Example: "read foo.py and add a docstring"
|
|
452
|
+
|
|
453
|
+
Sequence for a Telegram user sending this message:
|
|
454
|
+
|
|
455
|
+
```mermaid
|
|
456
|
+
sequenceDiagram
|
|
457
|
+
participant U as User
|
|
458
|
+
participant TG as TelegramChannel
|
|
459
|
+
participant B as MessageBus
|
|
460
|
+
participant L as AgentLoop
|
|
461
|
+
participant P as OllamaProvider
|
|
462
|
+
participant M as LLM
|
|
463
|
+
participant T as ToolExecutor
|
|
464
|
+
participant FS as Filesystem
|
|
465
|
+
|
|
466
|
+
U->>TG: "read foo.py and add a docstring"
|
|
467
|
+
TG->>B: handle_message("telegram", uid, text, _emit)
|
|
468
|
+
B->>L: run(text, session, on_stream=_emit)
|
|
469
|
+
L->>L: build_system_prompt(tier, tools, mem)
|
|
470
|
+
L->>P: chat_stream(messages, tools)
|
|
471
|
+
P->>M: POST /api/chat (stream)
|
|
472
|
+
M-->>P: tool_calls=[read_file(path=foo.py)]
|
|
473
|
+
P-->>L: ChatResponse(tool_calls=[...])
|
|
474
|
+
L->>T: execute("read_file", {"path":"foo.py"})
|
|
475
|
+
T->>FS: _resolve_path + read_text
|
|
476
|
+
FS-->>T: content
|
|
477
|
+
T-->>L: content string (truncated to tier cap)
|
|
478
|
+
L->>P: chat_stream(messages + tool result)
|
|
479
|
+
P->>M: POST /api/chat
|
|
480
|
+
M-->>P: tool_calls=[code_edit(...)]
|
|
481
|
+
P-->>L: ChatResponse
|
|
482
|
+
L->>T: execute("code_edit", ...)
|
|
483
|
+
T->>FS: read → replace → write
|
|
484
|
+
FS-->>T: ok
|
|
485
|
+
T-->>L: "Edited foo.py..."
|
|
486
|
+
L->>P: chat_stream(...)
|
|
487
|
+
P->>M: POST /api/chat
|
|
488
|
+
M-->>P: text: "Added docstring."
|
|
489
|
+
P-->>L: ChatResponse(content=..., no tool_calls)
|
|
490
|
+
L->>L: append turns, maybe summarize
|
|
491
|
+
L-->>B: "Added docstring."
|
|
492
|
+
B-->>TG: reply
|
|
493
|
+
TG->>U: edits placeholder → final text
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Every arrow above is a real function you can grep for in ~3 000 lines of Python.
|