lcode-cli 0.11.1__tar.gz → 0.12.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.
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/CHANGELOG.md +24 -1
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/PKG-INFO +1 -2
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/README.md +0 -1
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/bench.md +29 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/configuration.md +2 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/how-it-works.md +21 -5
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/usage.md +1 -1
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/pyproject.toml +1 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/agent.py +22 -3
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/bench.py +44 -11
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/cli.py +15 -2
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/config.py +1 -0
- lcode_cli-0.12.0/src/lcode/context.py +243 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/repl.py +24 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/tools.py +8 -2
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_bench.py +25 -0
- lcode_cli-0.12.0/tests/test_context.py +161 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.editorconfig +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.gitignore +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/AGENTS.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/LICENSE +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/SECURITY.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/agents.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/assets/logo.svg +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/assets/social-preview.png +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/changelog.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/commands.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/contributing.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/index.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/installation.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/mcp.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/memory.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/models.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/examples/README.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/install.sh +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/mkdocs.yml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/overrides/main.html +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/conftest.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_config.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/tests/test_web.py +0 -0
- {lcode_cli-0.11.1 → lcode_cli-0.12.0}/uv.lock +0 -0
|
@@ -6,6 +6,28 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `/context` shows what uses the context window, by category: the system prompt and its parts
|
|
14
|
+
(`AGENTS.md`, memory notes, skills, MCP servers), tool definitions, your messages, attached files,
|
|
15
|
+
the model's replies and each tool's results.
|
|
16
|
+
- `lcode bench --session` runs all tasks in one conversation, to test long sessions and context
|
|
17
|
+
management; `--rounds N` repeats the tasks and `--no-prune` turns the removal of old tool output
|
|
18
|
+
off, to compare.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Smarter context management, which matters most at 32K. When the window is 85% full, lcode first
|
|
23
|
+
replaces old tool output (file contents, command output, search results from before your last
|
|
24
|
+
two requests, and earlier copies of files that were read again) with one-line notes saying what
|
|
25
|
+
was there, then the output of your previous request if needed; it summarizes the conversation
|
|
26
|
+
only if that doesn't free enough room. Measured on 24-task sessions at 32K: fewer summaries, with
|
|
27
|
+
about as many tasks solved. The `prune` setting turns it off.
|
|
28
|
+
- Long command and MCP output keeps its beginning, its end and the lines that look like errors;
|
|
29
|
+
the full output is saved to a file the model can read.
|
|
30
|
+
|
|
9
31
|
## [0.11.1] - 2026-10-05
|
|
10
32
|
|
|
11
33
|
### Fixed
|
|
@@ -311,7 +333,8 @@ First public release.
|
|
|
311
333
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
312
334
|
- One-line installer for Linux and macOS.
|
|
313
335
|
|
|
314
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
336
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.12.0...HEAD
|
|
337
|
+
[0.12.0]: https://github.com/nasser1941/lcode/compare/v0.11.1...v0.12.0
|
|
315
338
|
[0.11.1]: https://github.com/nasser1941/lcode/compare/v0.11.0...v0.11.1
|
|
316
339
|
[0.11.0]: https://github.com/nasser1941/lcode/compare/v0.10.0...v0.11.0
|
|
317
340
|
[0.10.0]: https://github.com/nasser1941/lcode/compare/v0.9.0...v0.10.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
|
|
5
5
|
Project-URL: Homepage, https://nasser1941.github.io/lcode/
|
|
6
6
|
Project-URL: Documentation, https://nasser1941.github.io/lcode/
|
|
@@ -201,7 +201,6 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
|
|
|
201
201
|
|
|
202
202
|
See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
|
|
203
203
|
|
|
204
|
-
- [Smarter context for small windows](https://github.com/nasser1941/lcode/issues/46)
|
|
205
204
|
- [Code intelligence with language servers](https://github.com/nasser1941/lcode/issues/44)
|
|
206
205
|
- [A repository map and semantic search with local embeddings](https://github.com/nasser1941/lcode/issues/45)
|
|
207
206
|
- [Hooks and permission rules](https://github.com/nasser1941/lcode/issues/43)
|
|
@@ -165,7 +165,6 @@ fills up. Details: [How it works](https://nasser1941.github.io/lcode/how-it-work
|
|
|
165
165
|
|
|
166
166
|
See the pinned [Roadmap issue](https://github.com/nasser1941/lcode/issues/10). Next up:
|
|
167
167
|
|
|
168
|
-
- [Smarter context for small windows](https://github.com/nasser1941/lcode/issues/46)
|
|
169
168
|
- [Code intelligence with language servers](https://github.com/nasser1941/lcode/issues/44)
|
|
170
169
|
- [A repository map and semantic search with local embeddings](https://github.com/nasser1941/lcode/issues/45)
|
|
171
170
|
- [Hooks and permission rules](https://github.com/nasser1941/lcode/issues/43)
|
|
@@ -31,6 +31,35 @@ At the end it prints a table comparing the models (see [results](#results-on-a-1
|
|
|
31
31
|
|
|
32
32
|
A model usually takes 4 to 12 minutes; each task stops after 5 minutes (`--timeout`).
|
|
33
33
|
|
|
34
|
+
### Long sessions
|
|
35
|
+
|
|
36
|
+
Each task normally starts a new conversation. `--session` runs them all in **one** conversation
|
|
37
|
+
instead, as a long working session would, so the context fills up and lcode's
|
|
38
|
+
[context management](how-it-works.md#context-management) has to work: the summary shows how often
|
|
39
|
+
it removed old tool output and how often it summarized the conversation. `--no-prune` turns the
|
|
40
|
+
removal off, to compare.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
lcode bench --context 32k --session --rounds 3
|
|
44
|
+
lcode bench --context 32k --session --rounds 3 --no-prune
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--rounds 3` runs the tasks three times, 24 in one conversation, which fills a 32K window several
|
|
48
|
+
times over. With qwen3.6-35b on a 12 GB laptop (two runs each; runs vary a lot from one to the
|
|
49
|
+
next):
|
|
50
|
+
|
|
51
|
+
| | Removing old tool output first | Summarizing only (`--no-prune`) |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Conversation summarized | 1 and 2 times | 1 and 3 times |
|
|
54
|
+
| Old tool output removed | 3 times in each run | — |
|
|
55
|
+
| Tasks solved | 21 and 22 of 24 | 23 and 21 of 24 |
|
|
56
|
+
| Time | 26 and 28 min | 21 and 30 min |
|
|
57
|
+
|
|
58
|
+
Removing old tool output means fewer summaries, so more of the conversation stays word for word,
|
|
59
|
+
at no measurable cost in tasks solved or time. The bench tasks are independent of each other,
|
|
60
|
+
which is where summaries hurt least; in a real session, where later requests build on earlier
|
|
61
|
+
ones, keeping the conversation intact matters more.
|
|
62
|
+
|
|
34
63
|
## The tasks
|
|
35
64
|
|
|
36
65
|
Every task runs in a new temporary folder, with every permission granted (`yolo` mode) and web
|
|
@@ -32,6 +32,7 @@ lcode config path # print the file location
|
|
|
32
32
|
| `mcp_tools` | `auto` | How [MCP](mcp.md#context) tool definitions reach the model: `auto`, `direct` or `search` (on demand) |
|
|
33
33
|
| `memory` | `ask` | [Notes that carry over](memory.md) to later sessions: `ask` (confirm each), `auto` or `off` |
|
|
34
34
|
| `skills` | `all` | Which [skills](commands.md#skills) the model is offered: `all` (lcode's folders and the ones other agents share), `lcode` (only `.lcode/skills/` and `~/.config/lcode/skills/`) or `off` |
|
|
35
|
+
| `prune` | `true` | When the context is 85% full, first remove old tool output, and summarize the conversation only if that's not enough ([how](how-it-works.md#context-management)) |
|
|
35
36
|
| `subagents` | `true` | Let the model hand tasks to [subagents](agents.md) with their own context |
|
|
36
37
|
| `max_parallel_agents` | `1` | Subagents that run at the same time; more needs `OLLAMA_NUM_PARALLEL` on the Ollama server ([details](agents.md#several-at-once)) |
|
|
37
38
|
| `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |
|
|
@@ -78,6 +79,7 @@ Environment variables override the file, which is useful for one-off runs and CI
|
|
|
78
79
|
| `~/.config/lcode/commands/`, `~/.config/lcode/skills/` | Your [commands and skills](commands.md) (a repository's go in `.lcode/`) |
|
|
79
80
|
| `~/.local/state/lcode/extensions-approved.json` | Repositories whose commands, skills and agents you approved |
|
|
80
81
|
| `~/.config/lcode/agents/` | Your [custom agents](agents.md#custom-agents) (a repository's go in `.lcode/agents/`) |
|
|
82
|
+
| `~/.local/state/lcode/outputs/` | The full text of long command output that was cut short for the model (kept a week) |
|
|
81
83
|
| `~/.local/state/lcode/limits.json` | Context sizes that ran out of GPU memory on this machine (safe to delete) |
|
|
82
84
|
|
|
83
85
|
Sessions contain everything the model read, including file contents. Delete the folder to clear them.
|
|
@@ -41,11 +41,27 @@ in a temporary git worktree, and their changes come back as a diff.
|
|
|
41
41
|
## Context management
|
|
42
42
|
|
|
43
43
|
Ollama keeps the processed conversation in its KV cache, so each turn only processes the new tokens.
|
|
44
|
-
lcode tracks how full the window is from Ollama's token counts.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
lcode tracks how full the window is from Ollama's token counts. Most of a long session's context is
|
|
45
|
+
old tool output, so when the window is 85% full:
|
|
46
|
+
|
|
47
|
+
1. **lcode removes old tool output first.** File contents, command output and search results from
|
|
48
|
+
before your last two requests become one-line notes that say what was there, such as
|
|
49
|
+
`[lcode: read src/app.py earlier; removed to save context. Read it again if you need it.]`.
|
|
50
|
+
An earlier copy of a file that was read again later goes too, even in recent requests. Subagent
|
|
51
|
+
reports, skill instructions and the approved plan stay.
|
|
52
|
+
2. **If that brings the conversation under 70%,** the work simply continues: nothing is summarized,
|
|
53
|
+
and the conversation itself stays word for word.
|
|
54
|
+
If it's not, lcode also removes the tool output of your previous request, keeping only the
|
|
55
|
+
latest one's. (`lcode config set prune false` skips this step.)
|
|
56
|
+
3. **Otherwise lcode summarizes.** It asks whether anything is worth [remembering](memory.md), since
|
|
57
|
+
the summary leaves details out, then has the model write a summary of the conversation (goals,
|
|
58
|
+
findings, files changed, next steps) and continues from it.
|
|
59
|
+
|
|
60
|
+
Long tool output (over 30,000 characters) keeps its beginning, its end and the lines in between
|
|
61
|
+
that look like errors. The full output is saved under `~/.local/state/lcode/outputs/` (for a week)
|
|
62
|
+
and the model is told where, so it can read the rest if it needs to. `/context` shows what's using
|
|
63
|
+
the window, by category: the system prompt and its parts, tool definitions, your messages, attached
|
|
64
|
+
files, the model's replies and each tool's results.
|
|
49
65
|
|
|
50
66
|
## Memory sizing
|
|
51
67
|
|
|
@@ -67,7 +67,7 @@ reasoning is on.
|
|
|
67
67
|
| `/mcp [tools\|login\|restart NAME]` | [MCP servers](mcp.md), their status and tools |
|
|
68
68
|
| `/plan [request]` | Work out a [plan](#plan-mode) before changing anything, or show the approved plan |
|
|
69
69
|
| `/compact [focus]` | Summarize the conversation to free context |
|
|
70
|
-
| `/context [size]` | Show how full the context window is and change its size: pick from a list with memory estimates, or give a size like `/context 128k` (`/ctx` is a shortcut) |
|
|
70
|
+
| `/context [size]` | Show how full the context window is, what's using it (by category), and change its size: pick from a list with memory estimates, or give a size like `/context 128k` (`/ctx` is a shortcut) |
|
|
71
71
|
| `/model [name]`, `/models` | Switch model, list models |
|
|
72
72
|
| `/think [on\|off]` | Toggle reasoning |
|
|
73
73
|
| `/verbose` | Toggle showing the reasoning text |
|
|
@@ -22,6 +22,7 @@ from rich.panel import Panel
|
|
|
22
22
|
from rich.text import Text
|
|
23
23
|
|
|
24
24
|
from lcode import catalog, extensions, limits, planning, sessions, subagents, vision, web
|
|
25
|
+
from lcode import context as context_tools
|
|
25
26
|
from lcode import memory as memory_notes
|
|
26
27
|
from lcode.checkpoints import Checkpoints
|
|
27
28
|
from lcode.config import format_tokens
|
|
@@ -50,6 +51,7 @@ SAFE_NUM_BATCH = 512 # Ollama's default prompt batch
|
|
|
50
51
|
MAX_STEPS_PER_TURN = 150
|
|
51
52
|
MAX_MALFORMED_CALL_RETRIES = 2 # Ollama rejects tool calls whose arguments aren't valid JSON # safety cap on tool-call iterations for one request
|
|
52
53
|
AUTO_COMPACT_RATIO = 0.85 # summarize the history when the context is this full
|
|
54
|
+
PRUNED_ENOUGH = 0.6 # after pruning old tool output, summarize only if the context is still this full
|
|
53
55
|
PROJECT_FILES = ("AGENTS.md", "LCODE.md", "CLAUDE.md")
|
|
54
56
|
|
|
55
57
|
SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent running in the user's terminal on their own machine, powered by the local model {model}. You help the user understand codebases, answer questions about code, write scripts (Python by default), fix bugs, refactor, and run commands.
|
|
@@ -173,6 +175,7 @@ class Settings:
|
|
|
173
175
|
subagents: bool = False # the agent tool (lcode.subagents)
|
|
174
176
|
trust_project: bool = False # use the repository's own commands, skills and agents (lcode.extensions)
|
|
175
177
|
skills: str = "off" # all, lcode (only lcode's own skill folders) or off
|
|
178
|
+
prune: bool = True # remove old tool output before summarizing (lcode.context)
|
|
176
179
|
max_parallel_agents: int = 1
|
|
177
180
|
|
|
178
181
|
|
|
@@ -202,6 +205,7 @@ class Agent:
|
|
|
202
205
|
self._agent_types: tuple[Path, dict[str, subagents.AgentType], list[str]] | None = None
|
|
203
206
|
self.agent_runs: list[subagents.Record] = [] # subagents run in this session, for /agents
|
|
204
207
|
self.plan = "" # the plan the user approved; kept through compaction
|
|
208
|
+
self.pruned = self.compacted = 0 # how often the context was pruned or summarized automatically
|
|
205
209
|
self._extensions: tuple[Path, extensions.Extensions] | None = None
|
|
206
210
|
self.skills_loaded: set[str] = set() # skills whose instructions are in the conversation
|
|
207
211
|
# Set on subagents (see lcode.subagents):
|
|
@@ -775,9 +779,24 @@ class Agent:
|
|
|
775
779
|
|
|
776
780
|
# -- context management
|
|
777
781
|
def maybe_compact(self) -> None:
|
|
778
|
-
if self.ctx_used
|
|
779
|
-
|
|
780
|
-
|
|
782
|
+
if self.ctx_used <= AUTO_COMPACT_RATIO * self.settings.context:
|
|
783
|
+
return
|
|
784
|
+
if self.settings.prune:
|
|
785
|
+
saved = context_tools.prune(self.messages) # tool output from before the last two requests
|
|
786
|
+
if self.ctx_used - saved // 3 > PRUNED_ENOUGH * self.settings.context:
|
|
787
|
+
saved += context_tools.prune(self.messages, keep=1) # not enough: before the last request
|
|
788
|
+
if saved:
|
|
789
|
+
self.pruned += 1
|
|
790
|
+
self.ctx_used = max(len(self.messages[0]["content"]) // 3, self.ctx_used - saved // 3)
|
|
791
|
+
self.console.print(
|
|
792
|
+
f"[dim]Removed old tool output from the conversation (~{format_tokens(saved // 3)} tokens) to "
|
|
793
|
+
"make room.[/]"
|
|
794
|
+
)
|
|
795
|
+
if self.ctx_used <= PRUNED_ENOUGH * self.settings.context:
|
|
796
|
+
return
|
|
797
|
+
self.console.print("[yellow]Context is nearly full — compacting the conversation…[/]")
|
|
798
|
+
self.compacted += 1
|
|
799
|
+
self.compact()
|
|
781
800
|
|
|
782
801
|
def compact(self, focus: str = "") -> None:
|
|
783
802
|
if len(self.messages) <= 2:
|
|
@@ -784,6 +784,9 @@ class ModelRun:
|
|
|
784
784
|
tasks: list[TaskResult] = field(default_factory=list)
|
|
785
785
|
error: str | None = None # the model couldn't run at all
|
|
786
786
|
interrupted: bool = False
|
|
787
|
+
session: bool = False # all tasks in one conversation (--session)
|
|
788
|
+
pruned: int = 0 # times old tool output was removed to make room
|
|
789
|
+
compacted: int = 0 # times the conversation was summarized
|
|
787
790
|
|
|
788
791
|
@property
|
|
789
792
|
def passed(self) -> int:
|
|
@@ -847,14 +850,28 @@ def count_tool_errors(messages: list[dict]) -> int:
|
|
|
847
850
|
|
|
848
851
|
|
|
849
852
|
def run_task(
|
|
850
|
-
ollama: Ollama,
|
|
853
|
+
ollama: Ollama,
|
|
854
|
+
settings: Settings,
|
|
855
|
+
task: Task,
|
|
856
|
+
timeout: float,
|
|
857
|
+
log: Console,
|
|
858
|
+
keep: bool,
|
|
859
|
+
usage: dict,
|
|
860
|
+
agent: Agent | None = None,
|
|
851
861
|
) -> TaskResult:
|
|
862
|
+
"""Run one task in a new folder: in a new conversation, or continuing `agent`'s (--session)."""
|
|
852
863
|
folder = Path(tempfile.mkdtemp(prefix=f"lcode-bench-{task.id}-"))
|
|
853
864
|
try:
|
|
854
865
|
for rel, content in task.files.items():
|
|
855
866
|
(folder / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
856
867
|
(folder / rel).write_text(content)
|
|
857
|
-
agent
|
|
868
|
+
if agent is None:
|
|
869
|
+
agent = Agent(ollama, settings, folder, console=log)
|
|
870
|
+
else: # the same conversation moves on to this task's folder, as with /cd
|
|
871
|
+
agent.cwd = folder.resolve()
|
|
872
|
+
agent.messages[0]["content"] = agent.system_prompt()
|
|
873
|
+
before = dict(agent.usage)
|
|
874
|
+
first = len(agent.messages)
|
|
858
875
|
error = None
|
|
859
876
|
started = time.monotonic()
|
|
860
877
|
try:
|
|
@@ -866,7 +883,7 @@ def run_task(
|
|
|
866
883
|
error = str(e).splitlines()[0][:200]
|
|
867
884
|
seconds = time.monotonic() - started
|
|
868
885
|
for key, value in agent.usage.items():
|
|
869
|
-
usage[key] = usage.get(key, 0) + value
|
|
886
|
+
usage[key] = usage.get(key, 0) + value - before.get(key, 0)
|
|
870
887
|
answer = next((m.get("content", "") for m in reversed(agent.messages) if m.get("role") == "assistant"), "")
|
|
871
888
|
try:
|
|
872
889
|
check = task.check(folder, answer)
|
|
@@ -882,8 +899,8 @@ def run_task(
|
|
|
882
899
|
passed=check.passed and error is None,
|
|
883
900
|
seconds=round(seconds, 1),
|
|
884
901
|
detail=error or check.detail,
|
|
885
|
-
steps=
|
|
886
|
-
tool_errors=count_tool_errors(agent.messages),
|
|
902
|
+
steps=agent.usage["requests"] - before.get("requests", 0),
|
|
903
|
+
tool_errors=count_tool_errors(agent.messages[first:] if len(agent.messages) > first else agent.messages),
|
|
887
904
|
error=error,
|
|
888
905
|
)
|
|
889
906
|
|
|
@@ -940,8 +957,10 @@ def run_model(
|
|
|
940
957
|
timeout: float = DEFAULT_TIMEOUT,
|
|
941
958
|
keep: bool = False,
|
|
942
959
|
verbose: bool = False,
|
|
960
|
+
session: bool = False,
|
|
943
961
|
) -> ModelRun:
|
|
944
|
-
run = ModelRun(name, settings.model, settings.context, settings.num_batch, settings.think)
|
|
962
|
+
run = ModelRun(name, settings.model, settings.context, settings.num_batch, settings.think, session=session)
|
|
963
|
+
shared: Agent | None = None # with --session, one conversation for all tasks
|
|
945
964
|
console.print(
|
|
946
965
|
f"\n[bold]{escape(settings.model)}[/] · {format_tokens(settings.context)} context · "
|
|
947
966
|
f"reasoning {'on' if settings.think else 'off'} · {len(tasks)} task{'' if len(tasks) == 1 else 's'}"
|
|
@@ -969,13 +988,17 @@ def run_model(
|
|
|
969
988
|
console.print(f" [dim]Loaded in {run.load_seconds:.0f}s{where}{reads}[/]")
|
|
970
989
|
for task in tasks:
|
|
971
990
|
log = console if verbose else Console(file=io.StringIO(), width=120, force_terminal=False)
|
|
991
|
+
if session and shared is None:
|
|
992
|
+
shared = Agent(ollama, settings, Path(tempfile.gettempdir()), console=log)
|
|
993
|
+
if shared is not None:
|
|
994
|
+
shared.console = log
|
|
972
995
|
try:
|
|
973
996
|
if verbose:
|
|
974
997
|
console.rule(f"{task.id}: {task.title}")
|
|
975
|
-
result = run_task(ollama, settings, task, timeout, log, keep, run.usage)
|
|
998
|
+
result = run_task(ollama, settings, task, timeout, log, keep, run.usage, shared)
|
|
976
999
|
else:
|
|
977
1000
|
with console.status(Elapsed(f" {task.id:<13} {task.title}")):
|
|
978
|
-
result = run_task(ollama, settings, task, timeout, log, keep, run.usage)
|
|
1001
|
+
result = run_task(ollama, settings, task, timeout, log, keep, run.usage, shared)
|
|
979
1002
|
except KeyboardInterrupt:
|
|
980
1003
|
console.print(" [yellow]Stopped.[/]")
|
|
981
1004
|
run.interrupted = True
|
|
@@ -985,6 +1008,8 @@ def run_model(
|
|
|
985
1008
|
console.print(
|
|
986
1009
|
f" {mark} {task.id:<13} {task.title:<32} {result.seconds:>5.0f}s [dim]{escape(result.detail)}[/]"
|
|
987
1010
|
)
|
|
1011
|
+
if shared is not None:
|
|
1012
|
+
run.pruned, run.compacted = shared.pruned, shared.compacted
|
|
988
1013
|
if run.memory_gb is None:
|
|
989
1014
|
run.memory_gb, run.gpu_percent = memory_use(ollama, settings.model)
|
|
990
1015
|
# lcode lowers the batch size or context if the GPU runs out of memory; report what was used.
|
|
@@ -1001,11 +1026,12 @@ def duration(seconds: float) -> str:
|
|
|
1001
1026
|
|
|
1002
1027
|
def summary_rows(runs: list[ModelRun], tasks: list[Task]) -> list[tuple[str, list[str]]]:
|
|
1003
1028
|
rows = []
|
|
1004
|
-
for task in tasks:
|
|
1029
|
+
for task in {t.id: t for t in tasks}.values(): # with --rounds a task runs several times: one row, a mark each
|
|
1005
1030
|
cells = []
|
|
1006
1031
|
for run in runs:
|
|
1007
|
-
|
|
1008
|
-
|
|
1032
|
+
results = [t for t in run.tasks if t.id == task.id]
|
|
1033
|
+
marks = "".join("✓" if r.passed else "✗" for r in results)
|
|
1034
|
+
cells.append(f"{marks} {duration(sum(r.seconds for r in results))}" if results else "")
|
|
1009
1035
|
rows.append((f"{task.id}: {task.title}", cells))
|
|
1010
1036
|
|
|
1011
1037
|
def cell(run: ModelRun, value: str) -> str:
|
|
@@ -1023,6 +1049,13 @@ def summary_rows(runs: list[ModelRun], tasks: list[Task]) -> list[tuple[str, lis
|
|
|
1023
1049
|
),
|
|
1024
1050
|
("Context", [cell(r, format_tokens(r.context)) for r in runs]),
|
|
1025
1051
|
]
|
|
1052
|
+
if any(r.session for r in runs):
|
|
1053
|
+
rows.append(
|
|
1054
|
+
(
|
|
1055
|
+
"One conversation",
|
|
1056
|
+
[cell(r, f"pruned {r.pruned}×, summarized {r.compacted}×" if r.session else "no") for r in runs],
|
|
1057
|
+
)
|
|
1058
|
+
)
|
|
1026
1059
|
return rows
|
|
1027
1060
|
|
|
1028
1061
|
|
|
@@ -436,7 +436,7 @@ def cmd_bench(args) -> None:
|
|
|
436
436
|
try:
|
|
437
437
|
cfg = config.load()
|
|
438
438
|
context = parse_context(args.context) if args.context else bench.DEFAULT_CONTEXT
|
|
439
|
-
tasks = bench.select_tasks(args.tasks)
|
|
439
|
+
tasks = bench.select_tasks(args.tasks) * max(1, args.rounds)
|
|
440
440
|
except (ConfigError, ValueError) as e:
|
|
441
441
|
fail(str(e))
|
|
442
442
|
if args.timeout <= 0:
|
|
@@ -483,8 +483,11 @@ def cmd_bench(args) -> None:
|
|
|
483
483
|
permission_mode="yolo",
|
|
484
484
|
web="off",
|
|
485
485
|
checkpoints=False,
|
|
486
|
+
prune=not args.no_prune,
|
|
487
|
+
)
|
|
488
|
+
run = bench.run_model(
|
|
489
|
+
ollama, name, settings, tasks, console, args.timeout, args.keep, args.verbose, args.session
|
|
486
490
|
)
|
|
487
|
-
run = bench.run_model(ollama, name, settings, tasks, console, args.timeout, args.keep, args.verbose)
|
|
488
491
|
runs.append(run)
|
|
489
492
|
if run.interrupted:
|
|
490
493
|
break
|
|
@@ -566,6 +569,7 @@ def cmd_chat(args) -> None:
|
|
|
566
569
|
max_parallel_agents=cfg["max_parallel_agents"],
|
|
567
570
|
trust_project=trust_project,
|
|
568
571
|
skills=cfg["skills"],
|
|
572
|
+
prune=cfg["prune"],
|
|
569
573
|
)
|
|
570
574
|
agent = Agent(ollama, settings, cwd, console=console)
|
|
571
575
|
agent.interactive = not args.prompt
|
|
@@ -651,6 +655,15 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
|
|
|
651
655
|
p.add_argument("--keep", action="store_true", help="keep each task's folder and transcript for inspection")
|
|
652
656
|
p.add_argument("-v", "--verbose", action="store_true", help="show the model working, like a normal session")
|
|
653
657
|
p.add_argument("--list", action="store_true", help="list the tasks and exit")
|
|
658
|
+
p.add_argument(
|
|
659
|
+
"--session",
|
|
660
|
+
action="store_true",
|
|
661
|
+
help="run all tasks in one conversation, to test a long session and its context management",
|
|
662
|
+
)
|
|
663
|
+
p.add_argument(
|
|
664
|
+
"--rounds", type=int, default=1, help="run the tasks this many times (with --session: a longer session)"
|
|
665
|
+
)
|
|
666
|
+
p.add_argument("--no-prune", action="store_true", help="don't remove old tool output (to compare, with --session)")
|
|
654
667
|
subs["bench"] = p
|
|
655
668
|
from lcode.mcp.commands import build_parser as mcp_parser
|
|
656
669
|
|
|
@@ -49,6 +49,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
|
49
49
|
"mcp_tools": ("auto", str, "how MCP tools reach the model: auto | direct | search (on demand, saves context)"),
|
|
50
50
|
"memory": ("ask", str, "notes that carry over to later sessions: off | ask (confirm each) | auto"),
|
|
51
51
|
"skills": ("all", str, "skills to offer the model: all (also other agents' folders) | lcode | off"),
|
|
52
|
+
"prune": (True, bool, "before summarizing a full conversation, first remove old tool output from it"),
|
|
52
53
|
"subagents": (True, bool, "let the model hand tasks to subagents that have their own context"),
|
|
53
54
|
"max_parallel_agents": (1, int, "subagents that may run at the same time (more needs OLLAMA_NUM_PARALLEL)"),
|
|
54
55
|
}
|