lcode-cli 0.34.0__tar.gz → 0.35.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.34.0 → lcode_cli-0.35.0}/CHANGELOG.md +31 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/PKG-INFO +1 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/bench.md +16 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/configuration.md +3 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/usage.md +46 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/pyproject.toml +1 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/agent.py +32 -3
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/api.py +5 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/backends.py +7 -2
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/bench.py +53 -17
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/cli.py +22 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/config.py +24 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/context.py +40 -15
- lcode_cli-0.35.0/src/lcode/escalate.py +315 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/repl.py +38 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/screenshots.py +21 -9
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/tools.py +2 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/conftest.py +1 -1
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_bench.py +2 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_context.py +35 -0
- lcode_cli-0.35.0/tests/test_escalate.py +333 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_screenshots.py +37 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.editorconfig +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.gitignore +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/AGENTS.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/LICENSE +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/README.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/SECURITY.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/action.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/agents.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/automation.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/changelog.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/commands.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/contributing.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/editors.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/git.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/hooks.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/index.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/installation.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/lsp.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/mcp.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/memory.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/models.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/search.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/servers.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/examples/README.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/install.sh +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/mkdocs.yml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/overrides/main.html +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/scripts/make_logos.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/residency.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/review.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/steer.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_action.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_api.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_backends.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_config.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_residency.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_review.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_steer.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_update.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_web.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.34.0 → lcode_cli-0.35.0}/uv.lock +0 -0
|
@@ -6,6 +6,35 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.35.0] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- A stronger model when stuck: with `escalate_model` set (a cloud model with your API key, such as
|
|
14
|
+
`anthropic/…`, `openai/…` or `xai/…`, or a bigger local one, `ollama/qwen3.8-27b`), lcode
|
|
15
|
+
offers to ask it for advice when the loop breaker would stop a request or the checks fail twice
|
|
16
|
+
in a row, and `/escalate` asks at any time. Nothing is sent without asking, in any permission
|
|
17
|
+
mode: you see why, what goes (your requests, the task sheet, the failure, the last steps and the
|
|
18
|
+
files in question, never the whole session), where, and the cost (`escalate_price`), and can
|
|
19
|
+
read it all first. Keys come only from environment variables and go only to their provider.
|
|
20
|
+
`lcode -p --escalate` allows it for one run, and `lcode bench --retry --escalate MODEL`
|
|
21
|
+
measures it. Measured on a 12 GB laptop with qwen3.6-35b and the bigger local qwen3.8-27b as
|
|
22
|
+
the advisor, on `runtime-bug` with a second try: 1 of 3 passed with its advice, as without it
|
|
23
|
+
(6 of 9 hidden-test problems fixed against 4), at about 9 minutes per question. A local advisor
|
|
24
|
+
helps a little; a cloud model is the one to try. See
|
|
25
|
+
[A stronger model when stuck](https://nasser1941.github.io/lcode/usage/#a-stronger-model-when-stuck).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Long command output can't fill the disk any more. lcode keeps the full text of long outputs so the
|
|
30
|
+
model can read parts of them, but a command that printed gigabytes was saved whole, and the
|
|
31
|
+
saved outputs had no total limit, only an age (7 days). Now one is kept to its first 20 MB (the
|
|
32
|
+
model still gets the end and the error lines), and all of them together to 200 MB, oldest first.
|
|
33
|
+
|
|
34
|
+
- A screenshot of a web app no longer starts its server a second time when one already answers at
|
|
35
|
+
the URL (one the model started itself, say), which used to fail on the busy port and put the
|
|
36
|
+
error in the result. A page that answers also counts when the server command itself exits.
|
|
37
|
+
|
|
9
38
|
## [0.34.0] - 2026-10-14
|
|
10
39
|
|
|
11
40
|
### Added
|
|
@@ -740,7 +769,8 @@ First public release.
|
|
|
740
769
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
741
770
|
- One-line installer for Linux and macOS.
|
|
742
771
|
|
|
743
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
772
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.35.0...HEAD
|
|
773
|
+
[0.35.0]: https://github.com/nasser1941/lcode/compare/v0.34.0...v0.35.0
|
|
744
774
|
[0.34.0]: https://github.com/nasser1941/lcode/compare/v0.33.1...v0.34.0
|
|
745
775
|
[0.33.1]: https://github.com/nasser1941/lcode/compare/v0.33.0...v0.33.1
|
|
746
776
|
[0.33.0]: https://github.com/nasser1941/lcode/compare/v0.32.0...v0.33.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.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/
|
|
@@ -45,6 +45,22 @@ cause. Neither is in the default set, so results stay comparable with earlier ve
|
|
|
45
45
|
`--code-search` indexes each task's folder for [semantic search](search.md#semantic-code-search);
|
|
46
46
|
both are off in `lcode bench` by default, for the same reason.
|
|
47
47
|
|
|
48
|
+
### A second try, and a stronger model
|
|
49
|
+
|
|
50
|
+
`--retry` gives a failed task one more try, as a user would ask: "That didn't fix it: it still
|
|
51
|
+
doesn't work right. Look again." `--escalate MODEL` sets a [stronger model](usage.md#a-stronger-model-when-stuck)
|
|
52
|
+
that's asked when the model is stuck and, with `--retry`, before the second try, as `/escalate`
|
|
53
|
+
would: no questions asked, since the flag is your OK. Comparing the two shows what the stronger
|
|
54
|
+
model's advice adds to the same second chance:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
lcode bench --tasks runtime-bug --retry --rounds 3
|
|
58
|
+
lcode bench --tasks runtime-bug --retry --rounds 3 --escalate ollama/qwen3.8-27b
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The JSON report has, for each task, `first_passed` (with `--retry`, whether the first try passed)
|
|
62
|
+
and `escalations`.
|
|
63
|
+
|
|
48
64
|
### Long sessions
|
|
49
65
|
|
|
50
66
|
Each task normally starts a new conversation. `--session` runs them all in **one** conversation
|
|
@@ -45,6 +45,9 @@ lcode config path # print the file location
|
|
|
45
45
|
| `notify_after` | `30` | Seconds a request runs before it notifies |
|
|
46
46
|
| `review` | `off` | Before the answer, [review a change](usage.md#a-review-before-the-answer) against the request and send problems back to the model: `auto` or `off` |
|
|
47
47
|
| `checks` | `auto` | Run the project's [checks](usage.md#checks-before-done) after a request changed files: `auto` (found from the project), `off`, or a command |
|
|
48
|
+
| `escalate_model` | — | A [stronger model](usage.md#a-stronger-model-when-stuck) to ask, with your OK, when the model is stuck: `anthropic/…`, `openai/…`, `xai/…` (keys from `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`), `ollama/<model>`, or `custom/<model>` with `escalate_url` |
|
|
49
|
+
| `escalate_url` | — | Its server, when it isn't the provider's usual one: another Ollama, or any OpenAI-compatible API |
|
|
50
|
+
| `escalate_price` | — | Its price in dollars per million tokens, input/output (e.g. `3/15`), for the cost shown before each use |
|
|
48
51
|
| `repeat_limit` | `3` | Identical tool calls with identical results before lcode [steps in](how-it-works.md#when-the-model-repeats-itself); `0` turns it off |
|
|
49
52
|
| `secret_check` | `true` | Before `git commit` and `git push`, look for passwords, keys and tokens they'd add ([details](usage.md#secrets-in-commits)) |
|
|
50
53
|
| `checkpoints` | `true` | Save a checkpoint before the model changes files, so [`/undo`](usage.md#undo-and-checkpoints) can restore them |
|
|
@@ -83,6 +83,7 @@ reasoning is on.
|
|
|
83
83
|
| `/cd DIR` | Change the working directory |
|
|
84
84
|
| `/todos` | Show the model's task list |
|
|
85
85
|
| `/status` | The task sheet: your latest requests, the plan and the model's notes, files changed, the last check |
|
|
86
|
+
| `/escalate` | Ask the [stronger model](#a-stronger-model-when-stuck) for advice on the work, with your OK; `/escalate <what's wrong>` says what to look at |
|
|
86
87
|
| `/jobs [stop ID]` | The [background commands](#background-commands) the model started; stop one |
|
|
87
88
|
| `/commit [notes]` | [Commit](git.md#commit) the changes with a message in the repository's style, once you approve it |
|
|
88
89
|
| `/review [base]` | [Review](git.md#review) the uncommitted changes, or the branch against a base |
|
|
@@ -208,6 +209,51 @@ The review is asked on top of the conversation rather than in a fresh one. The m
|
|
|
208
209
|
the processed conversation cached, and a separate review would push it out, so the next request
|
|
209
210
|
would have to read the whole conversation again. On a small GPU that's tens of seconds.
|
|
210
211
|
|
|
212
|
+
## A stronger model when stuck
|
|
213
|
+
|
|
214
|
+
Some problems are beyond a local model. When it's stuck, lcode can ask a stronger one for
|
|
215
|
+
advice: a cloud model with your API key, or a bigger local model. The local model gets the advice
|
|
216
|
+
and carries on with its own tools. It's off until you choose the model:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
lcode config set escalate_model anthropic/<model> # with ANTHROPIC_API_KEY in your environment
|
|
220
|
+
lcode config set escalate_model openai/<model> # OPENAI_API_KEY
|
|
221
|
+
lcode config set escalate_model xai/<model> # XAI_API_KEY
|
|
222
|
+
lcode config set escalate_model ollama/qwen3.8-27b # a bigger local model: free, and nothing leaves
|
|
223
|
+
lcode config set escalate_price 3/15 # dollars per million tokens in/out, for the cost
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**When.** lcode offers it when the [loop breaker](how-it-works.md#when-the-model-repeats-itself)
|
|
227
|
+
would stop the request, and when the project's [checks](#checks-before-done) fail twice in a row
|
|
228
|
+
after the model's fixes, at most once per request. You can also ask at any time: `/escalate`, or
|
|
229
|
+
`/escalate the window still grows` to say what's wrong.
|
|
230
|
+
|
|
231
|
+
**Nothing is sent without asking**, in any permission mode:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
╭─ Ask claude-… for help? ──────────────────────────────────────────────────╮
|
|
235
|
+
│ Why: the project's checks (`pytest -q`) failed 2 times in a row │
|
|
236
|
+
│ What goes: your requests, the task sheet, the failure, the last steps │
|
|
237
|
+
│ and these files: wall.py, simulate.py (3.1K tokens) │
|
|
238
|
+
│ Where: claude-… (Anthropic), at api.anthropic.com │
|
|
239
|
+
│ Cost: about $0.009 to $0.071 (3.1K tokens in at $3/M, up to 4.1K out …) │
|
|
240
|
+
╰───────────────────────────────────────────────────────────────────────────╯
|
|
241
|
+
Send it? [y]es / [n]o / [s]how what's sent:
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **What goes:** your last requests, the [task sheet](how-it-works.md#context-management), why
|
|
245
|
+
it's stuck (the failing check's output, the repeated call), the last few steps, and the files the
|
|
246
|
+
request changed or read: never the whole session or the rest of the repository. Press `s` to read
|
|
247
|
+
all of it before you decide.
|
|
248
|
+
- **Keys** come from environment variables, are read when needed, and go only to their own
|
|
249
|
+
provider. lcode never stores them.
|
|
250
|
+
- **Cost.** The token count is always shown; the price only when you set `escalate_price`, since
|
|
251
|
+
prices change. A local model is free.
|
|
252
|
+
- **Another server:** `escalate_url` points at another Ollama (`ollama/<model>`) or any OpenAI
|
|
253
|
+
API (`custom/<model>`, key in `LCODE_ESCALATE_API_KEY`).
|
|
254
|
+
- `lcode -p` can't ask, so it doesn't escalate unless you add `--escalate`, which is your OK for
|
|
255
|
+
that run. `--output json` lists what was asked in `escalations`.
|
|
256
|
+
|
|
211
257
|
## Background commands
|
|
212
258
|
|
|
213
259
|
Some commands keep running: a dev server, a test watcher, a build in watch mode. The model starts
|
|
@@ -82,6 +82,7 @@ ignore = [
|
|
|
82
82
|
"src/lcode/subagents.py" = ["E501"]
|
|
83
83
|
"src/lcode/planning.py" = ["E501"]
|
|
84
84
|
"src/lcode/review.py" = ["E501"] # the review prompt is prose
|
|
85
|
+
"src/lcode/escalate.py" = ["E501"] # the advisor prompt is prose
|
|
85
86
|
"src/lcode/extensions.py" = ["E501"]
|
|
86
87
|
"src/lcode/context.py" = ["E501"]
|
|
87
88
|
"src/lcode/lsp.py" = ["E501"]
|
|
@@ -29,6 +29,7 @@ from lcode import (
|
|
|
29
29
|
catalog,
|
|
30
30
|
checks,
|
|
31
31
|
codesearch,
|
|
32
|
+
escalate,
|
|
32
33
|
extensions,
|
|
33
34
|
limits,
|
|
34
35
|
notify,
|
|
@@ -307,6 +308,9 @@ class Settings:
|
|
|
307
308
|
repeat_limit: int = 3 # identical calls with identical results before the model is told (lcode.repeats)
|
|
308
309
|
checks: str = "off" # the project's checks after a change (lcode.checks): auto, off or a command
|
|
309
310
|
review: str = "off" # check a change against the request before the answer (lcode.review): auto or off
|
|
311
|
+
escalate_model: str | None = None # a stronger model to ask when stuck (lcode.escalate): provider/model
|
|
312
|
+
escalate_url: str | None = None # its server, when not the provider's usual one
|
|
313
|
+
escalate_price: str | None = None # dollars per million tokens in/out, e.g. "3/15", for the cost estimate
|
|
310
314
|
|
|
311
315
|
|
|
312
316
|
class Agent:
|
|
@@ -369,6 +373,8 @@ class Agent:
|
|
|
369
373
|
self._check: tuple[Path, checks.Check | None] | None = None
|
|
370
374
|
self.check_results: list[dict] = [] # the checks of the last request: command, exit code, who ran them
|
|
371
375
|
self.review_results: list[str] | None = None # what the last request's review found (None: no review)
|
|
376
|
+
self.escalations: list[dict] = [] # the stronger model asked in the last request (lcode.escalate)
|
|
377
|
+
self.escalate_approved: bool | None = None # decided in advance (lcode -p --escalate, the benchmark)
|
|
372
378
|
self.sent_chars = 0 # characters of the conversation the model had already read
|
|
373
379
|
self.counted_chars: int | None = None # characters that ctx_used covers (None: all of them)
|
|
374
380
|
self.turn_status = "" # how the last request ended: success, max_steps, loop
|
|
@@ -952,9 +958,8 @@ class Agent:
|
|
|
952
958
|
)
|
|
953
959
|
return None
|
|
954
960
|
if round_ >= checks.MAX_ROUNDS:
|
|
955
|
-
self.say
|
|
956
|
-
|
|
957
|
-
)
|
|
961
|
+
then = "" if self.can_escalate() else ": the model will say what's left"
|
|
962
|
+
self.say(f"Checks still fail ({check.command}, exit code {code}){then}.", warning=True)
|
|
958
963
|
else:
|
|
959
964
|
self.say(
|
|
960
965
|
f"Checks fail ({check.command}, exit code {code}): the model gets the output to fix it.", warning=True
|
|
@@ -1103,6 +1108,8 @@ class Agent:
|
|
|
1103
1108
|
self.repeats.reset()
|
|
1104
1109
|
self.check_results = []
|
|
1105
1110
|
self.review_results = None
|
|
1111
|
+
self.escalations = []
|
|
1112
|
+
escalated = False # the stronger model is asked at most once per request
|
|
1106
1113
|
self.tools.before = {}
|
|
1107
1114
|
reviewed = False
|
|
1108
1115
|
changed = verified = False # files changed in this request; the checks passed since
|
|
@@ -1166,6 +1173,15 @@ class Agent:
|
|
|
1166
1173
|
check_rounds += 1
|
|
1167
1174
|
feedback = self.run_check(check, check_rounds)
|
|
1168
1175
|
if feedback:
|
|
1176
|
+
if check_rounds >= checks.MAX_ROUNDS and not escalated and self.can_escalate():
|
|
1177
|
+
escalated = True
|
|
1178
|
+
why = f"the project's checks (`{check.command}`) failed {check_rounds} times in a row"
|
|
1179
|
+
advice = escalate.run(self, why, failure=feedback)
|
|
1180
|
+
if advice:
|
|
1181
|
+
self.messages.append({"role": "user", "content": advice})
|
|
1182
|
+
check_rounds -= 1 # the checks run once more after the advice
|
|
1183
|
+
changed = False
|
|
1184
|
+
continue
|
|
1169
1185
|
self.messages.append({"role": "user", "content": feedback})
|
|
1170
1186
|
changed = False # run them again only if the model changes something more
|
|
1171
1187
|
continue
|
|
@@ -1259,6 +1275,14 @@ class Agent:
|
|
|
1259
1275
|
skipped["tool_call_id"] = rest["id"]
|
|
1260
1276
|
self.messages.append(skipped)
|
|
1261
1277
|
break
|
|
1278
|
+
if looping and not escalated and self.can_escalate():
|
|
1279
|
+
escalated = True
|
|
1280
|
+
advice = escalate.run(self, f"it kept making the same call with the same result: {looping}")
|
|
1281
|
+
if advice:
|
|
1282
|
+
self.messages.append({"role": "user", "content": advice})
|
|
1283
|
+
self.repeats.reset()
|
|
1284
|
+
looping = ""
|
|
1285
|
+
continue
|
|
1262
1286
|
if looping:
|
|
1263
1287
|
self.turn_status = "loop"
|
|
1264
1288
|
self.console.print(
|
|
@@ -1276,6 +1300,11 @@ class Agent:
|
|
|
1276
1300
|
self.turn_status = "success"
|
|
1277
1301
|
self.print_stats()
|
|
1278
1302
|
|
|
1303
|
+
def can_escalate(self) -> bool:
|
|
1304
|
+
"""Whether a stronger model may be asked here: one is set, and this isn't a subagent or plan mode."""
|
|
1305
|
+
model = (self.settings.escalate_model or "").strip().lower()
|
|
1306
|
+
return bool(model) and model != "off" and self.allowed_tools is None and not self.planning()
|
|
1307
|
+
|
|
1279
1308
|
def steer(self) -> None:
|
|
1280
1309
|
"""Between two steps: pause if the user pressed Esc, then hand the model the notes typed meanwhile."""
|
|
1281
1310
|
steering = self.steering
|
|
@@ -78,6 +78,7 @@ class Result:
|
|
|
78
78
|
usage: dict = field(default_factory=dict) # requests, prompt_tokens, output_tokens
|
|
79
79
|
checks: list[dict] = field(default_factory=list) # {"command", "exit_code", "by": "lcode" | "model"}
|
|
80
80
|
review: list[str] | None = None # problems the review sent back ([] when it found none; None: no review)
|
|
81
|
+
escalations: list[dict] = field(default_factory=list) # a stronger model asked: model, why, tokens, seconds
|
|
81
82
|
seconds: float = 0.0
|
|
82
83
|
error: str = ""
|
|
83
84
|
|
|
@@ -200,6 +201,9 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
200
201
|
repeat_limit=cfg["repeat_limit"],
|
|
201
202
|
checks=cfg["checks"],
|
|
202
203
|
review=cfg["review"],
|
|
204
|
+
escalate_model=cfg["escalate_model"],
|
|
205
|
+
escalate_url=cfg["escalate_url"],
|
|
206
|
+
escalate_price=cfg["escalate_price"],
|
|
203
207
|
compact_at=cfg["compact_at"],
|
|
204
208
|
)
|
|
205
209
|
agent = Agent(ollama, settings, cwd, console=console)
|
|
@@ -309,6 +313,7 @@ def run_request(agent: Agent, prompt: str) -> Result:
|
|
|
309
313
|
usage=usage,
|
|
310
314
|
checks=list(agent.check_results),
|
|
311
315
|
review=agent.review_results,
|
|
316
|
+
escalations=list(agent.escalations),
|
|
312
317
|
seconds=round(time.monotonic() - started, 2),
|
|
313
318
|
error=error,
|
|
314
319
|
)
|
|
@@ -265,11 +265,16 @@ class OpenAICompatible:
|
|
|
265
265
|
return self.chat({k: v for k, v in payload.items() if k not in ("format", "tools")}, timeout)
|
|
266
266
|
if r.status_code != 200:
|
|
267
267
|
raise self._failed(r.status_code, r.text)
|
|
268
|
-
|
|
268
|
+
data = r.json()
|
|
269
|
+
choice = (data.get("choices") or [{}])[0].get("message") or {}
|
|
269
270
|
message: dict = {"role": "assistant", "content": strip_thinking(choice.get("content") or "")}
|
|
270
271
|
if choice.get("tool_calls"):
|
|
271
272
|
message["tool_calls"] = [self._ollama_call(c) for c in choice["tool_calls"]]
|
|
272
|
-
|
|
273
|
+
reply: dict = {"message": message}
|
|
274
|
+
usage = data.get("usage") or {}
|
|
275
|
+
if usage.get("prompt_tokens") is not None: # as Ollama counts them
|
|
276
|
+
reply["prompt_eval_count"], reply["eval_count"] = usage["prompt_tokens"], usage.get("completion_tokens")
|
|
277
|
+
return reply
|
|
273
278
|
|
|
274
279
|
@staticmethod
|
|
275
280
|
def _ollama_call(call: dict) -> dict:
|
|
@@ -29,7 +29,7 @@ from rich.markup import escape
|
|
|
29
29
|
from rich.table import Table
|
|
30
30
|
from rich.text import Text
|
|
31
31
|
|
|
32
|
-
from lcode import __version__
|
|
32
|
+
from lcode import __version__, escalate
|
|
33
33
|
from lcode.agent import Agent, Settings
|
|
34
34
|
from lcode.config import format_tokens
|
|
35
35
|
from lcode.hardware import Hardware
|
|
@@ -790,6 +790,8 @@ class TaskResult:
|
|
|
790
790
|
tool_errors: int = 0 # failed tool calls, including malformed ones
|
|
791
791
|
error: str | None = None # why the run stopped early: a timeout or an Ollama error
|
|
792
792
|
errors_by_tool: dict[str, int] = field(default_factory=dict) # which tools failed, "malformed" for unreadable calls
|
|
793
|
+
first_passed: bool | None = None # with --retry: whether the first try passed (None: no retry was needed)
|
|
794
|
+
escalations: int = 0 # times the stronger model was asked (--escalate)
|
|
793
795
|
|
|
794
796
|
|
|
795
797
|
@dataclass
|
|
@@ -887,8 +889,13 @@ def run_task(
|
|
|
887
889
|
keep: bool,
|
|
888
890
|
usage: dict,
|
|
889
891
|
agent: Agent | None = None,
|
|
892
|
+
retry: bool = False,
|
|
890
893
|
) -> TaskResult:
|
|
891
|
-
"""Run one task in a new folder: in a new conversation, or continuing `agent`'s (--session).
|
|
894
|
+
"""Run one task in a new folder: in a new conversation, or continuing `agent`'s (--session).
|
|
895
|
+
|
|
896
|
+
With `retry`, a failed task gets one more try, as a user would ask: "That didn't fix it. Look again." With a
|
|
897
|
+
stronger model set (--escalate), its advice goes along, as with /escalate.
|
|
898
|
+
"""
|
|
892
899
|
folder = Path(tempfile.mkdtemp(prefix=f"lcode-bench-{task.id}-"))
|
|
893
900
|
try:
|
|
894
901
|
for rel, content in task.files.items():
|
|
@@ -906,25 +913,28 @@ def run_task(
|
|
|
906
913
|
if model:
|
|
907
914
|
codesearch.Index(folder, model).update(ollama) # on the CPU: lcode's model stays loaded
|
|
908
915
|
agent._code_index = None
|
|
916
|
+
agent.escalate_approved = True if settings.escalate_model else None # the --escalate flag is the OK
|
|
909
917
|
before = dict(agent.usage)
|
|
910
918
|
first = len(agent.messages)
|
|
911
|
-
error = None
|
|
912
919
|
started = time.monotonic()
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
+
escalations = 0
|
|
921
|
+
error = attempt(agent, task.prompt, timeout)
|
|
922
|
+
escalations += len(agent.escalations)
|
|
923
|
+
check = checked(task, folder, agent)
|
|
924
|
+
first_passed = None
|
|
925
|
+
if retry and not (check.passed and error is None):
|
|
926
|
+
first_passed = False
|
|
927
|
+
text = RETRY
|
|
928
|
+
if error is None and agent.can_escalate():
|
|
929
|
+
advice = escalate.run(agent, "the user says the fix didn't work")
|
|
930
|
+
escalations += 1 if advice else 0
|
|
931
|
+
text = f"{RETRY}\n\n{advice}" if advice else RETRY
|
|
932
|
+
error = attempt(agent, text, timeout)
|
|
933
|
+
escalations += len(agent.escalations)
|
|
934
|
+
check = checked(task, folder, agent)
|
|
920
935
|
seconds = time.monotonic() - started
|
|
921
936
|
for key, value in agent.usage.items():
|
|
922
937
|
usage[key] = usage.get(key, 0) + value - before.get(key, 0)
|
|
923
|
-
answer = next((m.get("content", "") for m in reversed(agent.messages) if m.get("role") == "assistant"), "")
|
|
924
|
-
try:
|
|
925
|
-
check = task.check(folder, answer)
|
|
926
|
-
except Exception as e: # a check must never crash the benchmark
|
|
927
|
-
check = Check(False, f"check failed: {type(e).__name__}: {e}")
|
|
928
938
|
finally: # also when Ctrl+C stops the task
|
|
929
939
|
from lcode.codesearch import index_dir
|
|
930
940
|
|
|
@@ -942,9 +952,34 @@ def run_task(
|
|
|
942
952
|
tool_errors=count_tool_errors(agent.messages[first:] if len(agent.messages) > first else agent.messages),
|
|
943
953
|
errors_by_tool=errors_by_tool(agent.messages[first:] if len(agent.messages) > first else agent.messages),
|
|
944
954
|
error=error,
|
|
955
|
+
first_passed=first_passed,
|
|
956
|
+
escalations=escalations,
|
|
945
957
|
)
|
|
946
958
|
|
|
947
959
|
|
|
960
|
+
RETRY = "That didn't fix it: it still doesn't work right. Look again."
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def attempt(agent: Agent, text: str, timeout: float) -> str | None:
|
|
964
|
+
"""One request of a task; why it stopped early, if it did."""
|
|
965
|
+
try:
|
|
966
|
+
with time_limit(timeout):
|
|
967
|
+
agent.run_turn(text)
|
|
968
|
+
except TaskTimeout:
|
|
969
|
+
return f"timed out after {timeout:.0f}s"
|
|
970
|
+
except OllamaError as e:
|
|
971
|
+
return str(e).splitlines()[0][:200]
|
|
972
|
+
return None
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def checked(task: Task, folder: Path, agent: Agent) -> Check:
|
|
976
|
+
answer = next((m.get("content", "") for m in reversed(agent.messages) if m.get("role") == "assistant"), "")
|
|
977
|
+
try:
|
|
978
|
+
return task.check(folder, answer)
|
|
979
|
+
except Exception as e: # a check must never crash the benchmark
|
|
980
|
+
return Check(False, f"check failed: {type(e).__name__}: {e}")
|
|
981
|
+
|
|
982
|
+
|
|
948
983
|
def prompt_speed(ollama: Ollama, settings: Settings, options: dict) -> float | None:
|
|
949
984
|
"""Prompt tokens per second for prompts the model hasn't seen: the faster of two readings.
|
|
950
985
|
|
|
@@ -998,6 +1033,7 @@ def run_model(
|
|
|
998
1033
|
keep: bool = False,
|
|
999
1034
|
verbose: bool = False,
|
|
1000
1035
|
session: bool = False,
|
|
1036
|
+
retry: bool = False,
|
|
1001
1037
|
) -> ModelRun:
|
|
1002
1038
|
run = ModelRun(name, settings.model, settings.context, settings.num_batch, settings.think, session=session)
|
|
1003
1039
|
shared: Agent | None = None # with --session, one conversation for all tasks
|
|
@@ -1038,10 +1074,10 @@ def run_model(
|
|
|
1038
1074
|
try:
|
|
1039
1075
|
if verbose:
|
|
1040
1076
|
console.rule(f"{task.id}: {task.title}")
|
|
1041
|
-
result = run_task(ollama, settings, task, timeout, log, keep, run.usage, shared)
|
|
1077
|
+
result = run_task(ollama, settings, task, timeout, log, keep, run.usage, shared, retry)
|
|
1042
1078
|
else:
|
|
1043
1079
|
with console.status(Elapsed(f" {task.id:<13} {task.title}")):
|
|
1044
|
-
result = run_task(ollama, settings, task, timeout, log, keep, run.usage, shared)
|
|
1080
|
+
result = run_task(ollama, settings, task, timeout, log, keep, run.usage, shared, retry)
|
|
1045
1081
|
except KeyboardInterrupt:
|
|
1046
1082
|
console.print(" [yellow]Stopped.[/]")
|
|
1047
1083
|
run.interrupted = True
|
|
@@ -582,11 +582,14 @@ def cmd_bench(args) -> None:
|
|
|
582
582
|
compact_at=cfg["compact_at"] if args.compact_at is None else args.compact_at,
|
|
583
583
|
checks=cfg["checks"] if args.checks is None else args.checks,
|
|
584
584
|
review=cfg["review"] if args.review is None else args.review,
|
|
585
|
+
escalate_model=args.escalate_model,
|
|
586
|
+
escalate_url=cfg["escalate_url"],
|
|
587
|
+
escalate_price=cfg["escalate_price"],
|
|
585
588
|
repo_map=args.repo_map,
|
|
586
589
|
embed_model=cfg["embed_model"] if args.code_search else "off",
|
|
587
590
|
)
|
|
588
591
|
run = bench.run_model(
|
|
589
|
-
ollama, name, settings, tasks, console, args.timeout, args.keep, args.verbose, args.session
|
|
592
|
+
ollama, name, settings, tasks, console, args.timeout, args.keep, args.verbose, args.session, args.retry
|
|
590
593
|
)
|
|
591
594
|
runs.append(run)
|
|
592
595
|
if run.interrupted:
|
|
@@ -662,6 +665,8 @@ def cmd_chat(args) -> None:
|
|
|
662
665
|
sys.exit(1)
|
|
663
666
|
fail(str(e))
|
|
664
667
|
exit_code = 0
|
|
668
|
+
if args.escalate and args.prompt:
|
|
669
|
+
agent.escalate_approved = True # the flag is the user's OK for this run
|
|
665
670
|
try:
|
|
666
671
|
if quiet:
|
|
667
672
|
agent.perms.approve = lambda request: False # nobody to ask: refuse what needs permission
|
|
@@ -727,6 +732,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
727
732
|
parser.add_argument("--plan", action="store_true", help="start in plan mode: agree on a plan before any change")
|
|
728
733
|
parser.add_argument("--auto-edit", action="store_true", help="apply file edits without asking")
|
|
729
734
|
parser.add_argument("--yolo", action="store_true", help="never ask for permission (edits and commands)")
|
|
735
|
+
parser.add_argument(
|
|
736
|
+
"--escalate",
|
|
737
|
+
action="store_true",
|
|
738
|
+
help="with -p: let lcode ask escalate_model for help when stuck, without asking you (it costs money)",
|
|
739
|
+
)
|
|
730
740
|
parser.add_argument("--no-think", action="store_true", help="disable model reasoning (faster, less accurate)")
|
|
731
741
|
parser.add_argument("--no-web", action="store_true", help="don't let the model search or fetch web pages")
|
|
732
742
|
parser.add_argument("--no-mcp", action="store_true", help="don't start MCP servers in this session")
|
|
@@ -779,6 +789,17 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
|
|
|
779
789
|
p.add_argument(
|
|
780
790
|
"--review", choices=["auto", "off"], help="review each change before it counts (default: the setting)"
|
|
781
791
|
)
|
|
792
|
+
p.add_argument(
|
|
793
|
+
"--retry",
|
|
794
|
+
action="store_true",
|
|
795
|
+
help='give a failed task one more try, as a user would: "That didn\'t fix it. Look again."',
|
|
796
|
+
)
|
|
797
|
+
p.add_argument(
|
|
798
|
+
"--escalate",
|
|
799
|
+
dest="escalate_model",
|
|
800
|
+
metavar="MODEL",
|
|
801
|
+
help="a stronger model to ask when stuck and on --retry, e.g. ollama/qwen3.8-27b (no questions asked)",
|
|
802
|
+
)
|
|
782
803
|
p.add_argument(
|
|
783
804
|
"--checks",
|
|
784
805
|
metavar="auto|off|COMMAND",
|
|
@@ -61,6 +61,14 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
|
61
61
|
"notify_after": (30, int, "seconds a request runs before it notifies (with notify on)"),
|
|
62
62
|
"review": ("off", str, "before the answer, check a change against the request and send problems back: auto | off"),
|
|
63
63
|
"checks": ("auto", str, "run the project's checks before calling work done: auto | off | a command"),
|
|
64
|
+
"escalate_model": (
|
|
65
|
+
None,
|
|
66
|
+
str,
|
|
67
|
+
"a stronger model to ask, with your OK, when the model is stuck: anthropic/…, openai/…, xai/… (key from "
|
|
68
|
+
"ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY) or ollama/<model> (local or at escalate_url)",
|
|
69
|
+
),
|
|
70
|
+
"escalate_url": (None, str, "the stronger model's server, if not its provider's (another Ollama, any OpenAI API)"),
|
|
71
|
+
"escalate_price": (None, str, "its price in dollars per million tokens in/out, e.g. 3/15, for the cost shown"),
|
|
64
72
|
"repeat_limit": (3, int, "identical tool calls with identical results before lcode steps in (0: never)"),
|
|
65
73
|
"secret_check": (True, bool, "before git commit and git push, look for passwords, keys and tokens they'd add"),
|
|
66
74
|
"compact_at": (65536, int, "summarize at this many tokens even when the window is larger (0: only at 85%)"),
|
|
@@ -165,6 +173,22 @@ def coerce(key: str, value: object) -> object:
|
|
|
165
173
|
if value not in MEMORY_MODES:
|
|
166
174
|
raise ConfigError(f"memory must be one of {', '.join(MEMORY_MODES)}")
|
|
167
175
|
return value
|
|
176
|
+
if key in ("escalate_model", "escalate_url", "escalate_price"):
|
|
177
|
+
from lcode import escalate
|
|
178
|
+
|
|
179
|
+
text = str(value).strip()
|
|
180
|
+
if key == "escalate_url":
|
|
181
|
+
return normalize_host(text)
|
|
182
|
+
if key == "escalate_model":
|
|
183
|
+
provider = text.partition("/")[0].lower()
|
|
184
|
+
if "/" in text and provider not in escalate.PROVIDERS:
|
|
185
|
+
raise ConfigError(f"escalate_model's provider must be one of {', '.join(escalate.PROVIDERS)}")
|
|
186
|
+
return text
|
|
187
|
+
try:
|
|
188
|
+
escalate.parse_price(text)
|
|
189
|
+
except escalate.EscalateError as e:
|
|
190
|
+
raise ConfigError(str(e)) from e
|
|
191
|
+
return text
|
|
168
192
|
if key == "lsp" and str(value).lower() not in ("auto", "off"):
|
|
169
193
|
raise ConfigError("lsp must be auto or off")
|
|
170
194
|
if key == "lsp":
|
|
@@ -41,6 +41,8 @@ ERROR_LINE = re.compile(
|
|
|
41
41
|
)
|
|
42
42
|
MAX_ERROR_LINES = 40
|
|
43
43
|
OUTPUT_DAYS = 7 # saved full outputs are deleted after this
|
|
44
|
+
MAX_SAVED = 20_000_000 # characters of one saved output: a runaway command doesn't fill the disk
|
|
45
|
+
MAX_SAVED_TOTAL = 200_000_000 # bytes of saved outputs in all, the oldest go first
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
# ----------------------------------------------------------------------------- the user's requests
|
|
@@ -262,17 +264,38 @@ def save_output(text: str, label: str) -> Path | None:
|
|
|
262
264
|
folder = outputs_dir()
|
|
263
265
|
try:
|
|
264
266
|
folder.mkdir(parents=True, exist_ok=True)
|
|
265
|
-
|
|
266
|
-
for old in folder.glob("*.txt"):
|
|
267
|
-
if old.stat().st_mtime < cutoff:
|
|
268
|
-
old.unlink(missing_ok=True)
|
|
267
|
+
clean_outputs(folder, room=min(len(text), MAX_SAVED))
|
|
269
268
|
path = folder / f"{time.strftime('%Y%m%d-%H%M%S')}-{re.sub(r'[^a-z0-9]+', '-', label.lower())[:30]}.txt"
|
|
270
|
-
path.write_text(text)
|
|
269
|
+
path.write_text(text if len(text) <= MAX_SAVED else text[:MAX_SAVED] + CUT_SAVED)
|
|
271
270
|
return path
|
|
272
271
|
except OSError:
|
|
273
272
|
return None
|
|
274
273
|
|
|
275
274
|
|
|
275
|
+
CUT_SAVED = "\n[lcode stopped saving here: the output went on, but a saved output is kept to 20 MB]\n"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def clean_outputs(folder: Path, room: int = 0) -> None:
|
|
279
|
+
"""Delete saved outputs older than OUTPUT_DAYS, then the oldest until they take MAX_SAVED_TOTAL with `room`."""
|
|
280
|
+
cutoff = time.time() - OUTPUT_DAYS * 86400
|
|
281
|
+
files = []
|
|
282
|
+
for old in folder.glob("*.txt"):
|
|
283
|
+
try:
|
|
284
|
+
info = old.stat()
|
|
285
|
+
except OSError:
|
|
286
|
+
continue
|
|
287
|
+
if info.st_mtime < cutoff:
|
|
288
|
+
old.unlink(missing_ok=True)
|
|
289
|
+
else:
|
|
290
|
+
files.append((info.st_mtime, info.st_size, old))
|
|
291
|
+
total = sum(size for _, size, _ in files) + room
|
|
292
|
+
for _, size, old in sorted(files):
|
|
293
|
+
if total <= MAX_SAVED_TOTAL:
|
|
294
|
+
break
|
|
295
|
+
old.unlink(missing_ok=True)
|
|
296
|
+
total -= size
|
|
297
|
+
|
|
298
|
+
|
|
276
299
|
def shorten(text: str, limit: int, label: str = "output") -> str:
|
|
277
300
|
"""Head, tail and the error-looking lines in between; the full text goes to a file."""
|
|
278
301
|
if len(text) <= limit:
|
|
@@ -315,6 +338,7 @@ class OutputSpool:
|
|
|
315
338
|
self.chars = 0
|
|
316
339
|
self.file = None
|
|
317
340
|
self.path: Path | None = None
|
|
341
|
+
self.saved = 0 # characters written to the file
|
|
318
342
|
|
|
319
343
|
def add(self, line: str) -> None:
|
|
320
344
|
self.chars += len(line)
|
|
@@ -323,10 +347,12 @@ class OutputSpool:
|
|
|
323
347
|
if self.chars > self.limit * 2:
|
|
324
348
|
self._spool()
|
|
325
349
|
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
if self.saved <= MAX_SAVED: # past it, only the end and the error lines are kept (in memory)
|
|
351
|
+
try:
|
|
352
|
+
self.file.write(line if self.saved + len(line) <= MAX_SAVED else CUT_SAVED)
|
|
353
|
+
except OSError:
|
|
354
|
+
pass
|
|
355
|
+
self.saved += len(line)
|
|
330
356
|
if ERROR_LINE.search(line) and len(self.errors) < MAX_ERROR_LINES:
|
|
331
357
|
self.errors.append(line.strip()[:300])
|
|
332
358
|
self.tail.append(line)
|
|
@@ -337,6 +363,7 @@ class OutputSpool:
|
|
|
337
363
|
def _spool(self) -> None:
|
|
338
364
|
text = "".join(self.head)
|
|
339
365
|
self.path = save_output(text, self.label)
|
|
366
|
+
self.saved = len(text)
|
|
340
367
|
if self.path is not None:
|
|
341
368
|
try:
|
|
342
369
|
self.file = self.path.open("a")
|
|
@@ -359,12 +386,10 @@ class OutputSpool:
|
|
|
359
386
|
note += "\n[...]\n\n"
|
|
360
387
|
else:
|
|
361
388
|
note += "] ...\n\n"
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
"
|
|
365
|
-
|
|
366
|
-
else ""
|
|
367
|
-
)
|
|
389
|
+
whole = f"The full {self.label} ({self.chars} characters)"
|
|
390
|
+
if self.saved > MAX_SAVED:
|
|
391
|
+
whole = f"The first {MAX_SAVED // 1_000_000} MB of the {self.label} ({self.chars} characters in all)"
|
|
392
|
+
where = f"\n[{whole} is in {self.path}: read parts of it with read_file if you need them.]" if self.path else ""
|
|
368
393
|
return f"{head}{note}{tail}{where}"
|
|
369
394
|
|
|
370
395
|
|