chad-code 2.0.1__tar.gz → 2.0.2__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.2}/PKG-INFO +3 -3
- {chad_code-2.0.1 → chad_code-2.0.2}/README.md +2 -2
- {chad_code-2.0.1 → chad_code-2.0.2}/pyproject.toml +1 -1
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/__init__.py +1 -1
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/agent.py +19 -3
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad_code.egg-info/PKG-INFO +3 -3
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_agent.py +67 -3
- {chad_code-2.0.1 → chad_code-2.0.2}/LICENSE +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/setup.cfg +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/ambient.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/atif.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/base_engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/bench.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/checkpoint.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/cli.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/compaction.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/completion_engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/config.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/diag.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/guardrails.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/ignore.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/levers.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/mcp.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/mcp_oauth.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/mlx_dflash.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/mlx_fastpath.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/mlx_qmm_mma.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/LICENSE +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/__init__.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/alignment.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/attention.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/audio.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/cache.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/conformer.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/ctc.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/parakeet.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/rnnt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/tokenizer.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/parakeet/utils.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/prompt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/prove.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/render.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/repomap.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/seatbelt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/serve.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/session.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/skills.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/speech.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/spill.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/syntaxgate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/toolcall_parse.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/tools.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/tui.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad/validate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad_code.egg-info/SOURCES.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_agent_e2e.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_agent_guards.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_ambient.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_atif.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_bench.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_checkpoint.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_cli.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_compaction.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_completion_engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_config.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_confirm_preview.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_edit.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_engine.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_engine_dflash.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_engine_kvquant.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_engine_pld_hybrid.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_engine_pld_wide.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_feel_pack.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_gate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_ignore.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_intent.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_levers.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_log_redaction.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_mcp.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_mcp_oauth.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_mlx_qmm_mma.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_prove.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_render.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_repomap.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_repomap_polyglot.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_seatbelt.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_serve.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_session.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_skills.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_speech.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_speech_tui.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_spill.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_syntaxgate.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_toolcall_dialect.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_toolcall_parse.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_tools.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/tests/test_tui.py +0 -0
- {chad_code-2.0.1 → chad_code-2.0.2}/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.2
|
|
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.*
|
|
@@ -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.*
|
|
@@ -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.2"
|
|
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"
|
|
@@ -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.2"
|
|
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
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chad-code
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
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.*
|
|
@@ -99,9 +99,15 @@ def test_close_unclosed_think():
|
|
|
99
99
|
check("closed think untouched", close_unclosed_think(closed, True) == closed)
|
|
100
100
|
# Thinking disabled (--no-think) -> never inject a tag.
|
|
101
101
|
check("no-think untouched", close_unclosed_think("plain answer", False) == "plain answer")
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
# A stray opening <think> the model wrote INSIDE its own reasoning does not close
|
|
103
|
+
# anything. Only `</think>` decides. Reading the stray tag as "already handled" left
|
|
104
|
+
# the turn unclosed, which is the full re-prefill this helper exists to avoid.
|
|
105
|
+
check("stray <think>, no close -> closed",
|
|
106
|
+
close_unclosed_think("thinking <think> wait", True) == "thinking <think> wait\n</think>")
|
|
107
|
+
# ...and one that IS closed stays untouched, stray tag or not.
|
|
108
|
+
stray_closed = "thinking <think> wait</think>answer"
|
|
109
|
+
check("stray <think>, already closed -> untouched",
|
|
110
|
+
close_unclosed_think(stray_closed, True) == stray_closed)
|
|
105
111
|
# Empty text -> no spurious tag.
|
|
106
112
|
check("empty untouched", close_unclosed_think("", True) == "")
|
|
107
113
|
|
|
@@ -128,6 +134,14 @@ def test_split_inline_reasoning():
|
|
|
128
134
|
check("tool turn untouched", split_inline_reasoning(tool) is tool)
|
|
129
135
|
check("user turn untouched",
|
|
130
136
|
split_inline_reasoning({"role": "user", "content": "a</think>b"})["content"] == "a</think>b")
|
|
137
|
+
# A stray <think> mid-reasoning is reasoning text, not a delimiter. Splitting on the
|
|
138
|
+
# LAST tag discarded everything before it — reasoning the model really emitted, gone
|
|
139
|
+
# from the transcript, and the shortened re-render no longer extends the KV cache.
|
|
140
|
+
out = split_inline_reasoning(
|
|
141
|
+
{"role": "assistant", "content": "first half <think> second half\n</think>\n\nans"})
|
|
142
|
+
check("stray tag keeps whole reasoning",
|
|
143
|
+
out["reasoning_content"] == "first half <think> second half", out)
|
|
144
|
+
check("stray tag keeps action", out["content"] == "ans", out)
|
|
131
145
|
|
|
132
146
|
|
|
133
147
|
def test_reasoning_split_probe_classifies_templates():
|
|
@@ -181,6 +195,55 @@ def test_reasoning_split_probe_classifies_templates():
|
|
|
181
195
|
check("unprobeable template -> off", a._reasoning_split_supported() is False)
|
|
182
196
|
|
|
183
197
|
|
|
198
|
+
def test_stored_turn_extends_the_kv_cache():
|
|
199
|
+
"""The invariant the two helpers exist to hold: whatever the model generated, the
|
|
200
|
+
NEXT render must begin with the bytes already in the KV cache.
|
|
201
|
+
|
|
202
|
+
Break it and the server re-evaluates the whole turn it just produced — the longer
|
|
203
|
+
the reasoning, the more it costs. Testing the property rather than each helper is
|
|
204
|
+
what catches a turn shape neither of them was written for; the shapes below are all
|
|
205
|
+
ones a real 27B emits. Model-free: `render` is a stand-in for Qwen3.8's template,
|
|
206
|
+
which reads `reasoning_content` and does NOT recover inline `</think>` itself.
|
|
207
|
+
"""
|
|
208
|
+
def render(msgs):
|
|
209
|
+
out = []
|
|
210
|
+
for m in msgs:
|
|
211
|
+
role = m["role"]
|
|
212
|
+
if role == "assistant":
|
|
213
|
+
out.append("<|im_start|>assistant\n<think>\n"
|
|
214
|
+
+ (m.get("reasoning_content") or "")
|
|
215
|
+
+ "\n</think>\n\n" + (m.get("content") or "") + "<|im_end|>\n")
|
|
216
|
+
else:
|
|
217
|
+
out.append(f"<|im_start|>{role}\n{m.get('content') or ''}<|im_end|>\n")
|
|
218
|
+
# The generation prompt auto-opens the think block, so the turn continues inside it.
|
|
219
|
+
return "".join(out) + "<|im_start|>assistant\n<think>\n"
|
|
220
|
+
|
|
221
|
+
prefix = [{"role": "system", "content": "SYS"}, {"role": "user", "content": "task"}]
|
|
222
|
+
reasoning = "weighing the ragged-row rule " * 8
|
|
223
|
+
call = "<tool_call>\n<function=bash>\n</function>\n</tool_call>"
|
|
224
|
+
|
|
225
|
+
shapes = {
|
|
226
|
+
"normal turn": reasoning + "\n</think>\n\nrunning it." + call,
|
|
227
|
+
"cut off mid-think": reasoning,
|
|
228
|
+
"stray <think>, never closed": reasoning + "<think> wait " + reasoning,
|
|
229
|
+
"stray <think>, then closed": (reasoning + "<think> wait " + reasoning
|
|
230
|
+
+ "\n</think>\n\nrunning it." + call),
|
|
231
|
+
}
|
|
232
|
+
for name, generated in shapes.items():
|
|
233
|
+
# What the server holds after decoding this turn: the prompt it was given, then
|
|
234
|
+
# the raw bytes it emitted.
|
|
235
|
+
cached = render(prefix) + generated
|
|
236
|
+
# What chad sends next, once the turn is stored and a tool result appended.
|
|
237
|
+
stored = {"role": "assistant", "content": close_unclosed_think(generated, True)}
|
|
238
|
+
nxt = render([split_inline_reasoning(m) for m in
|
|
239
|
+
prefix + [stored, {"role": "tool", "name": "bash", "content": "ok"}]])
|
|
240
|
+
keep = len(os.path.commonprefix([cached, nxt]))
|
|
241
|
+
check(f"{name}: render extends the cache",
|
|
242
|
+
keep >= len(cached) - len("\n"), f"diverged after {keep} of {len(cached)} chars")
|
|
243
|
+
# Reasoning the model emitted must survive into the transcript, all of it.
|
|
244
|
+
check(f"{name}: reasoning kept",
|
|
245
|
+
reasoning.strip() in split_inline_reasoning(stored).get("reasoning_content", ""))
|
|
246
|
+
|
|
184
247
|
if __name__ == "__main__":
|
|
185
248
|
with pytest.MonkeyPatch.context() as mp:
|
|
186
249
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -198,6 +261,7 @@ if __name__ == "__main__":
|
|
|
198
261
|
test_close_unclosed_think()
|
|
199
262
|
test_split_inline_reasoning()
|
|
200
263
|
test_reasoning_split_probe_classifies_templates()
|
|
264
|
+
test_stored_turn_extends_the_kv_cache()
|
|
201
265
|
print(f"\n{PASS} passed, {FAIL} failed")
|
|
202
266
|
raise SystemExit(1 if FAIL else 0)
|
|
203
267
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|