chad-code 1.0.4__tar.gz → 1.0.5__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.
- {chad_code-1.0.4 → chad_code-1.0.5}/PKG-INFO +22 -9
- {chad_code-1.0.4 → chad_code-1.0.5}/README.md +21 -8
- {chad_code-1.0.4 → chad_code-1.0.5}/pyproject.toml +1 -1
- chad_code-1.0.5/src/chad/__init__.py +14 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/agent.py +132 -42
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/base_engine.py +67 -7
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/bench.py +2 -2
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/cli.py +261 -124
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/completion_engine.py +168 -15
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/engine.py +61 -52
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/guardrails.py +160 -72
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/levers.py +132 -49
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/mlx_fastpath.py +26 -7
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/mlx_qsdpa.py +333 -32
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/prompt.py +40 -8
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/prove.py +4 -4
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/repomap.py +4 -4
- chad_code-1.0.5/src/chad/serve.py +814 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/syntaxgate.py +18 -19
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/toolcall_parse.py +4 -4
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/tools.py +262 -16
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/tui.py +2 -2
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad_code.egg-info/PKG-INFO +22 -9
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad_code.egg-info/SOURCES.txt +3 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_agent.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_agent_e2e.py +75 -19
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_agent_guards.py +79 -13
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_cli.py +248 -3
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_completion_engine.py +84 -2
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_config.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_confirm_preview.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_done_audit.py +237 -104
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_edit.py +6 -6
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_edit_corruption.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_engine.py +4 -8
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_engine_kvquant.py +0 -8
- chad_code-1.0.5/tests/test_engine_pld_hybrid.py +227 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_gate.py +20 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_intent.py +4 -4
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_lever_bite.py +100 -14
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_mlx_fastpath.py +2 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_mlx_qsdpa.py +75 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_render.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_repomap.py +1 -1
- chad_code-1.0.5/tests/test_serve.py +815 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_session.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_symbols.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_syntaxgate.py +4 -4
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_toolcall_parse.py +2 -2
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_tools.py +204 -17
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_tui.py +1 -1
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_validate.py +1 -1
- chad_code-1.0.4/src/chad/__init__.py +0 -7
- {chad_code-1.0.4 → chad_code-1.0.5}/LICENSE +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/setup.cfg +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/atif.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/compaction.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/config.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/diag.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/ignore.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/lsp.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/mcp.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/mcp_oauth.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/profiles.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/render.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/session.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/skills.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/symbols.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad/validate.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_atif.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_bench.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_compact_notice.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_compaction.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_drift_warn.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_feel_pack.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_garble_invariant.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_ignore.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_levers.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_log_redaction.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_lsp.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_mcp.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_mcp_oauth.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_plan_review.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_prove.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_replace_lines.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_skills.py +0 -0
- {chad_code-1.0.4 → chad_code-1.0.5}/tests/test_subagent.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chad-code
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Local MLX-backed, Claude-Code-style coding agent (Apple Silicon, Ornith 35B/9B)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/nathansutton/chad
|
|
@@ -58,7 +58,7 @@ uvx chad-code # runs chad anywhere — the command is still `chad`
|
|
|
58
58
|
uvx chad-code prove # 2-min offline smoke test: 4 tiny fix-it tasks, verified, timed 🗿
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
First run picks the right Ornith model for your RAM (9B under
|
|
61
|
+
First run picks the right Ornith model for your RAM (9B under 24 GB, 35B at 24 GB+), asks,
|
|
62
62
|
and downloads it once (~5 GB / ~13 GB, resumable) into the shared Hugging Face cache. While
|
|
63
63
|
it downloads, `cd` into a project and think of a scoped first ask — *"fix the failing test
|
|
64
64
|
in `tests/test_x.py`"* lands; *"improve my codebase"* flails.
|
|
@@ -139,16 +139,20 @@ uv run chad -c # resume this directory's last conversation
|
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
**The model.** chad picks one for you by RAM and downloads it once into the shared Hugging
|
|
142
|
-
Face cache (`~/.cache/huggingface`, reused across every project).
|
|
143
|
-
|
|
142
|
+
Face cache (`~/.cache/huggingface`, reused across every project). Override with
|
|
143
|
+
`--model 9b` / `--model 35b`, or `--model <repo or local dir>` for anything else.
|
|
144
144
|
|
|
145
145
|
| Your Mac | Model | Footprint |
|
|
146
146
|
|---|---|---|
|
|
147
|
-
| **≥
|
|
148
|
-
| **16 / 18
|
|
149
|
-
|
|
150
|
-
The 35B's working set
|
|
151
|
-
|
|
147
|
+
| **≥ 24 GB** | [Ornith-1.0-35B `UD-Q2_K_XL`](https://huggingface.co/nathansutton/Ornith-1.0-35B-UD-Q2_K_XL-MLX) — 35B MoE, 2-bit experts | ~13 GB resident (~16 GB with KV) |
|
|
148
|
+
| **16 / 18 GB** | [Ornith-1.0-9B `UD-Q4_K_XL`](https://huggingface.co/nathansutton/Ornith-1.0-9B-UD-Q4_K_XL-MLX) — 4-bit AWQ | ~5 GB |
|
|
149
|
+
|
|
150
|
+
The 35B's floor used to be 32 GB — its working set SIGKILLed a 24 GB Mac mid-turn. The
|
|
151
|
+
fused attention kernel and the 8-bit-from-the-start KV cache it enables cut the per-token
|
|
152
|
+
cache cost enough to give that headroom back, and the compaction trigger now sizes itself
|
|
153
|
+
from the live Metal budget, so a tight box narrows its context window instead of dying.
|
|
154
|
+
24 GB runs the 35B; 16/18 GB still get the 9B. If a 24 GB machine feels tight next to your
|
|
155
|
+
other apps, `--model 9b` puts it back. Quant names follow
|
|
152
156
|
[Unsloth's dynamic-quant convention](https://docs.unsloth.ai/) (`UD-…`).
|
|
153
157
|
|
|
154
158
|
**Upgrading** — depends on how you installed: `uv tool upgrade chad-code`; `uvx --refresh
|
|
@@ -187,8 +191,17 @@ it chad uses the tree-sitter fallback automatically.
|
|
|
187
191
|
| `--plan` | start in read-only plan mode (investigate + propose, edits blocked) |
|
|
188
192
|
| `--yolo` | auto-approve bash/write/edit (skip confirm prompts) |
|
|
189
193
|
| `--no-think` | skip Ornith's `<think>` blocks — faster on well-scoped work |
|
|
194
|
+
| `--model` | `35b`, `9b`, `auto`, or any HF repo id / local model dir |
|
|
190
195
|
| `--repl` | plain line REPL instead of the TUI |
|
|
191
196
|
|
|
197
|
+
Plus three subcommands, each with its own `--help`:
|
|
198
|
+
|
|
199
|
+
| Command | What it does |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `chad serve` | serve this Mac's model to a container or the LAN ([Configuration](docs/configuration.md#serving-the-local-model-to-a-container-chad-serve)) |
|
|
202
|
+
| `chad prove` | 2-minute offline smoke test: 4 tiny fix-it tasks, verified, timed |
|
|
203
|
+
| `chad levers` | print the harness lever registry as JSON (ablation driver) |
|
|
204
|
+
|
|
192
205
|
A headless task (positional, or piped with no TTY) auto-approves mutating tools; the model
|
|
193
206
|
runs greedy (temp 0). Every conversation is persisted under `~/.chad/sessions/`, and every
|
|
194
207
|
resume forks a new branch rather than overwriting — details in
|
|
@@ -21,7 +21,7 @@ uvx chad-code # runs chad anywhere — the command is still `chad`
|
|
|
21
21
|
uvx chad-code prove # 2-min offline smoke test: 4 tiny fix-it tasks, verified, timed 🗿
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
First run picks the right Ornith model for your RAM (9B under
|
|
24
|
+
First run picks the right Ornith model for your RAM (9B under 24 GB, 35B at 24 GB+), asks,
|
|
25
25
|
and downloads it once (~5 GB / ~13 GB, resumable) into the shared Hugging Face cache. While
|
|
26
26
|
it downloads, `cd` into a project and think of a scoped first ask — *"fix the failing test
|
|
27
27
|
in `tests/test_x.py`"* lands; *"improve my codebase"* flails.
|
|
@@ -102,16 +102,20 @@ uv run chad -c # resume this directory's last conversation
|
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
**The model.** chad picks one for you by RAM and downloads it once into the shared Hugging
|
|
105
|
-
Face cache (`~/.cache/huggingface`, reused across every project).
|
|
106
|
-
|
|
105
|
+
Face cache (`~/.cache/huggingface`, reused across every project). Override with
|
|
106
|
+
`--model 9b` / `--model 35b`, or `--model <repo or local dir>` for anything else.
|
|
107
107
|
|
|
108
108
|
| Your Mac | Model | Footprint |
|
|
109
109
|
|---|---|---|
|
|
110
|
-
| **≥
|
|
111
|
-
| **16 / 18
|
|
112
|
-
|
|
113
|
-
The 35B's working set
|
|
114
|
-
|
|
110
|
+
| **≥ 24 GB** | [Ornith-1.0-35B `UD-Q2_K_XL`](https://huggingface.co/nathansutton/Ornith-1.0-35B-UD-Q2_K_XL-MLX) — 35B MoE, 2-bit experts | ~13 GB resident (~16 GB with KV) |
|
|
111
|
+
| **16 / 18 GB** | [Ornith-1.0-9B `UD-Q4_K_XL`](https://huggingface.co/nathansutton/Ornith-1.0-9B-UD-Q4_K_XL-MLX) — 4-bit AWQ | ~5 GB |
|
|
112
|
+
|
|
113
|
+
The 35B's floor used to be 32 GB — its working set SIGKILLed a 24 GB Mac mid-turn. The
|
|
114
|
+
fused attention kernel and the 8-bit-from-the-start KV cache it enables cut the per-token
|
|
115
|
+
cache cost enough to give that headroom back, and the compaction trigger now sizes itself
|
|
116
|
+
from the live Metal budget, so a tight box narrows its context window instead of dying.
|
|
117
|
+
24 GB runs the 35B; 16/18 GB still get the 9B. If a 24 GB machine feels tight next to your
|
|
118
|
+
other apps, `--model 9b` puts it back. Quant names follow
|
|
115
119
|
[Unsloth's dynamic-quant convention](https://docs.unsloth.ai/) (`UD-…`).
|
|
116
120
|
|
|
117
121
|
**Upgrading** — depends on how you installed: `uv tool upgrade chad-code`; `uvx --refresh
|
|
@@ -150,8 +154,17 @@ it chad uses the tree-sitter fallback automatically.
|
|
|
150
154
|
| `--plan` | start in read-only plan mode (investigate + propose, edits blocked) |
|
|
151
155
|
| `--yolo` | auto-approve bash/write/edit (skip confirm prompts) |
|
|
152
156
|
| `--no-think` | skip Ornith's `<think>` blocks — faster on well-scoped work |
|
|
157
|
+
| `--model` | `35b`, `9b`, `auto`, or any HF repo id / local model dir |
|
|
153
158
|
| `--repl` | plain line REPL instead of the TUI |
|
|
154
159
|
|
|
160
|
+
Plus three subcommands, each with its own `--help`:
|
|
161
|
+
|
|
162
|
+
| Command | What it does |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `chad serve` | serve this Mac's model to a container or the LAN ([Configuration](docs/configuration.md#serving-the-local-model-to-a-container-chad-serve)) |
|
|
165
|
+
| `chad prove` | 2-minute offline smoke test: 4 tiny fix-it tasks, verified, timed |
|
|
166
|
+
| `chad levers` | print the harness lever registry as JSON (ablation driver) |
|
|
167
|
+
|
|
155
168
|
A headless task (positional, or piped with no TTY) auto-approves mutating tools; the model
|
|
156
169
|
runs greedy (temp 0). Every conversation is persisted under `~/.chad/sessions/`, and every
|
|
157
170
|
resume forks a new branch rather than overwriting — details in
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# import name, and command name are independent. `uvx chad-code` runs the alias
|
|
5
5
|
# script added under [project.scripts].
|
|
6
6
|
name = "chad-code"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.5"
|
|
8
8
|
description = "Local MLX-backed, Claude-Code-style coding agent (Apple Silicon, Ornith 35B/9B)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""chad — a local MLX-backed, Claude-Code-style coding agent.
|
|
2
|
+
|
|
3
|
+
A flat collection of cooperating modules behind one console script (``chad``):
|
|
4
|
+
the inference engine, the tool layer, the agent loop, and the terminal UI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.5"
|
|
8
|
+
|
|
9
|
+
# chad sets no MLX_* runtime vars. MLX_METAL_FAST_SYNCH, MLX_MAX_OPS_PER_BUFFER
|
|
10
|
+
# and MLX_MAX_MB_PER_BUFFER were each measured end-to-end on the 35B and every
|
|
11
|
+
# setting was slower than mlx's own defaults (1-4% at both 8k and 32k), so the
|
|
12
|
+
# mechanism that used to apply them at package import is gone. Any future entry
|
|
13
|
+
# has to be set before mlx's backend initializes — package import is the last
|
|
14
|
+
# hook early enough, since chad imports mlx lazily inside functions.
|
|
@@ -373,8 +373,10 @@ class Agent:
|
|
|
373
373
|
# a transcript whose protected floor exceeds ctx_limit isn't re-compacted —
|
|
374
374
|
# and its warm prefix cache destroyed — on every single step.
|
|
375
375
|
self._compact_state: dict = {}
|
|
376
|
+
self._deny_reason: str | None = None # headless guard-block explanation for the model
|
|
376
377
|
# Live ctx-limit recheck: the startup limit was computed on
|
|
377
|
-
# an idle box;
|
|
378
|
+
# an idle box; containers or other engines spinning up mid-session change
|
|
379
|
+
# what's safe.
|
|
378
380
|
# Called at the top of each turn; only a >10% move is applied (hysteresis).
|
|
379
381
|
self._ctx_limit_fn = ctx_limit_fn
|
|
380
382
|
# Runaway-turn governor: a per-turn budget on cumulative prefill tokens
|
|
@@ -597,6 +599,17 @@ class Agent:
|
|
|
597
599
|
if self._confirm_cb is None and not sys.stdin.isatty():
|
|
598
600
|
self._emit("info", f" [blocked destructive command in auto mode: "
|
|
599
601
|
f"{args.get('command', '')!r}; set CHAD_NO_DESTRUCTIVE_GUARD=1 to allow]")
|
|
602
|
+
# Tell the MODEL the truth about who blocked it and why: "[denied by
|
|
603
|
+
# user]" reads as a human refusal and teaches the wrong lesson (the
|
|
604
|
+
# measured trace: a model re-phrasing the same delete 30 times). The
|
|
605
|
+
# guard names itself and the fix — narrow the target.
|
|
606
|
+
if levers.enabled("scoped_destructive_guard"):
|
|
607
|
+
self._deny_reason = (
|
|
608
|
+
"[blocked by the destructive-command guard, not by a person: "
|
|
609
|
+
"the command matches a catastrophic pattern (recursive delete "
|
|
610
|
+
"of a filesystem root, top-level directory, or home tree — or "
|
|
611
|
+
"mkfs / dd-to-device / curl|sh). Re-issue it with a narrower "
|
|
612
|
+
"target (a specific subdirectory), or skip the deletion.]")
|
|
600
613
|
return False
|
|
601
614
|
if self._confirm_cb is not None:
|
|
602
615
|
return self._confirm_cb(name, args)
|
|
@@ -675,7 +688,7 @@ class Agent:
|
|
|
675
688
|
# Fail-safe: a sub-agent that ends early — step cap, crash, interrupt — or
|
|
676
689
|
# that returns nothing at all must still hand back where it got to. Never
|
|
677
690
|
# surface a bare sentinel: the parent then restarts the localization from
|
|
678
|
-
# zero (the
|
|
691
|
+
# zero (the demonstrated failure), and the anti-respawn guard
|
|
679
692
|
# above refuses the retry, so the turn dies with the findings still in the
|
|
680
693
|
# dead sub-agent's transcript. progress_note is deterministic and model-free,
|
|
681
694
|
# so it works even from a crashed turn: it re-reads the sub-agent's own tool
|
|
@@ -683,7 +696,7 @@ class Agent:
|
|
|
683
696
|
# hypothesis it stated.
|
|
684
697
|
def _salvage(res: str) -> str:
|
|
685
698
|
if not levers.enabled("subagent_budget_note"):
|
|
686
|
-
return res #
|
|
699
|
+
return res # legacy arm: the capped sub-agent's findings are discarded
|
|
687
700
|
note = sub.budget_note or guardrails.progress_note(sub.messages)
|
|
688
701
|
if not note:
|
|
689
702
|
return res
|
|
@@ -753,7 +766,7 @@ class Agent:
|
|
|
753
766
|
self.ctx_limit = fresh
|
|
754
767
|
except Exception: # noqa: BLE001 — a pressure probe must never kill a turn
|
|
755
768
|
pass
|
|
756
|
-
#
|
|
769
|
+
# Warm start: on a cold cache, load the system+tools KV from disk
|
|
757
770
|
# (or prefill+persist it once) so the first turn doesn't re-prefill the
|
|
758
771
|
# ~3.2k-token stable prefix every session. Cheap no-op on a warm cache.
|
|
759
772
|
if self.engine.cache_dir and not self.engine._cached_ids:
|
|
@@ -814,8 +827,8 @@ class Agent:
|
|
|
814
827
|
plan_reviews = 0 # one-shot "re-read the plan you just wrote" (levers.plan_review)
|
|
815
828
|
# Files edited this turn -> mtime at last syntax check. Bash can mutate files
|
|
816
829
|
# too (sed -i, python rewrites) but bypasses the write/edit syntax gate; watch
|
|
817
|
-
# edited files and re-check them after any bash that touched them (
|
|
818
|
-
#
|
|
830
|
+
# edited files and re-check them after any bash that touched them (a measured
|
|
831
|
+
# file survived 9 blind `sed -i` "fixes" unparseable and unflagged).
|
|
819
832
|
edited_syntax_watch: dict = {}
|
|
820
833
|
think_cap_hits = 0 # soft think-cap firings this turn (drives escalation)
|
|
821
834
|
repeat_stops = 0 # degenerate-repetition cut-offs this turn (3rd aborts the turn)
|
|
@@ -832,8 +845,8 @@ class Agent:
|
|
|
832
845
|
# decode-speed-aware budget. turn_think_exhausted latches the once-only
|
|
833
846
|
# log/steer; past it, no-think steps are paid on a duty cycle
|
|
834
847
|
# (guardrails.turn_think_throttle) rather than muting the rest of the turn —
|
|
835
|
-
# the blanket mute regressed
|
|
836
|
-
# landing_no_think is the
|
|
848
|
+
# the blanket mute regressed passing runs with garbled no-think tails.
|
|
849
|
+
# landing_no_think is the hard-wrapup landing's own unconditional latch: once
|
|
837
850
|
# wrap-up fires, the landing and everything after it stay no-think regardless.
|
|
838
851
|
turn_think_tokens = 0
|
|
839
852
|
turn_think_half_fired = False
|
|
@@ -876,7 +889,7 @@ class Agent:
|
|
|
876
889
|
# Run-task intent (start/boot/serve/… — system-state imperatives with no file
|
|
877
890
|
# deliverable): arms the anti-bail nudges alongside action_task but is kept OUT
|
|
878
891
|
# of the no-empty-diff done gates below, which demand a landed edit a run task
|
|
879
|
-
# legitimately never makes (
|
|
892
|
+
# legitimately never makes (a qemu-startup task was classified as
|
|
880
893
|
# neither, so a prose give-up with 81% of the wall left took the weakest path).
|
|
881
894
|
run_task = self.mode != "plan" and _intent.get("run", False)
|
|
882
895
|
# Progress-aware step cap (see guardrails.extend_step_cap): max_steps is the
|
|
@@ -1000,7 +1013,7 @@ class Agent:
|
|
|
1000
1013
|
# Turn-level think budget: past exhaustion, forced no-think steps are
|
|
1001
1014
|
# paid on a duty cycle (one per TURN_THINK_REARM_TOK further think tokens)
|
|
1002
1015
|
# so thinking RESTORES once the model stops over-spending — a blanket
|
|
1003
|
-
# rest-of-turn mute regressed
|
|
1016
|
+
# rest-of-turn mute regressed passing runs. The hard-landing's
|
|
1004
1017
|
# landing_no_think stays unconditional.
|
|
1005
1018
|
_tt_throttled = (turn_think_exhausted
|
|
1006
1019
|
and guardrails.turn_think_throttle(
|
|
@@ -1133,7 +1146,7 @@ class Agent:
|
|
|
1133
1146
|
# set AND this step is actually thinking (a no-think escalation step has no
|
|
1134
1147
|
# <think> to salvage). None => the engine path is byte-identical to before.
|
|
1135
1148
|
step_ceiling = self.think_ceiling if (self.think_ceiling and step_thinking) else None
|
|
1136
|
-
# Landing generations (after the
|
|
1149
|
+
# Landing generations (after the deadline abort armed the latch) are
|
|
1137
1150
|
# token-boxed so the forced landing can't itself run long or spiral: the
|
|
1138
1151
|
# remaining wall only affords so many tokens, halved to leave room for the
|
|
1139
1152
|
# landing's own tool dispatch. Default path is byte-identical (self.max_gen_tokens).
|
|
@@ -1150,8 +1163,8 @@ class Agent:
|
|
|
1150
1163
|
except BackendError as e:
|
|
1151
1164
|
# A transient backend fault (5xx / mid-stream error chunk) used to escape
|
|
1152
1165
|
# run_turn and kill the process from cli.main — forfeiting the rest of an
|
|
1153
|
-
# unattended task's budget
|
|
1154
|
-
#
|
|
1166
|
+
# unattended task's budget — a measured run died at 721s of a 1770s
|
|
1167
|
+
# budget on a single llama.cpp 500. Re-issue the step instead: the
|
|
1155
1168
|
# prompt is rebuilt from `messages` each iteration and the failed
|
|
1156
1169
|
# generation was never appended, so a retry is a clean re-roll — and at
|
|
1157
1170
|
# temp>0 a resample usually clears a parser-rejected completion.
|
|
@@ -1202,9 +1215,9 @@ class Agent:
|
|
|
1202
1215
|
self._emit("stream", "\n")
|
|
1203
1216
|
# strip any trailing special tokens the template will re-add — and any
|
|
1204
1217
|
# LEAKED special-token literal anywhere in the text. A quantized model
|
|
1205
|
-
# can emit a stray marker like <|mask_end|> mid-turn (
|
|
1206
|
-
#
|
|
1207
|
-
#
|
|
1218
|
+
# can emit a stray marker like <|mask_end|> mid-turn (one leaked at
|
|
1219
|
+
# step 12 of a measured run and the turn read as a clean final answer,
|
|
1220
|
+
# ending rc=0 with an unverified edit); scrubbed
|
|
1208
1221
|
# here so it can neither pollute the transcript nor masquerade as
|
|
1209
1222
|
# content.
|
|
1210
1223
|
text = _SPECIAL_TOKEN_RE.sub("", text).rstrip()
|
|
@@ -1237,6 +1250,20 @@ class Agent:
|
|
|
1237
1250
|
elif "</think>" in text and len(text):
|
|
1238
1251
|
frac = len(text.split("</think>", 1)[0]) / len(text)
|
|
1239
1252
|
_think_delta = int(stats.generated_tokens * frac)
|
|
1253
|
+
elif (hit_cap and step_thinking and text
|
|
1254
|
+
and levers.enabled("capped_think_credit")):
|
|
1255
|
+
# Same reasoning as the soft-stop branch, for the generation that ran
|
|
1256
|
+
# to the RAW token cap while still inside <think>: no </think> was
|
|
1257
|
+
# emitted, so every token is reasoning. Without this the biggest
|
|
1258
|
+
# thinks in a run — a full cap each, and the ones the budget exists
|
|
1259
|
+
# to bound — credit zero and the throttle never engages.
|
|
1260
|
+
# close_unclosed_think above rests on the same premise: a thinking
|
|
1261
|
+
# generation with no </think> never left the block. The predicate is
|
|
1262
|
+
# the reasoning_length_stop telemetry's, minus the soft-stop overlap
|
|
1263
|
+
# the first branch already credits; an unclosed generation that
|
|
1264
|
+
# stopped SHORT of the cap is a truncation of some other kind, not
|
|
1265
|
+
# the reasoning overspend this counts.
|
|
1266
|
+
_think_delta = stats.generated_tokens
|
|
1240
1267
|
else:
|
|
1241
1268
|
_think_delta = 0
|
|
1242
1269
|
self.think_tokens += _think_delta
|
|
@@ -1249,7 +1276,7 @@ class Agent:
|
|
|
1249
1276
|
turn_think_tokens += _think_delta
|
|
1250
1277
|
# Inert below TURN_THINK_MIN_WALL_S: a short auto-continue tail clamps to
|
|
1251
1278
|
# the LO budget and half-fires on its first step, churning against
|
|
1252
|
-
# hard_wrapup's landing (
|
|
1279
|
+
# hard_wrapup's landing (the regex-log relaunch signature).
|
|
1253
1280
|
if (self._turn_budget_s
|
|
1254
1281
|
and self._turn_budget_s >= guardrails.TURN_THINK_MIN_WALL_S
|
|
1255
1282
|
and self.mode != "plan" and not read_only_intent
|
|
@@ -1475,21 +1502,55 @@ class Agent:
|
|
|
1475
1502
|
"— progress note banked; say 'continue' to retry]")
|
|
1476
1503
|
return ("[stopped: the model kept emitting malformed tool calls "
|
|
1477
1504
|
"— say 'continue' to resume]")
|
|
1478
|
-
#
|
|
1479
|
-
#
|
|
1480
|
-
#
|
|
1481
|
-
#
|
|
1482
|
-
#
|
|
1483
|
-
#
|
|
1505
|
+
# Did-nothing gate: in auto/headless mode a turn that ends having
|
|
1506
|
+
# executed ZERO real tools is never a legitimate completion — the keyword
|
|
1507
|
+
# intent classifier misses tasks like "extract the secret and save it"
|
|
1508
|
+
# (no action verb), so action_task is False and the gate below wouldn't
|
|
1509
|
+
# fire, which is how a turn once shipped a 28-token garble as its final
|
|
1510
|
+
# answer. read_only/explain asks are exempt. Banks a note so
|
|
1484
1511
|
# auto-continue relaunches fresh instead of shipping nothing.
|
|
1485
1512
|
did_nothing = self.mode == "auto" and not read_only_intent and not did_work
|
|
1486
1513
|
if (action_task and not read_only_intent
|
|
1487
1514
|
and (not made_edit or unverified_edit)) or did_nothing:
|
|
1515
|
+
# Churn→audit handoff: this hard stop used to fire with the
|
|
1516
|
+
# audit still silent — the turn ends, a progress note carrying
|
|
1517
|
+
# the model's own completion claim gets banked, and each
|
|
1518
|
+
# relaunch re-dones into the same stop until the continue
|
|
1519
|
+
# allowance is exhausted. Hand the ending to the audit ONCE
|
|
1520
|
+
# instead: quoted requirements + path facts land IN CONTEXT
|
|
1521
|
+
# with the turn's work. A further empty-diff ending still
|
|
1522
|
+
# hard-stops exactly as below (churn capped, not replaced);
|
|
1523
|
+
# latch shared with the accept-path audit.
|
|
1524
|
+
if self.mode != "plan" and not self._subagent \
|
|
1525
|
+
and levers.enabled("done_audit") \
|
|
1526
|
+
and levers.enabled("audit_churn_handoff") \
|
|
1527
|
+
and not done_audit_fired:
|
|
1528
|
+
audit_task = guardrails.audit_task_text(user_text)
|
|
1529
|
+
audit = guardrails.done_audit(audit_task, {
|
|
1530
|
+
"turn_start_epoch": turn_start_epoch,
|
|
1531
|
+
"wall_s": time.monotonic() - turn_start,
|
|
1532
|
+
"wall_budget_s": self._turn_budget_s,
|
|
1533
|
+
"step_walls": step_walls,
|
|
1534
|
+
}, entry="handoff")
|
|
1535
|
+
if audit:
|
|
1536
|
+
done_audit_fired = True
|
|
1537
|
+
done_audit_bounces += 1
|
|
1538
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1539
|
+
_runway = ((self._turn_budget_s
|
|
1540
|
+
- (time.monotonic() - turn_start))
|
|
1541
|
+
if self._turn_budget_s else float("inf"))
|
|
1542
|
+
log.info("DONE-AUDIT bounce (churn-handoff, "
|
|
1543
|
+
"final-answer): paths=%s runway=%.0fs",
|
|
1544
|
+
guardrails.audit_extract_paths(audit_task),
|
|
1545
|
+
_runway)
|
|
1546
|
+
self.messages.append({"role": "tool", "name": "edit",
|
|
1547
|
+
"content": audit})
|
|
1548
|
+
continue
|
|
1488
1549
|
# Iter-2 no-empty-diff gate: an action task may not END on a prose
|
|
1489
1550
|
# "final answer" while no change landed (or the change is
|
|
1490
|
-
# unverified) — the demonstrated failures
|
|
1491
|
-
#
|
|
1492
|
-
#
|
|
1551
|
+
# unverified) — the demonstrated failures: 49–97s bails accepted
|
|
1552
|
+
# as final answers with an empty diff and 97% of the budget
|
|
1553
|
+
# unused. Bank a progress note
|
|
1493
1554
|
# and end as a hard stop, so --auto-continue (headless) or the
|
|
1494
1555
|
# user's 'continue' (TUI) relaunches a fresh attempt with the
|
|
1495
1556
|
# note instead of silently shipping nothing.
|
|
@@ -1503,11 +1564,10 @@ class Agent:
|
|
|
1503
1564
|
"— progress note banked; say 'continue' to retry]")
|
|
1504
1565
|
return ("[stopped: the turn ended without applying a verified "
|
|
1505
1566
|
"change — say 'continue' to resume]")
|
|
1506
|
-
# Done-audit, final-answer twin
|
|
1507
|
-
#
|
|
1508
|
-
#
|
|
1509
|
-
#
|
|
1510
|
-
# log-summary on.1, large-scale-text-editing off.1). Same lever, same
|
|
1567
|
+
# Done-audit, final-answer twin: a prose final answer on an action
|
|
1568
|
+
# task is a `done` in all but name, and this accept path bypassed every
|
|
1569
|
+
# done gate — in the measured set several wrong-dones exited here with
|
|
1570
|
+
# the lever ON but never engaged. Same lever, same
|
|
1511
1571
|
# guards, same once-per-turn latch as the done-tool branch below; the
|
|
1512
1572
|
# steer's "call done again" converts a prose-ender into a done-caller,
|
|
1513
1573
|
# which the latch then accepts.
|
|
@@ -1606,11 +1666,39 @@ class Agent:
|
|
|
1606
1666
|
continue
|
|
1607
1667
|
if action_task and not read_only_intent and self.mode != "plan" \
|
|
1608
1668
|
and (not made_edit or unverified_edit):
|
|
1669
|
+
# Churn→audit handoff, done-tool twin (see the final-answer
|
|
1670
|
+
# site above): one audit bounce before the hard stop; the
|
|
1671
|
+
# next empty-diff done stops exactly as below.
|
|
1672
|
+
if not self._subagent \
|
|
1673
|
+
and levers.enabled("done_audit") \
|
|
1674
|
+
and levers.enabled("audit_churn_handoff") \
|
|
1675
|
+
and not done_audit_fired:
|
|
1676
|
+
audit_task = guardrails.audit_task_text(user_text)
|
|
1677
|
+
audit = guardrails.done_audit(audit_task, {
|
|
1678
|
+
"turn_start_epoch": turn_start_epoch,
|
|
1679
|
+
"wall_s": time.monotonic() - turn_start,
|
|
1680
|
+
"wall_budget_s": self._turn_budget_s,
|
|
1681
|
+
"step_walls": step_walls,
|
|
1682
|
+
}, entry="handoff")
|
|
1683
|
+
if audit:
|
|
1684
|
+
done_audit_fired = True
|
|
1685
|
+
done_audit_bounces += 1
|
|
1686
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1687
|
+
_runway = ((self._turn_budget_s
|
|
1688
|
+
- (time.monotonic() - turn_start))
|
|
1689
|
+
if self._turn_budget_s else float("inf"))
|
|
1690
|
+
log.info("DONE-AUDIT bounce (churn-handoff): paths=%s "
|
|
1691
|
+
"runway=%.0fs",
|
|
1692
|
+
guardrails.audit_extract_paths(audit_task),
|
|
1693
|
+
_runway)
|
|
1694
|
+
self.messages.append({"role": "tool", "name": "done",
|
|
1695
|
+
"content": audit})
|
|
1696
|
+
continue
|
|
1609
1697
|
# Same no-empty-diff gate as the prose-final-answer path: `done`
|
|
1610
1698
|
# with nothing landed (or landed-unverified after the verify
|
|
1611
1699
|
# nudges ran out) becomes a resumable hard stop, not a success
|
|
1612
|
-
# (
|
|
1613
|
-
#
|
|
1700
|
+
# (measured: done accepted at 84s with edits in tree and zero
|
|
1701
|
+
# successful post-edit commands).
|
|
1614
1702
|
self.budget_note = guardrails.progress_note(
|
|
1615
1703
|
self.messages,
|
|
1616
1704
|
rejected_claim=str(terminal.get("summary") or ""))
|
|
@@ -1621,12 +1709,12 @@ class Agent:
|
|
|
1621
1709
|
"progress note banked; say 'continue' to retry]")
|
|
1622
1710
|
return ("[stopped: `done` was called without a landed+verified "
|
|
1623
1711
|
"change — say 'continue' to resume]")
|
|
1624
|
-
# Done-audit: the
|
|
1712
|
+
# Done-audit: the largest measured fail bucket (20/43)
|
|
1625
1713
|
# was dones whose claimed verification was a WEAKER predicate than the
|
|
1626
|
-
# task's own
|
|
1714
|
+
# task's own wording — and the generic recheck below was ON for all of
|
|
1627
1715
|
# them. On a done every gate above would accept, bounce ONCE with the
|
|
1628
1716
|
# task statement's own requirement lines quoted plus stat facts for each
|
|
1629
|
-
# path it names; the NEXT done is accepted unconditionally (the
|
|
1717
|
+
# path it names; the NEXT done is accepted unconditionally (the
|
|
1630
1718
|
# anti-spiral latch — the model is told so, which keeps the steer
|
|
1631
1719
|
# credible). Supersedes done_spec_recheck while enabled: stacking both
|
|
1632
1720
|
# would force two bounces per turn. Post-audit edits deliberately do NOT
|
|
@@ -1638,8 +1726,8 @@ class Agent:
|
|
|
1638
1726
|
# as possible…" name no action verb); the anchors requirement inside
|
|
1639
1727
|
# done_audit (concrete paths / imperative requirement lines) is the
|
|
1640
1728
|
# task-text-derived action detector.
|
|
1641
|
-
# Not in sub-agents (
|
|
1642
|
-
#
|
|
1729
|
+
# Not in sub-agents (a measured bounce fired inside one at
|
|
1730
|
+
# runway=inf — sub-agents carry no wall budget, so the runway
|
|
1643
1731
|
# guard is inert there, and their delegated prompt is not the task
|
|
1644
1732
|
# statement the audit should quote). audit_task strips the harness
|
|
1645
1733
|
# appendices (progress note / review-pass preamble) a relaunched
|
|
@@ -1821,8 +1909,11 @@ class Agent:
|
|
|
1821
1909
|
"plan to ./plans/NNN-title.md.]")
|
|
1822
1910
|
elif not plan_write and not self._confirm(name, args):
|
|
1823
1911
|
# A plan write is the expected action in plan mode, so it skips the
|
|
1824
|
-
# confirm prompt; everything else still goes through _confirm.
|
|
1825
|
-
|
|
1912
|
+
# confirm prompt; everything else still goes through _confirm. A
|
|
1913
|
+
# genuine human "no" stays "[denied by user]"; the headless guard
|
|
1914
|
+
# block carries its own explanation (set in _confirm).
|
|
1915
|
+
result = self._deny_reason or "[denied by user]"
|
|
1916
|
+
self._deny_reason = None
|
|
1826
1917
|
else:
|
|
1827
1918
|
_t0 = time.perf_counter()
|
|
1828
1919
|
self.tool_dispatches += 1
|
|
@@ -2017,8 +2108,7 @@ def repl(engine: BaseEngine, yolo: bool, ctx_limit: int = 24000, resume: list =
|
|
|
2017
2108
|
thinking: bool = True, ctx_limit_fn=None):
|
|
2018
2109
|
agent = Agent(engine, yolo=yolo, ctx_limit=ctx_limit, thinking=thinking,
|
|
2019
2110
|
resume=resume, persist=True, ctx_limit_fn=ctx_limit_fn)
|
|
2020
|
-
|
|
2021
|
-
print(banner(label, ctx_limit, mode=agent.mode))
|
|
2111
|
+
print(banner(engine.model_id.split("/")[-1], ctx_limit, mode=agent.mode))
|
|
2022
2112
|
print(f"{C_DIM}type a task, or /reset, /exit.{C_RST}")
|
|
2023
2113
|
while True:
|
|
2024
2114
|
try:
|
|
@@ -13,26 +13,73 @@ by a backend that never loads mlx. `GenStats` lives here (not `engine.py`) preci
|
|
|
13
13
|
the remote adapter can build one without dragging in `mlx.core`.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
from typing import Any, Callable, Optional, Protocol, runtime_checkable
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Callable, Iterable, Optional, Protocol, runtime_checkable
|
|
18
18
|
|
|
19
19
|
# Close-and-continue salvage: the marker injected to force-close a <think> run
|
|
20
20
|
# that has blown past the ceiling, so decoding continues into the ACTION *in the same step*
|
|
21
21
|
# — vs the soft think-cap, which ends the step and lets the next one re-derive the whole
|
|
22
|
-
# reasoning (the
|
|
22
|
+
# reasoning (the measured anti-fix: force-close-then-new-step cost 3.8x total think). Both
|
|
23
23
|
# engines inject this same string: the MLX path tokenizes it and feeds the ids through the
|
|
24
24
|
# append-only cache; the llama path hands it back as an assistant-prefix continuation.
|
|
25
25
|
THINK_CLOSE = "\n</think>\n\n"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def think_ceiling_hit(text: str, n_tokens: int, ceiling: Optional[int]
|
|
28
|
+
def think_ceiling_hit(text: str, n_tokens: int, ceiling: Optional[int],
|
|
29
|
+
think_closed: Optional[bool] = None) -> bool:
|
|
29
30
|
"""True when generation is still inside the auto-opened <think> block (no </think>
|
|
30
31
|
emitted yet) and has run past `ceiling` tokens — the close-and-continue trigger
|
|
31
32
|
`ceiling` None/0 disables it (byte-identical to no ceiling). Pure so both
|
|
32
|
-
engines share one definition and it is unit-testable without a model.
|
|
33
|
-
|
|
33
|
+
engines share one definition and it is unit-testable without a model.
|
|
34
|
+
|
|
35
|
+
Called once per decoded token by both engines, so the cheap token-count test runs
|
|
36
|
+
FIRST: below the ceiling — which is almost always — this never touches `text` at all.
|
|
37
|
+
`think_closed` lets a caller that already tracks the marker incrementally (see
|
|
38
|
+
`TailWatch`) skip the scan entirely; None means "work it out from `text`", which is
|
|
39
|
+
the original behavior and what the unit tests exercise."""
|
|
40
|
+
if not ceiling or n_tokens < ceiling:
|
|
34
41
|
return False
|
|
35
|
-
|
|
42
|
+
if think_closed is None:
|
|
43
|
+
think_closed = "</think>" in text
|
|
44
|
+
return not think_closed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TailWatch:
|
|
48
|
+
"""Has any of these needles appeared yet in an append-only text? Answered per
|
|
49
|
+
segment, in constant time, without re-reading the text.
|
|
50
|
+
|
|
51
|
+
The naive spelling — `any(n in text for n in needles)` after every decoded token —
|
|
52
|
+
is quadratic in the generated length, because every token re-scans everything
|
|
53
|
+
generated so far. Measured on the streaming client: 500 tokens costs 6ms, 2k costs
|
|
54
|
+
41ms, 8k costs 242ms, 16k costs 777ms of pure CPU spent re-reading. That CPU is on
|
|
55
|
+
the thread draining the token stream, so it is also what decides how fast the
|
|
56
|
+
stream gets drained.
|
|
57
|
+
|
|
58
|
+
A needle can only become newly complete if it straddles the segment just appended,
|
|
59
|
+
so only `len(needle) - 1` characters of carry-over are ever relevant:
|
|
60
|
+
|
|
61
|
+
…already scanned…│ tail │ new segment │
|
|
62
|
+
└──────┬──────────┘
|
|
63
|
+
the only window a new match can live in
|
|
64
|
+
|
|
65
|
+
Latching: once found, always found (the text only grows), so `hit` never goes back
|
|
66
|
+
to False and later feeds short-circuit."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, needles: Iterable[str]):
|
|
69
|
+
self._needles = [n for n in (needles or []) if n]
|
|
70
|
+
self._carry = max((len(n) for n in self._needles), default=1) - 1
|
|
71
|
+
self._tail = ""
|
|
72
|
+
self.hit = False
|
|
73
|
+
|
|
74
|
+
def feed(self, seg: str) -> bool:
|
|
75
|
+
"""Absorb the next segment; return whether any needle has been seen yet."""
|
|
76
|
+
if self.hit or not self._needles or not seg:
|
|
77
|
+
return self.hit
|
|
78
|
+
window = self._tail + seg
|
|
79
|
+
if any(n in window for n in self._needles):
|
|
80
|
+
self.hit = True
|
|
81
|
+
self._tail = window[-self._carry:] if self._carry else ""
|
|
82
|
+
return self.hit
|
|
36
83
|
|
|
37
84
|
|
|
38
85
|
class BackendError(RuntimeError):
|
|
@@ -82,6 +129,19 @@ class GenStats:
|
|
|
82
129
|
# that can't report cached_tokens / per-forward
|
|
83
130
|
# accounting sets this so callers know the
|
|
84
131
|
# throughput/prefill numbers are estimates.
|
|
132
|
+
gen_ids: list = field(default_factory=list)
|
|
133
|
+
# the token ids generated this turn. The engine knows
|
|
134
|
+
# them exactly; a caller must never re-derive them by
|
|
135
|
+
# slicing `_cached_ids`, whose shape differs per decode
|
|
136
|
+
# path (the prompt-lookup path stores what it FED the
|
|
137
|
+
# cache, which omits the final pending token, and an
|
|
138
|
+
# OOM empties it). `chad serve` puts these on the wire
|
|
139
|
+
# so a remote client can mirror the server's cache.
|
|
140
|
+
cache_reset: bool = False # the prefix cache was DROPPED during this turn (Metal
|
|
141
|
+
# OOM recovery), so nothing is resident afterwards —
|
|
142
|
+
# not even the prompt. A caller mirroring cache state
|
|
143
|
+
# must clear its mirror rather than assume prompt+gen,
|
|
144
|
+
# which is the one thing `gen_ids` alone cannot say.
|
|
85
145
|
|
|
86
146
|
@property
|
|
87
147
|
def tok_per_s(self) -> float:
|
|
@@ -97,7 +97,7 @@ def _agentic(model_id: str, why: str, context_tokens: int, apply_fix: bool):
|
|
|
97
97
|
from .agent import build_system_prompt, close_unclosed_think
|
|
98
98
|
from .engine import Engine
|
|
99
99
|
|
|
100
|
-
eng = Engine(model_id=model_id,
|
|
100
|
+
eng = Engine(model_id=model_id, cache_dir=None)
|
|
101
101
|
eng.load()
|
|
102
102
|
tok = eng.tok
|
|
103
103
|
|
|
@@ -203,7 +203,7 @@ def main(argv=None) -> int:
|
|
|
203
203
|
|
|
204
204
|
# cache_dir=None so the on-disk warm-prefix can't pre-load and skew the *cold* prefill
|
|
205
205
|
# number; this benchmark measures from a genuinely cold cache.
|
|
206
|
-
eng = Engine(model_id=model_id,
|
|
206
|
+
eng = Engine(model_id=model_id, cache_dir=None)
|
|
207
207
|
sys.stderr.write(f"loading {model_id} [{why}] ...\n")
|
|
208
208
|
load_s = eng.load()
|
|
209
209
|
|