lcode-cli 0.33.1__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.33.1 → lcode_cli-0.35.0}/CHANGELOG.md +50 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/PKG-INFO +1 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/bench.md +16 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/configuration.md +3 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/usage.md +84 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/pyproject.toml +1 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/agent.py +64 -8
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/api.py +5 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/backends.py +7 -2
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/bench.py +53 -17
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/cli.py +22 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/config.py +24 -0
- {lcode_cli-0.33.1 → 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.33.1 → lcode_cli-0.35.0}/src/lcode/repl.py +38 -1
- lcode_cli-0.35.0/src/lcode/screenshots.py +1098 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/tools.py +113 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/conftest.py +1 -1
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_agent.py +19 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_bench.py +2 -0
- {lcode_cli-0.33.1 → 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.35.0/tests/test_screenshots.py +386 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_vision.py +2 -2
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.editorconfig +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.gitignore +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/AGENTS.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/LICENSE +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/README.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/SECURITY.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/action.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/agents.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/automation.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/changelog.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/commands.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/contributing.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/editors.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/git.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/hooks.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/index.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/installation.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/lsp.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/mcp.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/memory.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/models.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/search.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/servers.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/examples/README.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/install.sh +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/mkdocs.yml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/overrides/main.html +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/scripts/make_logos.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/residency.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/review.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/steer.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_action.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_api.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_backends.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_config.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_residency.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_review.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_steer.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_update.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_web.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.33.1 → lcode_cli-0.35.0}/uv.lock +0 -0
|
@@ -6,6 +6,53 @@ 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
|
+
|
|
38
|
+
## [0.34.0] - 2026-10-14
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Screenshots without a screen: a `screenshot` tool runs a desktop program or opens a web page the
|
|
43
|
+
way you would see it, saves the shot in `.lcode/screenshots/`, and returns what it shows with the
|
|
44
|
+
sizes behind it. For a Python Qt program, that's each window against the screen, the size its
|
|
45
|
+
layout needs, and the widgets that stick out or overlap. For a web page or a local server
|
|
46
|
+
(headless Chrome), it's the page's size, what scrolls sideways, and the console errors and
|
|
47
|
+
failed requests. Other desktop programs run on a virtual display (Xvfb). See
|
|
48
|
+
[Screenshots](https://nasser1941.github.io/lcode/usage/#screenshots).
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- In a project with a virtual environment (`.venv`, `venv` or `env`), the model is told to run its
|
|
53
|
+
Python (`.venv/bin/python`) rather than plain `python`, which lacks the project's packages. Found
|
|
54
|
+
when the model checked a PySide6 app with the system Python, failed, and stopped checking.
|
|
55
|
+
|
|
9
56
|
## [0.33.1] - 2026-10-14
|
|
10
57
|
|
|
11
58
|
### Changed
|
|
@@ -722,7 +769,9 @@ First public release.
|
|
|
722
769
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
723
770
|
- One-line installer for Linux and macOS.
|
|
724
771
|
|
|
725
|
-
[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
|
|
774
|
+
[0.34.0]: https://github.com/nasser1941/lcode/compare/v0.33.1...v0.34.0
|
|
726
775
|
[0.33.1]: https://github.com/nasser1941/lcode/compare/v0.33.0...v0.33.1
|
|
727
776
|
[0.33.0]: https://github.com/nasser1941/lcode/compare/v0.32.0...v0.33.0
|
|
728
777
|
[0.32.0]: https://github.com/nasser1941/lcode/compare/v0.31.0...v0.32.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 |
|
|
@@ -106,6 +107,7 @@ reasoning is on.
|
|
|
106
107
|
| `web_search` | Search the web for current information (needs a [search provider](#web-search)) |
|
|
107
108
|
| `web_fetch` | Read a web page or text file by URL as clean text |
|
|
108
109
|
| `view_image` | Look at an image: a model that can see describes it ([Images](#images)) |
|
|
110
|
+
| `screenshot` | Run a desktop program or open a web page without a screen, and see it with its sizes ([Screenshots](#screenshots)) |
|
|
109
111
|
| `agent` | Hand a task to a [subagent](agents.md) with its own context; only its report comes back |
|
|
110
112
|
| `memory` | Save, update or delete [notes](memory.md) that later sessions load |
|
|
111
113
|
| `repo_map` | The [repository map](search.md): important files, classes and functions, ranked (larger repositories; small ones get it in the instructions) |
|
|
@@ -207,6 +209,51 @@ The review is asked on top of the conversation rather than in a fresh one. The m
|
|
|
207
209
|
the processed conversation cached, and a separate review would push it out, so the next request
|
|
208
210
|
would have to read the whole conversation again. On a small GPU that's tens of seconds.
|
|
209
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
|
+
|
|
210
257
|
## Background commands
|
|
211
258
|
|
|
212
259
|
Some commands keep running: a dev server, a test watcher, a build in watch mode. The model starts
|
|
@@ -342,6 +389,43 @@ Loading a second model takes a moment: on a 12 GB GPU, describing an image with
|
|
|
342
389
|
`qwen3.6:35b-a3b-coding` takes about a minute, and the next request reloads the main model. Turn
|
|
343
390
|
image support off with `lcode config set vision_model off`.
|
|
344
391
|
|
|
392
|
+
## Screenshots
|
|
393
|
+
|
|
394
|
+
Layout bugs are invisible in the code: a window bigger than the screen, panels cut off, a page that
|
|
395
|
+
scrolls sideways. With the `screenshot` tool the model runs the program, or opens the page, without
|
|
396
|
+
a screen and sees what you would, measured:
|
|
397
|
+
|
|
398
|
+
```text
|
|
399
|
+
● screenshot(python camera_wall.py)
|
|
400
|
+
⎿ .lcode/screenshots/20261014-120211-camera-wall.png (Qt offscreen (PyQt6), 1366x768 screen)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
```text
|
|
404
|
+
- Window "Camera Wall" (CameraWall): 1362x764 at (0, 0)
|
|
405
|
+
Its layout needs at least 1950x984, more than the screen has
|
|
406
|
+
Sticks out of the window: Panel #panel-4: 640x480 at (9, 385)
|
|
407
|
+
Overlap (190x480): Panel #panel-1: 640x480 at (9, 9) and Panel #panel-2: 640x480 at (459, 9)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
| What | How | Besides the picture |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| A Python Qt program (PyQt5/6, PySide2/6) | Qt's offscreen platform, on a screen of the size asked for (Qt 6) | Each window's size and position, the size its layout needs, widgets that stick out or overlap |
|
|
413
|
+
| Any other desktop program (Linux) | A virtual display, Xvfb | The size and position of each window |
|
|
414
|
+
| A web page, an `.html` file or a local server | Headless Chrome, Chromium or Edge (Firefox takes the picture only) | The page's size against the window and what sticks out sideways, console errors, failed requests |
|
|
415
|
+
|
|
416
|
+
- For a web app, the model gives the command that starts the server and the page's URL: lcode
|
|
417
|
+
starts the server, waits for the page to answer, takes the shot and stops the server.
|
|
418
|
+
- A model that can see describes the picture with the model's question in mind (see
|
|
419
|
+
[Which model looks](#images)). Without one, the measurements still come back.
|
|
420
|
+
- Shots are saved in `.lcode/screenshots/` in the project, which keeps itself out of git, so you
|
|
421
|
+
can look too.
|
|
422
|
+
- Running a program asks for permission like any command. A web page that isn't local follows the
|
|
423
|
+
`web` setting. With the [sandbox](sandbox.md) on, programs can't be shown, since they don't run on
|
|
424
|
+
your machine.
|
|
425
|
+
- Needs: Xvfb for other desktop programs (`sudo apt install xvfb x11-apps`), and Chrome or
|
|
426
|
+
Chromium for web pages. On macOS only Qt programs and web pages work, since there's no virtual
|
|
427
|
+
display.
|
|
428
|
+
|
|
345
429
|
## Permissions
|
|
346
430
|
|
|
347
431
|
| Mode | File edits | Shell commands |
|
|
@@ -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,
|
|
@@ -48,7 +49,7 @@ from lcode.config import format_tokens
|
|
|
48
49
|
from lcode.jobs import Jobs
|
|
49
50
|
from lcode.mcp import McpManager
|
|
50
51
|
from lcode.ollama import Ollama, OllamaError
|
|
51
|
-
from lcode.permissions import Permissions
|
|
52
|
+
from lcode.permissions import LEADING_CD, Permissions
|
|
52
53
|
from lcode.render import MarkdownStreamer
|
|
53
54
|
from lcode.repeats import Repeats
|
|
54
55
|
from lcode.sandbox import Sandbox, SandboxError, project_root
|
|
@@ -122,7 +123,7 @@ SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent runni
|
|
|
122
123
|
# Verifying your work
|
|
123
124
|
- A syntax check or an import only shows that code loads. Run what you changed the way it will be used (the script, the tests, the app) and fix what fails.
|
|
124
125
|
- To fix a bug, first reproduce each problem the user reports with a test, a command or a short script, then run the same checks to show they're gone.
|
|
125
|
-
- For what you can't see, like a GUI, a web page or a layout,
|
|
126
|
+
- For what you can't see, like a GUI, a web page or a layout, use the screenshot tool: it runs the program (or opens the page) without a screen and returns what it shows, with the sizes of windows, widgets or the page measured{look}. Measure instead of assuming.
|
|
126
127
|
- Never claim something works, mark a todo done, or call it tested unless you ran it. If you couldn't check something, say so and tell the user exactly what to look for. If something fails, say so and show the relevant error.{checks}
|
|
127
128
|
|
|
128
129
|
# Communication
|
|
@@ -131,7 +132,7 @@ SYSTEM_PROMPT = """You are lcode, an autonomous software-engineering agent runni
|
|
|
131
132
|
|
|
132
133
|
# Environment
|
|
133
134
|
- Working directory: {cwd}
|
|
134
|
-
- OS: {os}; shell: bash; Python command:
|
|
135
|
+
- OS: {os}; shell: bash; Python command: {python}
|
|
135
136
|
- Date: {date}
|
|
136
137
|
- Git: {git}
|
|
137
138
|
|
|
@@ -218,6 +219,30 @@ def call_arguments(call: dict) -> dict:
|
|
|
218
219
|
return args if isinstance(args, dict) else {}
|
|
219
220
|
|
|
220
221
|
|
|
222
|
+
VENV_DIRS = (".venv", "venv", "env")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def project_python(cwd: Path) -> Path | None:
|
|
226
|
+
"""The project's virtual environment's Python (.venv, venv or env, here or up to the repository's root)."""
|
|
227
|
+
for folder in [cwd, *cwd.parents][:4]:
|
|
228
|
+
for name in VENV_DIRS:
|
|
229
|
+
path = folder / name / "bin" / "python"
|
|
230
|
+
if path.exists() and (folder / name / "pyvenv.cfg").exists():
|
|
231
|
+
return path
|
|
232
|
+
if (folder / ".git").exists():
|
|
233
|
+
break # not beyond the repository
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def python_line(cwd: Path) -> str:
|
|
238
|
+
"""How the system prompt names the Python command."""
|
|
239
|
+
venv = project_python(cwd)
|
|
240
|
+
if venv is None:
|
|
241
|
+
return f"`{'python' if shutil.which('python') else 'python3'}`"
|
|
242
|
+
shown = venv.relative_to(cwd) if cwd in venv.parents else venv
|
|
243
|
+
return f"`{shown}` (the project's virtual environment: plain python lacks its packages)"
|
|
244
|
+
|
|
245
|
+
|
|
221
246
|
class Progress:
|
|
222
247
|
"""The status line under a running request.
|
|
223
248
|
|
|
@@ -283,6 +308,9 @@ class Settings:
|
|
|
283
308
|
repeat_limit: int = 3 # identical calls with identical results before the model is told (lcode.repeats)
|
|
284
309
|
checks: str = "off" # the project's checks after a change (lcode.checks): auto, off or a command
|
|
285
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
|
|
286
314
|
|
|
287
315
|
|
|
288
316
|
class Agent:
|
|
@@ -345,6 +373,8 @@ class Agent:
|
|
|
345
373
|
self._check: tuple[Path, checks.Check | None] | None = None
|
|
346
374
|
self.check_results: list[dict] = [] # the checks of the last request: command, exit code, who ran them
|
|
347
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)
|
|
348
378
|
self.sent_chars = 0 # characters of the conversation the model had already read
|
|
349
379
|
self.counted_chars: int | None = None # characters that ctx_used covers (None: all of them)
|
|
350
380
|
self.turn_status = "" # how the last request ended: success, max_steps, loop
|
|
@@ -372,7 +402,7 @@ class Agent:
|
|
|
372
402
|
model=self.settings.model,
|
|
373
403
|
cwd=self.cwd,
|
|
374
404
|
os=f"{platform.system()} {platform.release()} ({platform.machine()})",
|
|
375
|
-
python=
|
|
405
|
+
python=python_line(self.cwd),
|
|
376
406
|
date=dt.date.today().isoformat(),
|
|
377
407
|
git=git_info(self.cwd),
|
|
378
408
|
tree=tree(self.cwd, depth=1, limit=120),
|
|
@@ -383,7 +413,7 @@ class Agent:
|
|
|
383
413
|
skills=extensions.skills_prompt(self.extensions().skills, self.settings.context),
|
|
384
414
|
code=self.code_prompt(),
|
|
385
415
|
map=self.map_prompt(),
|
|
386
|
-
look="
|
|
416
|
+
look="; look at other images with view_image" if self.vision_model() else "",
|
|
387
417
|
checks=self.checks_prompt(),
|
|
388
418
|
)
|
|
389
419
|
|
|
@@ -928,9 +958,8 @@ class Agent:
|
|
|
928
958
|
)
|
|
929
959
|
return None
|
|
930
960
|
if round_ >= checks.MAX_ROUNDS:
|
|
931
|
-
self.say
|
|
932
|
-
|
|
933
|
-
)
|
|
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)
|
|
934
963
|
else:
|
|
935
964
|
self.say(
|
|
936
965
|
f"Checks fail ({check.command}, exit code {code}): the model gets the output to fix it.", warning=True
|
|
@@ -1000,6 +1029,9 @@ class Agent:
|
|
|
1000
1029
|
return f"agent({args.get('type', '')}: {args.get('description') or str(args.get('task', ''))[:50]})"
|
|
1001
1030
|
if name == "bash":
|
|
1002
1031
|
return f"$ {str(args.get('command', ''))[:60]}"
|
|
1032
|
+
if name == "screenshot":
|
|
1033
|
+
what = args.get("url") or LEADING_CD.sub("", str(args.get("command") or ""))
|
|
1034
|
+
return f"screenshot({what[:60]})"
|
|
1003
1035
|
if name in ("web_search", "web_fetch", "view_image"):
|
|
1004
1036
|
return f"{name}({args.get('query') or args.get('url') or args.get('path') or ''})"
|
|
1005
1037
|
if self.mcp and self.mcp.owns(name):
|
|
@@ -1076,6 +1108,8 @@ class Agent:
|
|
|
1076
1108
|
self.repeats.reset()
|
|
1077
1109
|
self.check_results = []
|
|
1078
1110
|
self.review_results = None
|
|
1111
|
+
self.escalations = []
|
|
1112
|
+
escalated = False # the stronger model is asked at most once per request
|
|
1079
1113
|
self.tools.before = {}
|
|
1080
1114
|
reviewed = False
|
|
1081
1115
|
changed = verified = False # files changed in this request; the checks passed since
|
|
@@ -1139,6 +1173,15 @@ class Agent:
|
|
|
1139
1173
|
check_rounds += 1
|
|
1140
1174
|
feedback = self.run_check(check, check_rounds)
|
|
1141
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
|
|
1142
1185
|
self.messages.append({"role": "user", "content": feedback})
|
|
1143
1186
|
changed = False # run them again only if the model changes something more
|
|
1144
1187
|
continue
|
|
@@ -1232,6 +1275,14 @@ class Agent:
|
|
|
1232
1275
|
skipped["tool_call_id"] = rest["id"]
|
|
1233
1276
|
self.messages.append(skipped)
|
|
1234
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
|
|
1235
1286
|
if looping:
|
|
1236
1287
|
self.turn_status = "loop"
|
|
1237
1288
|
self.console.print(
|
|
@@ -1249,6 +1300,11 @@ class Agent:
|
|
|
1249
1300
|
self.turn_status = "success"
|
|
1250
1301
|
self.print_stats()
|
|
1251
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
|
+
|
|
1252
1308
|
def steer(self) -> None:
|
|
1253
1309
|
"""Between two steps: pause if the user pressed Esc, then hand the model the notes typed meanwhile."""
|
|
1254
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
|