chad-code 2.0.1__tar.gz → 2.0.3__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-2.0.1 → chad_code-2.0.3}/PKG-INFO +4 -4
- {chad_code-2.0.1 → chad_code-2.0.3}/README.md +3 -3
- {chad_code-2.0.1 → chad_code-2.0.3}/pyproject.toml +3 -1
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/__init__.py +1 -1
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/agent.py +79 -8
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/base_engine.py +6 -3
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/completion_engine.py +7 -3
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/engine.py +100 -38
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/prompt.py +15 -3
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/serve.py +11 -2
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad_code.egg-info/PKG-INFO +4 -4
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad_code.egg-info/SOURCES.txt +3 -1
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_agent.py +67 -3
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_agent_e2e.py +1 -1
- chad_code-2.0.3/tests/test_matrix_bench.py +381 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_serve.py +1 -1
- chad_code-2.0.3/tests/test_warm_prefix_tiers.py +271 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/LICENSE +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/setup.cfg +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/ambient.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/atif.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/bench.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/checkpoint.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/cli.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/compaction.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/config.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/diag.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/guardrails.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/ignore.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/levers.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/mcp.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/mcp_oauth.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/mlx_dflash.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/mlx_fastpath.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/mlx_qmm_mma.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/LICENSE +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/__init__.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/alignment.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/attention.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/audio.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/cache.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/conformer.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/ctc.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/parakeet.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/rnnt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/tokenizer.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/parakeet/utils.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/prove.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/render.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/repomap.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/seatbelt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/session.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/skills.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/speech.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/spill.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/syntaxgate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/toolcall_parse.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/tools.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/tui.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad/validate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_agent_guards.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_ambient.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_atif.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_bench.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_checkpoint.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_cli.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_compaction.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_completion_engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_config.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_confirm_preview.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_edit.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_engine_dflash.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_engine_kvquant.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_engine_pld_hybrid.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_engine_pld_wide.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_feel_pack.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_gate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_ignore.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_intent.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_levers.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_log_redaction.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_mcp.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_mcp_oauth.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_mlx_qmm_mma.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_prove.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_render.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_repomap.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_repomap_polyglot.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_seatbelt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_session.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_skills.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_speech.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_speech_tui.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_spill.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_syntaxgate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_toolcall_dialect.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_toolcall_parse.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_tools.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_tui.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.3}/tests/test_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chad-code
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/nathansutton/chad
|
|
@@ -33,11 +33,11 @@ Provides-Extra: speech
|
|
|
33
33
|
Requires-Dist: sounddevice>=0.5; extra == "speech"
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
|
-
# chad: a Claude-Code-style coding agent
|
|
36
|
+
# chad: a local Claude-Code-style coding agent for your laptop
|
|
37
37
|
|
|
38
38
|
[](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
|
|
39
39
|
|
|
40
|
-
<img src="docs/claude-vs-chad.jpg" width="
|
|
40
|
+
<img src="docs/claude-vs-chad.jpg" width="66%" alt="Two staircase newel posts side by side: Claude is a hand-carved wooden horse head, chad is a scuffed plastic toy horse tied on with twine">
|
|
41
41
|
|
|
42
42
|
> Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
|
|
43
43
|
> *Coding under supervision.*
|
|
@@ -118,7 +118,7 @@ one checkpoint it ships:
|
|
|
118
118
|
cache, so a follow-up step prefills the ~16 tokens it appended instead of the 5,000 it
|
|
119
119
|
already read: **~0.75 s per step instead of ~50 s**. Any server with prompt caching gets
|
|
120
120
|
the easy case; the work is holding it true across compaction, truncated turns and restarts.
|
|
121
|
-
The system prefix is checkpointed to disk, so the second session
|
|
121
|
+
The system prefix is checkpointed to disk, so the second session anywhere starts warm
|
|
122
122
|
(75.6 s → 5.5 s to the first tool call).
|
|
123
123
|
- **Fused Metal kernels.** Quantized-KV attention, a small-M matmul for speculative verify,
|
|
124
124
|
and a compiled single-token layer step, chosen per machine at load time, no knobs.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# chad: a Claude-Code-style coding agent
|
|
1
|
+
# chad: a local Claude-Code-style coding agent for your laptop
|
|
2
2
|
|
|
3
3
|
[](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
|
|
4
4
|
|
|
5
|
-
<img src="docs/claude-vs-chad.jpg" width="
|
|
5
|
+
<img src="docs/claude-vs-chad.jpg" width="66%" alt="Two staircase newel posts side by side: Claude is a hand-carved wooden horse head, chad is a scuffed plastic toy horse tied on with twine">
|
|
6
6
|
|
|
7
7
|
> Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
|
|
8
8
|
> *Coding under supervision.*
|
|
@@ -83,7 +83,7 @@ one checkpoint it ships:
|
|
|
83
83
|
cache, so a follow-up step prefills the ~16 tokens it appended instead of the 5,000 it
|
|
84
84
|
already read: **~0.75 s per step instead of ~50 s**. Any server with prompt caching gets
|
|
85
85
|
the easy case; the work is holding it true across compaction, truncated turns and restarts.
|
|
86
|
-
The system prefix is checkpointed to disk, so the second session
|
|
86
|
+
The system prefix is checkpointed to disk, so the second session anywhere starts warm
|
|
87
87
|
(75.6 s → 5.5 s to the first tool call).
|
|
88
88
|
- **Fused Metal kernels.** Quantized-KV attention, a small-M matmul for speculative verify,
|
|
89
89
|
and a compiled single-token layer step, chosen per machine at load time, no knobs.
|
|
@@ -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 = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
8
8
|
description = "Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -138,6 +138,8 @@ testpaths = ["tests"]
|
|
|
138
138
|
|
|
139
139
|
[tool.ruff]
|
|
140
140
|
line-length = 100
|
|
141
|
+
# Vendored Exercism exercises (benchmarks/matrix/tasks/) are upstream files, kept byte-identical.
|
|
142
|
+
extend-exclude = ["benchmarks/matrix/tasks"]
|
|
141
143
|
src = ["src", "tests"]
|
|
142
144
|
|
|
143
145
|
[tool.ruff.lint]
|
|
@@ -4,7 +4,7 @@ A flat collection of cooperating modules behind one console script (``chad``):
|
|
|
4
4
|
the inference engine, the tool layer, the agent loop, and the terminal UI.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "2.0.
|
|
7
|
+
__version__ = "2.0.3"
|
|
8
8
|
|
|
9
9
|
# chad sets no MLX_* runtime vars. MLX_METAL_FAST_SYNCH, MLX_MAX_OPS_PER_BUFFER
|
|
10
10
|
# and MLX_MAX_MB_PER_BUFFER were each measured end-to-end on the 35B and every
|
|
@@ -29,7 +29,7 @@ from . import (
|
|
|
29
29
|
)
|
|
30
30
|
from .base_engine import BaseEngine
|
|
31
31
|
from .diag import args_preview, log, redact, result_preview
|
|
32
|
-
from .prompt import build_system_prompt, classify_intent
|
|
32
|
+
from .prompt import build_system_prompt, classify_intent, static_system_prompt
|
|
33
33
|
from .render import (
|
|
34
34
|
C_DIM,
|
|
35
35
|
C_RED,
|
|
@@ -206,8 +206,15 @@ def close_unclosed_think(text: str, thinking: bool) -> str:
|
|
|
206
206
|
re-prefill of the whole transcript next step (measured: tens of thousands of tokens
|
|
207
207
|
at large context). Appending the missing `</think>` keeps the cached tokens a strict
|
|
208
208
|
prefix of the re-render, so only a couple of tokens prefill instead. No-op when
|
|
209
|
-
thinking is off or the block is already closed.
|
|
210
|
-
|
|
209
|
+
thinking is off or the block is already closed.
|
|
210
|
+
|
|
211
|
+
Only the CLOSE decides this. An earlier guard also required no `<think>` in the
|
|
212
|
+
text, which read a stray opening tag — one the model wrote inside its own reasoning,
|
|
213
|
+
not the template's — as "already a block" and skipped the append. That turn then
|
|
214
|
+
reached `split_inline_reasoning` with no `</think>` to split on, so it was stored raw
|
|
215
|
+
and re-rendered with an injected empty think block: the exact full re-prefill this
|
|
216
|
+
function exists to prevent, on the longest turns, where it costs the most."""
|
|
217
|
+
if thinking and "</think>" not in text and text:
|
|
211
218
|
return text + "\n</think>"
|
|
212
219
|
return text
|
|
213
220
|
|
|
@@ -237,8 +244,17 @@ def split_inline_reasoning(m: dict) -> dict:
|
|
|
237
244
|
if m.get("role") != "assistant" or "</think>" not in content:
|
|
238
245
|
return m
|
|
239
246
|
head, _, tail = content.partition("</think>")
|
|
247
|
+
head = head.rstrip("\n")
|
|
248
|
+
# Strip a LEADING `<think>` only. The generation prompt already opened the block, so
|
|
249
|
+
# any later tag is reasoning the model wrote, not a delimiter. Taking the LAST one
|
|
250
|
+
# (`split("<think>")[-1]`) silently dropped every reasoning token before a stray tag:
|
|
251
|
+
# transcript content lost, and the shortened re-render diverges from the cache right
|
|
252
|
+
# where the turn began.
|
|
253
|
+
unwrapped = head.lstrip()
|
|
254
|
+
if unwrapped.startswith("<think>"):
|
|
255
|
+
head = unwrapped[len("<think>"):]
|
|
240
256
|
return {**m,
|
|
241
|
-
"reasoning_content": head.
|
|
257
|
+
"reasoning_content": head.lstrip("\n"),
|
|
242
258
|
"content": tail.lstrip("\n")}
|
|
243
259
|
|
|
244
260
|
|
|
@@ -788,6 +804,36 @@ class Agent:
|
|
|
788
804
|
n += 1
|
|
789
805
|
return list(a[:n])
|
|
790
806
|
|
|
807
|
+
def _static_head_ids(self) -> list:
|
|
808
|
+
"""The project-independent head of `_stable_prefix_ids`: everything the
|
|
809
|
+
template renders before the per-session tail of the system prompt (cwd,
|
|
810
|
+
workspace listing, project docs). Found the same way — two renders differenced,
|
|
811
|
+
one with the full system prompt and one with only its static text — so it is
|
|
812
|
+
exact under any template, wherever that template puts the tool schemas. The
|
|
813
|
+
engine checkpoints this ONCE for every project and the full prefix once per
|
|
814
|
+
project (`Engine.warm_prefix`). Empty when the system prompt is not chad's own
|
|
815
|
+
or has no per-session tail, in which case only the full prefix is used."""
|
|
816
|
+
sysm = self.messages[0]
|
|
817
|
+
static = static_system_prompt()
|
|
818
|
+
content = sysm.get("content") or ""
|
|
819
|
+
if not content.startswith(static) or content == static:
|
|
820
|
+
return []
|
|
821
|
+
schemas = self._active_schemas()
|
|
822
|
+
def render1(m):
|
|
823
|
+
return self._template_ids(self.engine.tok.apply_chat_template(
|
|
824
|
+
[m, {"role": "user", "content": "a"}], tools=schemas,
|
|
825
|
+
add_generation_prompt=True, enable_thinking=self.thinking))
|
|
826
|
+
a, b = render1({"role": "system", "content": static}), render1(sysm)
|
|
827
|
+
n = 0
|
|
828
|
+
for x, y in zip(a, b):
|
|
829
|
+
if x != y:
|
|
830
|
+
break
|
|
831
|
+
n += 1
|
|
832
|
+
head, full = list(b[:n]), self._stable_prefix_ids()
|
|
833
|
+
if len(head) >= len(full) or full[: len(head)] != head:
|
|
834
|
+
return []
|
|
835
|
+
return head
|
|
836
|
+
|
|
791
837
|
def _confirm(self, name, args) -> bool:
|
|
792
838
|
# Destructive-bash seatbelt: a catastrophic shell command (rm -rf ~, mkfs,
|
|
793
839
|
# curl|sh, …) is screened even in --yolo mode, because the model acts on
|
|
@@ -861,14 +907,39 @@ class Agent:
|
|
|
861
907
|
pass
|
|
862
908
|
# Warm start: on a cold cache, load the system+tools KV from disk
|
|
863
909
|
# (or prefill+persist it once) so the first turn doesn't re-prefill the
|
|
864
|
-
# ~
|
|
910
|
+
# ~2.5k-token stable prefix every session. Two tiers: the full prefix (same
|
|
911
|
+
# project) and its static head (any project — only the per-project tail is
|
|
912
|
+
# prefilled). Cheap no-op on a warm cache.
|
|
865
913
|
if self.engine.cache_dir and not self.engine._cached_ids:
|
|
866
914
|
try:
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
915
|
+
t_warm = time.time()
|
|
916
|
+
full = self._stable_prefix_ids()
|
|
917
|
+
status, n = self.engine.warm_prefix(full, should_stop=self._should_stop,
|
|
918
|
+
head_ids=self._static_head_ids())
|
|
919
|
+
warm_s = time.time() - t_warm
|
|
920
|
+
log.info("CACHE warm-start %s: %d prefix tokens (disk KV cache, %.1fs)",
|
|
921
|
+
status, n, warm_s)
|
|
870
922
|
if status == "hit":
|
|
871
923
|
self._emit("info", f" [warm start: {n:,} prefix tokens from disk cache]")
|
|
924
|
+
elif status == "partial":
|
|
925
|
+
self._emit("info", f" [warm start: {n:,} prefix tokens from disk cache; "
|
|
926
|
+
f"{len(full) - n:,} project tokens prefilled "
|
|
927
|
+
f"in {warm_s:.1f}s]")
|
|
928
|
+
# The prefix prefill happens BEFORE step 1, so a per-step trace would
|
|
929
|
+
# never see it: a miss is the whole cold prefill of the system prompt,
|
|
930
|
+
# paid while the user is already waiting. Recorded as its own row (seq 0)
|
|
931
|
+
# so an offline reader can add it to the first step's wait; step rows
|
|
932
|
+
# are untouched (their cached_tokens already count the prefix).
|
|
933
|
+
if _PREFILL_TRACE and status != "skip":
|
|
934
|
+
# prefix_tokens is what `status` refers to (hit/partial: restored,
|
|
935
|
+
# miss: prefilled); prefilled_tokens is the cold part either way.
|
|
936
|
+
_trace_prefill({"seq": 0, "step": -1, "kind": "warm_prefix",
|
|
937
|
+
"status": status, "prefix_tokens": n,
|
|
938
|
+
"total_prefix": len(full),
|
|
939
|
+
"prefilled_tokens": (0 if status == "hit"
|
|
940
|
+
else len(full) - n
|
|
941
|
+
if status == "partial" else n),
|
|
942
|
+
"prefill_s": round(warm_s, 4)})
|
|
872
943
|
except Exception as e: # never let cache warming break a turn
|
|
873
944
|
log.warning("warm_prefix failed: %s", e)
|
|
874
945
|
# @file mentions: inline any referenced files so the model has them without a
|
|
@@ -207,10 +207,13 @@ class BaseEngine(Protocol):
|
|
|
207
207
|
`_reset_cache`). Called on `/reset` and by the governor's fresh-turn relaunch."""
|
|
208
208
|
...
|
|
209
209
|
|
|
210
|
-
def warm_prefix(self, prefix_ids: list, should_stop: Optional[Callable[[], bool]] = None
|
|
211
|
-
) -> tuple[str, int]:
|
|
210
|
+
def warm_prefix(self, prefix_ids: list, should_stop: Optional[Callable[[], bool]] = None,
|
|
211
|
+
head_ids: Optional[list] = None) -> tuple[str, int]:
|
|
212
212
|
"""Warm-start the stable system+tools prefix from disk (MLX) or no-op ('skip', 0)
|
|
213
|
-
on a stateless backend.
|
|
213
|
+
on a stateless backend. `head_ids`, if given, is the project-independent head
|
|
214
|
+
of `prefix_ids` (a proper prefix of it), checkpointed once for every project so
|
|
215
|
+
a fresh directory restores it and prefills only the tail. Returns
|
|
216
|
+
(status, n_tokens) with status 'hit' | 'partial' | 'miss' | 'skip'."""
|
|
214
217
|
...
|
|
215
218
|
|
|
216
219
|
def push_cache(self) -> None:
|
|
@@ -236,7 +236,8 @@ class CompletionEngine:
|
|
|
236
236
|
_reset_cache = reset # cli/bench call the private spelling; keep it working
|
|
237
237
|
|
|
238
238
|
def warm_prefix(self, prefix_ids: list,
|
|
239
|
-
should_stop: Optional[Callable[[], bool]] = None
|
|
239
|
+
should_stop: Optional[Callable[[], bool]] = None,
|
|
240
|
+
head_ids: Optional[list] = None) -> tuple[str, int]:
|
|
240
241
|
"""Warm-start the stable prefix. Against stock llama.cpp there is nothing to do
|
|
241
242
|
— the KV checkpoint would live on the SERVER's disk, which we can't reach — so
|
|
242
243
|
this stays 'skip'. A `chad serve` server advertising `warm_prefix` does the
|
|
@@ -246,12 +247,15 @@ class CompletionEngine:
|
|
|
246
247
|
self._ensure_caps()
|
|
247
248
|
if CAP_WARM_PREFIX not in self._caps:
|
|
248
249
|
return ("skip", 0)
|
|
249
|
-
|
|
250
|
+
body = {"prefix": list(prefix_ids)}
|
|
251
|
+
if head_ids:
|
|
252
|
+
body["head"] = list(head_ids)
|
|
253
|
+
resp = self._post_json("/warm", body)
|
|
250
254
|
if not resp:
|
|
251
255
|
return ("skip", 0)
|
|
252
256
|
status = str(resp.get("status") or "skip")
|
|
253
257
|
fed = int(resp.get("fed") or 0)
|
|
254
|
-
if status in ("hit", "miss") and fed:
|
|
258
|
+
if status in ("hit", "miss", "partial") and fed:
|
|
255
259
|
# Mirror what the server now holds, so the first turn's prefill estimate
|
|
256
260
|
# accounts for the prefix we just warmed instead of assuming a cold cache.
|
|
257
261
|
self._cached_ids = list(prefix_ids[:fed])
|
|
@@ -550,6 +550,9 @@ class Engine:
|
|
|
550
550
|
_pld_hybrid: bool = field(init=False, default=False)
|
|
551
551
|
_model_path: str = field(init=False, default="") # resolved weights dir
|
|
552
552
|
_warm_prefix_ids: Any = field(init=False, default=None)
|
|
553
|
+
# The project-independent head of that prefix (tool schemas + behavioral prompt),
|
|
554
|
+
# checkpointed once for every working directory; see warm_prefix.
|
|
555
|
+
_warm_head_ids: Any = field(init=False, default=None)
|
|
553
556
|
kv_bytes_per_token: float = field(init=False, default=0.0) # measured at load (036)
|
|
554
557
|
# Model-shape facts read from config at load, for the adaptive prefill chunk.
|
|
555
558
|
# MoE prefill amortizes routing with bigger chunks (+14% measured 512→2048 on
|
|
@@ -934,43 +937,92 @@ class Engine:
|
|
|
934
937
|
kind = _CKPT_PUSH if tag == _CKPT_PUSH else _CKPT_WARM
|
|
935
938
|
return os.path.join(self.cache_dir, f"{kind}-{h.hexdigest()}.safetensors") # type: ignore[arg-type]
|
|
936
939
|
|
|
937
|
-
def warm_prefix(self, prefix_ids: list, should_stop=None):
|
|
938
|
-
"""Make a cold session start warm.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
940
|
+
def warm_prefix(self, prefix_ids: list, should_stop=None, head_ids=None):
|
|
941
|
+
"""Make a cold session start warm. Two checkpoints can serve it, longest first:
|
|
942
|
+
|
|
943
|
+
- the FULL prefix (tool schemas + system prompt, per-project tail included):
|
|
944
|
+
a hit is a restart in the same project — ZERO prefill;
|
|
945
|
+
- the static HEAD (`head_ids`: the part of the prefix that is byte-identical in
|
|
946
|
+
every project — tool schemas + behavioral prompt): a hit restores the head
|
|
947
|
+
and prefills only the per-project tail (cwd, workspace listing, project
|
|
948
|
+
docs — a few hundred tokens), so a fresh directory no longer pays the whole
|
|
949
|
+
~2.5k-token cold prefill. Before this tier existed, the checkpoint was
|
|
950
|
+
keyed on the whole prompt and a new directory could never hit it.
|
|
951
|
+
|
|
952
|
+
A miss prefills the prefix once and persists BOTH checkpoints for next time.
|
|
953
|
+
Only valid on a cold cache. Returns (status, n_tokens): 'hit' (n = tokens
|
|
954
|
+
restored), 'partial' (head restored, n = its tokens; the rest was prefilled),
|
|
955
|
+
'miss' (n = tokens prefilled), 'skip'."""
|
|
942
956
|
if not self.cache_dir or not prefix_ids:
|
|
943
957
|
return ("skip", 0)
|
|
944
958
|
if self._cached_ids: # cache already populated this session
|
|
945
959
|
return ("skip", 0)
|
|
960
|
+
prefix_ids = list(prefix_ids)
|
|
946
961
|
path = self._ckpt_path(prefix_ids)
|
|
947
|
-
if os.path.isfile(path):
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
#
|
|
962
|
+
if os.path.isfile(path) and self._load_ckpt(path, prefix_ids):
|
|
963
|
+
self._warm_prefix_ids = list(prefix_ids)
|
|
964
|
+
return ("hit", len(prefix_ids))
|
|
965
|
+
head = list(head_ids or [])
|
|
966
|
+
if not (head and len(head) < len(prefix_ids) and prefix_ids[: len(head)] == head):
|
|
967
|
+
head = [] # not a proper prefix of this prefix: unusable
|
|
968
|
+
tail = prefix_ids[len(head):]
|
|
969
|
+
if head:
|
|
970
|
+
self._warm_head_ids = list(head)
|
|
971
|
+
hp = self._ckpt_path(head)
|
|
972
|
+
if os.path.isfile(hp) and self._load_ckpt(hp, head):
|
|
973
|
+
fed = self._prefill(tail, should_stop)
|
|
974
|
+
if fed < len(tail): # interrupted -> don't persist a partial
|
|
975
|
+
self._cached_ids = prefix_ids[: len(head) + fed]
|
|
976
|
+
return ("partial", len(head))
|
|
977
|
+
self._cached_ids = list(prefix_ids)
|
|
978
|
+
self._warm_prefix_ids = list(prefix_ids)
|
|
979
|
+
self._save_ckpt(path)
|
|
980
|
+
return ("partial", len(head))
|
|
981
|
+
# miss: prefill into a fresh cache and persist — the head first (so the NEXT
|
|
982
|
+
# fresh directory gets a partial hit), then the whole prefix.
|
|
960
983
|
self._reset_cache()
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
984
|
+
if head:
|
|
985
|
+
fed = self._prefill(head, should_stop)
|
|
986
|
+
if fed < len(head):
|
|
987
|
+
self._cached_ids = head[:fed]
|
|
988
|
+
return ("miss", fed)
|
|
989
|
+
self._cached_ids = list(head)
|
|
990
|
+
self._save_ckpt(hp)
|
|
991
|
+
fed = self._prefill(tail, should_stop)
|
|
992
|
+
if fed < len(tail):
|
|
993
|
+
self._cached_ids = prefix_ids[: len(head) + fed]
|
|
994
|
+
return ("miss", len(head) + fed)
|
|
965
995
|
self._cached_ids = list(prefix_ids)
|
|
966
996
|
self._warm_prefix_ids = list(prefix_ids)
|
|
997
|
+
self._save_ckpt(path)
|
|
998
|
+
return ("miss", len(prefix_ids))
|
|
999
|
+
|
|
1000
|
+
def _load_ckpt(self, path: str, ids: list) -> bool:
|
|
1001
|
+
"""Install a KV checkpoint as the live cache for exactly `ids`. False on
|
|
1002
|
+
anything short of a clean load (unreadable, wrong layer count), leaving the
|
|
1003
|
+
caller to recompute."""
|
|
1004
|
+
try:
|
|
1005
|
+
loaded = cache_utils.load_prompt_cache(path)
|
|
1006
|
+
except Exception:
|
|
1007
|
+
return False # corrupt/incompatible -> recompute
|
|
1008
|
+
if len(loaded) != self._n_model_layers():
|
|
1009
|
+
return False
|
|
1010
|
+
self._cache = loaded
|
|
1011
|
+
self._cached_ids = list(ids)
|
|
1012
|
+
self._set_cache_flags()
|
|
1013
|
+
return True
|
|
1014
|
+
|
|
1015
|
+
def _save_ckpt(self, path: str) -> None:
|
|
1016
|
+
"""Persist the live cache. Best-effort: a full or read-only disk skips the
|
|
1017
|
+
checkpoint, never the turn."""
|
|
1018
|
+
if not self.cache_dir:
|
|
1019
|
+
return
|
|
967
1020
|
try:
|
|
968
1021
|
os.makedirs(self.cache_dir, exist_ok=True)
|
|
969
1022
|
cache_utils.save_prompt_cache(path, self._cache)
|
|
970
1023
|
self._enforce_kv_budget(path)
|
|
971
1024
|
except Exception:
|
|
972
|
-
pass
|
|
973
|
-
return ("miss", len(prefix_ids))
|
|
1025
|
+
pass
|
|
974
1026
|
|
|
975
1027
|
def _n_model_layers(self) -> int:
|
|
976
1028
|
return len(self.model.layers)
|
|
@@ -1094,25 +1146,32 @@ class Engine:
|
|
|
1094
1146
|
def _reload_warm_prefix(self, target_ids: list) -> int:
|
|
1095
1147
|
"""If a disk checkpoint of the warm system prefix exists and target_ids still
|
|
1096
1148
|
begins with it, load it into the freshly-reset cache and return its length
|
|
1097
|
-
(tokens we skip re-prefilling).
|
|
1149
|
+
(tokens we skip re-prefilling). Falls back to the static head checkpoint when
|
|
1150
|
+
the full one is gone (evicted) or unreadable. Returns 0 if neither applies."""
|
|
1098
1151
|
wp = self._warm_prefix_ids
|
|
1099
|
-
if
|
|
1152
|
+
if (wp and self.cache_dir and len(target_ids) >= len(wp)
|
|
1100
1153
|
and target_ids[: len(wp)] == wp):
|
|
1154
|
+
path = self._ckpt_path(wp)
|
|
1155
|
+
if os.path.isfile(path):
|
|
1156
|
+
if self._load_ckpt(path, wp):
|
|
1157
|
+
return len(wp)
|
|
1158
|
+
self._reset_cache() # corrupt/incompatible -> clean rebuild
|
|
1159
|
+
return self._reload_warm_head(target_ids)
|
|
1160
|
+
|
|
1161
|
+
def _reload_warm_head(self, target_ids: list) -> int:
|
|
1162
|
+
"""Second tier of `_reload_warm_prefix`: the project-independent head (tool
|
|
1163
|
+
schemas + behavioral prompt), when target_ids still begins with it."""
|
|
1164
|
+
hd = getattr(self, "_warm_head_ids", None)
|
|
1165
|
+
if not (hd and self.cache_dir and len(target_ids) >= len(hd)
|
|
1166
|
+
and target_ids[: len(hd)] == hd):
|
|
1101
1167
|
return 0
|
|
1102
|
-
path = self._ckpt_path(
|
|
1168
|
+
path = self._ckpt_path(hd)
|
|
1103
1169
|
if not os.path.isfile(path):
|
|
1104
1170
|
return 0
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
self._cache = loaded
|
|
1110
|
-
self._cached_ids = list(wp)
|
|
1111
|
-
self._set_cache_flags()
|
|
1112
|
-
return len(wp)
|
|
1113
|
-
except Exception:
|
|
1114
|
-
self._reset_cache() # corrupt/incompatible -> clean rebuild
|
|
1115
|
-
return 0
|
|
1171
|
+
if self._load_ckpt(path, hd):
|
|
1172
|
+
return len(hd)
|
|
1173
|
+
self._reset_cache()
|
|
1174
|
+
return 0
|
|
1116
1175
|
|
|
1117
1176
|
# -- one-deep cache quarantine -----------------------------
|
|
1118
1177
|
# A subagent explores in a SEPARATE small context so the main transcript's warm
|
|
@@ -1228,6 +1287,9 @@ class Engine:
|
|
|
1228
1287
|
protect = {just_written}
|
|
1229
1288
|
if self._warm_prefix_ids:
|
|
1230
1289
|
protect.add(self._ckpt_path(self._warm_prefix_ids))
|
|
1290
|
+
hd = getattr(self, "_warm_head_ids", None)
|
|
1291
|
+
if hd:
|
|
1292
|
+
protect.add(self._ckpt_path(hd))
|
|
1231
1293
|
enforce_cache_budget(self.cache_dir, self.kv_cache_max_bytes, protect)
|
|
1232
1294
|
|
|
1233
1295
|
# -- generation -------------------------------------------------------
|
|
@@ -217,12 +217,24 @@ def _dynamic_context() -> list:
|
|
|
217
217
|
return dynamic
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
def static_system_prompt() -> str:
|
|
221
|
+
"""The part of the system prompt that is byte-identical in every project and every
|
|
222
|
+
session: the behavioral prompt and the verify-first block. What the chat template
|
|
223
|
+
renders before it (the tool schemas) is static too, so the rendered prefix up to the
|
|
224
|
+
end of this text is the same token sequence in every working directory — which is
|
|
225
|
+
what lets the engine checkpoint it ONCE, globally, instead of once per directory
|
|
226
|
+
(`Agent._static_head_ids`, `Engine.warm_prefix`)."""
|
|
227
|
+
return _BASE_PROMPT + _verify_baseline_block()
|
|
228
|
+
|
|
229
|
+
|
|
220
230
|
def build_system_prompt() -> str:
|
|
221
231
|
# Cache-boundary trick (from the Claude Code teardown): everything above the
|
|
222
232
|
# boundary is static behavioral text that stays identical across sessions, so the
|
|
223
|
-
# prefix KV cache reuses it. Volatile per-session context (cwd,
|
|
224
|
-
# below, where re-prefilling a few hundred tokens is cheap.
|
|
225
|
-
|
|
233
|
+
# prefix KV cache reuses it. Volatile per-session context (cwd, workspace listing,
|
|
234
|
+
# project docs) goes below, where re-prefilling a few hundred tokens is cheap. The
|
|
235
|
+
# boundary is load-bearing for the on-disk warm start: the static head above it is
|
|
236
|
+
# checkpointed once for every project, the full prompt once per project.
|
|
237
|
+
return static_system_prompt() + "\n".join(_dynamic_context())
|
|
226
238
|
|
|
227
239
|
|
|
228
240
|
def _verify_baseline_block() -> str:
|
|
@@ -37,7 +37,9 @@ forfeits come back:
|
|
|
37
37
|
return trip re-prefills it. Here the sub-agent's excursion is bracketed by a real
|
|
38
38
|
``Engine.push_cache``/``pop_cache``, so the main transcript's KV survives it.
|
|
39
39
|
- ``POST /warm`` — the on-disk KV warm-start of the stable system+tools prefix, which
|
|
40
|
-
a remote client cannot do because the checkpoint lives on the *server's* disk.
|
|
40
|
+
a remote client cannot do because the checkpoint lives on the *server's* disk. An
|
|
41
|
+
optional ``head`` (the project-independent part of ``prefix``) lets a client in a
|
|
42
|
+
fresh directory restore that head and prefill only its per-project tail.
|
|
41
43
|
|
|
42
44
|
Both are latency, never correctness: a client that doesn't speak them, or a call that
|
|
43
45
|
fails, degrades to the plain remote behavior.
|
|
@@ -704,6 +706,12 @@ def _make_handler(state: ServerState) -> type:
|
|
|
704
706
|
if not isinstance(prefix, list) or not all(
|
|
705
707
|
isinstance(t, int) and not isinstance(t, bool) for t in prefix):
|
|
706
708
|
return self._error(400, "prefix must be an array of token ids")
|
|
709
|
+
# Optional second tier: the project-independent head of `prefix`, so a
|
|
710
|
+
# client in a fresh directory restores the head and prefills only its tail.
|
|
711
|
+
head = body.get("head")
|
|
712
|
+
if head is not None and (not isinstance(head, list) or not all(
|
|
713
|
+
isinstance(t, int) and not isinstance(t, bool) for t in head)):
|
|
714
|
+
return self._error(400, "head must be an array of token ids")
|
|
707
715
|
# Same wall as /completion: warming a prefix that cannot fit is a prefill
|
|
708
716
|
# that cannot fit, and it reaches the allocator the same way.
|
|
709
717
|
_, too_big = admit(len(prefix), 0, state.n_ctx(), state.safe_ctx())
|
|
@@ -711,7 +719,8 @@ def _make_handler(state: ServerState) -> type:
|
|
|
711
719
|
return self._error(400, too_big)
|
|
712
720
|
state.busy = True
|
|
713
721
|
try:
|
|
714
|
-
status, fed = state.call(state.eng.warm_prefix, list(prefix)
|
|
722
|
+
status, fed = state.call(state.eng.warm_prefix, list(prefix),
|
|
723
|
+
head_ids=list(head) if head else None)
|
|
715
724
|
except Exception as e: # noqa: BLE001 — same: degrade, don't fail a run
|
|
716
725
|
return self._send_json(200, {"status": "error", "fed": 0,
|
|
717
726
|
"error": str(e)})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chad-code
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/nathansutton/chad
|
|
@@ -33,11 +33,11 @@ Provides-Extra: speech
|
|
|
33
33
|
Requires-Dist: sounddevice>=0.5; extra == "speech"
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
|
-
# chad: a Claude-Code-style coding agent
|
|
36
|
+
# chad: a local Claude-Code-style coding agent for your laptop
|
|
37
37
|
|
|
38
38
|
[](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
|
|
39
39
|
|
|
40
|
-
<img src="docs/claude-vs-chad.jpg" width="
|
|
40
|
+
<img src="docs/claude-vs-chad.jpg" width="66%" alt="Two staircase newel posts side by side: Claude is a hand-carved wooden horse head, chad is a scuffed plastic toy horse tied on with twine">
|
|
41
41
|
|
|
42
42
|
> Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
|
|
43
43
|
> *Coding under supervision.*
|
|
@@ -118,7 +118,7 @@ one checkpoint it ships:
|
|
|
118
118
|
cache, so a follow-up step prefills the ~16 tokens it appended instead of the 5,000 it
|
|
119
119
|
already read: **~0.75 s per step instead of ~50 s**. Any server with prompt caching gets
|
|
120
120
|
the easy case; the work is holding it true across compaction, truncated turns and restarts.
|
|
121
|
-
The system prefix is checkpointed to disk, so the second session
|
|
121
|
+
The system prefix is checkpointed to disk, so the second session anywhere starts warm
|
|
122
122
|
(75.6 s → 5.5 s to the first tool call).
|
|
123
123
|
- **Fused Metal kernels.** Quantized-KV attention, a small-M matmul for speculative verify,
|
|
124
124
|
and a compiled single-token layer step, chosen per machine at load time, no knobs.
|
|
@@ -80,6 +80,7 @@ tests/test_ignore.py
|
|
|
80
80
|
tests/test_intent.py
|
|
81
81
|
tests/test_levers.py
|
|
82
82
|
tests/test_log_redaction.py
|
|
83
|
+
tests/test_matrix_bench.py
|
|
83
84
|
tests/test_mcp.py
|
|
84
85
|
tests/test_mcp_oauth.py
|
|
85
86
|
tests/test_mlx_fastpath.py
|
|
@@ -101,4 +102,5 @@ tests/test_toolcall_dialect.py
|
|
|
101
102
|
tests/test_toolcall_parse.py
|
|
102
103
|
tests/test_tools.py
|
|
103
104
|
tests/test_tui.py
|
|
104
|
-
tests/test_validate.py
|
|
105
|
+
tests/test_validate.py
|
|
106
|
+
tests/test_warm_prefix_tiers.py
|