chad-code 1.0.2__tar.gz → 1.0.4__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.2/src/chad_code.egg-info → chad_code-1.0.4}/PKG-INFO +15 -15
- {chad_code-1.0.2 → chad_code-1.0.4}/README.md +14 -14
- {chad_code-1.0.2 → chad_code-1.0.4}/pyproject.toml +1 -1
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/agent.py +130 -9
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/config.py +15 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/diag.py +10 -7
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/guardrails.py +230 -35
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/levers.py +69 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/prompt.py +36 -4
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/tools.py +34 -6
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/tui.py +4 -3
- {chad_code-1.0.2 → chad_code-1.0.4/src/chad_code.egg-info}/PKG-INFO +15 -15
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad_code.egg-info/SOURCES.txt +1 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_agent_guards.py +144 -4
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_done_audit.py +97 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_edit.py +35 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_feel_pack.py +20 -4
- chad_code-1.0.4/tests/test_garble_invariant.py +86 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_lever_bite.py +102 -6
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_log_redaction.py +38 -13
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_subagent.py +8 -1
- {chad_code-1.0.2 → chad_code-1.0.4}/LICENSE +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/setup.cfg +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/__init__.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/atif.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/base_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/bench.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/cli.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/compaction.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/completion_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/ignore.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/lsp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/mcp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/mcp_oauth.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/mlx_fastpath.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/profiles.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/prove.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/render.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/repomap.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/session.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/skills.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/symbols.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/syntaxgate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/toolcall_parse.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad/validate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_agent.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_agent_e2e.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_atif.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_bench.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_cli.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_compact_notice.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_compaction.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_completion_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_config.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_confirm_preview.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_drift_warn.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_edit_corruption.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_engine_kvquant.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_gate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_ignore.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_intent.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_levers.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_lsp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_mcp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_mcp_oauth.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_plan_review.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_prove.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_render.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_replace_lines.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_repomap.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_session.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_skills.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_symbols.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_syntaxgate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_toolcall_parse.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_tools.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_tui.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.4}/tests/test_validate.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.4
|
|
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
|
|
@@ -39,7 +39,7 @@ Dynamic: license-file
|
|
|
39
39
|
|
|
40
40
|
[](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
|
|
41
41
|
|
|
42
|
-
<img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.
|
|
42
|
+
<img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.1: accuracy vs. cost per run. Every verified entry is a proprietary frontier model in a datacenter, costing $130–$2,000 per run. chad + Ornith (a 35B MoE) clears 57% on an Apple Silicon laptop for the electricity — the only no-API-cost point on the board.">
|
|
43
43
|
|
|
44
44
|
> Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
|
|
45
45
|
> *Coding under supervision.*
|
|
@@ -86,20 +86,20 @@ instrument:
|
|
|
86
86
|
> Real session, unedited (cold model load cut): a local 35B reasons through the failure,
|
|
87
87
|
> edits the file, reruns the tests, confirms green.
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## Frontier scores, laptop cost
|
|
90
90
|
|
|
91
91
|
The exam is [Terminal-Bench](https://www.tbench.ai/leaderboard), the standard benchmark for
|
|
92
|
-
CLI coding agents. chad won't top it —
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
> **
|
|
100
|
-
> benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the
|
|
101
|
-
> and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it,
|
|
102
|
-
> trust it.
|
|
92
|
+
CLI coding agents. chad won't top it — every verified entry on Terminal-Bench 2.1 is a
|
|
93
|
+
frontier model in a datacenter, scoring 59–84%. The number worth looking at is what that
|
|
94
|
+
capability **costs**: the paid field spends **$130–$2,000 in API fees per run**. chad +
|
|
95
|
+
Ornith clears **57%** on an Apple Silicon laptop, for the electricity — no API, no tokens,
|
|
96
|
+
no datacenter. On a laptop, *capability per dollar* is the axis you actually compete on, and
|
|
97
|
+
chad is the only point on the board that runs there.
|
|
98
|
+
|
|
99
|
+
> **Provisional number** — `57%` (51/89, k=1, self-run, not yet leaderboard-verified). The
|
|
100
|
+
> whole benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the
|
|
101
|
+
> runner, and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it,
|
|
102
|
+
> don't trust it.
|
|
103
103
|
|
|
104
104
|
## The bet: at this end of the report card, the harness beats the model
|
|
105
105
|
|
|
@@ -214,7 +214,7 @@ Both are covered in full in the [Configuration reference](docs/configuration.md)
|
|
|
214
214
|
cache, the trimmable/append-only trade, and the ideas borrowed from other agents.
|
|
215
215
|
- **[Throughput & performance](docs/benchmarks.md)** — prefill / decode / warm-step numbers
|
|
216
216
|
you can reproduce with `chad-bench`.
|
|
217
|
-
- **[Terminal-Bench 2.
|
|
217
|
+
- **[Terminal-Bench 2.1 reproduction](benchmarks/tb2/README.md)** — the exact Harbor adapter
|
|
218
218
|
and runner behind the chart; serve Ornith yourself and check the number.
|
|
219
219
|
- **[Configuration reference](docs/configuration.md)** — Agent Skills, MCP servers, the
|
|
220
220
|
context window, every environment variable, and the safety opt-outs.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
|
|
4
4
|
|
|
5
|
-
<img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.
|
|
5
|
+
<img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.1: accuracy vs. cost per run. Every verified entry is a proprietary frontier model in a datacenter, costing $130–$2,000 per run. chad + Ornith (a 35B MoE) clears 57% on an Apple Silicon laptop for the electricity — the only no-API-cost point on the board.">
|
|
6
6
|
|
|
7
7
|
> Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
|
|
8
8
|
> *Coding under supervision.*
|
|
@@ -49,20 +49,20 @@ instrument:
|
|
|
49
49
|
> Real session, unedited (cold model load cut): a local 35B reasons through the failure,
|
|
50
50
|
> edits the file, reruns the tests, confirms green.
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Frontier scores, laptop cost
|
|
53
53
|
|
|
54
54
|
The exam is [Terminal-Bench](https://www.tbench.ai/leaderboard), the standard benchmark for
|
|
55
|
-
CLI coding agents. chad won't top it —
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
> **
|
|
63
|
-
> benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the
|
|
64
|
-
> and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it,
|
|
65
|
-
> trust it.
|
|
55
|
+
CLI coding agents. chad won't top it — every verified entry on Terminal-Bench 2.1 is a
|
|
56
|
+
frontier model in a datacenter, scoring 59–84%. The number worth looking at is what that
|
|
57
|
+
capability **costs**: the paid field spends **$130–$2,000 in API fees per run**. chad +
|
|
58
|
+
Ornith clears **57%** on an Apple Silicon laptop, for the electricity — no API, no tokens,
|
|
59
|
+
no datacenter. On a laptop, *capability per dollar* is the axis you actually compete on, and
|
|
60
|
+
chad is the only point on the board that runs there.
|
|
61
|
+
|
|
62
|
+
> **Provisional number** — `57%` (51/89, k=1, self-run, not yet leaderboard-verified). The
|
|
63
|
+
> whole benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the
|
|
64
|
+
> runner, and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it,
|
|
65
|
+
> don't trust it.
|
|
66
66
|
|
|
67
67
|
## The bet: at this end of the report card, the harness beats the model
|
|
68
68
|
|
|
@@ -177,7 +177,7 @@ Both are covered in full in the [Configuration reference](docs/configuration.md)
|
|
|
177
177
|
cache, the trimmable/append-only trade, and the ideas borrowed from other agents.
|
|
178
178
|
- **[Throughput & performance](docs/benchmarks.md)** — prefill / decode / warm-step numbers
|
|
179
179
|
you can reproduce with `chad-bench`.
|
|
180
|
-
- **[Terminal-Bench 2.
|
|
180
|
+
- **[Terminal-Bench 2.1 reproduction](benchmarks/tb2/README.md)** — the exact Harbor adapter
|
|
181
181
|
and runner behind the chart; serve Ornith yourself and check the number.
|
|
182
182
|
- **[Configuration reference](docs/configuration.md)** — Agent Skills, MCP servers, the
|
|
183
183
|
context window, every environment variable, and the safety opt-outs.
|
|
@@ -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.4"
|
|
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"
|
|
@@ -435,6 +435,11 @@ class Agent:
|
|
|
435
435
|
self.draft_accepted = 0
|
|
436
436
|
self.think_tokens = 0 # tokens spent inside <think> blocks (reasoning overhead)
|
|
437
437
|
self.think_capped = 0 # times the soft think-cap force-closed a step
|
|
438
|
+
# Real tool dispatches this agent has executed (fn actually ran — excludes
|
|
439
|
+
# terminal `done`, validation rejects, and harness-injected nudge messages,
|
|
440
|
+
# which reuse real tool names in the transcript). The evidence signal behind
|
|
441
|
+
# the sub-agent zero-evidence warning in _run_subagent.
|
|
442
|
+
self.tool_dispatches = 0
|
|
438
443
|
# prefill accounting: the master cost for a local model is how many *new*
|
|
439
444
|
# tokens it has to prefill across a turn (context bloat -> big prefills).
|
|
440
445
|
# This is the metric symbolic/repo-map retrieval is meant to shrink.
|
|
@@ -689,12 +694,22 @@ class Agent:
|
|
|
689
694
|
elif (not result or result.startswith("[stopped:")
|
|
690
695
|
or result.startswith("[task failed:")):
|
|
691
696
|
result = _salvage(result or "[task returned nothing]")
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
697
|
+
else:
|
|
698
|
+
# A confident, non-empty report produced with ZERO tool dispatches came
|
|
699
|
+
# from model memory, not this repo — the one sub-agent failure the empty/
|
|
700
|
+
# crashed salvage above cannot see, and the most dangerous fold-back: it
|
|
701
|
+
# reads as evidence (guardrails.subagent_evidence_warning).
|
|
702
|
+
warned = guardrails.subagent_evidence_warning(result, sub.tool_dispatches)
|
|
703
|
+
if warned is not None:
|
|
704
|
+
log.info("TASK zero-evidence warning appended | desc=%r", description)
|
|
705
|
+
result = warned
|
|
706
|
+
# tool_dispatches (not a transcript count — harness nudges reuse real tool names
|
|
707
|
+
# in tool-role messages) is the number that diagnoses a sub-agent returning
|
|
708
|
+
# nothing: it separates "never got to search" from "searched and lost its
|
|
709
|
+
# findings", and 0 with a confident report is the answered-from-memory tell.
|
|
710
|
+
log.info("TASK end | desc=%r | %.1fs | tool_dispatches=%d gen=%d prefill=%d | -> %s",
|
|
696
711
|
description, time.perf_counter() - _t0,
|
|
697
|
-
|
|
712
|
+
sub.tool_dispatches if sub else 0,
|
|
698
713
|
sub.gen_tokens if sub else 0,
|
|
699
714
|
sub.prefill_tokens if sub else 0, result_preview(result or ""))
|
|
700
715
|
return result or "[task returned nothing]"
|
|
@@ -786,6 +801,13 @@ class Agent:
|
|
|
786
801
|
gate_nudges = 0 # times the investigation->edit gate fired this turn
|
|
787
802
|
subagent_sigs = set() # (description, prompt) of sub-agents already spawned this turn
|
|
788
803
|
truncation_nudges = 0 # times we pushed past a token-cap truncation this turn
|
|
804
|
+
garble_nudges = 0 # malformed-tool-call re-nudges (own counter, so a step-0
|
|
805
|
+
# cap-hit can't spend the garble recovery budget)
|
|
806
|
+
consecutive_garbles = 0 # back-to-back garbled steps (drives exemplar + scrub)
|
|
807
|
+
last_garble_idx = None # self.messages index of the previous garbled assistant
|
|
808
|
+
# message (scrubbed if the next step garbles too)
|
|
809
|
+
done_audit_bounces = 0 # total audit bounces this turn (hard cap 2)
|
|
810
|
+
audit_absent_list = [] # task-named paths the FIRST audit stat'ed as absent
|
|
789
811
|
landing_nudges = 0 # one-shot "you're out of steps, land the edit" near the cap
|
|
790
812
|
consecutive_failed_bash = 0 # back-to-back errored bash with no edit (thrash)
|
|
791
813
|
thrash_nudges = 0
|
|
@@ -1400,12 +1422,33 @@ class Agent:
|
|
|
1400
1422
|
_stripped_for_markers = strip_think(text)
|
|
1401
1423
|
garbled = ("<tool_call>" in _stripped_for_markers
|
|
1402
1424
|
or "</function>" in _stripped_for_markers
|
|
1403
|
-
or "<function=" in _stripped_for_markers
|
|
1425
|
+
or "<function=" in _stripped_for_markers
|
|
1426
|
+
or _has_open_tool_call(text))
|
|
1427
|
+
if garbled:
|
|
1428
|
+
consecutive_garbles += 1
|
|
1429
|
+
# From the 2nd consecutive garble, scrub the PREVIOUS garbled
|
|
1430
|
+
# assistant body: the wrong dialect must not stay in context as a
|
|
1431
|
+
# few-shot example (repeated garbles in a row each condition on the
|
|
1432
|
+
# last). Costs one prefix-cache invalidation on this rare path.
|
|
1433
|
+
if (levers.enabled("garble_never_final")
|
|
1434
|
+
and consecutive_garbles >= 2 and last_garble_idx is not None):
|
|
1435
|
+
self.messages[last_garble_idx]["content"] = \
|
|
1436
|
+
guardrails.GARBLE_SCRUBBED
|
|
1437
|
+
log.info("GARBLE scrub at step %d: previous garbled message "
|
|
1438
|
+
"body removed from context (consecutive=%d)",
|
|
1439
|
+
step, consecutive_garbles)
|
|
1440
|
+
last_garble_idx = len(self.messages) - 1
|
|
1441
|
+
else:
|
|
1442
|
+
consecutive_garbles = 0
|
|
1404
1443
|
kind, nudge = guardrails.nudge_for_no_calls(
|
|
1405
1444
|
text, hit_cap, made_edit, unverified_edit, read_only_intent,
|
|
1406
1445
|
action_task or run_task, truncation_nudges, answer_nudges,
|
|
1407
|
-
verify_nudges, _has_open_tool_call(text), garbled_call=garbled
|
|
1408
|
-
|
|
1446
|
+
verify_nudges, _has_open_tool_call(text), garbled_call=garbled,
|
|
1447
|
+
garble_nudges=garble_nudges,
|
|
1448
|
+
consecutive_garbles=consecutive_garbles)
|
|
1449
|
+
if kind == "garble":
|
|
1450
|
+
garble_nudges += 1
|
|
1451
|
+
elif kind == "truncated":
|
|
1409
1452
|
truncation_nudges += 1
|
|
1410
1453
|
elif kind == "no-edit":
|
|
1411
1454
|
answer_nudges += 1
|
|
@@ -1417,6 +1460,21 @@ class Agent:
|
|
|
1417
1460
|
has_code, action_task, run_task)
|
|
1418
1461
|
self.messages.append({"role": "tool", "name": "edit", "content": nudge})
|
|
1419
1462
|
continue
|
|
1463
|
+
# Invariant: a step whose text contains tool-call markers is NEVER a
|
|
1464
|
+
# final answer — the model was trying to act, not to finish. With the
|
|
1465
|
+
# nudge budget spent, hard-stop with a banked note instead of accepting
|
|
1466
|
+
# the garble (otherwise a run of garbles can burn the shared counter and
|
|
1467
|
+
# the audit latch, and the last one ships as the answer with most of the
|
|
1468
|
+
# wall budget still unspent).
|
|
1469
|
+
if garbled and levers.enabled("garble_never_final"):
|
|
1470
|
+
self.budget_note = guardrails.progress_note(self.messages)
|
|
1471
|
+
log.info("END step %d: GARBLE hard-stop (%d garble nudges spent) — "
|
|
1472
|
+
"a garbled tool call is never a final answer",
|
|
1473
|
+
step, garble_nudges)
|
|
1474
|
+
self._emit("info", " [turn stopped: repeated malformed tool calls "
|
|
1475
|
+
"— progress note banked; say 'continue' to retry]")
|
|
1476
|
+
return ("[stopped: the model kept emitting malformed tool calls "
|
|
1477
|
+
"— say 'continue' to resume]")
|
|
1420
1478
|
# Iter-3 did-nothing gate: in auto/headless mode (every benchmark run), a
|
|
1421
1479
|
# turn that ends having executed ZERO real tools is never a legitimate
|
|
1422
1480
|
# completion — the keyword intent classifier misses tasks like "extract the
|
|
@@ -1465,6 +1523,8 @@ class Agent:
|
|
|
1465
1523
|
})
|
|
1466
1524
|
if audit:
|
|
1467
1525
|
done_audit_fired = True
|
|
1526
|
+
done_audit_bounces += 1
|
|
1527
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1468
1528
|
_runway = ((self._turn_budget_s
|
|
1469
1529
|
- (time.monotonic() - turn_start))
|
|
1470
1530
|
if self._turn_budget_s else float("inf"))
|
|
@@ -1474,6 +1534,26 @@ class Agent:
|
|
|
1474
1534
|
self.messages.append({"role": "tool", "name": "edit",
|
|
1475
1535
|
"content": audit})
|
|
1476
1536
|
continue
|
|
1537
|
+
# Absent-path re-bounce: the first audit stat'ed task-named paths as
|
|
1538
|
+
# ABSENT and promised acceptance anyway; if they are STILL absent with
|
|
1539
|
+
# runway to spare, bounce one final time (cap 2/turn) — otherwise a
|
|
1540
|
+
# task-named deliverable the audit already flagged absent can go
|
|
1541
|
+
# unwritten and the accept still goes through.
|
|
1542
|
+
if self.mode != "plan" and not self._subagent \
|
|
1543
|
+
and done_audit_fired and done_audit_bounces < 2 \
|
|
1544
|
+
and audit_absent_list:
|
|
1545
|
+
_runway = ((self._turn_budget_s
|
|
1546
|
+
- (time.monotonic() - turn_start))
|
|
1547
|
+
if self._turn_budget_s else None)
|
|
1548
|
+
rebounce = guardrails.audit_rebounce(audit_absent_list, _runway)
|
|
1549
|
+
if rebounce:
|
|
1550
|
+
done_audit_bounces += 1
|
|
1551
|
+
audit_absent_list = []
|
|
1552
|
+
log.info("DONE-AUDIT rebounce (final-answer): still-absent "
|
|
1553
|
+
"task paths, runway=%s", _runway)
|
|
1554
|
+
self.messages.append({"role": "tool", "name": "edit",
|
|
1555
|
+
"content": rebounce})
|
|
1556
|
+
continue
|
|
1477
1557
|
log.info("END step %d: model produced a FINAL ANSWER, no tool calls "
|
|
1478
1558
|
"(did_work=%s, made_edit=%s, unverified_edit=%s)",
|
|
1479
1559
|
step, did_work, made_edit, unverified_edit)
|
|
@@ -1483,6 +1563,8 @@ class Agent:
|
|
|
1483
1563
|
# The step acted — the spiral broke on its own, so clear the no-think escalation
|
|
1484
1564
|
# streak: escalation is only for consecutive capped/salvaged stalls.
|
|
1485
1565
|
capped_stall_streak = 0
|
|
1566
|
+
consecutive_garbles = 0 # a parsed call ends the garble streak
|
|
1567
|
+
last_garble_idx = None
|
|
1486
1568
|
|
|
1487
1569
|
# Terminal tool -> end the turn cleanly, but enforce verify-before-done
|
|
1488
1570
|
# (forge's prerequisite idea): if files were changed and nothing has been
|
|
@@ -1574,6 +1656,8 @@ class Agent:
|
|
|
1574
1656
|
})
|
|
1575
1657
|
if audit:
|
|
1576
1658
|
done_audit_fired = True
|
|
1659
|
+
done_audit_bounces += 1
|
|
1660
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1577
1661
|
_runway = ((self._turn_budget_s
|
|
1578
1662
|
- (time.monotonic() - turn_start))
|
|
1579
1663
|
if self._turn_budget_s else float("inf"))
|
|
@@ -1582,6 +1666,22 @@ class Agent:
|
|
|
1582
1666
|
self.messages.append({"role": "tool", "name": "done",
|
|
1583
1667
|
"content": audit})
|
|
1584
1668
|
continue
|
|
1669
|
+
# Absent-path re-bounce (see the final-answer twin above): a
|
|
1670
|
+
# still-absent task-named path at accept time gets ONE more bounce,
|
|
1671
|
+
# then the next done is accepted unconditionally.
|
|
1672
|
+
if done_audit_fired and done_audit_bounces < 2 and audit_absent_list:
|
|
1673
|
+
_runway = ((self._turn_budget_s
|
|
1674
|
+
- (time.monotonic() - turn_start))
|
|
1675
|
+
if self._turn_budget_s else None)
|
|
1676
|
+
rebounce = guardrails.audit_rebounce(audit_absent_list, _runway)
|
|
1677
|
+
if rebounce:
|
|
1678
|
+
done_audit_bounces += 1
|
|
1679
|
+
audit_absent_list = []
|
|
1680
|
+
log.info("DONE-AUDIT rebounce: still-absent task paths, "
|
|
1681
|
+
"runway=%s", _runway)
|
|
1682
|
+
self.messages.append({"role": "tool", "name": "done",
|
|
1683
|
+
"content": rebounce})
|
|
1684
|
+
continue
|
|
1585
1685
|
# Iter-3 deliverable recheck (levers.done_spec_recheck): one last self-check
|
|
1586
1686
|
# that the required outputs actually exist at the right path/format before
|
|
1587
1687
|
# we accept done — the hidden container-end-state verifier gives no second
|
|
@@ -1725,6 +1825,7 @@ class Agent:
|
|
|
1725
1825
|
result = "[denied by user]"
|
|
1726
1826
|
else:
|
|
1727
1827
|
_t0 = time.perf_counter()
|
|
1828
|
+
self.tool_dispatches += 1
|
|
1728
1829
|
try:
|
|
1729
1830
|
result = fn(args, self._should_stop)
|
|
1730
1831
|
if plan_write and result.startswith("[wrote"):
|
|
@@ -1736,6 +1837,15 @@ class Agent:
|
|
|
1736
1837
|
# whole — several calls in one step stack into one prefill, so later
|
|
1737
1838
|
# results only get what's left of the step budget (floor-protected).
|
|
1738
1839
|
result = _clip_tool_result(result, cap=_step_tool_cap(step_tool_chars))
|
|
1840
|
+
# Duplicate read-only output: if this (clipped) result is
|
|
1841
|
+
# byte-identical to a tool message still in the transcript, append a
|
|
1842
|
+
# short pointer instead of paying the body's prefill again — see
|
|
1843
|
+
# guardrails.elide_duplicate_result for the safety argument.
|
|
1844
|
+
_elided = guardrails.elide_duplicate_result(name, result, self.messages)
|
|
1845
|
+
if _elided is not None:
|
|
1846
|
+
log.info("TOOL %s duplicate result elided (%d chars)",
|
|
1847
|
+
name, len(result))
|
|
1848
|
+
result = _elided
|
|
1739
1849
|
step_tool_chars += len(result)
|
|
1740
1850
|
if _PREFILL_TRACE:
|
|
1741
1851
|
self._trace_tools_pending.append([name, round(_tool_s, 4)])
|
|
@@ -1854,7 +1964,18 @@ class Agent:
|
|
|
1854
1964
|
if made_edit and not _gov_prev_made:
|
|
1855
1965
|
readonly_streak = 0
|
|
1856
1966
|
elif did_work and not made_edit:
|
|
1857
|
-
|
|
1967
|
+
# A bash step that isn't provably read-only is ACTION, not
|
|
1968
|
+
# investigation — `git merge`, `apt-get install`, redirects are ops
|
|
1969
|
+
# progress. Otherwise the gate can count an entire git/ops workflow as
|
|
1970
|
+
# "investigation" and demand an edit at a decision point where there is
|
|
1971
|
+
# nothing to edit yet.
|
|
1972
|
+
if levers.enabled("gate_ops_exempt") and any(
|
|
1973
|
+
n == "bash"
|
|
1974
|
+
and not guardrails.is_readonly_bash(str(a.get("command", "")))
|
|
1975
|
+
for n, a in calls):
|
|
1976
|
+
readonly_streak = 0
|
|
1977
|
+
else:
|
|
1978
|
+
readonly_streak += 1
|
|
1858
1979
|
if not read_only_intent:
|
|
1859
1980
|
gate = guardrails.investigation_gate(readonly_streak, made_edit, gate_nudges)
|
|
1860
1981
|
if gate:
|
|
@@ -31,6 +31,21 @@ def eq(name: str, expected: str) -> bool:
|
|
|
31
31
|
return os.environ.get(name) == expected
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def traces_enabled() -> bool:
|
|
35
|
+
"""Whether chad may write local diagnostic traces under ~/.chad — the readable
|
|
36
|
+
`session.log` (user query, tool-call args, bash/write/edit previews, result previews)
|
|
37
|
+
and the persistent input history at ~/.chad/history.
|
|
38
|
+
|
|
39
|
+
Privacy-first default: **OFF**. chad is a local, single-user agent, so these traces
|
|
40
|
+
never leave the machine, but they still record command/file previews in plaintext
|
|
41
|
+
outside the repo — so they are opt-in. Set **`CHAD_SESSION_LOG`** (any truthy value)
|
|
42
|
+
to turn them on. `CHAD_NO_SESSION_LOG`, if set, forces them OFF and wins over the
|
|
43
|
+
opt-in (a hard kill switch, kept for compatibility with the pre-opt-in default)."""
|
|
44
|
+
if flag("CHAD_NO_SESSION_LOG"):
|
|
45
|
+
return False
|
|
46
|
+
return flag("CHAD_SESSION_LOG")
|
|
47
|
+
|
|
48
|
+
|
|
34
49
|
def env_str(name, default=None):
|
|
35
50
|
"""The env value if set to a non-empty string, else `default`. Mirrors the common
|
|
36
51
|
`os.environ.get(name) or <fallback>` idiom (empty string collapses to the default)."""
|
|
@@ -9,8 +9,10 @@ declare done, which cache served the turn. This module is that trace.
|
|
|
9
9
|
|
|
10
10
|
The log is bounded (5 MB x3 rotation) and previews pass through a best-effort secret
|
|
11
11
|
redactor, but it still records command/file previews in plaintext outside the repo, so
|
|
12
|
-
treat it as sensitive.
|
|
13
|
-
|
|
12
|
+
treat it as sensitive. For that reason it is **opt-in**: privacy-first, chad writes no
|
|
13
|
+
trace under ~/.chad unless CHAD_SESSION_LOG is set (see config.traces_enabled). None of
|
|
14
|
+
this touches the model-facing transcript or the tool results the model sees — it is the
|
|
15
|
+
diagnostic trace only.
|
|
14
16
|
"""
|
|
15
17
|
import json
|
|
16
18
|
import logging
|
|
@@ -22,11 +24,12 @@ from . import config
|
|
|
22
24
|
|
|
23
25
|
_LOG_DIR = os.path.expanduser("~/.chad")
|
|
24
26
|
log = logging.getLogger("chad")
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
27
|
+
# Privacy-first default: the diagnostic session log is OFF unless opted in. Set
|
|
28
|
+
# CHAD_SESSION_LOG (any truthy value) to enable it; CHAD_NO_SESSION_LOG still forces it
|
|
29
|
+
# off (see config.traces_enabled). When disabled we install a NullHandler (so the many
|
|
30
|
+
# log.info calls stay cheap no-ops and Python never warns about missing handlers) and
|
|
31
|
+
# never create ~/.chad for the log's sake.
|
|
32
|
+
_DISABLED = not config.traces_enabled()
|
|
30
33
|
if _DISABLED:
|
|
31
34
|
log.addHandler(logging.NullHandler())
|
|
32
35
|
log.propagate = False
|